Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
963371d619
commit
69e548b13c
@ -65,6 +65,7 @@ struct AndromidaApp: App {
|
||||
SetupWizardView(
|
||||
store: store,
|
||||
categoryStore: categoryStore,
|
||||
reminderScheduler: store.reminderScheduler,
|
||||
onComplete: {
|
||||
justCompletedWizard = true
|
||||
withAnimation {
|
||||
|
||||
@ -958,6 +958,9 @@
|
||||
"comment" : "The title of the navigation bar for editing a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Enable Notifications" : {
|
||||
"comment" : "Primary button text on notification permission screen during onboarding."
|
||||
},
|
||||
"End" : {
|
||||
|
||||
},
|
||||
@ -1140,6 +1143,12 @@
|
||||
"comment" : "The text for the \"Get Started\" button in the welcome screen.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Get a gentle nudge each morning to start your day right" : {
|
||||
"comment" : "Description for notification permission screen when user selected morning rituals."
|
||||
},
|
||||
"Get a gentle reminder when it's time for your rituals" : {
|
||||
"comment" : "Default description for notification permission screen."
|
||||
},
|
||||
"Give your mind a break from screens." : {
|
||||
"comment" : "Notes for a ritual preset focused on giving the mind a break from screens.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -1405,6 +1414,9 @@
|
||||
"comment" : "Title of a navigation row in the Settings view that takes the user to a view managing ritual categories.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Maybe Later" : {
|
||||
"comment" : "Secondary button text on notification permission screen during onboarding to skip enabling notifications."
|
||||
},
|
||||
"Midday" : {
|
||||
"comment" : "Description of a ritual is typically performed during the day.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -2162,6 +2174,54 @@
|
||||
"comment" : "A button label that allows users to skip creating a new ritual for now.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Skip setup?" : {
|
||||
"comment" : "Alert title asking if the user wants to skip onboarding setup.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Skip setup?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Skip" : {
|
||||
"comment" : "Button label to skip onboarding.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Skip"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Keep going" : {
|
||||
"comment" : "Cancel button label to continue onboarding.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "Keep going"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"You can complete setup later in Settings." : {
|
||||
"comment" : "Alert message explaining that setup can be completed later in Settings.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "new",
|
||||
"value" : "You can complete setup later in Settings."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Sleep Preparation" : {
|
||||
"comment" : "Theme of the \"Sleep Preparation\" ritual preset.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -2222,6 +2282,9 @@
|
||||
"comment" : "Description of a trend direction when there is no significant change.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Stay on track" : {
|
||||
"comment" : "Headline on the notification permission screen during onboarding."
|
||||
},
|
||||
"Start" : {
|
||||
"comment" : "A button that starts a new arc for a ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -2560,6 +2623,9 @@
|
||||
"comment" : "The title of the welcome screen in the setup wizard.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"We'll remind you at the perfect times for your morning and evening rituals" : {
|
||||
"comment" : "Description for notification permission screen when user selected both morning and evening rituals."
|
||||
},
|
||||
"Wellness" : {
|
||||
"comment" : "The category of the morning ritual.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@ -2614,6 +2680,9 @@
|
||||
"comment" : "Notes section of a ritual preset focused on sleep preparation.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Wind down with a reminder when it's time for your evening ritual" : {
|
||||
"comment" : "Description for notification permission screen when user selected evening rituals."
|
||||
},
|
||||
"Wind down with intention" : {
|
||||
"comment" : "Subtitle for the \"Evening\" section of the \"Both\" time preference option in the onboarding flow.",
|
||||
"isCommentAutoGenerated" : true
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import SwiftData
|
||||
import CoreData
|
||||
import Bedrock
|
||||
|
||||
@MainActor
|
||||
@ -12,6 +13,7 @@ final class RitualStore: RitualStoreProviding {
|
||||
@ObservationIgnored private let calendar: Calendar
|
||||
@ObservationIgnored private let dayFormatter: DateFormatter
|
||||
@ObservationIgnored private let displayFormatter: DateFormatter
|
||||
@ObservationIgnored private var remoteChangeObserver: NSObjectProtocol?
|
||||
|
||||
private(set) var rituals: [Ritual] = []
|
||||
private(set) var currentRituals: [Ritual] = []
|
||||
@ -48,6 +50,26 @@ final class RitualStore: RitualStoreProviding {
|
||||
displayFormatter.dateStyle = .full
|
||||
displayFormatter.timeStyle = .none
|
||||
loadRitualsIfNeeded()
|
||||
observeRemoteChanges()
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let observer = remoteChangeObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
/// Observes CloudKit remote change notifications to auto-refresh UI when iCloud data syncs.
|
||||
private func observeRemoteChanges() {
|
||||
remoteChangeObserver = NotificationCenter.default.addObserver(
|
||||
forName: .NSPersistentStoreRemoteChange,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
self?.reloadRituals()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var activeRitual: Ritual? {
|
||||
|
||||
@ -6,6 +6,8 @@ import Bedrock
|
||||
@Observable
|
||||
final class SettingsStore: CloudSyncable, ThemeProviding {
|
||||
@ObservationIgnored private let cloudSync = CloudSyncManager<AppSettingsData>()
|
||||
@ObservationIgnored private var cloudChangeObserver: NSObjectProtocol?
|
||||
@ObservationIgnored private var isApplyingCloudUpdate = false
|
||||
|
||||
/// Observable copy of last sync date, updated when sync completes.
|
||||
private(set) var lastSyncDate: Date?
|
||||
@ -16,40 +18,56 @@ final class SettingsStore: CloudSyncable, ThemeProviding {
|
||||
/// Observable copy of initial sync state.
|
||||
private(set) var hasCompletedInitialSync: Bool = false
|
||||
|
||||
var hapticsEnabled: Bool {
|
||||
get { cloudSync.data.hapticsEnabled }
|
||||
set { update { $0.hapticsEnabled = newValue } }
|
||||
var hapticsEnabled: Bool = AppSettingsData.empty.hapticsEnabled {
|
||||
didSet {
|
||||
updateSetting(\.hapticsEnabled, value: hapticsEnabled, oldValue: oldValue)
|
||||
}
|
||||
}
|
||||
|
||||
var soundEnabled: Bool {
|
||||
get { cloudSync.data.soundEnabled }
|
||||
set { update { $0.soundEnabled = newValue } }
|
||||
var soundEnabled: Bool = AppSettingsData.empty.soundEnabled {
|
||||
didSet {
|
||||
updateSetting(\.soundEnabled, value: soundEnabled, oldValue: oldValue)
|
||||
}
|
||||
}
|
||||
|
||||
var theme: AppTheme {
|
||||
get { cloudSync.data.theme }
|
||||
set { update { $0.theme = newValue } }
|
||||
var theme: AppTheme = AppSettingsData.empty.theme {
|
||||
didSet {
|
||||
updateSetting(\.theme, value: theme, oldValue: oldValue)
|
||||
}
|
||||
}
|
||||
|
||||
var iCloudAvailable: Bool { cloudSync.iCloudAvailable }
|
||||
|
||||
var iCloudEnabled: Bool {
|
||||
get { cloudSync.iCloudEnabled }
|
||||
set { cloudSync.iCloudEnabled = newValue }
|
||||
set {
|
||||
cloudSync.iCloudEnabled = newValue
|
||||
refreshSyncState()
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
// Initialize observable properties from cloudSync
|
||||
refreshSettingsData()
|
||||
refreshSyncState()
|
||||
observeCloudChanges()
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let cloudChangeObserver {
|
||||
NotificationCenter.default.removeObserver(cloudChangeObserver)
|
||||
}
|
||||
}
|
||||
|
||||
func forceSync() {
|
||||
cloudSync.sync()
|
||||
refreshSettingsData()
|
||||
refreshSyncState()
|
||||
}
|
||||
|
||||
func refresh() {
|
||||
cloudSync.sync()
|
||||
refreshSettingsData()
|
||||
refreshSyncState()
|
||||
}
|
||||
|
||||
@ -57,6 +75,7 @@ final class SettingsStore: CloudSyncable, ThemeProviding {
|
||||
cloudSync.update { data in
|
||||
transform(&data)
|
||||
}
|
||||
refreshSettingsData()
|
||||
refreshSyncState()
|
||||
}
|
||||
|
||||
@ -66,6 +85,49 @@ final class SettingsStore: CloudSyncable, ThemeProviding {
|
||||
syncStatus = cloudSync.syncStatus
|
||||
hasCompletedInitialSync = cloudSync.hasCompletedInitialSync
|
||||
}
|
||||
|
||||
private func refreshSettingsData() {
|
||||
isApplyingCloudUpdate = true
|
||||
hapticsEnabled = cloudSync.data.hapticsEnabled
|
||||
soundEnabled = cloudSync.data.soundEnabled
|
||||
theme = cloudSync.data.theme
|
||||
isApplyingCloudUpdate = false
|
||||
}
|
||||
|
||||
private func observeCloudChanges() {
|
||||
cloudSync.onCloudDataReceived = { [weak self] _ in
|
||||
self?.handleCloudDataChange()
|
||||
}
|
||||
|
||||
cloudChangeObserver = NotificationCenter.default.addObserver(
|
||||
forName: .persistedDataDidChange,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] notification in
|
||||
guard let self,
|
||||
let identifier = notification.userInfo?["dataIdentifier"] as? String,
|
||||
identifier == AppSettingsData.dataIdentifier else {
|
||||
return
|
||||
}
|
||||
self.handleCloudDataChange()
|
||||
}
|
||||
}
|
||||
|
||||
private func handleCloudDataChange() {
|
||||
refreshSettingsData()
|
||||
refreshSyncState()
|
||||
}
|
||||
|
||||
private func updateSetting<T: Equatable>(
|
||||
_ keyPath: WritableKeyPath<AppSettingsData, T>,
|
||||
value: T,
|
||||
oldValue: T
|
||||
) {
|
||||
guard !isApplyingCloudUpdate, value != oldValue else { return }
|
||||
update { data in
|
||||
data[keyPath: keyPath] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsStore {
|
||||
|
||||
194
Andromida/App/Views/Onboarding/NotificationStepView.swift
Normal file
194
Andromida/App/Views/Onboarding/NotificationStepView.swift
Normal file
@ -0,0 +1,194 @@
|
||||
import SwiftUI
|
||||
import Bedrock
|
||||
|
||||
/// The notification permission screen where users can enable reminders.
|
||||
/// Shown after the first check-in to maximize conversion after experiencing value.
|
||||
struct NotificationStepView: View {
|
||||
let selectedTime: OnboardingTimePreference?
|
||||
let reminderScheduler: ReminderScheduler
|
||||
let onComplete: () -> Void
|
||||
|
||||
@State private var animateIcon = false
|
||||
@State private var animateContent = false
|
||||
@State private var animateButtons = false
|
||||
@State private var isRequestingPermission = false
|
||||
|
||||
/// Personalized description based on the user's selected time preference
|
||||
private var personalizedDescription: String {
|
||||
switch selectedTime {
|
||||
case .morning:
|
||||
return String(localized: "Get a gentle nudge each morning to start your day right")
|
||||
case .evening:
|
||||
return String(localized: "Wind down with a reminder when it's time for your evening ritual")
|
||||
case .both:
|
||||
return String(localized: "We'll remind you at the perfect times for your morning and evening rituals")
|
||||
case nil:
|
||||
return String(localized: "Get a gentle reminder when it's time for your rituals")
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: Design.Spacing.xxLarge) {
|
||||
Spacer()
|
||||
|
||||
// Animated bell icon
|
||||
animatedBellView
|
||||
.frame(width: 160, height: 160)
|
||||
.opacity(animateIcon ? 1 : 0)
|
||||
.scaleEffect(animateIcon ? 1 : 0.8)
|
||||
|
||||
// Text content
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
Text(String(localized: "Stay on track"))
|
||||
.typography(.heroBold)
|
||||
.foregroundStyle(AppTextColors.primary)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text(personalizedDescription)
|
||||
.typography(.body)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
||||
}
|
||||
.opacity(animateContent ? 1 : 0)
|
||||
.offset(y: animateContent ? 0 : 20)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Buttons
|
||||
VStack(spacing: Design.Spacing.medium) {
|
||||
// Primary CTA
|
||||
Button(action: handleEnableNotifications) {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
if isRequestingPermission {
|
||||
ProgressView()
|
||||
.tint(AppTextColors.inverse)
|
||||
} else {
|
||||
Image(systemName: "bell.fill")
|
||||
Text(String(localized: "Enable Notifications"))
|
||||
}
|
||||
}
|
||||
.typography(.heading)
|
||||
.foregroundStyle(AppTextColors.inverse)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: AppMetrics.Size.buttonHeight)
|
||||
.background(AppAccent.primary)
|
||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
|
||||
}
|
||||
.disabled(isRequestingPermission)
|
||||
|
||||
// Secondary skip option
|
||||
Button(action: onComplete) {
|
||||
Text(String(localized: "Maybe Later"))
|
||||
.typography(.body)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
.disabled(isRequestingPermission)
|
||||
}
|
||||
.padding(.horizontal, Design.Spacing.xxLarge)
|
||||
.opacity(animateButtons ? 1 : 0)
|
||||
.offset(y: animateButtons ? 0 : 20)
|
||||
|
||||
Spacer()
|
||||
.frame(height: Design.Spacing.xxLarge)
|
||||
}
|
||||
.onAppear {
|
||||
startAnimations()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Animated Bell
|
||||
|
||||
private var animatedBellView: some View {
|
||||
ZStack {
|
||||
// Outer pulse circle
|
||||
Circle()
|
||||
.fill(AppAccent.primary.opacity(0.1))
|
||||
.frame(width: 160, height: 160)
|
||||
.scaleEffect(animateIcon ? 1.1 : 1.0)
|
||||
.animation(
|
||||
.easeInOut(duration: 2).repeatForever(autoreverses: true),
|
||||
value: animateIcon
|
||||
)
|
||||
|
||||
// Middle circle
|
||||
Circle()
|
||||
.fill(AppAccent.primary.opacity(0.15))
|
||||
.frame(width: 120, height: 120)
|
||||
.scaleEffect(animateIcon ? 1.05 : 1.0)
|
||||
.animation(
|
||||
.easeInOut(duration: 1.5).repeatForever(autoreverses: true).delay(0.2),
|
||||
value: animateIcon
|
||||
)
|
||||
|
||||
// Inner circle with bell
|
||||
Circle()
|
||||
.fill(AppAccent.primary.opacity(0.2))
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
// Bell icon with ring animation
|
||||
Image(systemName: "bell.fill")
|
||||
.font(.system(size: 36, weight: .medium))
|
||||
.foregroundStyle(AppAccent.primary)
|
||||
.rotationEffect(.degrees(animateIcon ? 10 : -10))
|
||||
.animation(
|
||||
.easeInOut(duration: 0.5).repeatForever(autoreverses: true),
|
||||
value: animateIcon
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
private func handleEnableNotifications() {
|
||||
isRequestingPermission = true
|
||||
|
||||
Task {
|
||||
let granted = await reminderScheduler.requestAuthorization()
|
||||
|
||||
if granted {
|
||||
// Enable reminders in the scheduler
|
||||
reminderScheduler.remindersEnabled = true
|
||||
}
|
||||
|
||||
await MainActor.run {
|
||||
isRequestingPermission = false
|
||||
onComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Animations
|
||||
|
||||
private func startAnimations() {
|
||||
// Stagger the animations
|
||||
withAnimation(.easeOut(duration: 0.6)) {
|
||||
animateIcon = true
|
||||
}
|
||||
|
||||
withAnimation(.easeOut(duration: 0.6).delay(0.3)) {
|
||||
animateContent = true
|
||||
}
|
||||
|
||||
withAnimation(.easeOut(duration: 0.6).delay(0.5)) {
|
||||
animateButtons = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ZStack {
|
||||
LinearGradient(
|
||||
colors: [AppSurface.primary, AppSurface.secondary],
|
||||
startPoint: .topLeading,
|
||||
endPoint: .bottomTrailing
|
||||
)
|
||||
.ignoresSafeArea()
|
||||
|
||||
NotificationStepView(
|
||||
selectedTime: .morning,
|
||||
reminderScheduler: ReminderScheduler(),
|
||||
onComplete: {}
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -6,6 +6,7 @@ import Bedrock
|
||||
struct SetupWizardView: View {
|
||||
@Bindable var store: RitualStore
|
||||
@Bindable var categoryStore: CategoryStore
|
||||
let reminderScheduler: ReminderScheduler
|
||||
let onComplete: () -> Void
|
||||
|
||||
@State private var currentStep: WizardStep = .welcome
|
||||
@ -20,13 +21,16 @@ struct SetupWizardView: View {
|
||||
@State private var pendingPresets: [RitualPreset] = []
|
||||
@State private var currentPresetIndex: Int = 0
|
||||
|
||||
@State private var isShowingSkipConfirmation = false
|
||||
|
||||
enum WizardStep: Int, CaseIterable {
|
||||
case welcome = 0
|
||||
case goalSelection = 1
|
||||
case timeSelection = 2
|
||||
case ritualPreview = 3
|
||||
case firstCheckIn = 4
|
||||
case whatsNext = 5
|
||||
case notifications = 5
|
||||
case whatsNext = 6
|
||||
}
|
||||
|
||||
/// Whether the user selected "Both" for time preference
|
||||
@ -56,7 +60,7 @@ struct SetupWizardView: View {
|
||||
/// Whether to show the back button
|
||||
private var canGoBack: Bool {
|
||||
switch currentStep {
|
||||
case .welcome, .firstCheckIn, .whatsNext:
|
||||
case .welcome, .firstCheckIn, .notifications, .whatsNext:
|
||||
return false
|
||||
case .goalSelection:
|
||||
return true
|
||||
@ -78,8 +82,9 @@ struct SetupWizardView: View {
|
||||
.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// Header with back button and progress (hidden on welcome and whatsNext)
|
||||
if currentStep != .welcome && currentStep != .whatsNext {
|
||||
// Header with back button and progress (hidden on welcome, notifications, and whatsNext)
|
||||
// Notifications step has its own "Maybe Later" option so skip button is redundant
|
||||
if currentStep != .welcome && currentStep != .notifications && currentStep != .whatsNext {
|
||||
headerView
|
||||
.padding(.top, Design.Spacing.medium)
|
||||
.padding(.horizontal, Design.Spacing.large)
|
||||
@ -120,10 +125,17 @@ struct SetupWizardView: View {
|
||||
store: store,
|
||||
ritual: ritual,
|
||||
hasCompletedCheckIn: $hasCompletedFirstCheckIn,
|
||||
onComplete: { advanceToWhatsNext() }
|
||||
onComplete: { advanceToNotifications() }
|
||||
)
|
||||
}
|
||||
|
||||
case .notifications:
|
||||
NotificationStepView(
|
||||
selectedTime: selectedTime,
|
||||
reminderScheduler: reminderScheduler,
|
||||
onComplete: { advanceToWhatsNext() }
|
||||
)
|
||||
|
||||
case .whatsNext:
|
||||
WhatsNextStepView(onComplete: onComplete)
|
||||
}
|
||||
@ -136,6 +148,14 @@ struct SetupWizardView: View {
|
||||
}
|
||||
}
|
||||
.animation(.easeInOut(duration: Design.Animation.standard), value: currentStep)
|
||||
.alert(String(localized: "Skip setup?"), isPresented: $isShowingSkipConfirmation) {
|
||||
Button(String(localized: "Keep going"), role: .cancel) {}
|
||||
Button(String(localized: "Skip"), role: .destructive) {
|
||||
skipOnboarding()
|
||||
}
|
||||
} message: {
|
||||
Text(String(localized: "You can complete setup later in Settings."))
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Header View
|
||||
@ -156,6 +176,11 @@ struct SetupWizardView: View {
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Button(String(localized: "Skip")) {
|
||||
isShowingSkipConfirmation = true
|
||||
}
|
||||
.font(.body)
|
||||
.foregroundStyle(AppTextColors.secondary)
|
||||
}
|
||||
|
||||
// Progress indicator
|
||||
@ -190,13 +215,15 @@ struct SetupWizardView: View {
|
||||
case .welcome:
|
||||
return 0.0
|
||||
case .goalSelection:
|
||||
return 0.25
|
||||
return 0.2
|
||||
case .timeSelection:
|
||||
return 0.5
|
||||
return 0.4
|
||||
case .ritualPreview:
|
||||
return 0.7
|
||||
return 0.55
|
||||
case .firstCheckIn:
|
||||
return 0.9
|
||||
return 0.7
|
||||
case .notifications:
|
||||
return 0.85
|
||||
case .whatsNext:
|
||||
return 1.0
|
||||
}
|
||||
@ -227,12 +254,22 @@ struct SetupWizardView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func advanceToNotifications() {
|
||||
withAnimation {
|
||||
currentStep = .notifications
|
||||
}
|
||||
}
|
||||
|
||||
private func advanceToWhatsNext() {
|
||||
withAnimation {
|
||||
currentStep = .whatsNext
|
||||
}
|
||||
}
|
||||
|
||||
private func skipOnboarding() {
|
||||
onComplete()
|
||||
}
|
||||
|
||||
// MARK: - Time Selection Handler
|
||||
|
||||
private func handleTimeSelectionContinue() {
|
||||
@ -266,7 +303,7 @@ struct SetupWizardView: View {
|
||||
hasCompletedFirstCheckIn = false
|
||||
|
||||
if presets.isEmpty {
|
||||
currentStep = .whatsNext
|
||||
currentStep = .notifications
|
||||
} else {
|
||||
currentStep = .ritualPreview
|
||||
}
|
||||
@ -294,8 +331,8 @@ struct SetupWizardView: View {
|
||||
// Go to first check-in
|
||||
currentStep = .firstCheckIn
|
||||
} else {
|
||||
// No rituals created, go to what's next
|
||||
currentStep = .whatsNext
|
||||
// No rituals created, skip to notifications
|
||||
currentStep = .notifications
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -305,6 +342,7 @@ struct SetupWizardView: View {
|
||||
SetupWizardView(
|
||||
store: RitualStore.preview,
|
||||
categoryStore: CategoryStore.preview,
|
||||
reminderScheduler: ReminderScheduler(),
|
||||
onComplete: {}
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user