diff --git a/ios/Kith.xcodeproj/project.pbxproj b/ios/Kith.xcodeproj/project.pbxproj index 348ca35..629259e 100644 --- a/ios/Kith.xcodeproj/project.pbxproj +++ b/ios/Kith.xcodeproj/project.pbxproj @@ -31,6 +31,7 @@ A3E58ADFB234FD1407023392 /* KithOnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 416A75C69650AAD584FEE064 /* KithOnboardingView.swift */; }; A4336E3A96ACF2FE47DA6C35 /* HubSyncIssueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A41324D637F7A736FAD6B80A /* HubSyncIssueTests.swift */; }; B11A9EBD171FF12E6F7B0045 /* KithUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69B0BA41F320166AEA1728A2 /* KithUITests.swift */; }; + DAC253113D3DD2E0BB7067CC /* KithOnboardingReplayTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E19BA6F30BC6EADC0EA7CD /* KithOnboardingReplayTests.swift */; }; F3C48A6CA9EC720182B123BC /* Design.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE4DDB782CAF5D8FD571DC3B /* Design.swift */; }; F680576C1F45EE52AFBA44DE /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 2C466A859325D15A49C22248 /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ @@ -106,6 +107,7 @@ 79D378032A9722C9555ED8AE /* DemoData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoData.swift; sourceTree = ""; }; 7A8B5ADE80477C15D5324428 /* CloudStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudStore.swift; sourceTree = ""; }; 7AA76B6FCE2A1BD2FDDE7D45 /* BubbleField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BubbleField.swift; sourceTree = ""; }; + 80E19BA6F30BC6EADC0EA7CD /* KithOnboardingReplayTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KithOnboardingReplayTests.swift; sourceTree = ""; }; 8E3415A5E88E90361208B65E /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; 8FDEF1D744F055053CB588A9 /* KithCoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KithCoreTests.swift; sourceTree = ""; }; A41324D637F7A736FAD6B80A /* HubSyncIssueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HubSyncIssueTests.swift; sourceTree = ""; }; @@ -162,6 +164,7 @@ isa = PBXGroup; children = ( A41324D637F7A736FAD6B80A /* HubSyncIssueTests.swift */, + 80E19BA6F30BC6EADC0EA7CD /* KithOnboardingReplayTests.swift */, ); name = KithAppTests; path = Tests/KithAppTests; @@ -437,6 +440,7 @@ buildActionMask = 2147483647; files = ( A4336E3A96ACF2FE47DA6C35 /* HubSyncIssueTests.swift in Sources */, + DAC253113D3DD2E0BB7067CC /* KithOnboardingReplayTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ios/Sources/Kith/AppModel.swift b/ios/Sources/Kith/AppModel.swift index b1b0acb..251dcf4 100644 --- a/ios/Sources/Kith/AppModel.swift +++ b/ios/Sources/Kith/AppModel.swift @@ -13,6 +13,7 @@ final class AppModel { private(set) var document: KithDocument = .empty var isLoading = true var isOnboardingPresented = false + private(set) var isReplayingOnboarding = false private(set) var onboardingPersonID: UUID? var selectedPersonID: UUID? var isAddingPerson = false @@ -208,10 +209,19 @@ final class AppModel { func finishOnboarding(addAnother: Bool = false) { isOnboardingPresented = false + isReplayingOnboarding = false onboardingPersonID = nil if addAnother { isAddingPerson = true } } + /// Reopens the product tour without creating or editing a person. + func replayOnboarding() { + onboardingPersonID = nil + isReplayingOnboarding = true + isShowingConnection = false + isOnboardingPresented = true + } + func deletePerson(id: UUID) { let deletedIDs = [id] + document.entries.filter { $0.personID == id }.map(\.id) document.removePerson(id: id) diff --git a/ios/Sources/Kith/KithConnectionView.swift b/ios/Sources/Kith/KithConnectionView.swift index 79541cb..3aeb42b 100644 --- a/ios/Sources/Kith/KithConnectionView.swift +++ b/ios/Sources/Kith/KithConnectionView.swift @@ -106,6 +106,15 @@ struct KithConnectionView: View { Text("Connection setup is unavailable in this build.") .foregroundStyle(KithPalette.rust) } + + Button { + model.replayOnboarding() + } label: { + Label("Replay onboarding", systemImage: "sparkles") + .frame(maxWidth: .infinity, alignment: .leading) + } + .font(.headline) + .frame(minHeight: 44) } .padding(28) } diff --git a/ios/Sources/Kith/KithOnboardingView.swift b/ios/Sources/Kith/KithOnboardingView.swift index fcf0b5e..9aaa455 100644 --- a/ios/Sources/Kith/KithOnboardingView.swift +++ b/ios/Sources/Kith/KithOnboardingView.swift @@ -44,7 +44,16 @@ struct KithOnboardingView: View { ScrollView { VStack(alignment: .leading, spacing: 24) { introduction - lanternPreview + Image("KithOnboarding") + .resizable() + .scaledToFit() + .frame(maxHeight: 220) + .frame(maxWidth: .infinity) + .accessibilityLabel("A hand-drawn figure ties one remembered moment to the lantern of someone close.") + if model.isReplayingOnboarding { + replayActions + } else { + lanternPreview VStack(alignment: .leading, spacing: 16) { TextField("Their name", text: $name) .textFieldStyle(.roundedBorder) @@ -81,6 +90,7 @@ struct KithOnboardingView: View { .font(.footnote) .foregroundStyle(KithPalette.espresso.opacity(0.58)) .frame(maxWidth: .infinity, alignment: .center) + } } .frame(maxWidth: 620) .padding(24) @@ -89,6 +99,20 @@ struct KithOnboardingView: View { .navigationBarTitleDisplayMode(.inline) } + private var replayActions: some View { + VStack(alignment: .leading, spacing: 14) { + Text("Your existing constellation stays exactly as it is.") + .font(.body) + .foregroundStyle(KithPalette.espresso.opacity(0.66)) + Button("Return to Kith") { model.finishOnboarding() } + .buttonStyle(ClayButtonStyle()) + .frame(maxWidth: .infinity) + Button("Add someone in Kith") { model.finishOnboarding(addAnother: true) } + .font(.headline) + .frame(maxWidth: .infinity, minHeight: 44) + } + } + private var introduction: some View { VStack(alignment: .leading, spacing: 10) { Text("The people you keep close.") diff --git a/ios/Sources/Kith/Resources/Assets.xcassets/KithOnboarding.imageset/Contents.json b/ios/Sources/Kith/Resources/Assets.xcassets/KithOnboarding.imageset/Contents.json new file mode 100644 index 0000000..e8cb5af --- /dev/null +++ b/ios/Sources/Kith/Resources/Assets.xcassets/KithOnboarding.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "kith-onboarding.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ios/Sources/Kith/Resources/Assets.xcassets/KithOnboarding.imageset/kith-onboarding.png b/ios/Sources/Kith/Resources/Assets.xcassets/KithOnboarding.imageset/kith-onboarding.png new file mode 100644 index 0000000..02d2f20 Binary files /dev/null and b/ios/Sources/Kith/Resources/Assets.xcassets/KithOnboarding.imageset/kith-onboarding.png differ diff --git a/ios/Tests/KithAppTests/KithOnboardingReplayTests.swift b/ios/Tests/KithAppTests/KithOnboardingReplayTests.swift new file mode 100644 index 0000000..b2ef7d6 --- /dev/null +++ b/ios/Tests/KithAppTests/KithOnboardingReplayTests.swift @@ -0,0 +1,16 @@ +import XCTest + +@testable import Kith + +@MainActor +final class KithOnboardingReplayTests: XCTestCase { + func testReplayOpensNonDestructiveTourMode() { + let model = AppModel(platform: nil) + + model.replayOnboarding() + + XCTAssertTrue(model.isOnboardingPresented) + XCTAssertTrue(model.isReplayingOnboarding) + XCTAssertNil(model.onboardingPersonID) + } +}