Skip to content

Sean/magic links#393

Draft
seanperez29 wants to merge 27 commits into
mainfrom
sean/Magic-links
Draft

Sean/magic links#393
seanperez29 wants to merge 27 commits into
mainfrom
sean/Magic-links

Conversation

@seanperez29
Copy link
Copy Markdown
Collaborator

@seanperez29 seanperez29 commented Apr 3, 2026

Summary by CodeRabbit

  • New Features

    • Native email‑link sign‑in & sign‑up with PKCE-backed send/resend, “Check your email” UI, “Open email app”, resumed continuation of recovered flows, and smarter factor selection favoring email‑link when appropriate
    • App-level deep‑link handling to complete email‑link flows and deduplicate concurrent callbacks
  • Localization

    • Added localized strings for email‑link prompts and actions
  • Chore

    • Registered app URL callback scheme for native email‑link flows
  • Tests

    • Extensive tests for parsing, completion, continuations, deduplication, and selection logic

Implement email_link as a first-factor authentication strategy:
- Add NativeMagicLink module for PKCE generation and flow storage
- Add signInWithEmailLink and completeMagicLink methods to Auth
- Add sendEmailLink method to SignIn for preparing the factor
- Add emailLink case to FactorStrategy enum
- Add SignInFactorOneEmailLinkView for the UI flow
- Add Clerk.handle(_:) for routing incoming URLs
- Update factor selection logic to prefer email link for email identifiers

# Conflicts:
#	Sources/ClerkKitUI/Components/Auth/AuthView.swift
#	Tests/Domains/Auth/AuthTests.swift
Extend magic link handling to support sign-up flows alongside sign-in.
Rename NativeMagicLink types to MagicLink for broader applicability.

- Add SignUp.sendEmailLink() with PKCE code challenge generation
- Unify EmailLinkVerificationView for both sign-in and sign-up flows
- Add URLHandlingCoordinator to deduplicate concurrent callbacks
- Update AuthNavigation to defer email link verification until other
  requirements are satisfied
- Return TransferFlowResult from completeMagicLink for flexibility
- Add tests for URL deduplication and sign-up email link verification
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 3, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fe732913-d76c-4e4a-89f7-885602fbf43b

📥 Commits

Reviewing files that changed from the base of the PR and between bd08699 and 117f5be.

📒 Files selected for processing (2)
  • Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift
  • Sources/ClerkKitUI/Extensions/SignUp+Ext.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/ClerkKitUI/Extensions/SignUp+Ext.swift

Walkthrough

Adds native email magic-link: PKCE generation and keychain persistence, deep-link parsing and deduplicated URL handling, prepare/complete API flows with new Auth entrypoints, UI screens and localization, DI for a MagicLinkStore, example app URL scheme, and extensive unit/UI tests.

Changes

Cohort / File(s) Summary
Magic link domain & storage
Sources/ClerkKit/Domains/Auth/MagicLink.swift, Sources/ClerkKit/Storage/Keychain/ClerkKeychainKey.swift
Adds PKCE utilities, MagicLinkCallback, request/response and PendingMagicLinkFlow models, MagicLinkStore (keychain-backed TTL), and new key pendingMagicLinkFlow.
Core routing & coordination
Sources/ClerkKit/Core/ClerkURLRoute.swift, Sources/ClerkKit/Core/URLHandlingCoordinator.swift
Adds ClerkURLRoute URL-to-route parsing/normalization and a URLHandlingCoordinator that de-duplicates and manages in-flight route handling and cancellation.
Auth surface & Clerk entrypoints
Sources/ClerkKit/Core/Auth.swift, Sources/ClerkKit/Core/Clerk.swift
Auth stores apiClient, magicLinkStore, urlHandlingCoordinator; adds signInWithEmailLink, handleMagicLinkCallback, completeMagicLink and internal route dispatch; Clerk adds handle(_:), callback continuation state, and coordinator cleanup integration.
SignIn / SignUp prepare & send flows
Sources/ClerkKit/Domains/Auth/FactorStrategy.swift, .../SignIn/SignIn+ServiceParams.swift, .../SignIn/SignIn.swift, .../SignUp/SignUp+ServiceParams.swift, .../SignUp/SignUp.swift
Introduces .emailLink strategy; extends prepare params with redirect/PKCE fields; implements sendEmailLink on SignIn and SignUp to generate PKCE, persist verifier, and call prepare endpoints.
UI, views & localization
Sources/ClerkKitUI/Components/Auth/..., Sources/ClerkKitUI/Resources/Localizable.xcstrings, Examples/Quickstart/Quickstart/Info.plist, Examples/Quickstart/Quickstart/ContentView.swift
Adds EmailLinkVerificationView, new navigation destination .signUpEmailLink, onOpenURL wiring to clerk.handle(_:), localized email-link strings, example app URL scheme entry, and example app event handling.
Factor selection, sorting & UX
Sources/ClerkKitUI/Extensions/SignIn+Ext.swift, Sources/ClerkKitUI/Extensions/SignUp+Ext.swift, Sources/ClerkKitUI/Extensions/Factor+Sorting.swift, SignIn views
Refactors starting-factor selection to prefer prepared → email-link → fallback; adds SignUp emailVerificationStrategy; inserts .emailLink into ordering and updates views (icons, action text) to surface email-link flows.
Dependencies & DI containers
Sources/ClerkKit/Dependencies/Dependencies.swift, Sources/ClerkKit/Dependencies/DependencyContainer.swift, Sources/ClerkKit/Mocks/MockDependencyContainer.swift
Adds magicLinkStore to Dependencies protocol; real and mock containers expose and initialize MagicLinkStore from the keychain.
Utilities & URL helpers
Sources/ClerkKit/Utils/URL+Ext.swift, Sources/ClerkKit/Core/ClerkURLRoute.swift
Adds URL.queryParam(named:) (supports query or fragment) and route-matching/normalization helpers used to validate redirect origins and required params.
Session/status/events helpers
Sources/ClerkKit/Domains/Auth/Session/Session+Tasks.swift, .../SignIn+Status.swift, .../SignUp+Status.swift, .../TransferFlowResult.swift, Sources/ClerkKit/Events/AuthEvent.swift
Adds Session.pendingTasks, needsContinuation on SignIn/SignUp statuses, TransferFlowResult accessors, and new auth events (signInNeedsContinuation, signUpNeedsContinuation).
Mocks & tests (extensive)
Sources/ClerkKit/Mocks/MockModels.swift, Tests/... (many files), Tests/UI/TestHelpers.swift
Updates mocks to include .mockEmailLink, seeds keychain expectations, modifies test helpers, and adds broad tests for PKCE persistence, callback parsing, completion endpoint behavior, deduplication, continuation events, and factor-selection.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as "App / AuthView"
    participant Clerk as "Clerk"
    participant URLCoord as "URLHandlingCoordinator"
    participant Auth as "Auth"
    participant Store as "MagicLinkStore"
    participant API as "Clerk API"
    participant Session as "Session Service"

    User->>App: Request email magic link
    App->>Clerk: signInWithEmailLink(email)
    Clerk->>Auth: signInWithEmailLink
    Auth->>API: POST prepareFirstFactor (strategy=email_link, code_challenge, redirect_uri)
    API-->>Auth: SignIn prepared
    Auth->>Store: save(kind:.signIn, codeVerifier)

    Note over User,App: User taps deep link
    App->>Clerk: handle(url)
    Clerk->>URLCoord: handle(route, operation)
    URLCoord->>Auth: completeMagicLink(flowId, approvalToken)
    Auth->>Store: load() -> codeVerifier
    Auth->>API: POST /v1/client/magic_links/complete (flow_id, approval_token, code_verifier)
    API-->>Auth: { ticket }
    Auth->>Session: signInWithTicket(ticket) / activate
    Session-->>Auth: session active
    Auth->>Store: clear()
    URLCoord-->>Clerk: return success
    Clerk-->>App: return true
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐇 I hid a verifier in a tiny chest,
Sent a link that fluttered to the nest,
A route was found, the coordinator knew,
One ticket rose while the other waited through,
The rabbit twitched—magic link now true.

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Sean/magic links' is a branch name format rather than a descriptive summary of the main changes. It does not clearly communicate the primary functionality (native magic link authentication flow implementation). Consider using a more descriptive title like 'Add native magic link authentication support' or 'Implement email link sign-in/sign-up flows' to better summarize the main changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sean/Magic-links

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

🧹 Nitpick comments (6)
Sources/ClerkKitUI/Extensions/SignUp+Ext.swift (1)

26-27: Use enum raw value instead of hard-coded "email_link" literal.

This avoids silent drift between domain constants and fallback matching logic.

♻️ Suggested refactor
-    if let verifications = Clerk.shared.environment?.userSettings.attributes["email_address"]?.verifications,
-       verifications.contains("email_link")
+    if let verifications = Clerk.shared.environment?.userSettings.attributes["email_address"]?.verifications,
+       verifications.contains(FactorStrategy.emailLink.rawValue)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Extensions/SignUp`+Ext.swift around lines 26 - 27, The
code is matching the verification type using the hard-coded string "email_link";
replace this literal with the enum raw value to avoid drift—use the existing
verification enum (e.g., VerificationMethod.emailLink.rawValue) when checking
Clerk.shared.environment?.userSettings.attributes["email_address"]?.verifications
so the contains check becomes contains(VerificationMethod.emailLink.rawValue)
(or, if verifications are strongly typed, compare to
VerificationMethod.emailLink directly).
Tests/Domains/Auth/SignIn/SignInTests.swift (1)

69-73: Consider reusing a helper for dependency+configuration setup in this test.

This block bypasses the file’s existing setup helper path; centralizing setup will make the redirect/option assumptions less brittle over time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/Domains/Auth/SignIn/SignInTests.swift` around lines 69 - 73, Replace
the direct assignment to Clerk.shared.dependencies (the MockDependencyContainer
construction using createMockAPIClient(), keychain, and signInService: service)
with the file’s existing dependency/configuration setup helper used by other
tests (call the helper that centralizes dependency wiring and pass the same
createMockAPIClient(), keychain, and service values), so tests reuse the common
setup path instead of instantiating MockDependencyContainer inline.
Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift (1)

231-241: Consider edge case: both URL schemes unavailable on simulator.

