feat(mobile): native login & account creation (Apple, Google, email OTP)#4470
Conversation
Add createSignInCode/verifyAndConsumeSignInCode helpers (sha256(email:code) hash, 10-min expiry, one live code per email, 5-attempt lockout), a sendSignInCodeEmail template, and the /api/auth/native/otp route that issues codes with an anti-enumeration-safe response.
…UPDATE The attempts >= 5 pre-check reads a snapshot; two concurrent wrong guesses could both pass it and race past the budget. Adding attempts < 5 to the atomic consume UPDATE's WHERE guarantees an over-budget code can never consume, regardless of racing increments.
Adds POST /api/auth/native/token: verifies Apple/Google native ID tokens or an email sign-in code, then creates/updates the user and mints the same API token shape as the device-auth poll endpoint. Response contract is frozen for the parallel mobile client work.
The native token-exchange route was collapsing every apple/google verification failure into 401 INVALID_TOKEN, including JWKS-fetch and network errors that are server faults, not bad tokens. Rethrow anything that isn't NativeIdTokenError/AppleJwtClientError so it surfaces as a 500. Also cover the empty GOOGLE_IOS_CLIENT_ID audience-filtering case and email_verified: undefined for Google.
Installs @react-native-google-signin/google-signin and wires optional GOOGLE_WEB_CLIENT_ID/GOOGLE_IOS_CLIENT_ID config so the native Google button (added in a follow-up) can be gated on config presence before the OAuth clients exist. The config plugin is only registered when GOOGLE_IOS_CLIENT_ID is set, so prebuild keeps working without it.
Replace the login screen's idle state with native Apple/Google sign-in buttons and an email-code flow, backed by the frozen native-auth token endpoints, while keeping the browser device-auth flow as a "More sign-in options" fallback.
Native Apple/Google token exchange called createOrUpdateUser directly, which skips the SSO-domain enforcement, blacklist, and blocked-TLD checks that live inline in the NextAuth web signIn callback. This let a user on a mandatory-SSO domain sign in via native Google/Apple and get a bearer token, bypassing forced SSO. Extract the blacklist/TLD/SSO logic shared by email-signin-eligibility into checkDomainSignInEligibility and call it from both native token route branches before createOrUpdateUser. checkEmailSignInEligibility now delegates to it while keeping its rate-limit-first ordering and exact response shapes unchanged. Also lowercase the client-supplied email in the native token route's email branch before building args, since NextAuth normalizes casing on the web path and this is a public endpoint.
GoogleSignin is a Swift static lib that imports GoogleUtilities and RecaptchaInterop (pulled alongside expo-iap's AppCheckCore); those pods don't define modules, so pod install fails during prebuild. Force module maps on them via expo-build-properties. Unconditional because the google-signin pod autolinks whether or not the OAuth client is configured.
react-native-css maps textAlign to a native prop for TextInput and crashes (path.split is not a function) when a text-center class is present, so the OTP screen rendered a redbox. Apply textAlign via inline style instead, keeping the centered/large/spaced digit styling. Also log the previously swallowed signIn error in verifyEmailCode so on-device auth failures are diagnosable via Sentry.
The restore link sat directly under the Kilo Pass card near the top of the Profile screen, which read as jarring. Move it to its own row below the Notifications card. The button still self-hides on Android and is gated to personal (non-org) accounts.
The e2e guide only covered iOS. Add the one-time Android toolchain setup (command-line tools, JDK 17, AVD), the emulator/dev-client run workflow, and the Android-specific gotchas (adb reverse for host services, the exp+kilo-app dev-client deep link, pm clear to reset auth, WEB_BASE_URL for the browser fallback). Also note the Maestro MCP→CLI fallback and a11y-label matching, which apply to both platforms.
…es guard - Add the multi-color Google G logo and match the native Apple button exactly (44pt height, cornerRadius 8, full width, matching font size). - Call GoogleSignin.hasPlayServices() before signIn so Android surfaces the update-Play-Services prompt instead of a generic failure. - Lay out the profile footer actions (Support, Privacy, Sign Out, Delete) as a 2x2 tile grid matching the page's card language. - Note in e2e/AGENTS.md that local e2e screenshots should be attached to the PR.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryThe previously-flagged migration issue is resolved and no new issues were found in this incremental update. Resolved Since Last Review
Files Reviewed (2 files)
Previous Review Summaries (3 snapshots, latest commit ef791a1)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit ef791a1)Status: 1 Issue Found | Recommendation: Address before merge Executive SummaryAll 9 previously-flagged issues (2 CRITICAL, 5 WARNING, 2 SUGGESTION) were fixed in this update; the new migration adds a non- Overview
Issue Details (click to expand)WARNING
Resolved Since Last Review
Files Reviewed (26 files)
Fix these issues in Kilo Cloud Previous review (commit a2bec83)Status: 9 Issues Found | Recommendation: Address before merge Executive SummaryExpired/invalid Apple and Google ID tokens will surface as 500s instead of the frozen Overview
Issue Details (click to expand)CRITICAL
WARNING
SUGGESTION
Files Reviewed (32 files)
Fix these issues in Kilo Cloud Previous review (commit e75ce54)Status: 9 Issues Found | Recommendation: Address before merge Executive SummaryExpired/invalid Apple and Google ID tokens will surface as 500s instead of the frozen Overview
Issue Details (click to expand)CRITICAL
WARNING
SUGGESTION
Files Reviewed (28 files)
Reviewed by claude-sonnet-5 · Input: 24 · Output: 7K · Cached: 551.7K Review guidance: REVIEW.md from base branch |
The 19px semibold label rendered visibly larger than the Apple button's native label. Drop to 17px medium and shrink the G mark to 18px so the two buttons read as a matched pair.
What
Adds native login & account creation to the mobile app so users can sign in inside the app instead of being bounced to the browser:
AppleAuthenticationButton, ID-token exchange.@react-native-google-signin, ID-token exchange. Button hides itself until the Google client IDs are configured.The existing browser device-auth flow stays as the "More sign-in options" fallback.
How it works
Native clients obtain a provider credential on-device, then POST it to a new token-exchange endpoint that verifies it server-side and mints the same API token as the browser device-auth poll:
POST /api/auth/native/otp— issues an email sign-in code (rate-limited).POST /api/auth/native/token— verifies an Apple/Google ID token or an email code, runs SSO/blacklist eligibility checks, creates-or-updates the user, returns{ token }.Verification helpers:
lib/auth/apple-jwks.ts— Apple JWKS fetch + JWT verify (audiencecom.kilocode.kiloapp).lib/auth/native-id-tokens.ts— Apple/Google ID-token verification; Google audience = web + iOS client IDs.lib/auth/email-signin-eligibility.ts— shared SSO-enforcement / domain-blacklist checks.lib/auth/magic-link-tokens.ts— sign-in code issuance + atomic consume with an attempt budget.Provider-outage vs. invalid-token is distinguished so a JWKS/network failure surfaces as
500, not a misleading401 INVALID_TOKEN.Response contract (frozen — mobile is built against it)
Database
New migration
0180: addsattemptstomagic_link_tokens(backs the email-code attempt budget).Config / env
Backend (Vercel):
GOOGLE_CLIENT_ID— web/server client, verifies Google ID-token audience (already set).GOOGLE_IOS_CLIENT_ID— iOS client, added to the accepted audience list.Mobile (EAS/Expo):
GOOGLE_WEB_CLIENT_ID,GOOGLE_IOS_CLIENT_ID— passed toGoogleSignin.configure(). When unset, the Google button hides itself and prebuild still works (the google-signin config plugin is only registered when the iOS client ID exists).All three OAuth clients live in GCP project
1000529458754.google-services.jsonremains the separate Firebase project (FCM push only) — google-signin v16 uses Credential Manager +webClientId, so it does not read the OAuth client from that file.UI polish
GoogleSignin.hasPlayServices()is called beforesignIn()so Android surfaces the "update Play Services" prompt instead of a cryptic failure.Testing
native/otp,native/token,native-id-tokens,magic-link-tokens) — all passing.DEVELOPER_ERROR(code10) — a GCP-console config step, not a code issue (webClientIdmatches the backend audience; the error surfaces when Google validates the app signature, before any backend call). See the manual step below — each build signature (dev/EAS/Play) needs its SHA-1 registered.Screenshots
iOS: matched Apple/Google buttons · iOS Google sheet · Android branded Google button · Android native Google flow. (Attached below.)
Manual steps before merge
GOOGLE_IOS_CLIENT_IDis set on the Vercel backend andGOOGLE_WEB_CLIENT_ID/GOOGLE_IOS_CLIENT_IDon EAS....-ca74j3lf..., packagecom.kilocode.kiloapp) in GCP project1000529458754— without them Android Google sign-in fails withDEVELOPER_ERROR(reproduced above). Register:5E:8F:16:06:2E:A3:CD:2C:4A:0D:54:78:76:BA:A6:F3:8C:AB:F6:25(standard Android debug keystore — unblocks all local dev builds).eas credentials(Android) / Play Console → App integrity.