Skip to content

Commit 854bd52

Browse files
authored
fix(expo): Resolve Android native modules optionally (#9203)
1 parent 858a689 commit 854bd52

4 files changed

Lines changed: 63 additions & 4 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 a crash on Android in Expo Go where rendering `<ClerkProvider>` failed with `Cannot find native module 'ClerkExpo'`, even for JavaScript-only flows that use no native components.

packages/expo/src/specs/NativeClerkGoogleSignIn.android.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { requireNativeModule } from 'expo';
1+
import { requireOptionalNativeModule } from 'expo';
22

33
type NativeMap = Record<string, unknown>;
44

@@ -10,4 +10,6 @@ interface Spec {
1010
signOut(): Promise<void>;
1111
}
1212

13-
export default requireNativeModule<Spec>('ClerkGoogleSignIn');
13+
// Optional so getNativeModule() surfaces its actionable error rather than
14+
// throwing at import time.
15+
export default requireOptionalNativeModule<Spec>('ClerkGoogleSignIn');

packages/expo/src/specs/NativeClerkModule.android.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { requireNativeModule } from 'expo';
1+
import { requireOptionalNativeModule } from 'expo';
22

33
interface Spec {
44
// Exposed by Expo Modules EventEmitter for internal native client change events.
@@ -14,4 +14,5 @@ interface Spec {
1414
): Promise<void>;
1515
}
1616

17-
export default requireNativeModule<Spec>('ClerkExpo');
17+
// Optional so it resolves to null in Expo Go instead of throwing at import time.
18+
export default requireOptionalNativeModule<Spec>('ClerkExpo');
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { beforeEach, describe, expect, test, vi } from 'vitest';
2+
3+
const mocks = vi.hoisted(() => ({
4+
available: {} as Record<string, unknown>,
5+
}));
6+
7+
// Only returns a module when one is registered under the exact name requested.
8+
vi.mock('expo', () => ({
9+
requireNativeModule: (name: string) => {
10+
const nativeModule = mocks.available[name];
11+
if (!nativeModule) {
12+
throw new Error(`Cannot find native module '${name}'`);
13+
}
14+
return nativeModule;
15+
},
16+
requireOptionalNativeModule: (name: string) => mocks.available[name] ?? null,
17+
}));
18+
19+
// Path is a variable because TypeScript cannot resolve the platform suffix in a
20+
// dynamic import.
21+
const importSpec = async (path: string) => (await import(path)).default;
22+
23+
const importSpecs = async () => ({
24+
clerkExpo: await importSpec('../NativeClerkModule.android'),
25+
googleSignIn: await importSpec('../NativeClerkGoogleSignIn.android'),
26+
});
27+
28+
describe('android native module specs', () => {
29+
beforeEach(() => {
30+
vi.resetModules();
31+
mocks.available = {};
32+
});
33+
34+
test('resolve to null instead of throwing when not compiled in (Expo Go)', async () => {
35+
const { clerkExpo, googleSignIn } = await importSpecs();
36+
37+
expect(clerkExpo).toBeNull();
38+
expect(googleSignIn).toBeNull();
39+
});
40+
41+
test('resolve the module registered under the expected name (development build)', async () => {
42+
const clerkExpoModule = { configure: vi.fn() };
43+
const googleSignInModule = { signIn: vi.fn() };
44+
mocks.available = { ClerkExpo: clerkExpoModule, ClerkGoogleSignIn: googleSignInModule };
45+
46+
const { clerkExpo, googleSignIn } = await importSpecs();
47+
48+
expect(clerkExpo).toBe(clerkExpoModule);
49+
expect(googleSignIn).toBe(googleSignInModule);
50+
});
51+
});

0 commit comments

Comments
 (0)