Implement federated sign-in buttons and associated logic for Google and GitHub#3
Implement federated sign-in buttons and associated logic for Google and GitHub#3brionmario wants to merge 1 commit into
Conversation
📝 WalkthroughWalkthroughThis PR adds a federated/social login flow using a browser-based redirect bridge ( ChangesFederated Authentication Flow
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant User
participant SignIn as BaseSignIn
participant Session as FederatedAuthSession
participant Browser as CustomTabsIntent
User->>SignIn: taps Google/GitHub button
SignIn->>SignIn: handleSignInResponse (FlowStatus.INCOMPLETE, REDIRECTION)
SignIn->>Session: launch(context, redirectURL)
Session->>Browser: open redirect URL
Browser-->>User: complete provider login
User-->>Session: deep-link callback triggers onRedirect(uri)
Session-->>SignIn: return callback Uri
SignIn->>SignIn: extract code, build EmbeddedSignInPayload
SignIn->>SignIn: resume handleSignInResponse with follow-up response
Related Issues: None found. Related PRs: None found. Suggested labels: feature, android Suggested reviewers: None found. 🐰 A rabbit hops through browser doors, 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (3)
src/main/kotlin/dev/thunderid/compose/components/presentation/auth/adapters/OutlinedTriggerButton.kt (1)
34-35: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider setting
role = Role.Buttonfor proper a11y semantics.
clickable(onClick = onClick)doesn't setrole, so accessibility services may not announce this as a button. Optional polish.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/kotlin/dev/thunderid/compose/components/presentation/auth/adapters/OutlinedTriggerButton.kt` around lines 34 - 35, The clickable element in OutlinedTriggerButton should expose proper button semantics for accessibility. Update the Modifier chain around clickable(onClick = onClick) and the semantics block so the component explicitly sets role = Role.Button while keeping the existing contentDescription based on label. Use the OutlinedTriggerButton composable as the place to make this a11y fix.src/main/kotlin/dev/thunderid/compose/components/presentation/auth/SignIn.kt (2)
78-89: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueProvider detection via label/ref substring match is fragile.
Matching
hint.contains("google"/"github", ignoreCase = true)against a concatenation ofrefand localizedlabelis a heuristic. Ifrefdoesn't reliably encode a stable provider id, orlabelis server-localized, this could misclassify actions and fall back to the genericOutlinedTriggerButton, losing correct provider branding. Prefer a dedicated provider/type field if the backend contract can provide one.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/kotlin/dev/thunderid/compose/components/presentation/auth/SignIn.kt` around lines 78 - 89, Provider detection in SignIn.kt is currently based on substring matching against the concatenated ref/label hint, which is fragile and can misclassify actions. Update the SignIn composable’s trigger rendering logic to use a stable provider/type field from the action model instead of deriving Google/GitHub from localized text, and keep the fallback OutlinedTriggerButton only for truly unknown providers.
210-211: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueSwallowing
CancellationExceptionfor a business signal is fragile.Treating "browser dismissed" as a
CancellationExceptionconflates it with genuine structured-concurrency cancellation (e.g., the composable leaving composition mid-flow viarememberCoroutineScope()). Silently catching and discarding it here means a real external cancellation (not user dismissal) would also be swallowed without cleanup/propagation. Consider havingFederatedAuthSessionsignal user-dismissal via a dedicated result/sealed type rather than throwingCancellationException, so genuine coroutine cancellation is still cooperatively rethrown.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/main/kotlin/dev/thunderid/compose/components/presentation/auth/SignIn.kt` around lines 210 - 211, The `catch (e: CancellationException)` in the sign-in flow is using coroutine cancellation as a business signal, which can hide real structured-concurrency cancellation. Update the `SignIn` authentication handling so browser dismissal is represented by a dedicated result/sealed outcome from `FederatedAuthSession` instead of throwing `CancellationException`, and keep genuine coroutine cancellations cooperatively propagated/rethrown. Make sure the branch that currently swallows the exception distinguishes user dismissal from actual cancellation and preserves any required cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/main/kotlin/dev/thunderid/android/auth/FederatedAuthSession.kt`:
- Around line 23-32: The launch flow in FederatedAuthSession.launch can suspend
forever because deferred.await() has no fallback if neither onRedirect nor
cancelIfPending runs. Update launch to guard the await with a timeout safety net
(for example around the CompletableDeferred<Uri> await path), so pending is
still cleared in the finally block even when the browser is backgrounded and the
callback never arrives.
- Around line 23-32: The FederatedAuthSession.launch method leaves pending stale
if Uri.parse(redirectUrl) or CustomTabsIntent.Builder().build().launchUrl(...)
throws before the current try/finally runs. Move the parsing and launch steps
inside the try block around deferred.await() so pending is always cleared in the
finally path, and let the exception propagate normally to the caller; use the
launch, pending, and deferred symbols to update the flow safely.
- Around line 20-42: The singleton state in FederatedAuthSession is vulnerable
to overlapping launch() calls because the shared pending deferred can be
overwritten and leave the first coroutine hanging. Update launch() to explicitly
guard against an in-flight session by either rejecting a new call when pending
is already set or cancelling the existing session before assigning the new
CompletableDeferred, and make sure onRedirect() and cancelIfPending() continue
to clear the active session consistently.
- Line 26: The redirect launch in FederatedAuthSession.launchUrl currently
accepts a generic Context, which can fail when called from a non-Activity
context. Update the API to either require an Activity for the context parameter
or add Intent.FLAG_ACTIVITY_NEW_TASK before calling
CustomTabsIntent.Builder().build().launchUrl, and adjust any callers or helper
methods in FederatedAuthSession accordingly.
In
`@src/main/kotlin/dev/thunderid/compose/components/presentation/auth/adapters/OutlinedTriggerButton.kt`:
- Around line 22-41: OutlinedTriggerButton currently has no enabled/loading
guard, so trigger actions can still be tapped while a sign-in is in flight. Add
an enabled parameter to OutlinedTriggerButton and pass it through to the
clickable modifier, then thread enabled = !signInState.isLoading from the
SignIn.kt call sites for GoogleButton and GitHubButton so trigger submissions
and Custom Tab launches are blocked during loading; use the existing
OutlinedTriggerButton, GoogleButton, and GitHubButton symbols to update all
relevant call paths consistently.
---
Nitpick comments:
In
`@src/main/kotlin/dev/thunderid/compose/components/presentation/auth/adapters/OutlinedTriggerButton.kt`:
- Around line 34-35: The clickable element in OutlinedTriggerButton should
expose proper button semantics for accessibility. Update the Modifier chain
around clickable(onClick = onClick) and the semantics block so the component
explicitly sets role = Role.Button while keeping the existing contentDescription
based on label. Use the OutlinedTriggerButton composable as the place to make
this a11y fix.
In
`@src/main/kotlin/dev/thunderid/compose/components/presentation/auth/SignIn.kt`:
- Around line 78-89: Provider detection in SignIn.kt is currently based on
substring matching against the concatenated ref/label hint, which is fragile and
can misclassify actions. Update the SignIn composable’s trigger rendering logic
to use a stable provider/type field from the action model instead of deriving
Google/GitHub from localized text, and keep the fallback OutlinedTriggerButton
only for truly unknown providers.
- Around line 210-211: The `catch (e: CancellationException)` in the sign-in
flow is using coroutine cancellation as a business signal, which can hide real
structured-concurrency cancellation. Update the `SignIn` authentication handling
so browser dismissal is represented by a dedicated result/sealed outcome from
`FederatedAuthSession` instead of throwing `CancellationException`, and keep
genuine coroutine cancellations cooperatively propagated/rethrown. Make sure the
branch that currently swallows the exception distinguishes user dismissal from
actual cancellation and preserves any required cleanup.
🪄 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: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 43b6ba38-0c56-4357-b4c7-74decc3103ce
📒 Files selected for processing (9)
build.gradle.ktssrc/main/kotlin/dev/thunderid/android/Models.ktsrc/main/kotlin/dev/thunderid/android/auth/FederatedAuthSession.ktsrc/main/kotlin/dev/thunderid/compose/components/presentation/auth/SignIn.ktsrc/main/kotlin/dev/thunderid/compose/components/presentation/auth/SignUp.ktsrc/main/kotlin/dev/thunderid/compose/components/presentation/auth/adapters/GitHubButton.ktsrc/main/kotlin/dev/thunderid/compose/components/presentation/auth/adapters/GoogleButton.ktsrc/main/kotlin/dev/thunderid/compose/components/presentation/auth/adapters/OutlinedTriggerButton.ktsrc/main/kotlin/dev/thunderid/compose/components/presentation/organization/LanguageSwitcher.kt
| object FederatedAuthSession { | ||
| private var pending: CompletableDeferred<Uri>? = null | ||
|
|
||
| suspend fun launch(context: Context, redirectUrl: String): Uri { | ||
| val deferred = CompletableDeferred<Uri>() | ||
| pending = deferred | ||
| CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(redirectUrl)) | ||
| return try { | ||
| deferred.await() | ||
| } finally { | ||
| if (pending === deferred) pending = null | ||
| } | ||
| } | ||
|
|
||
| fun onRedirect(uri: Uri) { | ||
| pending?.complete(uri) | ||
| pending = null | ||
| } | ||
|
|
||
| fun cancelIfPending() { | ||
| pending?.cancel() | ||
| pending = null | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Singleton pending is not safe against overlapping launch calls.
Since FederatedAuthSession is a singleton with a single mutable pending field, a second concurrent launch() call (e.g. rapid double-tap on a provider button, or launching Google then GitHub before the first redirect returns) silently overwrites pending, orphaning the first deferred forever (it never completes and its awaiting coroutine hangs). Consider guarding against concurrent launches (e.g., cancel any existing pending session before starting a new one, or throw/reject if one is already in flight).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/kotlin/dev/thunderid/android/auth/FederatedAuthSession.kt` around
lines 20 - 42, The singleton state in FederatedAuthSession is vulnerable to
overlapping launch() calls because the shared pending deferred can be
overwritten and leave the first coroutine hanging. Update launch() to explicitly
guard against an in-flight session by either rejecting a new call when pending
is already set or cancelling the existing session before assigning the new
CompletableDeferred, and make sure onRedirect() and cancelIfPending() continue
to clear the active session consistently.
| suspend fun launch(context: Context, redirectUrl: String): Uri { | ||
| val deferred = CompletableDeferred<Uri>() | ||
| pending = deferred | ||
| CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(redirectUrl)) | ||
| return try { | ||
| deferred.await() | ||
| } finally { | ||
| if (pending === deferred) pending = null | ||
| } | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
No timeout on deferred.await().
If the user backgrounds the browser tab without returning to the app (so neither onRedirect nor cancelIfPending from onResume fires), the suspended coroutine awaits indefinitely, leaking the coroutine and leaving pending set. Relying entirely on the hosting Activity's onResume to call cancelIfPending is fragile; consider adding a timeout (withTimeout) as a safety net.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/kotlin/dev/thunderid/android/auth/FederatedAuthSession.kt` around
lines 23 - 32, The launch flow in FederatedAuthSession.launch can suspend
forever because deferred.await() has no fallback if neither onRedirect nor
cancelIfPending runs. Update launch to guard the await with a timeout safety net
(for example around the CompletableDeferred<Uri> await path), so pending is
still cleared in the finally block even when the browser is backgrounded and the
callback never arrives.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Uncaught launchUrl/Uri.parse failure leaves pending in a stale, leaked state.
If Uri.parse(redirectUrl) throws (malformed URL) or launchUrl(...) throws ActivityNotFoundException (no Custom Tabs–capable browser installed), the exception propagates before the try/finally block is reached. pending remains set to the never-completing deferred forever. A subsequent launch call overwrites it (leaking the deferred silently), and cancelIfPending/onRedirect invoked by the caller in the meantime would operate on the wrong/stale entry. The launch call needs to be inside the try block so pending is always cleared on failure, and the exception should propagate to the caller.
🐛 Proposed fix
suspend fun launch(context: Context, redirectUrl: String): Uri {
val deferred = CompletableDeferred<Uri>()
pending = deferred
- CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(redirectUrl))
return try {
+ CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(redirectUrl))
deferred.await()
} finally {
if (pending === deferred) pending = null
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| suspend fun launch(context: Context, redirectUrl: String): Uri { | |
| val deferred = CompletableDeferred<Uri>() | |
| pending = deferred | |
| CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(redirectUrl)) | |
| return try { | |
| deferred.await() | |
| } finally { | |
| if (pending === deferred) pending = null | |
| } | |
| } | |
| suspend fun launch(context: Context, redirectUrl: String): Uri { | |
| val deferred = CompletableDeferred<Uri>() | |
| pending = deferred | |
| return try { | |
| CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(redirectUrl)) | |
| deferred.await() | |
| } finally { | |
| if (pending === deferred) pending = null | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/kotlin/dev/thunderid/android/auth/FederatedAuthSession.kt` around
lines 23 - 32, The FederatedAuthSession.launch method leaves pending stale if
Uri.parse(redirectUrl) or CustomTabsIntent.Builder().build().launchUrl(...)
throws before the current try/finally runs. Move the parsing and launch steps
inside the try block around deferred.await() so pending is always cleared in the
finally path, and let the exception propagate normally to the caller; use the
launch, pending, and deferred symbols to update the flow safely.
| suspend fun launch(context: Context, redirectUrl: String): Uri { | ||
| val deferred = CompletableDeferred<Uri>() | ||
| pending = deferred | ||
| CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(redirectUrl)) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
Does CustomTabsIntent.launchUrl require an Activity context or does it add FLAG_ACTIVITY_NEW_TASK internally in androidx.browser 1.8.0?
💡 Result:
The CustomTabsIntent.launchUrl method in androidx.browser 1.8.0 requires an Activity context because it internally calls startActivity on the provided context [1][2][3]. It does not automatically add the FLAG_ACTIVITY_NEW_TASK flag to the intent [3][4]. If you call launchUrl with a non-Activity context (such as an Application context), the system will throw an android.util.AndroidRuntimeException stating: "Calling startActivity from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag" [3][4]. To resolve this when you must launch a Custom Tab from a non-Activity context, you can manually add the flag to the CustomTabsIntent's underlying intent before calling launchUrl: customTabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); customTabsIntent.launchUrl(context, uri); Alternatively, it is recommended to pass an Activity context whenever possible to ensure proper task stack management [3][5].
Citations:
- 1: https://android.googlesource.com/platform/frameworks/support/+/a9ac247af2afd4115c3eb6d16c05bc92737d6305/browser/src/main/java/androidx/browser/customtabs/CustomTabsIntent.java
- 2: https://developer.android.com/reference/kotlin/androidx/browser/customtabs/CustomTabsIntent
- 3: https://stackoverflow.com/questions/71436533/error-on-load-chrome-custom-tab-in-android-11
- 4: https://stackoverflow.com/questions/71101624/i-am-trying-to-open-custom-chrome-tab-from-nested-recycler-view-but-couldnt-hav
- 5: https://developer.chrome.com/blog/custom-tabs-android-11
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the target file and nearby context.
FILE="src/main/kotlin/dev/thunderid/android/auth/FederatedAuthSession.kt"
echo "== File outline =="
ast-grep outline "$FILE" --view expanded || true
echo
echo "== Relevant lines =="
nl -ba "$FILE" | sed -n '1,120p'
echo
echo "== Search for FederatedAuthSession call sites / context sources =="
rg -n "FederatedAuthSession|launchUrl\\(|CustomTabsIntent" src main . --glob '!**/build/**' || trueRepository: thunder-id/android-sdks
Length of output: 1969
launchUrl() needs an Activity context or FLAG_ACTIVITY_NEW_TASK.
This accepts any Context, but a non-Activity caller can crash when launchUrl() delegates to startActivity(). Narrow the parameter to Activity, or add Intent.FLAG_ACTIVITY_NEW_TASK before launching.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/main/kotlin/dev/thunderid/android/auth/FederatedAuthSession.kt` at line
26, The redirect launch in FederatedAuthSession.launchUrl currently accepts a
generic Context, which can fail when called from a non-Activity context. Update
the API to either require an Activity for the context parameter or add
Intent.FLAG_ACTIVITY_NEW_TASK before calling
CustomTabsIntent.Builder().build().launchUrl, and adjust any callers or helper
methods in FederatedAuthSession accordingly.
| internal fun OutlinedTriggerButton( | ||
| label: String, | ||
| modifier: Modifier = Modifier, | ||
| icon: (@Composable () -> Unit)? = null, | ||
| onClick: () -> Unit, | ||
| ) { | ||
| Row( | ||
| verticalAlignment = Alignment.CenterVertically, | ||
| horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), | ||
| modifier = modifier | ||
| .fillMaxWidth() | ||
| .border(BorderStroke(1.dp, Color.Gray.copy(alpha = 0.4f)), RoundedCornerShape(8.dp)) | ||
| .clickable(onClick = onClick) | ||
| .semantics { contentDescription = label } | ||
| .padding(horizontal = 16.dp, vertical = 12.dp), | ||
| ) { | ||
| icon?.invoke() | ||
| BasicText(label) | ||
| } | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
No enabled/loading control — risk of duplicate non-idempotent submissions.
OutlinedTriggerButton always accepts clicks; there's no way to disable it while a submission/redirection is in flight. In SignIn.kt, TRIGGER actions render this button (and GoogleButton/GitHubButton, which wrap it) without gating on signInState.isLoading, unlike the BaseSignInButton branch which explicitly passes isLoading. A rapid double-tap can fire two concurrent signIn() calls / launch two Custom Tabs for the same federated action.
💡 Proposed fix: add an `enabled` param
internal fun OutlinedTriggerButton(
label: String,
modifier: Modifier = Modifier,
icon: (`@Composable` () -> Unit)? = null,
+ enabled: Boolean = true,
onClick: () -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally),
modifier = modifier
.fillMaxWidth()
- .border(BorderStroke(1.dp, Color.Gray.copy(alpha = 0.4f)), RoundedCornerShape(8.dp))
- .clickable(onClick = onClick)
+ .border(BorderStroke(1.dp, Color.Gray.copy(alpha = 0.4f)), RoundedCornerShape(8.dp))
+ .clickable(enabled = enabled, onClick = onClick)
.semantics { contentDescription = label }
.padding(horizontal = 16.dp, vertical = 12.dp),
) {Then thread enabled = !signInState.isLoading through GoogleButton/GitHubButton/OutlinedTriggerButton call sites in SignIn.kt.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| internal fun OutlinedTriggerButton( | |
| label: String, | |
| modifier: Modifier = Modifier, | |
| icon: (@Composable () -> Unit)? = null, | |
| onClick: () -> Unit, | |
| ) { | |
| Row( | |
| verticalAlignment = Alignment.CenterVertically, | |
| horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), | |
| modifier = modifier | |
| .fillMaxWidth() | |
| .border(BorderStroke(1.dp, Color.Gray.copy(alpha = 0.4f)), RoundedCornerShape(8.dp)) | |
| .clickable(onClick = onClick) | |
| .semantics { contentDescription = label } | |
| .padding(horizontal = 16.dp, vertical = 12.dp), | |
| ) { | |
| icon?.invoke() | |
| BasicText(label) | |
| } | |
| } | |
| internal fun OutlinedTriggerButton( | |
| label: String, | |
| modifier: Modifier = Modifier, | |
| icon: (`@Composable` () -> Unit)? = null, | |
| enabled: Boolean = true, | |
| onClick: () -> Unit, | |
| ) { | |
| Row( | |
| verticalAlignment = Alignment.CenterVertically, | |
| horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), | |
| modifier = modifier | |
| .fillMaxWidth() | |
| .border(BorderStroke(1.dp, Color.Gray.copy(alpha = 0.4f)), RoundedCornerShape(8.dp)) | |
| .clickable(enabled = enabled, onClick = onClick) | |
| .semantics { contentDescription = label } | |
| .padding(horizontal = 16.dp, vertical = 12.dp), | |
| ) { | |
| icon?.invoke() | |
| BasicText(label) | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/main/kotlin/dev/thunderid/compose/components/presentation/auth/adapters/OutlinedTriggerButton.kt`
around lines 22 - 41, OutlinedTriggerButton currently has no enabled/loading
guard, so trigger actions can still be tapped while a sign-in is in flight. Add
an enabled parameter to OutlinedTriggerButton and pass it through to the
clickable modifier, then thread enabled = !signInState.isLoading from the
SignIn.kt call sites for GoogleButton and GitHubButton so trigger submissions
and Custom Tab launches are blocked during loading; use the existing
OutlinedTriggerButton, GoogleButton, and GitHubButton symbols to update all
relevant call paths consistently.
…
Purpose
Approach
Related Issues
Related PRs
Checklist
breaking changelabel added.Security checks
Summary by CodeRabbit
New Features
Bug Fixes