@@ -85,6 +85,7 @@ const mockCodeChallenge = 'mock-code-challenge-_';
8585describe ( 'useHostedAuth' , ( ) => {
8686 const mockClient = {
8787 lastActiveSessionId : null as string | null ,
88+ sessions : [ ] as Array < { id : string } > ,
8889 reload : vi . fn ( ) ,
8990 fromJSON : vi . fn ( ) ,
9091 } ;
@@ -128,8 +129,10 @@ describe('useHostedAuth', () => {
128129 mocks . platformOS = 'ios' ;
129130 mocks . expoConfig = undefined ;
130131 mockClient . lastActiveSessionId = null ;
132+ mockClient . sessions = [ ] ;
131133 mockClient . fromJSON . mockImplementation ( ( clientJSON : ClientJSON ) => {
132134 mockClient . lastActiveSessionId = clientJSON . last_active_session_id ;
135+ mockClient . sessions = clientJSON . sessions . map ( session => ( { id : session . id } ) ) ;
133136 return mockClient ;
134137 } ) ;
135138 mocks . makeRedirectUri . mockReturnValue ( 'myapp:///hosted-auth-callback' ) ;
@@ -253,6 +256,28 @@ describe('useHostedAuth', () => {
253256 expect ( mocks . makeRedirectUri ) . not . toHaveBeenCalled ( ) ;
254257 } ) ;
255258
259+ test ( 'uses Expo universal-link callbacks for HTTPS redirect URLs' , async ( ) => {
260+ const redirectUrl = 'https://mobile.example.com/hosted-auth-callback' ;
261+ mockHostedAuthResponse ( ) ;
262+ mocks . openAuthSessionAsync . mockResolvedValue ( {
263+ type : 'success' ,
264+ url : `${ redirectUrl } ?state=state-123&rotating_token_nonce=nonce-123&created_session_id=sess_123` ,
265+ } ) ;
266+ mockHostedAuthRedeemResponse ( { lastActiveSessionId : 'sess_123' } ) ;
267+
268+ const { result } = renderHook ( ( ) => useHostedAuth ( ) ) ;
269+ await result . current . startHostedAuth ( {
270+ redirectUrl,
271+ state : 'state-123' ,
272+ authSessionOptions : { showInRecents : true } ,
273+ } ) ;
274+
275+ expect ( mocks . openAuthSessionAsync ) . toHaveBeenCalledWith ( 'https://example.accounts.dev/sign-in' , redirectUrl , {
276+ preferUniversalLinks : true ,
277+ showInRecents : true ,
278+ } ) ;
279+ } ) ;
280+
256281 test ( 'falls back to the reloaded client session when callback session id is absent' , async ( ) => {
257282 mockHostedAuthResponse ( ) ;
258283 mocks . openAuthSessionAsync . mockResolvedValue ( {
@@ -270,7 +295,7 @@ describe('useHostedAuth', () => {
270295 expect ( response . client ) . toBe ( mockClient ) ;
271296 } ) ;
272297
273- test ( 'does not activate a session when the callback does not return one ' , async ( ) => {
298+ test ( 'rejects a redeemed client response without a created session ' , async ( ) => {
274299 mockHostedAuthResponse ( ) ;
275300 mocks . openAuthSessionAsync . mockResolvedValue ( {
276301 type : 'success' ,
@@ -279,15 +304,17 @@ describe('useHostedAuth', () => {
279304 mockHostedAuthRedeemResponse ( ) ;
280305
281306 const { result } = renderHook ( ( ) => useHostedAuth ( ) ) ;
282- const response = await result . current . startHostedAuth ( { state : 'state-123' } ) ;
307+
308+ await expect ( result . current . startHostedAuth ( { state : 'state-123' } ) ) . rejects . toThrow (
309+ 'Hosted auth completion did not include the created session.' ,
310+ ) ;
283311
284312 expect ( mockFapiRequest ) . toHaveBeenNthCalledWith ( 2 , expect . objectContaining ( { path : '/client' } ) ) ;
285313 expect ( mockUpdateClient ) . toHaveBeenCalledWith ( mockClient ) ;
286314 expect ( mockSetActive ) . not . toHaveBeenCalled ( ) ;
287- expect ( response . createdSessionId ) . toBeNull ( ) ;
288315 } ) ;
289316
290- test ( 'does not fall back to an existing active session when callback session params are missing ' , async ( ) => {
317+ test ( 'rejects a successful callback without a rotating token nonce ' , async ( ) => {
291318 mockClient . lastActiveSessionId = 'sess_existing' ;
292319 mockHostedAuthResponse ( ) ;
293320 mocks . openAuthSessionAsync . mockResolvedValue ( {
@@ -296,11 +323,29 @@ describe('useHostedAuth', () => {
296323 } ) ;
297324
298325 const { result } = renderHook ( ( ) => useHostedAuth ( ) ) ;
299- const response = await result . current . startHostedAuth ( { state : 'state-123' } ) ;
326+
327+ await expect ( result . current . startHostedAuth ( { state : 'state-123' } ) ) . rejects . toThrow (
328+ 'Hosted auth callback did not include a rotating token nonce.' ,
329+ ) ;
300330
301331 expect ( mockFapiRequest ) . toHaveBeenCalledTimes ( 1 ) ;
302332 expect ( mockSetActive ) . not . toHaveBeenCalled ( ) ;
303- expect ( response . createdSessionId ) . toBeNull ( ) ;
333+ } ) ;
334+
335+ test ( 'rejects a callback session that is absent from the redeemed client' , async ( ) => {
336+ mockHostedAuthResponse ( ) ;
337+ mocks . openAuthSessionAsync . mockResolvedValue ( {
338+ type : 'success' ,
339+ url : 'myapp:///hosted-auth-callback?state=state-123&rotating_token_nonce=nonce-123&created_session_id=sess_other' ,
340+ } ) ;
341+ mockHostedAuthRedeemResponse ( { lastActiveSessionId : 'sess_123' } ) ;
342+
343+ const { result } = renderHook ( ( ) => useHostedAuth ( ) ) ;
344+
345+ await expect ( result . current . startHostedAuth ( { state : 'state-123' } ) ) . rejects . toThrow (
346+ 'Hosted auth completion did not include the created session.' ,
347+ ) ;
348+ expect ( mockSetActive ) . not . toHaveBeenCalled ( ) ;
304349 } ) ;
305350
306351 test ( 'surfaces browser session open failures' , async ( ) => {
@@ -486,7 +531,13 @@ function mockHostedAuthResponse(url = 'https://example.accounts.dev/sign-in') {
486531 } ) ;
487532}
488533
489- function mockHostedAuthRedeemResponse ( { lastActiveSessionId = null } : { lastActiveSessionId ?: string | null } = { } ) {
534+ function mockHostedAuthRedeemResponse ( {
535+ lastActiveSessionId = null ,
536+ sessionIds = lastActiveSessionId ? [ lastActiveSessionId ] : [ ] ,
537+ } : {
538+ lastActiveSessionId ?: string | null ;
539+ sessionIds ?: string [ ] ;
540+ } = { } ) {
490541 mockFapiRequest . mockResolvedValueOnce ( {
491542 ok : true ,
492543 status : 200 ,
@@ -495,7 +546,7 @@ function mockHostedAuthRedeemResponse({ lastActiveSessionId = null }: { lastActi
495546 response : {
496547 object : 'client' ,
497548 id : 'client_123' ,
498- sessions : [ ] ,
549+ sessions : sessionIds . map ( id => ( { id } ) ) ,
499550 sign_in : null ,
500551 sign_up : null ,
501552 last_active_session_id : lastActiveSessionId ,
0 commit comments