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
13 changes: 10 additions & 3 deletions IngrediCheck/Components/PersistentBottomSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -585,18 +585,25 @@ struct PersistentBottomSheet: View {
case .whosThisFor:
WhosThisFor {
AnalyticsService.shared.trackOnboarding("Onboarding Flow Selected", properties: ["flow_type": "individual"])
// Guest login already happened on .heyThere screen, just proceed
await authController.signIn()
guard authController.session != nil else {
Log.error("PersistentBottomSheet", "Sign-in failed, cannot create Bite Buddy family")
return
}
do {
try await familyStore.createBiteBuddyFamily()
coordinator.showCanvas(.dietaryPreferencesAndRestrictions(isFamilyFlow: false))
coordinator.navigateInBottomSheet(.dietaryPreferencesSheet(isFamilyFlow: false))
} catch {
Log.error("PersistentBottomSheet", "Failed to create Bite Buddy family: \(error)")
// Don't navigate forward on error - user stays on current screen
}
} addFamilyPressed: {
AnalyticsService.shared.trackOnboarding("Onboarding Flow Selected", properties: ["flow_type": "family"])
// Guest login already happened on .heyThere screen, just proceed
await authController.signIn()
guard authController.session != nil else {
Log.error("PersistentBottomSheet", "Sign-in failed, cannot proceed to family flow")
return
}
coordinator.showCanvas(.letsMeetYourIngrediFam)
}

Expand Down
58 changes: 25 additions & 33 deletions IngrediCheck/Store/AuthController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,8 @@ private enum AuthFlowMode {

private let keychain = KeychainSwift()
@MainActor private var appleSignInCoordinator: AppleSignInCoordinator?

private static let anonUserNameKey = "anonEmail"
private static let anonPasswordKey = "anonPassword"
@MainActor private var signInTask: Task<Void, Never>?

private static let deviceIdKey = "deviceId"
private static var hasRegisteredDevice = false

Expand Down Expand Up @@ -293,27 +292,36 @@ private enum AuthFlowMode {

await MainActor.run {
OnboardingPersistence.shared.reset()
clearAnonymousCredentials()
Self.hasRegisteredDevice = false
}
}

@MainActor
func signIn() async {

Log.debug("AuthController", "signIn()")

guard await signInState != .signedIn else {
Log.debug("AuthController", "Already Signed In, so not Signing in again")
guard signInState != .signedIn else {
Log.debug("AuthController", "signIn(): Already signed in")
return
}

if let anonymousEmail = keychain.get(AuthController.anonUserNameKey),
let anonymousPassword = keychain.get(AuthController.anonPasswordKey),
await signInWithLegacyGuest(email: anonymousEmail, password: anonymousPassword) {
// Deduplicate: if sign-in is already in flight, join it
if let existing = signInTask {
Log.debug("AuthController", "signIn(): Joining existing sign-in task")
await existing.value
return
}

await signInWithNewAnonymousAccount()
Log.debug("AuthController", "signIn(): Starting new sign-in")

// Unstructured Task — does NOT inherit parent's cancellation,
// so SwiftUI .task(id:) cancellation won't kill the network request.
// Inherits @MainActor (no Sendable issues with self).
let task = Task {
await signInWithNewAnonymousAccount()
}

signInTask = task
await task.value
signInTask = nil
}

@MainActor
Expand Down Expand Up @@ -342,7 +350,6 @@ private enum AuthFlowMode {

let session = try await finalizeAuth(with: credentials, mode: .link)
self.session = session
clearAnonymousCredentials()
isUpgradingAccount = false
} catch {
isUpgradingAccount = false
Expand All @@ -356,24 +363,14 @@ private enum AuthFlowMode {
let webService = WebService()
try? await webService.deleteUserAccount()
await self.signOut()
clearAnonymousCredentials()
}

private func signInWithLegacyGuest(email: String, password: String) async -> Bool {
do {
_ = try await supabaseClient.auth.signIn(email: email, password: password)
return true
} catch {
Log.error("AuthController", "Anonymous signin failed for stored credentials: \(error)")
keychain.delete(AuthController.anonUserNameKey)
keychain.delete(AuthController.anonPasswordKey)
return false
}
}

private func signInWithNewAnonymousAccount() async {
do {
_ = try await supabaseClient.auth.signInAnonymously()
let session = try await supabaseClient.auth.signInAnonymously()
await MainActor.run {
self.handleSessionChange(event: .signedIn, session: session)
}
} catch {
Log.error("AuthController", "signInAnonymously failed: \(error)")
}
Expand Down Expand Up @@ -532,11 +529,6 @@ private enum AuthFlowMode {
return rootViewController
}

private func clearAnonymousCredentials() {
keychain.delete(AuthController.anonUserNameKey)
keychain.delete(AuthController.anonPasswordKey)
}

public func signInWithGoogle(completion: ((Result<Void, Error>) -> Void)? = nil) {
Task {
do {
Expand Down
16 changes: 8 additions & 8 deletions IngrediCheck/Views/HeyThereScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,19 @@ struct HeyThereScreen: View {
}
}
.task(id: coordinator.currentBottomSheetRoute) {
// Trigger anonymous sign-in only when explicitly asking "Who's this for?"
// This prevents "Get Started" or other pre-onboarding states from creating sessions.
if coordinator.currentBottomSheetRoute == .whosThisFor {
if authController.session == nil {
print("[OnboardingMeta] Auto-triggering guest login on .whosThisFor screen")
await authController.signIn()

// Sync initial state to Supabase immediately after session is created
print("[OnboardingMeta] Syncing initial state after guest login")
await OnboardingPersistence.shared.sync(from: coordinator)

// Pre-download tutorial video in background
Task { await TutorialVideoManager.shared.downloadIfNeeded() }
// signIn() creates an internal unstructured Task that survives
// cancellation, so the sign-in completes regardless. But post-work
// (sync, video download) should only run if we're still on this route.
if !Task.isCancelled {
print("[OnboardingMeta] Syncing initial state after guest login")
await OnboardingPersistence.shared.sync(from: coordinator)
Task { await TutorialVideoManager.shared.downloadIfNeeded() }
}
}
}
}
Expand Down