@@ -25,7 +25,12 @@ import {
2525} from "@/auth/billing" ;
2626import { refreshBillingEntitlement as retryBillingEntitlement } from "@/auth/billing-handoff" ;
2727import { authStorageKey , supabase } from "@/auth/client" ;
28- import { buildSignInUrl , type SignInMethod } from "@/auth/sign-in" ;
28+ import {
29+ buildSignInUrl ,
30+ lastSignInMethodStorageKey ,
31+ parseLastSignInMethod ,
32+ type SignInMethod ,
33+ } from "@/auth/sign-in" ;
2934import {
3035 captureAnalytics ,
3136 identifyAnalytics ,
@@ -37,6 +42,7 @@ import {
3742 captureOperationalError ,
3843 setErrorReportingUser ,
3944} from "@/lib/error-reporting" ;
45+ import { useMountEffect } from "@/lib/use-mount-effect" ;
4046import { suspendMobileSync } from "@/sync/mobile-sync" ;
4147import { updateWatchAccount } from "@/watch-connectivity" ;
4248
@@ -46,6 +52,7 @@ export type AuthState = {
4652 session : Session | null ;
4753 billing : BillingInfo ;
4854 billingReady : boolean ;
55+ lastSignInMethod : SignInMethod | null ;
4956 signIn : ( method : SignInMethod ) => Promise < void > ;
5057 refreshBilling : ( ) => Promise < boolean > ;
5158 signOut : ( ) => Promise < void > ;
@@ -102,13 +109,16 @@ function parseAuthCallbackUrl(
102109// Deep links can be delivered twice (auth-session result + Linking event);
103110// mirror desktop's 5s dedupe window (apps/desktop/src/auth/deeplink.ts).
104111const RECENT_CALLBACK_WINDOW_MS = 5_000 ;
105- const inFlightTokens = new Set < string > ( ) ;
112+ const inFlightTokens = new Map < string , Promise < boolean > > ( ) ;
106113const recentTokens = new Map < string , number > ( ) ;
107114
108- function acceptAuthTokens ( accessToken : string , refreshToken : string ) : void {
115+ function acceptAuthTokens (
116+ accessToken : string ,
117+ refreshToken : string ,
118+ ) : Promise < boolean > {
109119 const client = supabase ;
110120 if ( ! client ) {
111- return ;
121+ return Promise . resolve ( false ) ;
112122 }
113123
114124 const now = Date . now ( ) ;
@@ -119,16 +129,18 @@ function acceptAuthTokens(accessToken: string, refreshToken: string): void {
119129 }
120130
121131 const key = `${ accessToken } \n${ refreshToken } ` ;
122- if ( inFlightTokens . has ( key ) || recentTokens . has ( key ) ) {
123- return ;
132+ if ( recentTokens . has ( key ) ) {
133+ return Promise . resolve ( true ) ;
134+ }
135+ const inFlight = inFlightTokens . get ( key ) ;
136+ if ( inFlight ) {
137+ return inFlight ;
124138 }
125139
126- inFlightTokens . add ( key ) ;
127- void client . auth
140+ const request = client . auth
128141 . setSession ( { access_token : accessToken , refresh_token : refreshToken } )
129142 . then (
130143 ( { error } ) => {
131- inFlightTokens . delete ( key ) ;
132144 if ( error ) {
133145 captureOperationalError ( error , {
134146 operation : "auth_session_set" ,
@@ -138,12 +150,12 @@ function acceptAuthTokens(accessToken: string, refreshToken: string): void {
138150 method : "browser_handoff" ,
139151 failure_stage : "set_session" ,
140152 } ) ;
141- } else {
142- recentTokens . set ( key , Date . now ( ) ) ;
153+ return false ;
143154 }
155+ recentTokens . set ( key , Date . now ( ) ) ;
156+ return true ;
144157 } ,
145158 ( error ) => {
146- inFlightTokens . delete ( key ) ;
147159 captureOperationalError ( error , {
148160 operation : "auth_session_set" ,
149161 tags : { method : "browser_handoff" } ,
@@ -152,15 +164,20 @@ function acceptAuthTokens(accessToken: string, refreshToken: string): void {
152164 method : "browser_handoff" ,
153165 failure_stage : "set_session" ,
154166 } ) ;
167+ return false ;
155168 } ,
156- ) ;
169+ )
170+ . finally ( ( ) => inFlightTokens . delete ( key ) ) ;
171+ inFlightTokens . set ( key , request ) ;
172+ return request ;
157173}
158174
159- function handleAuthCallbackUrl ( url : string ) : void {
175+ function handleAuthCallbackUrl ( url : string ) : Promise < boolean > {
160176 const tokens = parseAuthCallbackUrl ( url ) ;
161- if ( tokens ) {
162- acceptAuthTokens ( tokens . accessToken , tokens . refreshToken ) ;
177+ if ( ! tokens ) {
178+ return Promise . resolve ( false ) ;
163179 }
180+ return acceptAuthTokens ( tokens . accessToken , tokens . refreshToken ) ;
164181}
165182
166183// Offline fallback: a retryable getSession error must not lock a Pro user out
@@ -201,6 +218,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
201218 const [ session , setSessionState ] = useState < Session | null | undefined > (
202219 bypass ? null : undefined ,
203220 ) ;
221+ const [ lastSignInMethod , setLastSignInMethod ] = useState < SignInMethod | null > (
222+ null ,
223+ ) ;
204224 const sessionRef = useRef < Session | null | undefined > ( session ) ;
205225 const identifiedUserIdRef = useRef < string | null > ( null ) ;
206226 const reportedUndecodableUserIdRef = useRef < string | null > ( null ) ;
@@ -242,6 +262,26 @@ export function AuthProvider({ children }: { children: ReactNode }) {
242262 }
243263 } , [ ] ) ;
244264
265+ useMountEffect ( ( ) => {
266+ let active = true ;
267+ void AsyncStorage . getItem ( lastSignInMethodStorageKey ) . then (
268+ ( value ) => {
269+ if ( active ) {
270+ setLastSignInMethod ( parseLastSignInMethod ( value ) ) ;
271+ }
272+ } ,
273+ ( error ) => {
274+ captureOperationalError ( error , {
275+ operation : "auth_last_sign_in_method_read" ,
276+ level : "warning" ,
277+ } ) ;
278+ } ,
279+ ) ;
280+ return ( ) => {
281+ active = false ;
282+ } ;
283+ } ) ;
284+
245285 useEffect ( ( ) => {
246286 const client = supabase ;
247287 if ( ! client ) {
@@ -311,12 +351,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
311351 }
312352
313353 const subscription = Linking . addEventListener ( "url" , ( event ) => {
314- handleAuthCallbackUrl ( event . url ) ;
354+ void handleAuthCallbackUrl ( event . url ) ;
315355 } ) ;
316356 void Linking . getInitialURL ( ) . then (
317357 ( url ) => {
318358 if ( url ) {
319- handleAuthCallbackUrl ( url ) ;
359+ void handleAuthCallbackUrl ( url ) ;
320360 }
321361 } ,
322362 ( error ) => {
@@ -348,7 +388,19 @@ export function AuthProvider({ children }: { children: ReactNode }) {
348388 "anarlog://auth/callback" ,
349389 ) ;
350390 if ( result . type === "success" ) {
351- handleAuthCallbackUrl ( result . url ) ;
391+ const signedIn = await handleAuthCallbackUrl ( result . url ) ;
392+ if ( signedIn ) {
393+ setLastSignInMethod ( signInMethod ) ;
394+ await AsyncStorage . setItem (
395+ lastSignInMethodStorageKey ,
396+ signInMethod ,
397+ ) . catch ( ( error ) => {
398+ captureOperationalError ( error , {
399+ operation : "auth_last_sign_in_method_write" ,
400+ level : "warning" ,
401+ } ) ;
402+ } ) ;
403+ }
352404 } else {
353405 captureAnalytics ( "auth_failed" , {
354406 method : "browser_handoff" ,
@@ -464,6 +516,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
464516 session : session ?? null ,
465517 billing,
466518 billingReady,
519+ lastSignInMethod,
467520 signIn,
468521 refreshBilling,
469522 signOut,
@@ -474,6 +527,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
474527 session ,
475528 billing ,
476529 billingReady ,
530+ lastSignInMethod ,
477531 signIn ,
478532 refreshBilling ,
479533 signOut ,
0 commit comments