Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 58 additions & 24 deletions Cotabby.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

23 changes: 16 additions & 7 deletions Cotabby/App/Coordinators/WelcomeCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ import SwiftUI
@MainActor
final class WelcomeCoordinator: NSObject, NSWindowDelegate {
private enum Layout {
/// Match the first welcome step so the window does not flash at an oversized default before
/// SwiftUI has a chance to report its preferred content size.
static let initialContentSize = NSSize(width: 500, height: 360)

/// Keep a margin between the onboarding window and the screen edges when a step's preferred
/// height would otherwise exceed the visible screen. The SwiftUI content scrolls to absorb
/// the difference, so clamping here only ever shrinks the window, never clips the footer.
Expand Down Expand Up @@ -51,7 +47,13 @@ final class WelcomeCoordinator: NSObject, NSWindowDelegate {
/// relaunches Cotabby, which lands before the final "Start Using Cotabby" tap that stamps
/// completion. Without this, that relaunch drops them back at step one and onboarding repeats
/// every time (issue #314). Cleared on completion so a future version bump starts clean.
private static let onboardingProgressStepKey = "cotabbyOnboardingProgressStep"
///
/// The "2" suffix marks the second step-numbering scheme (the redesign folded the writing-style
/// step into "personalize", shifting every later raw index). Reading the old key would resume a
/// mid-flow user onto the wrong step, so the old key is abandoned rather than reinterpreted;
/// anyone caught mid-flow across the update restarts at the welcome step, which is the safe
/// outcome. Any future change to `WelcomeStep`'s numbering must bump this suffix again.
private static let onboardingProgressStepKey = "cotabbyOnboardingProgressStep2"

init(
permissionManager: PermissionManager,
Expand Down Expand Up @@ -117,6 +119,8 @@ final class WelcomeCoordinator: NSObject, NSWindowDelegate {
return
}

let resumeStepIndex = userDefaults.integer(forKey: Self.onboardingProgressStepKey)

let hostingController = NSHostingController(
rootView: WelcomeView(
permissionManager: permissionManager,
Expand All @@ -131,16 +135,21 @@ final class WelcomeCoordinator: NSObject, NSWindowDelegate {
onDismiss: { [weak self] in
self?.completeOnboarding()
},
initialStepIndex: userDefaults.integer(forKey: Self.onboardingProgressStepKey),
initialStepIndex: resumeStepIndex,
isReturningUser: userDefaults.integer(forKey: Self.onboardingCompletedVersionKey) > 0,
onStepChange: { [weak self] stepIndex in
self?.recordProgress(stepIndex: stepIndex)
}
)
)

// Size the window for the step the wizard actually opens on (the resume point, mirroring
// WelcomeView's own fallback for out-of-range indices) so it never flashes at one size and
// immediately animates to another.
let initialStep = WelcomeStep(rawValue: resumeStepIndex) ?? .welcome

let window = NSWindow(
contentRect: CGRect(origin: .zero, size: Layout.initialContentSize),
contentRect: CGRect(origin: .zero, size: initialStep.preferredWindowSize),
styleMask: [.titled, .closable, .fullSizeContentView],
backing: .buffered,
defer: false
Expand Down
82 changes: 82 additions & 0 deletions Cotabby/Support/OnboardingFlowSteps.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Foundation

/// File overview:
/// Pure sequencing model for the first-run onboarding wizard: the ordered steps, which of them
/// count toward the progress indicator, and the window size each one prefers. Extracted from
/// `WelcomeView` so the flow's shape is unit-testable without SwiftUI.
///
/// Raw values are persisted by `WelcomeCoordinator` as the wizard's resume point, so reordering or
/// inserting cases is a breaking change for stored progress. If the numbering scheme changes,
/// `WelcomeCoordinator` must move to a fresh UserDefaults key rather than reinterpret old indices
/// (see `onboardingProgressStepKey` there).
enum WelcomeStep: Int, CaseIterable, Comparable, Sendable {
case welcome
case permissions
case template
case personalize
case keybind
case done

static func < (lhs: WelcomeStep, rhs: WelcomeStep) -> Bool {
lhs.rawValue < rhs.rawValue
}

/// The flow is strictly linear, so navigation is derived from case order instead of hand-wired
/// per step. `nil` past either end keeps the terminal steps terminal.
var next: WelcomeStep? {
WelcomeStep(rawValue: rawValue + 1)
}

var previous: WelcomeStep? {
WelcomeStep(rawValue: rawValue - 1)
}

/// Number of steps shown in the progress indicator (the middle, non-terminal steps).
static let totalProgressSteps = 4

/// 1-based position within the progress indicator, or `nil` for the intro/outro steps that
/// intentionally sit outside the counted flow.
var progressIndex: Int? {
switch self {
case .welcome, .done:
return nil
case .permissions:
return 1
case .template:
return 2
case .personalize:
return 3
case .keybind:
return 4
}
}

/// Product-chosen window sizes. The coordinator clamps the height to the visible screen, and
/// the scrolling content absorbs any overflow, so these are targets rather than hard
/// guarantees. Width is constant across the flow on purpose: the window only ever morphs
/// vertically between steps, which reads as one calm surface instead of a window that jumps
/// around in both dimensions.
var preferredWindowSize: NSSize {
NSSize(width: WelcomeStep.windowWidth, height: preferredWindowHeight)
}

/// Single width shared by every step (see `preferredWindowSize`).
static let windowWidth: CGFloat = 640

private var preferredWindowHeight: CGFloat {
switch self {
case .welcome:
return 640
case .permissions:
return 600
case .template:
return 720
case .personalize:
return 620
case .keybind:
return 580
case .done:
return 740
}
}
}
Loading
Loading