Skip to content

feat(auth): Google sign-in across Android, iOS, and desktop#175

Open
plusmobileapps wants to merge 1 commit into
mainfrom
feat/google-sign-in
Open

feat(auth): Google sign-in across Android, iOS, and desktop#175
plusmobileapps wants to merge 1 commit into
mainfrom
feat/google-sign-in

Conversation

@plusmobileapps
Copy link
Copy Markdown
Collaborator

Summary

Adds a Continue with Google button to the auth screen, wired up natively on all three targets via Supabase's signInWith(IDToken):

  • Android: Credential Manager + googleid library
  • iOS: Swift bridge calling GoogleSignIn-iOS SDK
  • Desktop (JVM): system browser + loopback HTTP server on localhost:<random-port> (RFC 8252, with PKCE)

GoogleSignInProvider is an expect class in client/auth/data/impl. Each actual returns a (idToken, rawNonce) pair that SupabaseAuthenticationRepository hands to Supabase. The raw nonce → SHA-256 → ID-token nonce claim chain prevents replay attacks — Supabase verifies it on its side.

Cancellation is modelled as Result.success(GoogleSignInOutcome.Cancelled) so the UI just clears the loading dialog without navigating forward or showing an error.

Required setup before this works

Detailed walkthrough in docs/google-sign-in-setup.md. The short version:

  1. Google Cloud Console — create 4 OAuth client IDs: Web (for Supabase), Android, iOS, Desktop.
  2. Supabase dashboard — Auth → Providers → Google → paste Web client ID/secret + add Android/iOS/Desktop client IDs to "Authorized Client IDs".
  3. local.properties — add google.webClientId, google.desktopClientId, google.desktopClientSecret.
  4. Xcode (one-time) — File → Add Package Dependencies → https://github.com/google/GoogleSignIn-iOS, add GoogleSignInBridge.swift to the iosApp target, add GIDClientID + reverse-client URL scheme to Info.plist.

Until those are done, the button shows a "not configured" error on each platform — nothing else breaks.

Notable design choices

  • Activity holder pattern for Android — MyApplication registers an ActivityLifecycleCallbacks that populates a CurrentActivityHolder singleton, so the provider can resolve a resumed Activity at call time without baking it into the DI graph lifetime.
  • Swift owns nonce generation on iOS — CryptoKit gives us SHA-256 for free; avoids pulling cinterop / native crypto into Kotlin/Native.
  • Apple skipped intentionally — adding it on Android requires resurrecting the deeplink plumbing removed in feat(auth): replace email-confirm deeplink with OTP code #160. Plan is to add Apple as a follow-up PR (iOS native + Desktop loopback only, mirroring how Spotify/Slack handle it).
  • build/ excluded from ktfmt — newly-generated BuildConfig.kt fields were causing ktfmtCheck to fail on a generated file. The convention plugin now skips all build/ outputs, not just paths matching generated.

Test plan

  • ./gradlew ktfmtCheck — passes
  • ./gradlew :client:auth:data:impl:compileKotlinJvm :client:auth:data:impl:compileDebugKotlinAndroid :client:auth:data:impl:compileKotlinIosSimulatorArm64 — all three compile
  • ./gradlew :client:composeApp:compileKotlinJvm :client:composeApp:compileDebugKotlinAndroid :client:composeApp:compileKotlinIosSimulatorArm64 — all compile
  • ./gradlew :client:auth:ui:impl:test — new AuthenticationViewModelGoogleTest covers success / cancelled / failure cases
  • Manual: Android — installDebug → tap "Continue with Google" → Credential Manager sheet → sign in → land on main
  • Manual: iOS — open Xcode, add GoogleSignIn SPM dep + Info.plist entries, run on simulator → tap "Continue with Google" → consent sheet → sign in
  • Manual: Desktop — ./gradlew :client:composeApp:run → tap "Continue with Google" → system browser opens → sign in → browser auto-closes → land on main

🤖 Generated with Claude Code

Adds a "Continue with Google" button to the auth screen that drives
Supabase's signInWith(IDToken) using a platform-specific native flow:

- Android: Credential Manager + googleid library
- iOS: Swift bridge calling GoogleSignIn-iOS (SPM dep added in Xcode)
- Desktop: system browser + loopback HTTP server (RFC 8252, with PKCE)

The provider is an `expect class GoogleSignInProvider` in
`client/auth/data/impl`, with each actual returning a (idToken, rawNonce)
pair that SupabaseAuthenticationRepository hands to Supabase. The raw
nonce → SHA-256 → ID-token-nonce-claim chain prevents replay attacks.

Cancellation is modelled as `Result.success(GoogleSignInOutcome.Cancelled)`
so the UI clears loading without navigating or showing an error dialog.

Setup (Google Cloud client IDs, Supabase provider config, Xcode SDK install)
is documented in docs/google-sign-in-setup.md.

Also widens the ktfmt exclusion pattern to all `build/` outputs so newly
generated BuildKonfig fields don't fail formatting checks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

@Provides
@SingleIn(AppScope::class)
fun provideGoogleSignInProvider(): GoogleSignInProvider = GoogleSignInProvider()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

can an expect/actual function be created to create the google sign in provider? that way it can be called directly in the common main component to avoid declaring this in each platform component?

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.

2 participants