diff --git a/ios/Sources/Setline/AppModel.swift b/ios/Sources/Setline/AppModel.swift index 1cbd4c1..d5ea9c4 100644 --- a/ios/Sources/Setline/AppModel.swift +++ b/ios/Sources/Setline/AppModel.swift @@ -13,7 +13,7 @@ final class AppModel { private(set) var document: SetlineDocument = .initial var isLoading = true var isOnboardingPresented = false - private(set) var isReplayingOnboarding = false + private(set) var isExistingOwnerOrientation = false var isWorkoutPresented = false var selectedTab = 0 var message: String? @@ -97,7 +97,10 @@ final class AppModel { } else { document = try await store.load() } - isOnboardingPresented = arguments.contains("--onboarding-demo") + let showsDemo = arguments.contains("--onboarding-demo") + isExistingOwnerOrientation = !showsDemo + && SetlineOnboardingPolicy.hasExistingData(document) + isOnboardingPresented = showsDemo || SetlineOnboardingPolicy.shouldPresent( document: document, completed: UserDefaults.standard.bool(forKey: Self.onboardingCompletionKey) @@ -112,21 +115,15 @@ final class AppModel { } } - static let onboardingCompletionKey = "setline.onboarding.completed.v1" + static let onboardingCompletionKey = "setline.illustrated-onboarding.seen.v1" func completeOnboarding(openPlan: Bool = false) { UserDefaults.standard.set(true, forKey: Self.onboardingCompletionKey) isOnboardingPresented = false - isReplayingOnboarding = false + isExistingOwnerOrientation = false selectedTab = openPlan ? 1 : 0 } - /// Reopens the product tour without changing the current programme or workout history. - func replayOnboarding() { - isReplayingOnboarding = true - isOnboardingPresented = true - } - private func startDemoSessionIfRequested(_ arguments: [String]) throws { guard arguments.contains("--active-demo") || arguments.contains("--rest-demo") else { return } guard document.activeSession == nil, let resolved = document.session() else { return } diff --git a/ios/Sources/Setline/SecondaryViews.swift b/ios/Sources/Setline/SecondaryViews.swift index 8c80669..dca5a7b 100644 --- a/ios/Sources/Setline/SecondaryViews.swift +++ b/ios/Sources/Setline/SecondaryViews.swift @@ -40,13 +40,6 @@ struct SettingsView: View { .frame(minHeight: 48) } settingsSection("About") { - Button { - model.replayOnboarding() - } label: { - Label("Replay onboarding", systemImage: "sparkles") - .frame(maxWidth: .infinity, alignment: .leading) - } - .frame(minHeight: 44) LabeledContent("Version", value: "1.0.0 (2)") Link("Privacy", destination: URL(string: "https://setline.significanthobbies.com/privacy")!) .frame(minHeight: 44) diff --git a/ios/Sources/Setline/SetlineOnboardingView.swift b/ios/Sources/Setline/SetlineOnboardingView.swift index 9bc1e53..d2fbf6a 100644 --- a/ios/Sources/Setline/SetlineOnboardingView.swift +++ b/ios/Sources/Setline/SetlineOnboardingView.swift @@ -4,10 +4,14 @@ import SwiftUI enum SetlineOnboardingPolicy { static func shouldPresent(document: SetlineDocument, completed: Bool) -> Bool { guard !completed else { return false } - guard document.activeSession == nil, document.history.isEmpty else { return false } - guard document.templates.isEmpty, document.goals.isEmpty else { return false } - if case .custom = document.programme { return false } - return true + return document.activeSession == nil + } + + static func hasExistingData(_ document: SetlineDocument) -> Bool { + if document.activeSession != nil || !document.history.isEmpty { return true } + if !document.templates.isEmpty || !document.goals.isEmpty { return true } + if case .custom = document.programme { return true } + return false } } @@ -100,9 +104,9 @@ struct SetlineOnboardingView: View { .background(SetlinePalette.paper) .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) - Button(model.isReplayingOnboarding ? "Review the bundled programme" : "Use the bundled programme") { + Button(model.isExistingOwnerOrientation ? "Review the bundled programme" : "Use the bundled programme") { Task { - if !model.isReplayingOnboarding { + if !model.isExistingOwnerOrientation { await model.selectProgramme(.bundled(.twelveWeekStrengthCardioMobility)) } step = .preview @@ -165,8 +169,8 @@ struct SetlineOnboardingView: View { .font(.footnote) .foregroundStyle(SetlinePalette.ink.opacity(0.68)) - Button(model.isReplayingOnboarding ? "Return to Setline" : "Start this session") { - if model.isReplayingOnboarding { + Button(model.isExistingOwnerOrientation ? "Return to Setline" : "Start this session") { + if model.isExistingOwnerOrientation { model.completeOnboarding() } else { Task { await model.startWorkout(preview) } @@ -174,7 +178,7 @@ struct SetlineOnboardingView: View { } .buttonStyle(ActionSlabStyle()) .accessibilityHint( - model.isReplayingOnboarding + model.isExistingOwnerOrientation ? "Closes the onboarding tour without changing your programme" : "Starts the real offline workout player" ) diff --git a/ios/Tests/SetlineTests/SetlineOnboardingTests.swift b/ios/Tests/SetlineTests/SetlineOnboardingTests.swift index 16a6375..8737fb2 100644 --- a/ios/Tests/SetlineTests/SetlineOnboardingTests.swift +++ b/ios/Tests/SetlineTests/SetlineOnboardingTests.swift @@ -12,14 +12,17 @@ final class SetlineOnboardingTests: XCTestCase { XCTAssertFalse(SetlineOnboardingPolicy.shouldPresent(document: .initial, completed: true)) } - func testExistingActivityBypassesOnboarding() throws { + func testActiveWorkoutDefersIllustratedOrientation() throws { var document = SetlineDocument.initial let template = TwelveWeekProgramme.template(for: .lower, week: 1) try document.startWorkout(template: template) XCTAssertFalse(SetlineOnboardingPolicy.shouldPresent(document: document, completed: false)) + XCTAssertTrue(SetlineOnboardingPolicy.hasExistingData(document)) } - func testExistingCustomProgrammeBypassesOnboarding() { - XCTAssertFalse(SetlineOnboardingPolicy.shouldPresent(document: .sample, completed: false)) + func testExistingOwnerReceivesIllustratedOrientationOnceIdle() { + XCTAssertTrue(SetlineOnboardingPolicy.shouldPresent(document: .sample, completed: false)) + XCTAssertTrue(SetlineOnboardingPolicy.hasExistingData(.sample)) + XCTAssertFalse(SetlineOnboardingPolicy.shouldPresent(document: .sample, completed: true)) } } diff --git a/ios/Tests/SetlineUITests/SetlineUITests.swift b/ios/Tests/SetlineUITests/SetlineUITests.swift index 7e6e039..9a24eda 100644 --- a/ios/Tests/SetlineUITests/SetlineUITests.swift +++ b/ios/Tests/SetlineUITests/SetlineUITests.swift @@ -11,6 +11,9 @@ final class SetlineUITests: XCTestCase { private func launch(_ arguments: [String] = ["--ui-demo"]) -> XCUIApplication { let app = XCUIApplication() app.launchArguments = arguments + if !arguments.contains("--onboarding-demo") { + app.launchArguments += ["-setline.illustrated-onboarding.seen.v1", "YES"] + } app.launch() return app } @@ -22,7 +25,7 @@ final class SetlineUITests: XCTestCase { /// must explicitly provide the completion default rather than inherit state /// left behind by an earlier test or a developer's simulator. private func launchFreshSettledApp() -> XCUIApplication { - launch(["--fresh-demo", "-setline.onboarding.completed.v1", "YES"]) + launch(["--fresh-demo", "-setline.illustrated-onboarding.seen.v1", "YES"]) } /// The decimal keypad has no return key, so the player supplies a Done button