Skip to content

Commit 03e2bba

Browse files
authored
fix(expo): Load SSO deps via require to fix Metro async-chunk failure (#8720)
1 parent b295af3 commit 03e2bba

3 files changed

Lines changed: 116 additions & 7 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/expo': patch
3+
---
4+
5+
Fix `useSSO()` in Expo apps that hit module loading failures when starting an SSO flow under Metro.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { renderHook } from '@testing-library/react';
2+
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
3+
4+
import { useSSO } from '../useSSO';
5+
6+
const mocks = vi.hoisted(() => {
7+
return {
8+
useSignIn: vi.fn(),
9+
useSignUp: vi.fn(),
10+
openAuthSessionAsync: vi.fn(),
11+
};
12+
});
13+
14+
vi.mock('@clerk/react/legacy', () => {
15+
return {
16+
useSignIn: mocks.useSignIn,
17+
useSignUp: mocks.useSignUp,
18+
};
19+
});
20+
21+
vi.mock('react-native', () => {
22+
return {
23+
Platform: {
24+
OS: 'ios',
25+
},
26+
};
27+
});
28+
29+
vi.mock('expo-web-browser', () => {
30+
return {
31+
openAuthSessionAsync: mocks.openAuthSessionAsync,
32+
};
33+
});
34+
35+
// expo-auth-session is intentionally left unmocked: it cannot be require()'d in this environment,
36+
// which exercises the dependency-load failure path (the bug behind #8288). Only the error-path
37+
// test reaches that require(); the other tests return before it, so they are unaffected.
38+
39+
describe('useSSO', () => {
40+
const mockSignIn = {
41+
create: vi.fn(),
42+
};
43+
44+
const mockSignUp = {
45+
create: vi.fn(),
46+
createdSessionId: null,
47+
};
48+
49+
const mockSetActive = vi.fn();
50+
51+
beforeEach(() => {
52+
vi.clearAllMocks();
53+
54+
mocks.useSignIn.mockReturnValue({
55+
signIn: mockSignIn,
56+
setActive: mockSetActive,
57+
isLoaded: true,
58+
});
59+
60+
mocks.useSignUp.mockReturnValue({
61+
signUp: mockSignUp,
62+
isLoaded: true,
63+
});
64+
});
65+
66+
afterEach(() => {
67+
vi.restoreAllMocks();
68+
});
69+
70+
test('returns the startSSOFlow function', () => {
71+
const { result } = renderHook(() => useSSO());
72+
73+
expect(typeof result.current.startSSOFlow).toBe('function');
74+
});
75+
76+
test('returns early without starting the flow when Clerk is not loaded', async () => {
77+
mocks.useSignIn.mockReturnValue({
78+
signIn: mockSignIn,
79+
setActive: mockSetActive,
80+
isLoaded: false,
81+
});
82+
83+
const { result } = renderHook(() => useSSO());
84+
85+
const response = await result.current.startSSOFlow({ strategy: 'oauth_google' });
86+
87+
expect(mockSignIn.create).not.toHaveBeenCalled();
88+
expect(mocks.openAuthSessionAsync).not.toHaveBeenCalled();
89+
expect(response.createdSessionId).toBe(null);
90+
});
91+
92+
test('surfaces the underlying error when an auth-session dependency fails to load', async () => {
93+
const { result } = renderHook(() => useSSO());
94+
95+
await expect(result.current.startSSOFlow({ strategy: 'oauth_google' })).rejects.toThrow(
96+
/required for SSO: .+\. If they are not installed/s,
97+
);
98+
});
99+
});

packages/expo/src/hooks/useSSO.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,22 @@ export function useSSO() {
4747
};
4848
}
4949

50-
// Dynamically import expo-auth-session and expo-web-browser only when needed
51-
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- dynamic import of optional dependency
50+
// Load via synchronous require() instead of import(): Metro inlines require() into the main
51+
// bundle, while import() emits an async chunk that fails to resolve without @expo/metro-runtime.
52+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- type-only annotation for optional dependency
5253
let AuthSession: typeof import('expo-auth-session');
53-
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- dynamic import of optional dependency
54+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- type-only annotation for optional dependency
5455
let WebBrowserModule: typeof import('expo-web-browser');
5556
try {
56-
[AuthSession, WebBrowserModule] = await Promise.all([import('expo-auth-session'), import('expo-web-browser')]);
57-
} catch {
57+
// eslint-disable-next-line @typescript-eslint/no-require-imports
58+
AuthSession = require('expo-auth-session');
59+
// eslint-disable-next-line @typescript-eslint/no-require-imports
60+
WebBrowserModule = require('expo-web-browser');
61+
} catch (err) {
5862
return errorThrower.throw(
59-
'expo-auth-session and expo-web-browser are required for SSO. ' +
60-
'Install them: npx expo install expo-auth-session expo-web-browser',
63+
`Unable to load expo-auth-session and expo-web-browser, which are required for SSO: ${
64+
err instanceof Error ? err.message : 'Unknown error'
65+
}. If they are not installed, run: npx expo install expo-auth-session expo-web-browser`,
6166
);
6267
}
6368

0 commit comments

Comments
 (0)