The openEmailApp() function tries message:// and mailto: but on iOS Simulator neither may be available, which would always show an error. This is acceptable for production but may cause confusion during development/testing.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift`
around lines 231 - 241, openEmailApp currently sets an error if neither
"message://" nor "mailto:" can be opened (common on Simulator); update
openEmailApp to handle this edge case by providing a graceful fallback: if both
schemes fail, copy a mailto link or support email address to the clipboard via
UIPasteboard.general and present a user-facing fallback (e.g., an alert or
in-view message) with a "Paste/Copy" action so developers/testers can manually
open mail in the Simulator; keep the existing ClerkClientError assignment for
production but invoke the fallback before setting error to improve developer UX.
Sources/ClerkKitUI/Extensions/SignIn+Ext.swift (1)

42-47: Consider preserving optionality semantics.

Defaulting to [] when supportedFirstFactors is nil conflates "no factors available" with "factors not yet loaded." While pragmatic for UI rendering (no options to show in either case), this loses semantic distinction that could be useful for showing loading states vs empty states.

If the distinction matters in calling code, consider keeping this as [Factor]?:

-  var availableFirstFactors: [Factor] {
-    supportedFirstFactors?.filter { factor in
+  var availableFirstFactors: [Factor]? {
+    supportedFirstFactors?.filter { factor in
       if case .unknown = factor.strategy { return false }
       return true
-    } ?? []
+    }
   }

However, if all consumers already handle nil appropriately upstream, the current approach is acceptable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Extensions/SignIn`+Ext.swift around lines 42 - 47, The
computed property availableFirstFactors currently returns a non-optional
[Factor] by defaulting nil supportedFirstFactors to [], which loses the "not yet
loaded" vs "empty" distinction; change availableFirstFactors's signature to
return [Factor]? and remove the "?? []" so it simply maps
supportedFirstFactors?.filter { if case .unknown = $0.strategy { return false }
; return true }, and update any callers of availableFirstFactors to handle the
optional (e.g., show loading state when nil and empty-state when []).
Sources/ClerkKit/Domains/Auth/MagicLink.swift (1)

87-127: Synchronous keychain I/O on main actor may block UI.

MagicLinkStore is marked @MainActor but KeychainStorage operations (via SystemKeychain) perform synchronous calls to Security framework APIs (SecItemAdd, SecItemCopyMatching, etc.). These can block the main thread, potentially causing UI hitches—especially during first unlock or under disk pressure.

Consider one of these approaches:

  1. Move keychain operations off the main actor using Task.detached or a dedicated actor
  2. Accept this trade-off if the operations are consistently fast in practice

Also, save() throws but load() silently swallows errors with try?. Consider logging decode failures for debugging.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKit/Domains/Auth/MagicLink.swift` around lines 87 - 127,
MagicLinkStore is annotated `@MainActor` but performs synchronous keychain I/O via
the keychain property (any KeychainStorage), which can block the UI; move those
operations off the main actor by performing keychain accesses from a background
context (e.g., wrap calls in Task.detached or implement a dedicated
KeychainActor) and update save(), load(), and clear() to call into that
background execution so the main thread isn’t blocked. Also change load() to
surface or log decoding/keychain errors instead of silently swallowing them with
try? — capture the thrown error from keychain.data(...) and
JSONDecoder.clerkDecoder.decode(...) and log or rethrow it for diagnostics while
still handling expiry logic for PendingMagicLinkFlow.
Tests/Domains/Auth/AuthTests.swift (1)

283-343: Thorough callback completion test with minor inconsistency.

The test correctly validates the magic link callback flow including HTTP request verification, session activation, and keychain cleanup.

One minor inconsistency: Line 342 uses Clerk.shared.dependencies.keychain while the rest of the test uses the local keychain variable. They should be the same reference after configureDependencies, but using the local variable would be more consistent:

-    `#expect`(try (Clerk.shared.dependencies.keychain.hasItem(forKey: ClerkKeychainKey.pendingMagicLinkFlow.rawValue)) == false)
+    `#expect`(try keychain.hasItem(forKey: ClerkKeychainKey.pendingMagicLinkFlow.rawValue) == false)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/Domains/Auth/AuthTests.swift` around lines 283 - 343, In
handleMagicLinkCallbackCompletesPendingFlowAndActivatesSession, the final
assertion references Clerk.shared.dependencies.keychain but the test uses the
local keychain variable; replace Clerk.shared.dependencies.keychain with the
local keychain and call keychain.hasItem(forKey:
ClerkKeychainKey.pendingMagicLinkFlow.rawValue) to assert the item was removed,
keeping the rest of the check the same.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ClerkKit/Core/Auth.swift`:
- Around line 324-386: The docstring for completeMagicLink claims it may
complete as sign-up or sign-in but the implementation always calls
signInWithTicket and returns .signIn(signIn); either implement sign-up routing
or update docs: if sign-up should be supported, add logic after receiving
MagicLinkCompleteResponse (or inspect completionResponse.type/fields) to call
the appropriate sign-up flow and return .signUp(...) (or a
TransferFlowResult.signUp case), otherwise change the comment text to state it
only completes sign-in and remove references to sign-up so the docstring matches
the behavior of completeMagicLink, signInWithTicket, and the returned
TransferFlowResult.

In `@Sources/ClerkKit/Domains/Auth/FactorStrategy.swift`:
- Line 16: Add explicit handling for the new FactorStrategy.emailLink case
wherever the code switches on the factor strategy: update the switch in
SignInClientTrustView (the switch that currently falls through to GetHelpView)
to include a .emailLink branch that routes to the proper factor view for
magic-link UX; and update the computed properties actionText and iconName in
SignInFactorOneForgotPasswordView to include a .emailLink case returning the
appropriate label and icon (instead of nil). Ensure you reference the
FactorStrategy.emailLink enum case in each switch so the magic-link flow renders
correct text and icon.

In `@Sources/ClerkKit/Domains/Auth/MagicLink.swift`:
- Around line 45-48: canHandle(_:) currently returns true when either
queryParam(named: "flow_id", in: url) or queryParam(named: "approval_token", in:
url) exists, but init(url:) requires both values and will fail if one is
missing; change canHandle to require both params (use AND logic) so it only
returns true when queryParam(named: "flow_id", in: url) != nil &&
queryParam(named: "approval_token", in: url) != nil, ensuring callers routed by
canHandle won't hit init(url:) errors and update any tests or callers that
relied on the looser behavior of canHandle(_:).

In `@Sources/ClerkKit/Domains/Auth/SignIn/SignIn.swift`:
- Around line 135-148: The verifier is saved after the network call, causing a
race if the magic link is used quickly; move the
MagicLinkStore.save(codeVerifier:) call to occur immediately after obtaining
pkcePair (from MagicLinkPKCE.generatePair()) and before awaiting
signInService.prepareFirstFactor(...); ensure you still return the signIn result
and keep error behavior unchanged if prepareFirstFactor fails (orphaned verifier
TTL is acceptable).

In `@Sources/ClerkKit/Domains/Auth/SignUp/SignUp.swift`:
- Around line 171-184: The PKCE verifier is saved after calling
signUpService.prepareVerification, which can leave the server-side flow created
without a local verifier if keychain/save fails; move the call to
MagicLinkStore.save(codeVerifier: pkcePair.verifier) to occur immediately after
creating pkcePair (i.e., after MagicLinkPKCE.generatePair() and before calling
signUpService.prepareVerification(...)) and ensure any save error is
propagated/stops the flow so prepareVerification is not invoked without a
persisted verifier.

In
`@Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift`:
- Around line 225-228: The catch block in sendLink() updates deliveryState to
.failed(error) but never assigns the thrown error to the view's error property,
so the UI (which uses .clerkErrorPresenting($error)) won't show it; modify the
catch in sendLink() to also set self.error = error (or call a helper that sets
both deliveryState and error) so the error is surfaced, while keeping the
ClerkLogger.error("Failed to send email link", error: error) call intact.

In `@Sources/ClerkKitUI/Resources/Localizable.xcstrings`:
- Around line 3998-4001: The new localization entries for the email-link UI
(e.g., the key "Didn't receive an email?" and the other affected keys around the
identical pattern at the other reported block) only contain metadata and no
localized values, so add proper localized payloads under the "localizations"
dictionary for each locale needed (or add an explicit "nonLocalized" marker if
the string must remain untranslated); update the Localizable.xcstrings entries
to include per-locale mappings for the keys (preserving the existing
"comment"/"isCommentAutoGenerated" fields) so non-English users see the correct
copy rather than the English fallback.
- Around line 7303-7308: The two string-catalog records for "No email app is
available on this device." and "Open email app" are empty objects; replace each
empty entry with the full string-catalog structure (include metadata and
per-locale values) so localization tooling picks them up — e.g., add the
standard keys (NSStringLocalizedFormatKey and a nested value table with
NSStringFormatValueTypeKey and an "en" (and other locales as needed)
localization) for both the "No email app is available on this device." and "Open
email app" entries and populate the English/default text.

In `@Tests/UI/SignInFactorSelectionTests.swift`:
- Around line 9-11: SignInFactorSelectionTests in the ClerkKitUITests target
cannot access configureClerkForTesting (from
Tests/TestSupport/TestHelpers.swift) because that file and its Mocker dependency
are only in the ClerkKitTests target; to fix, create a lightweight test
utilities file inside Tests/UI (e.g., UI/TestHelpers.swift) that defines
configureClerkForTesting() and the minimal InMemoryKeychain replacement used by
SignInFactorSelectionTests without importing Mocker, update
SignInFactorSelectionTests.swift to import/use these local helpers, and keep the
original TestHelpers.swift intact for ClerkKitTests; alternatively if you prefer
sharing the existing helpers, move TestHelpers.swift and InMemoryKeychain.swift
into Tests/UI or add Mocker and include Tests/TestSupport/ in the
ClerkKitUITests target in Package.swift so the UITests target can access them.

In `@Tests/UI/SignUpEmailVerificationStrategyTests.swift`:
- Around line 9-11: The compile error is because configureClerkForTesting() is
not visible to the ClerkKitUITests target; fix it by placing the helper into a
source file that is compiled into the ClerkKitUITests target (e.g., a shared
test helper in the Tests target) or by making the helper visible to tests (mark
it public/internal in its module and use `@testable` import ClerkKit in the UI
test target). Update or add the helper so
SignUpEmailVerificationStrategyTests.swift can call configureClerkForTesting()
directly; ensure the symbol name matches exactly and the file is included in the
ClerkKitUITests target.

---

Nitpick comments:
In `@Sources/ClerkKit/Domains/Auth/MagicLink.swift`:
- Around line 87-127: MagicLinkStore is annotated `@MainActor` but performs
synchronous keychain I/O via the keychain property (any KeychainStorage), which
can block the UI; move those operations off the main actor by performing
keychain accesses from a background context (e.g., wrap calls in Task.detached
or implement a dedicated KeychainActor) and update save(), load(), and clear()
to call into that background execution so the main thread isn’t blocked. Also
change load() to surface or log decoding/keychain errors instead of silently
swallowing them with try? — capture the thrown error from keychain.data(...) and
JSONDecoder.clerkDecoder.decode(...) and log or rethrow it for diagnostics while
still handling expiry logic for PendingMagicLinkFlow.

In
`@Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift`:
- Around line 231-241: openEmailApp currently sets an error if neither
"message://" nor "mailto:" can be opened (common on Simulator); update
openEmailApp to handle this edge case by providing a graceful fallback: if both
schemes fail, copy a mailto link or support email address to the clipboard via
UIPasteboard.general and present a user-facing fallback (e.g., an alert or
in-view message) with a "Paste/Copy" action so developers/testers can manually
open mail in the Simulator; keep the existing ClerkClientError assignment for
production but invoke the fallback before setting error to improve developer UX.

In `@Sources/ClerkKitUI/Extensions/SignIn`+Ext.swift:
- Around line 42-47: The computed property availableFirstFactors currently
returns a non-optional [Factor] by defaulting nil supportedFirstFactors to [],
which loses the "not yet loaded" vs "empty" distinction; change
availableFirstFactors's signature to return [Factor]? and remove the "?? []" so
it simply maps supportedFirstFactors?.filter { if case .unknown = $0.strategy {
return false } ; return true }, and update any callers of availableFirstFactors
to handle the optional (e.g., show loading state when nil and empty-state when
[]).

In `@Sources/ClerkKitUI/Extensions/SignUp`+Ext.swift:
- Around line 26-27: The code is matching the verification type using the
hard-coded string "email_link"; replace this literal with the enum raw value to
avoid drift—use the existing verification enum (e.g.,
VerificationMethod.emailLink.rawValue) when checking
Clerk.shared.environment?.userSettings.attributes["email_address"]?.verifications
so the contains check becomes contains(VerificationMethod.emailLink.rawValue)
(or, if verifications are strongly typed, compare to
VerificationMethod.emailLink directly).

In `@Tests/Domains/Auth/AuthTests.swift`:
- Around line 283-343: In
handleMagicLinkCallbackCompletesPendingFlowAndActivatesSession, the final
assertion references Clerk.shared.dependencies.keychain but the test uses the
local keychain variable; replace Clerk.shared.dependencies.keychain with the
local keychain and call keychain.hasItem(forKey:
ClerkKeychainKey.pendingMagicLinkFlow.rawValue) to assert the item was removed,
keeping the rest of the check the same.

In `@Tests/Domains/Auth/SignIn/SignInTests.swift`:
- Around line 69-73: Replace the direct assignment to Clerk.shared.dependencies
(the MockDependencyContainer construction using createMockAPIClient(), keychain,
and signInService: service) with the file’s existing dependency/configuration
setup helper used by other tests (call the helper that centralizes dependency
wiring and pass the same createMockAPIClient(), keychain, and service values),
so tests reuse the common setup path instead of instantiating
MockDependencyContainer inline.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 72f90154-36a3-4f07-9f1a-f4df55cd2e8f

📥 Commits

Reviewing files that changed from the base of the PR and between e68abfe and 10a2553.

📒 Files selected for processing (31)
  • Examples/Quickstart/Quickstart/Info.plist
  • Sources/ClerkKit/Core/Auth.swift
  • Sources/ClerkKit/Core/Clerk.swift
  • Sources/ClerkKit/Core/ClerkURLRoute.swift
  • Sources/ClerkKit/Core/URLHandlingCoordinator.swift
  • Sources/ClerkKit/Domains/Auth/FactorStrategy.swift
  • Sources/ClerkKit/Domains/Auth/MagicLink.swift
  • Sources/ClerkKit/Domains/Auth/SignIn/SignIn+ServiceParams.swift
  • Sources/ClerkKit/Domains/Auth/SignIn/SignIn.swift
  • Sources/ClerkKit/Domains/Auth/SignUp/SignUp+ServiceParams.swift
  • Sources/ClerkKit/Domains/Auth/SignUp/SignUp.swift
  • Sources/ClerkKit/Mocks/MockModels.swift
  • Sources/ClerkKit/Storage/Keychain/ClerkKeychainKey.swift
  • Sources/ClerkKitUI/Components/Auth/AuthNavigation.swift
  • Sources/ClerkKitUI/Components/Auth/AuthView.swift
  • Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorAlternativeMethodsView.swift
  • Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift
  • Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneView.swift
  • Sources/ClerkKitUI/Extensions/Factor+Sorting.swift
  • Sources/ClerkKitUI/Extensions/SignIn+Ext.swift
  • Sources/ClerkKitUI/Extensions/SignUp+Ext.swift
  • Sources/ClerkKitUI/Resources/Localizable.xcstrings
  • Tests/Core/ClerkTests.swift
  • Tests/Domains/Auth/AuthTests.swift
  • Tests/Domains/Auth/MagicLinkTests.swift
  • Tests/Domains/Auth/SignIn/SignInServiceTests.swift
  • Tests/Domains/Auth/SignIn/SignInTests.swift
  • Tests/Domains/Auth/SignUp/SignUpTests.swift
  • Tests/Storage/ClerkKeychainKeyTests.swift
  • Tests/UI/SignInFactorSelectionTests.swift
  • Tests/UI/SignUpEmailVerificationStrategyTests.swift

Comment thread Sources/ClerkKit/Core/Auth.swift Outdated
Comment thread Sources/ClerkKit/Domains/Auth/FactorStrategy.swift
Comment thread Sources/ClerkKit/Domains/Auth/SignIn/SignIn.swift Outdated
Comment thread Sources/ClerkKit/Domains/Auth/SignUp/SignUp.swift Outdated
Comment thread Sources/ClerkKitUI/Resources/Localizable.xcstrings
Comment thread Sources/ClerkKitUI/Resources/Localizable.xcstrings Outdated
Comment thread Tests/UI/SignInFactorSelectionTests.swift
Comment thread Tests/UI/SignUpEmailVerificationStrategyTests.swift
- Change URL validation to require both flow_id AND approval_token
- Save PKCE code verifier before API call to avoid loss on failure
- Use FactorStrategy enum instead of hardcoded string literal
- Remove outdated doc comment about sign-in/sign-up determination
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ClerkKit/Domains/Auth/SignIn/SignIn.swift`:
- Around line 105-148: The current sendEmailLink flow saves a PKCE verifier via
MagicLinkStore.save which will overwrite any existing verifier and can orphan a
concurrent magic-link flow; update sendEmailLink to first check for an existing
unexpired verifier (via MagicLinkStore.hasUnexpiredVerifier or similar) and
either prevent/start a new flow accordingly or extend MagicLinkStore to expose a
method to atomically save-if-none or save-with-unique-key, or alternatively add
documentation in SignIn.sendEmailLink and MagicLinkStore explaining that only
one pending magic-link flow is supported; reference SendEmailLink
(sendEmailLink), MagicLinkStore.save, and the MagicLink store implementation to
implement the check-or-document fix.

In `@Tests/UI/TestHelpers.swift`:
- Around line 13-16: The configureClerkForTesting function currently calls
Clerk.configure(publishableKey: testPublishableKey) before
Clerk.shared.cleanupManagers(), which tears down managers created by configure
and can leave Clerk.shared partially initialized; swap the calls so you invoke
Clerk.shared.cleanupManagers() first to ensure a clean slate, then call
Clerk.configure(...) to create managers afresh (reference: function
configureClerkForTesting, Clerk.configure, Clerk.shared.cleanupManagers).
- Around line 8-11: Update the doc comment that begins with "Configures Clerk
for UI-focused unit tests." to remove historical/contextual reasoning (e.g.
"These tests only..." and "so they do not need...") and leave a concise,
behavior-focused description of what the helper does and how it should be used;
keep the summary statement and any usage or parameter notes, but strip any
past-setup rationale or history from the comment above the TestHelpers
configurator.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8f2bd400-fdef-4978-8fe5-2a0ab39dc777

📥 Commits

Reviewing files that changed from the base of the PR and between 10a2553 and b16a986.

📒 Files selected for processing (7)
  • Sources/ClerkKit/Core/Auth.swift
  • Sources/ClerkKit/Domains/Auth/MagicLink.swift
  • Sources/ClerkKit/Domains/Auth/SignIn/SignIn.swift
  • Sources/ClerkKit/Domains/Auth/SignUp/SignUp.swift
  • Sources/ClerkKitUI/Extensions/SignUp+Ext.swift
  • Sources/ClerkKitUI/Resources/Localizable.xcstrings
  • Tests/UI/TestHelpers.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • Sources/ClerkKitUI/Extensions/SignUp+Ext.swift
  • Sources/ClerkKitUI/Resources/Localizable.xcstrings
  • Sources/ClerkKit/Domains/Auth/MagicLink.swift

Comment thread Sources/ClerkKit/Domains/Auth/SignIn/SignIn.swift
Comment thread Tests/UI/TestHelpers.swift Outdated
Comment thread Tests/UI/TestHelpers.swift
Remove TransferFlowResult wrapper from handleMagicLinkCallback and
completeMagicLink since sign-up magic links are handled separately.
Add documentation for single-flow storage semantics, include email
link factor in forgot password view, and set error state on send
failure. Simplify test helpers by removing unnecessary cleanup call.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
Tests/Domains/Auth/AuthTests.swift (1)

239-339: Please add regression coverage for duplicate/stale callbacks too.

The happy path is covered well, but the risky branches are still missing: the same callback arriving twice, and an older callback returning pkce_verification_failed while a newer verifier is stored. Those cases exercise the dedup and cleanup behavior most likely to regress.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/Domains/Auth/AuthTests.swift` around lines 239 - 339, Add two new tests
around Clerk.shared.auth.handleMagicLinkCallback to cover duplicate and stale
callbacks: (1) duplicate callback — simulate the completion endpoint returning
the same MagicLinkCompleteResponse twice and call handleMagicLinkCallback twice;
assert MockSignInService.create is only invoked once,
MockSessionService.setActive only activates one session (use signInParams and
activatedSessionId locks), and the pending magic link key
(ClerkKeychainKey.pendingMagicLinkFlow) is cleared after the first/second call.
(2) stale PKCE callback — save a different verifier with MagicLinkStore.save
prior to invoking handleMagicLinkCallback, mock the completion endpoint to
return an error body with "pkce_verification_failed" for the older flow, and
assert that no session is activated, signInParams.ticket is not set (or create
not called), and the pending magic link flow is cleaned up appropriately. Use
existing helpers (MockSignInService, MockSessionService, Mock for completionUrl,
MagicLinkStore.save) and mirror the structure/assertions from
signInWithEmailLinkCreatesAndPreparesFirstFactor and
handleMagicLinkCallbackCompletesPendingFlowAndActivatesSession tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ClerkKit/Core/Auth.swift`:
- Around line 313-321: The handleMagicLinkCallback(_:) must go through the same
deduplication coordinator used by Clerk.handle(_:) to avoid duplicate
processing: instead of directly calling completeMagicLink(flowId:approvalToken:)
after creating MagicLinkCallback, build the equivalent .magicLink Route (or call
the existing Clerk.handle(route:) path) and pass it into the dedup coordinator /
Clerk.handle so deduplication runs; once the routed work completes, return the
resulting SignIn (or forward the completion result from completeMagicLink) and
do not bypass the coordinator that prevents duplicated handling of the same
flowId/approvalToken pair.

In `@Sources/ClerkKit/Domains/Auth/MagicLink.swift`:
- Around line 22-26: The current completeMagicLink path unconditionally clears
the stored PendingMagicLinkFlow when a PKCE failure occurs, but
PendingMagicLinkFlow has no flow_id so a stale callback can wipe out a newer
valid pending flow; update completeMagicLink so that it does NOT remove or
overwrite the stored PendingMagicLinkFlow on a pkce_verification_failed
error—only remove the pending flow on successful completion or on explicit
terminal errors that are known to apply to the current flow; keep
PendingMagicLinkFlow, completeMagicLink, and the pkce_verification_failed check
as the points to change.

---

Nitpick comments:
In `@Tests/Domains/Auth/AuthTests.swift`:
- Around line 239-339: Add two new tests around
Clerk.shared.auth.handleMagicLinkCallback to cover duplicate and stale
callbacks: (1) duplicate callback — simulate the completion endpoint returning
the same MagicLinkCompleteResponse twice and call handleMagicLinkCallback twice;
assert MockSignInService.create is only invoked once,
MockSessionService.setActive only activates one session (use signInParams and
activatedSessionId locks), and the pending magic link key
(ClerkKeychainKey.pendingMagicLinkFlow) is cleared after the first/second call.
(2) stale PKCE callback — save a different verifier with MagicLinkStore.save
prior to invoking handleMagicLinkCallback, mock the completion endpoint to
return an error body with "pkce_verification_failed" for the older flow, and
assert that no session is activated, signInParams.ticket is not set (or create
not called), and the pending magic link flow is cleaned up appropriately. Use
existing helpers (MockSignInService, MockSessionService, Mock for completionUrl,
MagicLinkStore.save) and mirror the structure/assertions from
signInWithEmailLinkCreatesAndPreparesFirstFactor and
handleMagicLinkCallbackCompletesPendingFlowAndActivatesSession tests.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 44ac057c-fa0a-4c89-afc6-277af0363dce

📥 Commits

Reviewing files that changed from the base of the PR and between b16a986 and 07ae109.

📒 Files selected for processing (7)
  • Sources/ClerkKit/Core/Auth.swift
  • Sources/ClerkKit/Domains/Auth/MagicLink.swift
  • Sources/ClerkKit/Domains/Auth/SignIn/SignIn.swift
  • Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift
  • Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneForgotPasswordView.swift
  • Tests/Domains/Auth/AuthTests.swift
  • Tests/UI/TestHelpers.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/ClerkKit/Domains/Auth/SignIn/SignIn.swift
  • Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift

Comment thread Sources/ClerkKit/Core/Auth.swift Outdated
Comment thread Sources/ClerkKit/Domains/Auth/MagicLink.swift
Convert MagicLinkStore from a static enum to an injectable class,
enabling isolated test instances and proper unit testing. Update
Auth, SignIn, and SignUp to use the injected store via Dependencies.

Also add deduplication for concurrent handleMagicLinkCallback calls
and preserve pending flow state when PKCE verification fails due to
stale callbacks, allowing fresh magic links to still complete.
The magic link tests were sharing mockBaseUrl, which caused
mock URL collisions when tests ran in parallel. Each test
now uses a distinct base URL to ensure proper mock isolation.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
Sources/ClerkKit/Core/Auth.swift (1)

371-375: ⚠️ Potential issue | 🟠 Major

Don't clear the stored verifier on callback-specific API errors.

PendingMagicLinkFlow still has no flowId in Sources/ClerkKit/Domains/Auth/MagicLink.swift:22-26, so this block cannot tell whether approval_token_expired, approval_token_consumed, approval_token_invalid, or flow_not_approved belongs to the currently stored flow or to an older link the user opened later. In that stale-link case, clearing here deletes the verifier for the newer valid link and makes that callback unrecoverable. Until the pending flow is bound to a flowId, the only safe automatic cleanup is on success.

🐛 Smallest safe change
-    } catch let error as ClerkAPIError {
-      if magicLinkTerminalErrorCodes.contains(error.code), error.code != "pkce_verification_failed" {
-        magicLinkStore.clear()
-      }
-      throw error
-    }
+    } catch {
+      throw error
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKit/Core/Auth.swift` around lines 371 - 375, The catch block in
Auth.swift currently calls magicLinkStore.clear() for certain ClerkAPIError
codes (checking magicLinkTerminalErrorCodes and excluding
"pkce_verification_failed"), which can mistakenly remove the verifier for a
newer pending flow; remove that automatic clear from this catch so
callback-specific errors (e.g., approval_token_* and flow_not_approved) do not
clear the stored verifier. Ensure magicLinkStore.clear() is only invoked on
successful completion of the magic-link flow (or after the pending flow has been
bound to a flowId), and keep references to magicLinkTerminalErrorCodes, the
"pkce_verification_failed" string, and magicLinkStore.clear() to locate and
update the code; do not clear in the ClerkAPIError catch that handles callback
errors.
🧹 Nitpick comments (1)
Tests/Domains/Auth/AuthTests.swift (1)

289-290: Minor inconsistency in URL construction.

Line 289 uses try #require(URL(string:)) while line 290 uses force-unwrap. Both approaches work for known-valid test URLs, but consider using the same pattern consistently.

Suggested consistency fix
     let callbackUrl = try `#require`(URL(string: "com.clerk.Quickstart://callback?flow_id=flow_123&approval_token=approval_123"))
-    let completionUrl = URL(string: mockBaseUrl.absoluteString + "/v1/client/magic_links/complete")!
+    let completionUrl = try `#require`(URL(string: mockBaseUrl.absoluteString + "/v1/client/magic_links/complete"))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/Domains/Auth/AuthTests.swift` around lines 289 - 290, The two test URL
initializations are inconsistent: callbackUrl uses the test helper pattern try
`#require`(URL(string: ...)) while completionUrl is force-unwrapped; make them
consistent by using the same construction pattern for both variables (e.g.,
change completionUrl to use try `#require`(URL(string: mockBaseUrl.absoluteString
+ "/v1/client/magic_links/complete")) or change callbackUrl to a force-unwrap)
so update the completionUrl or callbackUrl declaration accordingly in
Tests/Domains/Auth/AuthTests.swift (referencing the callbackUrl and
completionUrl variables).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ClerkKit/Domains/Auth/MagicLink.swift`:
- Around line 45-48: The canHandle(_:) matcher in MagicLink.swift currently
returns true solely based on presence of query params via queryParam(named:in:),
which lets any URL with those params be treated as a Clerk callback; update
canHandle(_:) to also validate the URL's scheme/host/path against the configured
redirect callback (or accept an expected callback URL as a parameter) before
returning true—e.g., compare url.scheme, url.host and url.path (or compare to a
preconfigured redirect URL) in MagicLink.canHandle(_:) so only legitimate
redirect URLs claiming flow_id and approval_token are accepted, or move this
logic into an API that receives the expected callback target and performs both
param and origin checks.

In `@Sources/ClerkKitUI/Resources/Localizable.xcstrings`:
- Around line 4492-4563: The new localization key "Email link to %@" is not used
— SignInFactorOneForgotPasswordView.swift currently builds the label with "Email
link to \(safeIdentifier)" directly; update that construction to fetch the
localized template and format it with safeIdentifier (e.g. use
NSLocalizedString("Email link to %@", comment: ...) or the String(localized:...)
API) so the UI uses the Localizable.xcstrings entry instead of a hardcoded
English string; change the string construction in
SignInFactorOneForgotPasswordView (the code that currently builds the email link
label around safeIdentifier) to use the localization key and pass safeIdentifier
as the argument.

---

Duplicate comments:
In `@Sources/ClerkKit/Core/Auth.swift`:
- Around line 371-375: The catch block in Auth.swift currently calls
magicLinkStore.clear() for certain ClerkAPIError codes (checking
magicLinkTerminalErrorCodes and excluding "pkce_verification_failed"), which can
mistakenly remove the verifier for a newer pending flow; remove that automatic
clear from this catch so callback-specific errors (e.g., approval_token_* and
flow_not_approved) do not clear the stored verifier. Ensure
magicLinkStore.clear() is only invoked on successful completion of the
magic-link flow (or after the pending flow has been bound to a flowId), and keep
references to magicLinkTerminalErrorCodes, the "pkce_verification_failed"
string, and magicLinkStore.clear() to locate and update the code; do not clear
in the ClerkAPIError catch that handles callback errors.

---

Nitpick comments:
In `@Tests/Domains/Auth/AuthTests.swift`:
- Around line 289-290: The two test URL initializations are inconsistent:
callbackUrl uses the test helper pattern try `#require`(URL(string: ...)) while
completionUrl is force-unwrapped; make them consistent by using the same
construction pattern for both variables (e.g., change completionUrl to use try
`#require`(URL(string: mockBaseUrl.absoluteString +
"/v1/client/magic_links/complete")) or change callbackUrl to a force-unwrap) so
update the completionUrl or callbackUrl declaration accordingly in
Tests/Domains/Auth/AuthTests.swift (referencing the callbackUrl and
completionUrl variables).
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 883587d4-1da3-43a5-868a-cd64ad1ce12c

📥 Commits

Reviewing files that changed from the base of the PR and between 07ae109 and 59e99b6.

📒 Files selected for processing (12)
  • Sources/ClerkKit/Core/Auth.swift
  • Sources/ClerkKit/Core/Clerk.swift
  • Sources/ClerkKit/Core/URLHandlingCoordinator.swift
  • Sources/ClerkKit/Dependencies/Dependencies.swift
  • Sources/ClerkKit/Dependencies/DependencyContainer.swift
  • Sources/ClerkKit/Domains/Auth/MagicLink.swift
  • Sources/ClerkKit/Domains/Auth/SignIn/SignIn.swift
  • Sources/ClerkKit/Domains/Auth/SignUp/SignUp.swift
  • Sources/ClerkKit/Mocks/MockDependencyContainer.swift
  • Sources/ClerkKitUI/Resources/Localizable.xcstrings
  • Tests/Core/ClerkTests.swift
  • Tests/Domains/Auth/AuthTests.swift
🚧 Files skipped from review as they are similar to previous changes (2)
  • Sources/ClerkKit/Core/URLHandlingCoordinator.swift
  • Sources/ClerkKit/Domains/Auth/SignIn/SignIn.swift

Comment thread Sources/ClerkKit/Domains/Auth/MagicLink.swift Outdated
Comment thread Sources/ClerkKitUI/Resources/Localizable.xcstrings
Validate that incoming callback URLs match the configured redirect URL before processing magic links. This prevents handling callbacks from mismatched origins.

- Add redirectUrl dependency to Auth for URL validation
- Introduce matchesRedirectUrl helper to compare scheme, host, and path
- Move URL routing logic from ClerkURLRoute init to Clerk.handle
- Make canHandleMagicLinkCallback internal
- Remove terminal error code clearing (already handled elsewhere)
- Add test for rejecting mismatched callback origins
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorAlternativeMethodsView.swift (2)

59-72: Code duplication: identifierActionText(factor:) is identical in both files.

This helper function is duplicated verbatim in SignInFactorOneForgotPasswordView.swift (lines 49-62). Consider extracting it to a shared extension on Factor or a common utility to avoid maintaining the same logic in multiple places.

♻️ Suggested shared extension
// In a shared file, e.g., Factor+ActionText.swift
extension Factor {
  func identifierActionText() -> LocalizedStringKey? {
    guard let safeIdentifier = safeIdentifier else { return nil }
    
    switch strategy {
    case .phoneCode:
      return "Send SMS code to \(safeIdentifier.formattedAsPhoneNumberIfPossible)"
    case .emailCode:
      return "Email code to \(safeIdentifier)"
    case .emailLink:
      return "Email link to \(safeIdentifier)"
    default:
      return nil
    }
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorAlternativeMethodsView.swift`
around lines 59 - 72, The function identifierActionText(factor:) is duplicated;
extract this logic into a shared extension on Factor (e.g., add an instance
method Factor.identifierActionText() or static utility) and replace the
duplicated implementations in SignInFactorAlternativeMethodsView and
SignInFactorOneForgotPasswordView to call the new Factor.identifierActionText()
method; ensure the method uses factor.safeIdentifier and factor.strategy and
returns the same LocalizedStringKey values (including
formattedAsPhoneNumberIfPossible for .phoneCode) so behavior remains identical.

80-83: Minor: Combine .emailCode and .emailLink cases.

These cases return the same icon. For consistency with SignInFactorOneForgotPasswordView.swift (line 70), combine them:

♻️ Suggested fix
-    case .emailCode:
-      "icon-email"
-    case .emailLink:
-      "icon-email"
+    case .emailCode, .emailLink:
+      "icon-email"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorAlternativeMethodsView.swift`
around lines 80 - 83, In SignInFactorAlternativeMethodsView update the switch
that handles the factor cases to combine the duplicate branches for .emailCode
and .emailLink into a single branch (e.g., use a single case handling both
.emailCode and .emailLink) so they return the same "icon-email" value, matching
the pattern used in SignInFactorOneForgotPasswordView and removing the redundant
duplicate case.
Sources/ClerkKit/Core/Auth.swift (1)

370-376: Redundant catch block.

This catch let error as ClerkAPIError block immediately rethrows, which is identical to not catching at all. If you intend to add terminal-error handling here (e.g., clearing magicLinkStore on specific error codes), implement it; otherwise remove the catch.

♻️ Suggested simplification
     do {
       completionResponse = try await apiClient.send(request).value
       magicLinkStore.clear()
-    } catch let error as ClerkAPIError {
-      throw error
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKit/Core/Auth.swift` around lines 370 - 376, The catch block
that matches `ClerkAPIError` and immediately rethrows is redundant; in the
`Auth.swift` function where you await `apiClient.send(request).value` into
`completionResponse: MagicLinkCompleteResponse` and call
`magicLinkStore.clear()`, either remove the `catch let error as ClerkAPIError {
throw error }` entirely so normal propagation happens, or if you need custom
handling (e.g., clear `magicLinkStore` for specific error codes), replace that
catch with logic that inspects the `ClerkAPIError` and performs the conditional
cleanup before rethrowing; reference `completionResponse`,
`apiClient.send(request).value`, `magicLinkStore.clear()`, and `ClerkAPIError`
to locate the code to change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@Sources/ClerkKit/Core/Auth.swift`:
- Around line 370-376: The catch block that matches `ClerkAPIError` and
immediately rethrows is redundant; in the `Auth.swift` function where you await
`apiClient.send(request).value` into `completionResponse:
MagicLinkCompleteResponse` and call `magicLinkStore.clear()`, either remove the
`catch let error as ClerkAPIError { throw error }` entirely so normal
propagation happens, or if you need custom handling (e.g., clear
`magicLinkStore` for specific error codes), replace that catch with logic that
inspects the `ClerkAPIError` and performs the conditional cleanup before
rethrowing; reference `completionResponse`, `apiClient.send(request).value`,
`magicLinkStore.clear()`, and `ClerkAPIError` to locate the code to change.

In
`@Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorAlternativeMethodsView.swift`:
- Around line 59-72: The function identifierActionText(factor:) is duplicated;
extract this logic into a shared extension on Factor (e.g., add an instance
method Factor.identifierActionText() or static utility) and replace the
duplicated implementations in SignInFactorAlternativeMethodsView and
SignInFactorOneForgotPasswordView to call the new Factor.identifierActionText()
method; ensure the method uses factor.safeIdentifier and factor.strategy and
returns the same LocalizedStringKey values (including
formattedAsPhoneNumberIfPossible for .phoneCode) so behavior remains identical.
- Around line 80-83: In SignInFactorAlternativeMethodsView update the switch
that handles the factor cases to combine the duplicate branches for .emailCode
and .emailLink into a single branch (e.g., use a single case handling both
.emailCode and .emailLink) so they return the same "icon-email" value, matching
the pattern used in SignInFactorOneForgotPasswordView and removing the redundant
duplicate case.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9a706481-19af-4fdd-8aae-b570c4e712ec

📥 Commits

Reviewing files that changed from the base of the PR and between 59e99b6 and 1871eda.

📒 Files selected for processing (10)
  • Sources/ClerkKit/Core/Auth.swift
  • Sources/ClerkKit/Core/Clerk.swift
  • Sources/ClerkKit/Core/ClerkURLRoute.swift
  • Sources/ClerkKit/Domains/Auth/MagicLink.swift
  • Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorAlternativeMethodsView.swift
  • Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneForgotPasswordView.swift
  • Tests/Core/ClerkTests.swift
  • Tests/Domains/Auth/AuthTests.swift
  • Tests/Domains/Auth/MagicLinkTests.swift
  • Tests/TestSupport/TestHelpers.swift
✅ Files skipped from review due to trivial changes (1)
  • Tests/Domains/Auth/MagicLinkTests.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • Tests/TestSupport/TestHelpers.swift
  • Tests/Domains/Auth/AuthTests.swift
  • Tests/Core/ClerkTests.swift

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
Sources/ClerkKit/Core/Auth.swift (2)

344-374: ⚠️ Potential issue | 🟠 Major

Route the public completeMagicLink entry point through urlHandlingCoordinator.

Deduplication only happens in handle(_:). Because Auth is @MainActor, a second task can still enter completeMagicLink(flowId:approvalToken:) after the first suspends on the network await, so two direct callers can load the same verifier and race the same approval token. Move the current body into a private helper and have the public method delegate through handle(.magicLink(...)).

Also applies to: 389-399

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKit/Core/Auth.swift` around lines 344 - 374, The public
completeMagicLink(flowId:approvalToken:) must be routed through
urlHandlingCoordinator to benefit from deduplication in handle(_:); refactor by
extracting the current body (validation, magicLinkStore.load(), creating
Request<MagicLinkCompleteResponse>, clearing store, and calling
signInWithTicket) into a private helper (e.g., private func
completeMagicLinkImpl(flowId:approvalToken:) async throws -> SignIn) and change
the public method to simply await
urlHandlingCoordinator.handle(.magicLink(flowId: resolvedFlowId, approvalToken:
resolvedApprovalToken)) so the call path goes through handle(_:) and avoids
concurrent races on magicLinkStore and codeVerifier. Ensure the private helper
name and the public completeMagicLink signature are referenced consistently and
preserve existing error behavior.

321-333: ⚠️ Potential issue | 🟠 Major

Reject non-matching URLs inside the public magic-link handler.

handleMagicLinkCallback(_:) skips the redirect-origin check and will process any URL that happens to contain flow_id and approval_token. Since canHandleMagicLinkCallback(_:) is internal, callers of the public API cannot reuse that guard themselves. Validate the URL here before parsing it.

🛡️ Small fix
 `@discardableResult`
 public func handleMagicLinkCallback(_ url: URL) async throws -> SignIn {
+  guard canHandleMagicLinkCallback(url) else {
+    throw ClerkClientError(message: "URL does not match the configured magic link redirect.")
+  }
   let callback = try MagicLinkCallback(url: url)
   return try await handle(.magicLink(flowId: callback.flowId, approvalToken: callback.approvalToken))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKit/Core/Auth.swift` around lines 321 - 333, Public handler
handleMagicLinkCallback(_:) currently parses any URL with flow_id and
approval_token; add a redirect-origin check first to reject non-matching URLs.
Inside handleMagicLinkCallback(_ url: URL), call matchesRedirectUrl(url) (or
reuse canHandleMagicLinkCallback(url)) and throw an appropriate error if it
returns false before creating MagicLinkCallback(url), then proceed with
extracting flowId/approvalToken and calling handle(.magicLink(...)); reference
the existing methods handleMagicLinkCallback(_:), matchesRedirectUrl(_:),
canHandleMagicLinkCallback(_:), and MagicLinkCallback(url) to locate where to
add the guard.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ClerkKit/Core/Auth.swift`:
- Around line 375-383: The catch currently suppresses setActive errors by
checking sessions.contains(where: { $0.id == sessionId }) which only tells if
the session exists, not whether it became the active session; change that check
to verify the active session identity (e.g., compare activeSession?.id ==
sessionId or use the client/instance property that stores the current active
session id) and only swallow the error when that check confirms the session is
actually active; update the block around setActive(sessionId:) to use
active-session comparison instead of sessions.contains so real activation
failures are not silently ignored.

---

Duplicate comments:
In `@Sources/ClerkKit/Core/Auth.swift`:
- Around line 344-374: The public completeMagicLink(flowId:approvalToken:) must
be routed through urlHandlingCoordinator to benefit from deduplication in
handle(_:); refactor by extracting the current body (validation,
magicLinkStore.load(), creating Request<MagicLinkCompleteResponse>, clearing
store, and calling signInWithTicket) into a private helper (e.g., private func
completeMagicLinkImpl(flowId:approvalToken:) async throws -> SignIn) and change
the public method to simply await
urlHandlingCoordinator.handle(.magicLink(flowId: resolvedFlowId, approvalToken:
resolvedApprovalToken)) so the call path goes through handle(_:) and avoids
concurrent races on magicLinkStore and codeVerifier. Ensure the private helper
name and the public completeMagicLink signature are referenced consistently and
preserve existing error behavior.
- Around line 321-333: Public handler handleMagicLinkCallback(_:) currently
parses any URL with flow_id and approval_token; add a redirect-origin check
first to reject non-matching URLs. Inside handleMagicLinkCallback(_ url: URL),
call matchesRedirectUrl(url) (or reuse canHandleMagicLinkCallback(url)) and
throw an appropriate error if it returns false before creating
MagicLinkCallback(url), then proceed with extracting flowId/approvalToken and
calling handle(.magicLink(...)); reference the existing methods
handleMagicLinkCallback(_:), matchesRedirectUrl(_:),
canHandleMagicLinkCallback(_:), and MagicLinkCallback(url) to locate where to
add the guard.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 5d0b2219-8ca6-4774-b586-274d670b2739

📥 Commits

Reviewing files that changed from the base of the PR and between 1871eda and 5bb74f1.

📒 Files selected for processing (2)
  • Sources/ClerkKit/Core/Auth.swift
  • Sources/ClerkKit/Domains/Auth/MagicLink.swift

Comment thread Sources/ClerkKit/Core/Auth.swift Outdated
Remove stored redirectUrl property from Auth struct and instead
read directly from Clerk.shared.options when needed. This eliminates
passing the same value through multiple layers and ensures the
single source of truth is used consistently.

Also simplifies session activation check to compare against
lastActiveSessionId instead of iterating through all sessions.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
Tests/Core/ClerkTests.swift (1)

276-276: Callback URL construction relies on Clerk.shared config.

The tests create a local clerk instance but construct the callback URL using Clerk.shared.options.redirectConfig.redirectUrl. This works because matchesRedirectUrl in Auth also references Clerk.shared, but it couples the test to shared global state set by configureClerkForTesting().

This is a minor test isolation concern—if configureClerkForTesting() changes the redirect config, these tests would need to match. Consider documenting this dependency or extracting the redirect URL to a shared test constant.

Also applies to: 331-331

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/Core/ClerkTests.swift` at line 276, The test couples to global state by
building callbackUrl from Clerk.shared.options.redirectConfig.redirectUrl;
update the test to use the local test instance instead (use
clerk.options.redirectConfig.redirectUrl when constructing callbackUrl) or
replace both occurrences with a shared test constant (e.g., TEST_REDIRECT_URL)
used by configureClerkForTesting() and the tests, ensuring consistency with
Auth.matchesRedirectUrl; modify the lines using Clerk.shared to reference the
local clerk or the shared test constant so the test no longer depends on
Clerk.shared.
Tests/Domains/Auth/AuthTests.swift (1)

333-333: Same Clerk.shared callback URL coupling as in ClerkTests.

Multiple tests construct callback URLs using Clerk.shared.options.redirectConfig.redirectUrl while testing a local clerk instance. As noted in ClerkTests, this works but creates implicit coupling to shared global state.

Also applies to: 394-394, 463-463

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/Domains/Auth/AuthTests.swift` at line 333, Tests are using the global
Clerk.shared.options.redirectConfig.redirectUrl when building callback URLs
which couples tests to global state; update the tests to use the local test
instance's redirect URL (e.g., replace
Clerk.shared.options.redirectConfig.redirectUrl with
clerk.options.redirectConfig.redirectUrl or a helper like makeCallbackURL(for:
clerk, flowId:..., approvalToken:...)) so each test constructs callbackUrl from
the local clerk instance (same change needed for the other occurrences that use
Clerk.shared).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ClerkKit/Core/Clerk.swift`:
- Line 544: The cleanup currently calls urlHandlingCoordinator.cancelAll() which
only signals cancellation and returns immediately; change the coordinator to
expose an awaitable drain (e.g., cancelAllAndWait() async or
drainInFlightTasks() async) that awaits any tracked Task(s) (like the Task
created by completeMagicLink) before returning, then update
cleanupManagersAndDrainCache to await that new async method (mirroring how
invalidAuthRefreshTask is awaited via its .value) so in-flight network work
cannot continue and mutate magicLinkStore after cleanup.

---

Nitpick comments:
In `@Tests/Core/ClerkTests.swift`:
- Line 276: The test couples to global state by building callbackUrl from
Clerk.shared.options.redirectConfig.redirectUrl; update the test to use the
local test instance instead (use clerk.options.redirectConfig.redirectUrl when
constructing callbackUrl) or replace both occurrences with a shared test
constant (e.g., TEST_REDIRECT_URL) used by configureClerkForTesting() and the
tests, ensuring consistency with Auth.matchesRedirectUrl; modify the lines using
Clerk.shared to reference the local clerk or the shared test constant so the
test no longer depends on Clerk.shared.

In `@Tests/Domains/Auth/AuthTests.swift`:
- Line 333: Tests are using the global
Clerk.shared.options.redirectConfig.redirectUrl when building callback URLs
which couples tests to global state; update the tests to use the local test
instance's redirect URL (e.g., replace
Clerk.shared.options.redirectConfig.redirectUrl with
clerk.options.redirectConfig.redirectUrl or a helper like makeCallbackURL(for:
clerk, flowId:..., approvalToken:...)) so each test constructs callbackUrl from
the local clerk instance (same change needed for the other occurrences that use
Clerk.shared).
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 76d457c3-6e21-4846-bc3c-cd8f20390b2a

📥 Commits

Reviewing files that changed from the base of the PR and between 5bb74f1 and 3febe59.

📒 Files selected for processing (4)
  • Sources/ClerkKit/Core/Auth.swift
  • Sources/ClerkKit/Core/Clerk.swift
  • Tests/Core/ClerkTests.swift
  • Tests/Domains/Auth/AuthTests.swift

Comment thread Sources/ClerkKit/Core/Clerk.swift
Move URL matching and parsing logic from Auth into ClerkURLRoute and
MagicLinkCallback. This centralizes route detection while preparing for
additional URL route types.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
Sources/ClerkKit/Core/Clerk.swift (1)

541-556: ⚠️ Potential issue | 🟡 Minor

cancelAll() still doesn’t guarantee in-flight URL work is drained in async cleanup.

cleanupManagersAndDrainCache() is async, but URL handling uses non-awaiting cancellation. A canceled in-flight magic-link task may still run briefly and mutate state after cleanup starts.

💡 Suggested direction
-    urlHandlingCoordinator.cancelAll()
+    await urlHandlingCoordinator.cancelAllAndWait()

This assumes URLHandlingCoordinator exposes an awaitable drain API (for example, cancelAllAndWait()).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKit/Core/Clerk.swift` around lines 541 - 556, The async cleanup
(cleanupManagersAndDrainCache) calls urlHandlingCoordinator.cancelAll() which
only signals cancellation and can allow in-flight magic-link tasks to mutate
state after cleanup; change URLHandlingCoordinator to expose an awaitable drain
API (e.g., cancelAllAndWait() that cancels and then awaits completion of
internal tasks) and update cleanupManagersAndDrainCache() to call and await
urlHandlingCoordinator.cancelAllAndWait(); keep the existing synchronous
cancelAll() for non-async paths but ensure the new awaited variant is used
wherever cleanup must guarantee no in-flight URL work remains (reference:
urlHandlingCoordinator, cleanupManagersAndDrainCache(), cancelAll()).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ClerkKit/Core/Auth.swift`:
- Around line 377-381: The call to magicLinkStore.clear() is happening before
the ticket exchange and should be deferred until
signInWithTicket(completionResponse.ticket) succeeds; change the sequence so you
call apiClient.send(request).value to get completionResponse, then call
signInWithTicket(completionResponse.ticket) and only after that completes
successfully call magicLinkStore.clear(), ensuring you still propagate errors
from signInWithTicket and do not clear the pending flow on failure.

---

Duplicate comments:
In `@Sources/ClerkKit/Core/Clerk.swift`:
- Around line 541-556: The async cleanup (cleanupManagersAndDrainCache) calls
urlHandlingCoordinator.cancelAll() which only signals cancellation and can allow
in-flight magic-link tasks to mutate state after cleanup; change
URLHandlingCoordinator to expose an awaitable drain API (e.g.,
cancelAllAndWait() that cancels and then awaits completion of internal tasks)
and update cleanupManagersAndDrainCache() to call and await
urlHandlingCoordinator.cancelAllAndWait(); keep the existing synchronous
cancelAll() for non-async paths but ensure the new awaited variant is used
wherever cleanup must guarantee no in-flight URL work remains (reference:
urlHandlingCoordinator, cleanupManagersAndDrainCache(), cancelAll()).
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 80daffd9-489e-469c-a33c-58cde218be89

📥 Commits

Reviewing files that changed from the base of the PR and between 3febe59 and 0171e1c.

📒 Files selected for processing (4)
  • Sources/ClerkKit/Core/Auth.swift
  • Sources/ClerkKit/Core/Clerk.swift
  • Sources/ClerkKit/Core/ClerkURLRoute.swift
  • Sources/ClerkKit/Domains/Auth/MagicLink.swift
✅ Files skipped from review due to trivial changes (1)
  • Sources/ClerkKit/Domains/Auth/MagicLink.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/ClerkKit/Core/ClerkURLRoute.swift

Comment thread Sources/ClerkKit/Core/Auth.swift Outdated
Move scheme/host/path normalization and required param matching
into ClerkURLRoute. Extract query param parsing into reusable
URL extension that also checks the URL fragment, enabling support
for magic link callbacks that use fragments instead of query params.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
Sources/ClerkKit/Core/Auth.swift (1)

378-381: ⚠️ Potential issue | 🟠 Major

Defer magicLinkStore.clear() until after ticket exchange succeeds.

The pending flow is cleared on line 379 before signInWithTicket completes on line 381. If ticket exchange fails (e.g., transient network error), the verifier is lost and the user cannot retry.

Suggested fix
     let completionResponse = try await apiClient.send(request).value
-    magicLinkStore.clear()

     let signIn = try await signInWithTicket(completionResponse.ticket)
+    magicLinkStore.clear()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKit/Core/Auth.swift` around lines 378 - 381, The code currently
calls magicLinkStore.clear() before exchanging the ticket, which loses the
verifier if signInWithTicket fails; move the magicLinkStore.clear() call so it
runs only after try await signInWithTicket(completionResponse.ticket) returns
successfully (i.e., call magicLinkStore.clear() after signInWithTicket
completes), or alternatively wrap the signInWithTicket call and on success then
clear — update the sequence around apiClient.send(...).value,
completionResponse.ticket and signInWithTicket to ensure the store is only
cleared after a successful ticket exchange.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@Sources/ClerkKit/Core/Auth.swift`:
- Around line 378-381: The code currently calls magicLinkStore.clear() before
exchanging the ticket, which loses the verifier if signInWithTicket fails; move
the magicLinkStore.clear() call so it runs only after try await
signInWithTicket(completionResponse.ticket) returns successfully (i.e., call
magicLinkStore.clear() after signInWithTicket completes), or alternatively wrap
the signInWithTicket call and on success then clear — update the sequence around
apiClient.send(...).value, completionResponse.ticket and signInWithTicket to
ensure the store is only cleared after a successful ticket exchange.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2d5fa170-d90f-4dd9-90de-fe2efd92e221

📥 Commits

Reviewing files that changed from the base of the PR and between 0171e1c and 96d39d0.

📒 Files selected for processing (5)
  • Sources/ClerkKit/Core/Auth.swift
  • Sources/ClerkKit/Core/ClerkURLRoute.swift
  • Sources/ClerkKit/Domains/Auth/MagicLink.swift
  • Sources/ClerkKit/Utils/URL+Ext.swift
  • Tests/Domains/Auth/MagicLinkTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/Domains/Auth/MagicLinkTests.swift

Handle native email-link callbacks as either sign-in or sign-up results.
Persist pending flow kind, expose auth presentation requirements, and let AuthView resume continuation or session-task flows after app return.
Add example wiring, investigation notes, and tests for the new continuation behavior.
Expose ignored, handled, and continuation URL results and rename the
stored pending auth result to callback continuation.

Remove the auth presentation requirement helper and update examples
and tests to present AuthView from URL results or session tasks.
Reduce `Clerk.handle(_:)` to a `Bool` return value instead of a custom
result enum and rely on `clerk.auth.events` to surface continuation
state. Update the quickstart flow and tests to follow the new API.
Group `Auth` computed properties, session APIs, events, and magic link
helpers into separate extensions and remove the temporary native magic
link investigation note.
Use isolated Clerk instances in callback tests to avoid shared
state leaking through `Clerk.shared`.

Simplify concurrent callback deduplication coverage by testing
`URLHandlingCoordinator` directly.
Replace `Clerk.shared` usage with per-test `Clerk` instances to avoid
shared state across test cases. Add a helper that creates and
configures an isolated test instance for these tests.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (2)
Sources/ClerkKit/Core/Auth.swift (2)

655-663: ⚠️ Potential issue | 🟠 Major

Clear the pending verifier after ticket exchange succeeds.

The store is cleared immediately after /magic_links/complete; if signInWithTicket or signUpWithTicket then fails, the local recovery state is gone. Move magicLinkStore.clear() until after the ticket flow returns a TransferFlowResult.

🛠️ Minimal ordering fix
     )
 
     let completionResponse = try await apiClient.send(request).value
-    magicLinkStore.clear()
 
     let result: TransferFlowResult = switch pendingFlow.kind {
     case .signIn:
       try await .signIn(signInWithTicket(completionResponse.ticket))
     case .signUp:
       try await .signUp(signUpWithTicket(completionResponse.ticket))
     }
+
+    magicLinkStore.clear()
 
     if let sessionId = result.createdSessionId {
       try await activateSession(sessionId: sessionId)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKit/Core/Auth.swift` around lines 655 - 663, The magic link
store is being cleared too early (magicLinkStore.clear()) before the ticket
exchange flows complete, losing recovery state if signInWithTicket or
signUpWithTicket fails; move the magicLinkStore.clear() to after the
TransferFlowResult is produced so that pendingFlow handling (the switch on
pendingFlow.kind producing .signIn via signInWithTicket and .signUp via
signUpWithTicket) runs successfully before clearing the store, i.e., call
magicLinkStore.clear() only after the await of signInWithTicket/signUpWithTicket
completes and the result is returned.

627-641: ⚠️ Potential issue | 🟠 Major

Route public completion through the same coalescer.

handleMagicLinkCallback(_:) is deduped, but direct concurrent calls to public completeMagicLink(flowId:approvalToken:) can both load the same verifier before either clears it and double-post the completion request. Split the raw body into a private helper, have public completeMagicLink call handle(.magicLink(...)), and have route dispatch call the private helper.

Also applies to: 676-687

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKit/Core/Auth.swift` around lines 627 - 641, Public
completeMagicLink(flowId:approvalToken:) must route through the same coalescer
used by handleMagicLinkCallback(_:) to avoid concurrent double-posting; create a
private helper (e.g., completeMagicLinkRaw or completeMagicLinkInternal) that
contains the current body logic that reads magicLinkStore.load(),
trims/validates flowId and approvalToken, and performs the completion request,
then change public completeMagicLink to call
handle(.magicLink(flowId:resolvedApprovalToken:)) so the request is deduped by
the coalescer, and update handleMagicLinkCallback(_:) (route dispatch) to call
the new private helper directly to preserve existing behavior. Ensure you
reference and reuse magicLinkStore.load(), the TransferFlowResult return, and
existing error throws in the private helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ClerkKit/Core/Clerk.swift`:
- Around line 160-161: The callbackContinuation property (callbackContinuation)
must be nulled during cleanup: update resetManagerStateForCleanup() to set
callbackContinuation = nil, and likewise add the same clear in any other
manager-reset/cleanup functions that currently reset related state (the methods
that clear auth continuation/current flow around where other manager state is
reset) so a reused/test Clerk instance cannot retain a stale continuation.

In `@Sources/ClerkKit/Core/URLHandlingCoordinator.swift`:
- Around line 29-39: The deferred cleanup can overwrite a newer task if
cancelAll() clears and a new task is inserted before the old task completes;
modify the logic that sets and clears inFlightTasks so each insertion records a
unique entry id (e.g., a UUID or incrementing counter) alongside the Task in the
inFlightTasks dictionary, have the defer closure compare the stored entry id
before removing so it only nils the tuple it created, and ensure cancelAll()
still cancels and removes all entries by clearing the whole dictionary; update
references to inFlightTasks, the route key, the local task variable, and the
defer removal to use and check this entry id.

In
`@Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift`:
- Around line 204-223: In sendLink (in SignInFactorOneEmailLinkView) the guards
that check clerk.auth.currentSignIn and currentSignUp return early while leaving
deliveryState as .sending; update both guard failure branches to reset
deliveryState (e.g., set deliveryState = .idle) before clearing navigation.path
and returning so the UI isn't left in a sending state. Ensure you modify the
guard branches within the sendLink function that reference signIn and signUp to
set deliveryState back and then return.

In `@Tests/Domains/Auth/AuthTests.swift`:
- Around line 41-63: Update continuation state to be instance-scoped: change
production code in Auth.swift so that any writes use the owning Clerk instance's
callbackContinuation (e.g., assign to clerk.callbackContinuation or call
instance method setCallbackContinuation on that instance) instead of writing to
Clerk.shared; then update the test helper makeIsolatedClerk() to clear the
isolated clerk's continuation (call setCallbackContinuation(nil) or set
clerk.callbackContinuation = nil on the returned clerk) rather than clearing
Clerk.shared, and update the tests' assertions to check the isolated clerk
instance's callbackContinuation (or its instance setCallbackContinuation/getter)
instead of Clerk.shared so AuthView and tests align with instance-scoped state.

---

Duplicate comments:
In `@Sources/ClerkKit/Core/Auth.swift`:
- Around line 655-663: The magic link store is being cleared too early
(magicLinkStore.clear()) before the ticket exchange flows complete, losing
recovery state if signInWithTicket or signUpWithTicket fails; move the
magicLinkStore.clear() to after the TransferFlowResult is produced so that
pendingFlow handling (the switch on pendingFlow.kind producing .signIn via
signInWithTicket and .signUp via signUpWithTicket) runs successfully before
clearing the store, i.e., call magicLinkStore.clear() only after the await of
signInWithTicket/signUpWithTicket completes and the result is returned.
- Around line 627-641: Public completeMagicLink(flowId:approvalToken:) must
route through the same coalescer used by handleMagicLinkCallback(_:) to avoid
concurrent double-posting; create a private helper (e.g., completeMagicLinkRaw
or completeMagicLinkInternal) that contains the current body logic that reads
magicLinkStore.load(), trims/validates flowId and approvalToken, and performs
the completion request, then change public completeMagicLink to call
handle(.magicLink(flowId:resolvedApprovalToken:)) so the request is deduped by
the coalescer, and update handleMagicLinkCallback(_:) (route dispatch) to call
the new private helper directly to preserve existing behavior. Ensure you
reference and reuse magicLinkStore.load(), the TransferFlowResult return, and
existing error throws in the private helper.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d9f07fdd-4593-4344-ad92-ca70b401569c

📥 Commits

Reviewing files that changed from the base of the PR and between 96d39d0 and 67ac2cf.

📒 Files selected for processing (19)
  • Examples/Quickstart/Quickstart/ContentView.swift
  • Sources/ClerkKit/Core/Auth.swift
  • Sources/ClerkKit/Core/Clerk.swift
  • Sources/ClerkKit/Core/URLHandlingCoordinator.swift
  • Sources/ClerkKit/Domains/Auth/MagicLink.swift
  • Sources/ClerkKit/Domains/Auth/Session/Session+Tasks.swift
  • Sources/ClerkKit/Domains/Auth/SignIn/SignIn+Status.swift
  • Sources/ClerkKit/Domains/Auth/SignIn/SignIn.swift
  • Sources/ClerkKit/Domains/Auth/SignUp/SignUp+Status.swift
  • Sources/ClerkKit/Domains/Auth/SignUp/SignUp.swift
  • Sources/ClerkKit/Domains/Auth/TransferFlowResult.swift
  • Sources/ClerkKit/Events/AuthEvent.swift
  • Sources/ClerkKitUI/Components/Auth/AuthView.swift
  • Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift
  • Sources/ClerkKitUI/Extensions/Session+Ext.swift
  • Tests/Core/ClerkResponseClientStateTests.swift
  • Tests/Core/ClerkTests.swift
  • Tests/Domains/Auth/AuthTests.swift
  • Tests/Middleware/ClerkAuthEventEmitterResponseMiddlewareTests.swift
💤 Files with no reviewable changes (1)
  • Sources/ClerkKitUI/Extensions/Session+Ext.swift
✅ Files skipped from review due to trivial changes (1)
  • Sources/ClerkKit/Domains/Auth/SignUp/SignUp.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/ClerkKitUI/Components/Auth/AuthView.swift

Comment thread Sources/ClerkKit/Core/Clerk.swift
Comment thread Sources/ClerkKit/Core/URLHandlingCoordinator.swift Outdated
Comment thread Tests/Domains/Auth/AuthTests.swift
Clear pending callback continuations during cleanup and
use isolated `Clerk` instances in tests.

Also prevent stale URL handling tasks from removing newer
entries and reset email link delivery state when auth
context is missing.
@seanperez29
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 21, 2026

Only repository collaborators, contributors, or members can run CodeRabbit commands.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/ClerkKit/Core/Clerk.swift`:
- Around line 341-361: Update the documentation on public func handle(_ url:
URL) async throws -> Bool to explicitly state that unrecognized URLs return
false, but URLs that match Clerk callback structure (created via ClerkURLRoute /
MagicLinkCallback) but lack required parameters (e.g. flow_id or approval_token)
will throw a ClerkClientError; mention that such errors propagate from the
initializer (ClerkURLRoute or MagicLinkCallback) and from the subsequent call to
auth.handle(route).

In
`@Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift`:
- Around line 234-246: The openEmailApp() method uses
UIApplication.shared.canOpenURL(url) with the "mailto:" scheme which will fail
on iOS9+ unless integrators add "mailto" to LSApplicationQueriesSchemes; update
the method to either (A) document in README/Docs that integrators must add
"mailto" to LSApplicationQueriesSchemes, referencing openEmailApp() and
ClerkClientError for the error path, or (B) remove the canOpenURL check and call
UIApplication.shared.open(url, options: [:]) with the completion handler and set
error = ClerkClientError(...) only if the completion's success == false; choose
one approach and apply it consistently in openEmailApp().
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2db22fd9-d065-4c69-a1f3-b753121a1184

📥 Commits

Reviewing files that changed from the base of the PR and between 67ac2cf and 25309b9.

📒 Files selected for processing (6)
  • Sources/ClerkKit/Core/Clerk.swift
  • Sources/ClerkKit/Core/URLHandlingCoordinator.swift
  • Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift
  • Tests/Core/ClerkResponseClientStateTests.swift
  • Tests/Core/ClerkTests.swift
  • Tests/Domains/Auth/AuthTests.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Tests/Core/ClerkTests.swift

Comment thread Sources/ClerkKit/Core/Clerk.swift
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift (2)

234-245: Past feedback addressed — canOpenURL replaced with open+completion.

openEmailApp() now calls UIApplication.shared.open(url, options:) and reacts to the success parameter, which does not require mailto to be listed in LSApplicationQueriesSchemes. One small note: the completion handler is invoked on the main thread, but assigning error from a non-@MainActor function inherits the enclosing isolation — since EmailLinkVerificationView is a View, SwiftUI property mutations from that closure are fine, but you may want to annotate openEmailApp() with @MainActor for consistency with sendLink() / sendInitialLinkIfNeeded().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift`
around lines 234 - 245, Annotate openEmailApp() with `@MainActor` to ensure UI
state mutations are performed on the main actor consistently with other methods
(e.g., sendLink() and sendInitialLinkIfNeeded()) in EmailLinkVerificationView;
update the openEmailApp() declaration to add the `@MainActor` attribute so
assignments to the error property from the UIApplication.shared.open completion
handler are guaranteed to run on the main thread and conform to the view's
isolation.

192-194: Prefer a typed accessor on SignUp mirroring firstFactorVerification on SignIn, rather than dictionary access with magic strings.

The double-optional chaining verifications["email_address"]??.strategy is fragile—a typo in the string literal or upstream changes to the dictionary value type would silently fail or return nil indefinitely. SignUp currently lacks a clean typed accessor for email verification state; consider adding emailVerification: Verification? to match the pattern established in SignIn. Alternatively, call the existing emailVerificationStrategy computed property from SignUp+Ext.swift.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift`
around lines 192 - 194, Replace the fragile dictionary lookup
`verifications["email_address"]??.strategy` with a typed accessor on SignUp
(e.g., add `var emailVerification: Verification?` to the SignUp model mirroring
`SignIn.firstFactorVerification`) or call the existing
`emailVerificationStrategy` computed property in `SignUp+Ext.swift`; update the
usage in `SignInFactorOneEmailLinkView` to read the typed accessor or
`emailVerificationStrategy` instead of the string-keyed dictionary to avoid
double-optional chaining and magic-string fragility.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift`:
- Around line 236-242: Replace the hard-coded user-facing error strings in
SignInFactorOneEmailLinkView where ClerkClientError is constructed (both the
pre-return assignment and inside the UIApplication.shared.open completion) with
localized values: use String(localized:bundle:) (iOS 15+) or NSLocalizedString
routed to the package bundle (bundle: .module) and reference the matching
Localizable key so the message uses the localized entry instead of a raw String.
- Around line 189-202: The current alreadySent boolean only checks strategy ==
.emailLink and thus treats expired/failed/consumed verifications as already
sent; change the check to include verification.status == .unverified as well
(for signIn: clerk.auth.currentSignIn?.firstFactorVerification?.strategy ==
.emailLink && clerk.auth.currentSignIn?.firstFactorVerification?.status ==
.unverified; for signUp:
clerk.auth.currentSignUp?.verifications["email_address"]??.strategy ==
.emailLink && clerk.auth.currentSignUp?.verifications["email_address"]??.status
== .unverified) so deliveryState is only set to .sent for an active unverified
email link, or alternatively always call sendLink()/sendEmailLink() on the first
appearance to force a fresh PKCE verifier; update the alreadySent logic
accordingly and keep deliveryState/sendLink flow unchanged.

---

Nitpick comments:
In
`@Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift`:
- Around line 234-245: Annotate openEmailApp() with `@MainActor` to ensure UI
state mutations are performed on the main actor consistently with other methods
(e.g., sendLink() and sendInitialLinkIfNeeded()) in EmailLinkVerificationView;
update the openEmailApp() declaration to add the `@MainActor` attribute so
assignments to the error property from the UIApplication.shared.open completion
handler are guaranteed to run on the main thread and conform to the view's
isolation.
- Around line 192-194: Replace the fragile dictionary lookup
`verifications["email_address"]??.strategy` with a typed accessor on SignUp
(e.g., add `var emailVerification: Verification?` to the SignUp model mirroring
`SignIn.firstFactorVerification`) or call the existing
`emailVerificationStrategy` computed property in `SignUp+Ext.swift`; update the
usage in `SignInFactorOneEmailLinkView` to read the typed accessor or
`emailVerificationStrategy` instead of the string-keyed dictionary to avoid
double-optional chaining and magic-string fragility.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 9a2e5c11-2324-4cb3-b39a-5a31758db4ab

📥 Commits

Reviewing files that changed from the base of the PR and between 25309b9 and bd08699.

📒 Files selected for processing (2)
  • Sources/ClerkKit/Core/Clerk.swift
  • Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift
🚧 Files skipped from review as they are similar to previous changes (1)
  • Sources/ClerkKit/Core/Clerk.swift

Comment thread Sources/ClerkKitUI/Components/Auth/SignIn/SignInFactorOneEmailLinkView.swift Outdated
Use the sign-up email verification accessor when checking whether an
email link has already been sent, and require the verification to
still be unverified before suppressing another send.

Also keep the email app failure error update on the main actor.
@seanperez29
Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@seanperez29 seanperez29 marked this pull request as draft April 24, 2026 00:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant