-
Notifications
You must be signed in to change notification settings - Fork 7
Update magic link flow and authentication UI #563
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
swolfand
wants to merge
17
commits into
main
Choose a base branch
from
sam/mobile-431-android-changes
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
b2d6251
feat(auth): add native email-link flows for AuthView
swolfand 159d82c
feat(ui): improve email-link check-email screens
swolfand 6d39091
fix(ui): hide instance logo when logo URL is missing
swolfand 57957bb
fix(android): restore user button visibility after native email-link
swolfand 90ec671
refactor(telemetry): inject runtime providers into compose telemetry env
swolfand 90afd24
wip
swolfand b6aa9f1
codex: fix CI failure on PR #563
swolfand 32b1873
codex: fix integration test collision on PR #563
swolfand 3796874
fix(api): track flow ID in native magic link to reject stale callbacks
swolfand 68ae584
fix(api): persist SSO callback URI and propagate magic link failures
swolfand d1f171c
fix(ui): prefer email-link factor and match factors by identifier
swolfand 297eb8c
fix(ui): propagate second-factor preparation errors to UI
swolfand 78062bb
fix(ui): resolve user button session state more robustly
swolfand 4d380e8
fix(android): use saved workbench config for native magic links
swolfand a71f114
fix(ui): expose accurate identifier autofill hints
swolfand dd2653a
fix(ui): collect sign-up fields before email link verification
swolfand 71a2c1d
codex: fix CI failure on PR #563
swolfand File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
55 changes: 55 additions & 0 deletions
55
source/api/src/main/kotlin/com/clerk/api/log/SafeUriLog.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| package com.clerk.api.log | ||
|
|
||
| import android.net.Uri | ||
|
|
||
| internal object SafeUriLog { | ||
| fun describe(uri: Uri?): String { | ||
| if (uri == null) return "uri=null" | ||
|
|
||
| val queryKeys = queryParamKeys(uri) | ||
| val fragmentKeys = fragmentParamKeys(uri.encodedFragment) | ||
| val allKeys = (queryKeys + fragmentKeys).sorted() | ||
|
|
||
| val port = if (uri.port == -1) "-" else uri.port.toString() | ||
| val path = uri.path ?: "-" | ||
| val scheme = uri.scheme ?: "-" | ||
| val host = uri.host ?: "-" | ||
|
|
||
| return buildString { | ||
| append("scheme=$scheme") | ||
| append(", host=$host") | ||
| append(", port=$port") | ||
| append(", path=$path") | ||
| append(", query_keys=") | ||
| append(queryKeys.sorted()) | ||
| append(", fragment_keys=") | ||
| append(fragmentKeys.sorted()) | ||
| append(", has_flow_id=") | ||
| append("flow_id" in allKeys) | ||
| append(", has_approval_token=") | ||
| append("approval_token" in allKeys) | ||
| append(", has_token=") | ||
| append("token" in allKeys) | ||
| append(", has_rotating_token_nonce=") | ||
| append("rotating_token_nonce" in allKeys) | ||
| } | ||
| } | ||
|
|
||
| private fun queryParamKeys(uri: Uri): Set<String> = | ||
| runCatching { uri.queryParameterNames.map { it.trim() }.filter { it.isNotEmpty() }.toSet() } | ||
| .getOrDefault(emptySet()) | ||
|
|
||
| private fun fragmentParamKeys(fragment: String?): Set<String> { | ||
| if (fragment.isNullOrBlank()) return emptySet() | ||
|
|
||
| return fragment | ||
| .split("&") | ||
| .mapNotNull { entry -> | ||
| val separator = entry.indexOf("=") | ||
| val rawKey = if (separator >= 0) entry.substring(0, separator) else entry | ||
| val key = Uri.decode(rawKey).trim() | ||
| key.takeIf { it.isNotEmpty() } | ||
| } | ||
| .toSet() | ||
| } | ||
| } |
78 changes: 78 additions & 0 deletions
78
source/api/src/main/kotlin/com/clerk/api/magiclink/NativeMagicLinkCompletionRunner.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| package com.clerk.api.magiclink | ||
|
|
||
| import com.clerk.api.Clerk | ||
| import com.clerk.api.network.ClerkApi | ||
| import com.clerk.api.network.model.error.ClerkErrorResponse | ||
| import com.clerk.api.network.model.magiclink.NativeMagicLinkCompleteRequest | ||
| import com.clerk.api.network.serialization.ClerkResult | ||
| import com.clerk.api.signin.SignIn | ||
|
|
||
| internal class NativeMagicLinkCompletionRunner( | ||
| private val attestationProvider: NativeMagicLinkAttestationProvider?, | ||
| private val clearPendingFlow: suspend () -> Unit, | ||
| private val activateCreatedSession: suspend (SignIn) -> ClerkResult<Unit, NativeMagicLinkError>, | ||
| private val refreshClientState: suspend () -> Unit, | ||
| ) { | ||
| suspend fun complete( | ||
| flowId: String, | ||
| approvalToken: String, | ||
| pending: PendingNativeMagicLinkFlow, | ||
| ): ClerkResult<SignIn, NativeMagicLinkError> { | ||
| val completeRequest = | ||
| NativeMagicLinkCompleteRequest( | ||
| flowId = flowId, | ||
| approvalToken = approvalToken, | ||
| codeVerifier = pending.codeVerifier, | ||
| attestation = attestationProvider?.attestation(), | ||
| ) | ||
|
|
||
| return when (val completeResult = ClerkApi.magicLink.complete(completeRequest.toFields())) { | ||
| is ClerkResult.Failure -> handleCompleteApiFailure(completeResult) | ||
| is ClerkResult.Success -> completeFromTicket(completeResult.value.ticket) | ||
| } | ||
| } | ||
|
|
||
| private suspend fun handleCompleteApiFailure( | ||
| completeResult: ClerkResult.Failure<ClerkErrorResponse> | ||
| ): ClerkResult.Failure<NativeMagicLinkError> { | ||
| val mapped = completeResult.toNativeMagicLinkError(NativeMagicLinkReason.COMPLETE_FAILED) | ||
| if (mapped.reasonCode in TERMINAL_REASON_CODES) { | ||
| clearPendingFlow() | ||
| } | ||
| NativeMagicLinkLogger.completeFailure(mapped.reasonCode) | ||
| return ClerkResult.apiFailure(mapped) | ||
| } | ||
|
|
||
| private suspend fun completeFromTicket( | ||
| ticket: String | ||
| ): ClerkResult<SignIn, NativeMagicLinkError> { | ||
| return when (val ticketSignInResult = Clerk.auth.signInWithTicket(ticket)) { | ||
| is ClerkResult.Failure -> { | ||
| clearPendingFlow() | ||
| val mapped = | ||
| ticketSignInResult.toNativeMagicLinkError(NativeMagicLinkReason.TICKET_SIGN_IN_FAILED) | ||
| NativeMagicLinkLogger.completeFailure(mapped.reasonCode) | ||
| ClerkResult.apiFailure(mapped) | ||
| } | ||
| is ClerkResult.Success -> completeAfterTicketSignIn(ticketSignInResult.value) | ||
| } | ||
| } | ||
|
|
||
| private suspend fun completeAfterTicketSignIn( | ||
| signIn: SignIn | ||
| ): ClerkResult<SignIn, NativeMagicLinkError> { | ||
| val activationResult = activateCreatedSession(signIn) | ||
| return if (activationResult is ClerkResult.Failure) { | ||
| clearPendingFlow() | ||
| val reasonCode = | ||
| activationResult.error?.reasonCode ?: NativeMagicLinkReason.SESSION_ACTIVATION_FAILED.code | ||
| NativeMagicLinkLogger.completeFailure(reasonCode) | ||
| ClerkResult.apiFailure(activationResult.error) | ||
| } else { | ||
| clearPendingFlow() | ||
| refreshClientState() | ||
| NativeMagicLinkLogger.completeSuccess() | ||
| ClerkResult.success(signIn) | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Potential null error passed to
apiFailurewhenactivationResult.erroris null.Line 67 handles potential null via
activationResult.error?.reasonCode, but line 70 passesactivationResult.errordirectly toapiFailure. If the error is null, the caller receives aFailurewith null error, which may cause downstream issues.Proposed fix
private suspend fun completeAfterTicketSignIn( signIn: SignIn ): ClerkResult<SignIn, NativeMagicLinkError> { val activationResult = activateCreatedSession(signIn) return if (activationResult is ClerkResult.Failure) { clearPendingFlow() val reasonCode = activationResult.error?.reasonCode ?: NativeMagicLinkReason.SESSION_ACTIVATION_FAILED.code NativeMagicLinkLogger.completeFailure(reasonCode) - ClerkResult.apiFailure(activationResult.error) + ClerkResult.apiFailure( + activationResult.error ?: NativeMagicLinkError(reasonCode = reasonCode) + ) } else {🤖 Prompt for AI Agents