Repository navigation
iOS sign-in: bound every phase, cancellable OAuth waits, Cancel escape hatch - #5728
Conversation
…generic error Red half of the sign-in-hang regression pair: a sign-in flow parked on a system auth callback that never fires currently cannot be backed out of. Cancellation surfaces as the generic "Something went wrong" server error instead of AuthError.cancelled, so a cancel affordance would spuriously show an error, and there is no timeout machinery at all. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… a Cancel escape hatch An external user reported sign-in loading forever: settings-style sheet, infinite spinner, no error, no timeout, no way out. The sign-in flow had two structural holes: 1. The interactive OAuth waits in the vendored Stack SDK (ASWebAuthenticationSession and ASAuthorizationController) parked a checked continuation on a system callback that is not guaranteed to fire, and the wait was not cancellation-responsive: cancelling the awaiting task did nothing, so the continuation (and the spinner it gated) could be suspended forever. 2. No sign-in phase had a deadline. Any wedged call (OAuth callback, magic-link send/verify, credential sign-in, post-sign-in user fetch, session validation, team list, the post-sign-in hook) held isLoading=true indefinitely with every control disabled. Mechanism: - AuthTimeouts + withAuthPhaseTimeout race each phase against a deadline on an injected Clock (virtual-time testable; structured task group, the loser is cancelled and joined). Interactive flows get a generous 300s bound (the user may legitimately be doing 2FA in the sheet); plain backend calls get 60s, above the SDK's 30s per-request timeout. - A deadline ends the phase as the new localized, retryable AuthError.timedOut (en + ja, in both the macOS and iOS catalogs); it maps to preserveCachedSession during validation, never signs out. - AuthFlowCancellationGate in the vendored Stack SDK makes both interactive waits cancellation-responsive: task cancellation claims the continuation exactly once (CancellationError), tears down the system sheet, and a late system callback is dropped instead of double-resuming. - CancellationError now maps to AuthError.cancelled (silent in the UI), not the generic "Something went wrong" server error. - SignInView keeps the sign-in Task and shows a Cancel button while a flow is in flight, so a stuck sheet can always be backed out of; the post-sign-in hook is bounded too so it cannot hold the spinner after the session is published. - Timeouts and OAuth failures log a redacted line via AuthDebugLog (phase + duration only, no tokens/emails). Green half of the regression pair; the previous commit's cancellation test now passes, and new virtual-clock tests cover the timeout, win, and hung-hook paths without real waiting. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR adds auth phase timeouts, cancellation-safe sign-in handling, cancelable UI sign-in tasks, timeout and cancel localizations, vendor OAuth/Apple flow cancellation guards, and tests covering bounded deadlines, cancellation mapping, and loading-state behavior. ChangesAuth timeout and cancellation-safe sign-in
Sequence DiagramssequenceDiagram
participant AuthCoordinator
participant withAuthPhaseTimeout
participant AuthClient
participant Clock
AuthCoordinator->>withAuthPhaseTimeout: runPhase(phase, duration, clock, operation)
par operation
withAuthPhaseTimeout->>AuthClient: start auth operation
AuthClient-->>withAuthPhaseTimeout: value or error
and deadline
withAuthPhaseTimeout->>Clock: sleep until deadline
Clock-->>withAuthPhaseTimeout: deadline reached
end
alt operation finishes first
withAuthPhaseTimeout-->>AuthCoordinator: value
else deadline wins
withAuthPhaseTimeout-->>AuthCoordinator: AuthError.timedOut
end
sequenceDiagram
participant SignInView
participant StackClientApp
participant AuthFlowCancellationGate
participant AuthSession
participant TokenStore
SignInView->>StackClientApp: start OAuth or Apple sign-in task
StackClientApp->>AuthFlowCancellationGate: create gated continuation
AuthFlowCancellationGate->>AuthSession: present auth session
alt callback arrives
AuthSession-->>AuthFlowCancellationGate: callback result
AuthFlowCancellationGate-->>StackClientApp: resume once
StackClientApp->>TokenStore: publishSessionTokens
else task cancelled
SignInView->>StackClientApp: cancel task
StackClientApp->>AuthFlowCancellationGate: run teardown
AuthFlowCancellationGate->>AuthSession: dismiss or cancel UI
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 20 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (20 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3d779d1292
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| try Task.checkCancellation() | ||
| if let tokenStoreOverride { | ||
| await client.setTokens(accessToken: accessToken, refreshToken: refreshToken, tokenStoreOverride: tokenStoreOverride) | ||
| } else { | ||
| await client.setTokens(accessToken: accessToken, refreshToken: refreshToken) |
There was a problem hiding this comment.
Close the cancellation window before writing tokens
When a user cancels or the phase timeout fires just after Task.checkCancellation() passes, the subsequent async hop into client.setTokens still runs and the token stores do not check cancellation before writing. In that timing window, the UI reports a cancelled/timed-out sign-in but live OAuth tokens can still be persisted, so the next restore can silently sign the user in behind the cancelled flow; the check needs to be atomic with the write or followed by cancellation-aware rollback that cannot clear a newer flow's tokens.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/SignInView.swift`:
- Line 261: Add the missing localization key referenced by SignInView.swift:
insert "mobile.signIn.cancel" into Resources/Localizable.xcstrings with English
and Japanese translations (matching the existing key in
ios/cmux/Resources/Localizable.xcstrings) so both locale catalogs are complete;
update the en and ja strings entries in Resources/Localizable.xcstrings to
include the appropriate translations for "Cancel" and the Japanese equivalent.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: c23e1566-56d6-4ec7-b44f-e1f2edb49257
📒 Files selected for processing (17)
Packages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthCoordinator.swiftPackages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthError+DisplaySafe.swiftPackages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthError.swiftPackages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthPhase.swiftPackages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthPhaseTimeout.swiftPackages/CmuxAuthRuntime/Sources/CmuxAuthRuntime/Coordinator/AuthTimeouts.swiftPackages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/AuthCoordinatorCancellationTests.swiftPackages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/AuthCoordinatorTimeoutTests.swiftPackages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/AuthErrorDisplaySafeTests.swiftPackages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/HangingMagicLinkAuthClient.swiftPackages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/HangingOAuthAuthClient.swiftPackages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/ManualTestClock.swiftPackages/CmuxAuthRuntime/Tests/CmuxAuthRuntimeTests/TestPhaseSignal.swiftPackages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/SignInView.swiftResources/Localizable.xcstringsios/cmux/Resources/Localizable.xcstringsvendor/stack-auth-swift-sdk-prerelease/Sources/StackAuth/StackClientApp.swift
| Button { | ||
| signInTask?.cancel() | ||
| } label: { | ||
| Text(L10n.string("mobile.signIn.cancel", defaultValue: "Cancel")) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify mobile.signIn.cancel key exists with translations in both xcstrings files
echo "=== Checking Resources/Localizable.xcstrings ==="
fd -t f 'Localizable.xcstrings' Resources -x sh -c 'echo "File: {}"; cat "{}" | python3 -c "
import sys, json
data = json.load(sys.stdin)
key = \"mobile.signIn.cancel\"
strings = data.get(\"strings\", {})
if key not in strings:
print(f\" ERROR: Key {key} not found\")
sys.exit(1)
entry = strings[key]
localizations = entry.get(\"localizations\", {})
print(f\" Key found with localizations: {list(localizations.keys())}\")
for loc, val in localizations.items():
sv = val.get(\"stringUnit\", {}).get(\"value\", \"(empty)\")
print(f\" {loc}: {sv[:50]}...\")
"'
echo ""
echo "=== Checking ios/cmux/Resources/Localizable.xcstrings ==="
fd -t f 'Localizable.xcstrings' ios/cmux/Resources -x sh -c 'echo "File: {}"; cat "{}" | python3 -c "
import sys, json
data = json.load(sys.stdin)
key = \"mobile.signIn.cancel\"
strings = data.get(\"strings\", {})
if key not in strings:
print(f\" ERROR: Key {key} not found\")
sys.exit(1)
entry = strings[key]
localizations = entry.get(\"localizations\", {})
print(f\" Key found with localizations: {list(localizations.keys())}\")
for loc, val in localizations.items():
sv = val.get(\"stringUnit\", {}).get(\"value\", \"(empty)\")
print(f\" {loc}: {sv[:50]}...\")
"'Repository: manaflow-ai/cmux
Length of output: 499
Add mobile.signIn.cancel to Resources/Localizable.xcstrings.
SignInView.swift references mobile.signIn.cancel; the key exists in ios/cmux/Resources/Localizable.xcstrings but is missing from Resources/Localizable.xcstrings. Add the entry (with en/ja translations) to keep all supported locale catalogs complete.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@Packages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/SignInView.swift` at
line 261, Add the missing localization key referenced by SignInView.swift:
insert "mobile.signIn.cancel" into Resources/Localizable.xcstrings with English
and Japanese translations (matching the existing key in
ios/cmux/Resources/Localizable.xcstrings) so both locale catalogs are complete;
update the en and ja strings entries in Resources/Localizable.xcstrings to
include the appropriate translations for "Cancel" and the Japanese equivalent.
Source: Coding guidelines
Greptile SummaryThis PR fixes iOS sign-in hanging forever by bounding every loading phase with a deadline and making the interactive OAuth waits (
Confidence Score: 4/5Safe to merge after adding the missing locale translations to the macOS string catalog — everything else is well-structured and covered by tests. The macOS Resources/Localizable.xcstrings was touched to add auth.error.timed_out, but only en and ja were provided. The catalog already carries 20 locales; the 18 missing entries mean macOS users in Arabic, German, French, Chinese, and 14 other locales will silently fall back to the English default string whenever a sign-in phase times out. Everything else — the structured task-group timeout, cancellation gate, token-write guard, and SwiftUI cancel affordance — is correct and well-tested. Resources/Localizable.xcstrings — new auth.error.timed_out key needs translations for all 18 non-en/ja locales already present in the macOS catalog. Important Files Changed
Sequence DiagramsequenceDiagram
participant SV as SignInView
participant AC as AuthCoordinator
participant PT as withAuthPhaseTimeout
participant SC as StackClientApp
participant Gate as AuthFlowCancellationGate
participant Sys as System UI (ASAuth*)
SV->>AC: signInWithApple()
AC->>PT: runPhase(.oauth, 300s)
PT->>SC: signInWithOAuth()
SC->>Gate: activate(continuation, onCancel: teardown)
SC->>Sys: start sheet
Note over Sys: User may interact
alt OAuth completes
Sys-->>Gate: resume(returning: callbackURL)
Gate-->>SC: callbackURL
SC->>SC: callOAuthCallback()
SC-->>PT: success
PT-->>AC: returns
AC->>AC: applySignedInUser()
AC->>SV: "isAuthenticated = true"
else Deadline fires
PT->>PT: deadline child wakes
PT-->>AC: throws AuthError.timedOut
AC-->>SV: "isLoading = false, show timedOut error"
else User taps Cancel
SV->>SV: signInTask.cancel()
Gate->>Gate: cancel() CancellationError
Gate->>Sys: teardown: session.cancel()
SC-->>PT: CancellationError
AC->>AC: AuthError(displaySafe) to .cancelled
AC-->>SV: .cancelled swallowed silently
end
Reviews (3): Last reviewed commit: "Merge branch 'main' into feat-ios-signin..." | Re-trigger Greptile |
| if gate.isFinished { | ||
| // Cancelled between the pre-start check and start(): the | ||
| // teardown already ran against an empty holder, so the | ||
| // just-presented sheet is dismissed here instead. This | ||
| // re-check runs synchronously on the main actor, so it | ||
| // cannot interleave with the teardown's main-actor task. | ||
| sessionHolder.session?.cancel() | ||
| sessionHolder.session = nil | ||
| } |
There was a problem hiding this comment.
The comment claims this re-check "runs synchronously on the main actor," but
StackClientApp is a plain actor — not @MainActor. The post-start check therefore runs on the StackClientApp actor's executor, while the teardown closure runs a Task { @MainActor in … }. These are two different executors, so they can interleave: the teardown task may run between the session.start() call and this guard, or concurrently with it. In practice the NSLock gate ensures the continuation is resumed exactly once, and double-cancelling ASWebAuthenticationSession is safe (idempotent), so there is no functional bug — but the comment describes a guarantee the code does not actually provide. A future reader relying on this comment to decide whether additional synchronization is needed would be misled.
| if gate.isFinished { | |
| // Cancelled between the pre-start check and start(): the | |
| // teardown already ran against an empty holder, so the | |
| // just-presented sheet is dismissed here instead. This | |
| // re-check runs synchronously on the main actor, so it | |
| // cannot interleave with the teardown's main-actor task. | |
| sessionHolder.session?.cancel() | |
| sessionHolder.session = nil | |
| } | |
| if gate.isFinished { | |
| // Cancelled between the pre-start check and start(): the | |
| // teardown already ran against an empty holder, so the | |
| // just-presented sheet is dismissed here instead. | |
| // Note: this runs on the StackClientApp actor, not the | |
| // main actor. The teardown Task { @MainActor in … } may | |
| // run concurrently, but double-cancelling the session is | |
| // safe (idempotent) and the gate ensures the continuation | |
| // is resumed exactly once. | |
| sessionHolder.session?.cancel() | |
| sessionHolder.session = nil | |
| } |
| try await client.currentUser(throwOnMissing: true) | ||
| } |
There was a problem hiding this comment.
Raw error logged at
.public privacy via AuthDebugLog
log.log("auth.oauth provider=\(provider) failed: \(error)") routes through AuthDebugLog, which passes the redacted string to logger.log(…, "\(redactedMessage, privacy: .public)"). The redaction patterns strip tokens, JWTs, and email addresses, but not the message field of StackAuthError or OAuthError — those can include server-controlled strings or the verbose Apple SIWA diagnostic messages hardcoded in AppleSignInDelegate (up to ~300 characters of Xcode/developer.apple.com instructions). Using .public means this content is visible to any log collector without the usual privacy: .private scrubbing. The existing error logs in validateCachedSession and tryAutoLogin use authLog.error(…, privacy: .private) for the same reason. Either route this through authLog with privacy: .private, or extend AuthDebugLog.redacted to strip the message field from Stack SDK error representations before the .public sink sees them.
| // Cancel the loser on every exit path; the group then joins it. | ||
| defer { group.cancelAll() } | ||
| do { | ||
| guard let first = try await group.next() else { | ||
| throw AuthError.timedOut | ||
| } |
There was a problem hiding this comment.
nil branch of group.next() is unreachable
With exactly two child tasks added (the operation and the deadline), the first call to group.next() can never return nil — nil signals an empty group, but both children are still pending at that point. The else branch mapping to AuthError.timedOut is dead code. While it doesn't affect correctness (the nil case would map to the same error as a real timeout), it could mislead a future reader into thinking group.next() has some documented empty-return path here.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| return | ||
| } | ||
| if error is CancellationError || Task.isCancelled { | ||
| // A cancelled flow (the user backed out, or the sheet's task was | ||
| // torn down) is not a failure to render. Mapping it to the generic | ||
| // server error would flash "Something went wrong" after every | ||
| // deliberate cancel. The `Task.isCancelled` arm matters because | ||
| // URLSession-backed phases surface task cancellation as | ||
| // `URLError(.cancelled)` (or a Stack transport error wrapping it), | ||
| // not as `CancellationError`: this mapping always runs in the | ||
| // cancelled flow's own catch, so any error caught on a cancelled | ||
| // task is the cancellation, not an independent failure. | ||
| self = .cancelled |
There was a problem hiding this comment.
Task.isCancelled check swallows all errors on a cancelled task, including real auth failures
The mapping if error is CancellationError || Task.isCancelled { self = .cancelled } is intentional (the PR description explains the URLSession surface), and the two new tests (anyErrorCaughtOnACancelledTaskMapsToCancelled, urlCancellationOnALiveTaskStaysNetworkError) lock the contract well. One narrow edge case worth noting in the comment: if a real auth error (e.g., serverError(401, "auth_failed")) is thrown at the exact moment the task is externally cancelled — cancelled wins and the error is silently dropped as .cancelled. In practice the window is tiny and the user chose to cancel, so the silent outcome is acceptable UX, but this could become surprising if AuthCoordinator is ever called from a context where task cancellation originates from something other than a user action (e.g., app-lifecycle teardown). A brief note in the comment would help the next reader not widen this mapping without understanding the trade-off.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Why
An external user reported sign-in loading forever on iOS: settings-style sheet, infinite spinner, no error, no timeout, no way to escape. Two structural holes made that possible:
ASWebAuthenticationSessionandASAuthorizationController) park a checked continuation on a system callback that is not guaranteed to fire. The wait was not cancellation-responsive, so nothing could unwind it.isLoading = trueindefinitely with every control disabled.Hang-point inventory
Every phase that holds the sign-in loading state, now bounded:
oauthclient.signInWithOAuth(SIWA native / web OAuth sheet)send_codesendMagicLinkEmailverify_codesignInWithMagicLinkpassword_sign_insignInWithCredentialfetch_usercurrentUserfetchvalidate_sessioncurrentUservalidationlist_teamslistTeamsafter sign-inpost_sign_inonSignedInhook (push token re-upload), runs whileisLoadingis still trueThe interactive bound is generous on purpose (the user may be doing 2FA inside the sheet); it exists to terminate the callback-never-fires hang, and the new Cancel button covers bailing out earlier.
Mechanism
AuthTimeouts+withAuthPhaseTimeout: each phase races a deadline on an injectedClock(defaultsContinuousClock; virtualManualTestClockin tests). Structured task group; the loser is cancelled and joined, so nothing outlives the call. A deadline ends the phase as the new localized, retryableAuthError.timedOut(en + ja in both the macOS and iOS catalogs)..timedOutmaps topreserveCachedSessionduring validation, never signs out.AuthFlowCancellationGate(vendored Stack SDK): makes both interactive waits cancellation-responsive. Cancellation claims the continuation exactly once (CancellationError), tears down the system sheet, and a late system callback is dropped instead of double-resuming. A post-present re-check (synchronous on the main actor) closes the race where cancellation lands between the pre-start check and presenting the sheet.Task; the continuation resumes with the callback URL and the exchange runs in the caller's task, so cancel/timeout cancels it too. Every sign-in token write goes through onepublishSessionTokenschokepoint that refuses to persist a session on a cancelled task, so a cancelled or timed-out flow can never silently sign the user in behind the UI's back.CancellationError(and any error caught on a cancelled task, since URLSession surfaces cancellation asURLError(.cancelled)or a wrapped Stack transport error) maps toAuthError.cancelled, whichSignInViewswallows (analyticsios_sign_in_cancelledonly) instead of flashing "Something went wrong".SignInViewkeeps the in-flight sign-inTaskand shows a localized Cancel button whenever a flow is in flight, on both the provider screen and the code-entry screen.AuthDebugLog(phase + duration / provider + error shape; tokens, JWTs, and emails are redacted before any sink).Relationship to #5600
Same swallowed-failure class, complementary scopes: that PR makes OAuth failures observable (
AuthErrorReportingseam, Sentry capture, PostHogfailure_reasonenrichment) without changing thrown types; this PR makes non-failures (hangs) terminate and cancellable. No duplication: this branch adds no error-reporting seam, that PR adds no deadlines. Both touchAuthCoordinator/SignInView, so whichever lands second takes a small rebase; the catch-site shape (AuthError(displaySafe:) ?? error) is preserved by both.Most plausible root cause for the reporter
A Sign in with Apple flow whose
ASAuthorizationControllerdelegate callback never fired. Evidence: the wait parks aCheckedContinuationresumed only by the delegate (StackClientApp.signInWithAppleNative); nothing else can resume it, there was no deadline anywhere in the coordinator, andsignInWithOAuthholdsisLoading = trueacross the whole await, which disables every button inSignInView(isAuthInProgress), exactly matching "infinite spinner, no error, no escape". The PR #5600 investigation already established SIWA is structurally broken on per-tag dev bundles (INVALID_APPLE_CREDENTIALScomes back as an error, which renders), but a stuck Apple ID system sheet (unverified iCloud session, parental approval, network loss mid-sheet) returns nothing at all, which is the only state that produces this exact symptom. The new phase-tagged timeout logs plus the 5600 capture will name the exact phase on the next report.Tests
Two-commit red/green:
.serverError(0, "auth_failed")(the generic "Something went wrong") instead of.cancelled, and no timeout machinery exists. Verified failing on the parent commit.CmuxAuthRuntime, including new virtual-clock tests: stuck OAuth callback times out as.timedOutwith loading stopped; wedgedsendCodetimes out; all phases win their deadlines with the clock frozen at zero (proves deadline-child cleanup); hung post-sign-in hook is bounded and tolerated; cancelled-task error shapes map to.cancelledwhile live-taskURLError(.cancelled)stays.networkError.Verification
swift test --package-path Packages/CmuxAuthRuntime: 60/60 pass./tmp/cmux-signin-hangDerivedData, build only): BUILD SUCCEEDED.ManualTestClock's nestedInstant/Sleeperare intrinsic to aClockconformance, andAuthFlowCancellationGateusesNSLockbecausewithTaskCancellationHandler'sonCancelis synchronous and may fire on any thread, so the claim cannot await actor isolation (documented in the source).🤖 Generated with Claude Code
Note
High Risk
Touches authentication flows, vendored Stack SDK OAuth/token persistence, and session validation behavior; incorrect timeout or cancellation handling could sign users out incorrectly or leave stale UI state.
Overview
Fixes infinite iOS sign-in spinners by bounding every loading phase and making OAuth waits cancellable end-to-end.
AuthCoordinatornow injectsAuthTimeouts(300s interactive OAuth, 60s network) and an optionalClock, and wraps OAuth, magic-link, password, user fetch, session validation, team list, and the post-sign-in hook inwithAuthPhaseTimeout, surfacingAuthError.timedOut(localized en/ja) instead of hanging forever. Session validation treats timeouts as transient (preserve cached session).AuthError(displaySafe:)mapsCancellationError/ cancelled-task errors to.cancelledso deliberate bail-out does not flash a generic server error.SignInViewtracks the in-flightTask, adds a Cancel button during auth, and swallows.cancelledfor analytics; timeouts get afailure_reasonof"timeout".The vendored Stack SDK adds
AuthFlowCancellationGatesoASWebAuthenticationSessionand Sign in with Apple respond to task cancellation, dismiss system UI, and avoid double-resuming; web OAuth runs token exchange in the caller task, andpublishSessionTokensrefuses writes after cancellation.New virtual-clock tests cover stuck OAuth, wedged send-code, post-sign-in hook bounds, and cancellation mapping.
Reviewed by Cursor Bugbot for commit 2f8bbf5. Bugbot is set up for automated code reviews on this repo. Configure here.
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Summary by cubic
Fixes iOS sign-in hangs by adding per‑phase timeouts and making OAuth waits cancellable. Adds a Cancel button so users can exit stuck flows; timeouts return a localized, retryable error and never sign users out.
New Features
AuthTimeouts/withAuthPhaseTimeout(300s interactive OAuth; 60s network).AuthError.timedOutwith en/ja strings; session validation treats it as transient.SignInView; cancellation maps to.cancelledand stays silent in UI.Bug Fixes
StackAuth(ASWebAuthenticationSession,ASAuthorizationController) are cancellation‑responsive and dismiss on cancel; token exchange runs in the caller task, andpublishSessionTokensrefuses writes on a cancelled task to prevent silent sign‑in.AuthCoordinatorbounds oauth, code send/verify, password, user fetch, session validation, team list, and the post‑sign‑in hook..cancelled; live‑taskURLError(.cancelled)stays.networkError.Written for commit 2f8bbf5. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes
Localization