Skip to content

Commit 7ec4a16

Browse files
committed
feat(expo,clerk-js): core-3 expo compatibility
- Prevent DOM-based captcha from hanging in React Native environments - Make expo-auth-session and expo-web-browser optional via dynamic imports - Add SignedIn, SignedOut, and RedirectToTasks convenience components
1 parent 551fcc8 commit 7ec4a16

8 files changed

Lines changed: 91 additions & 15 deletions

File tree

.changeset/core-3-expo-release.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/expo': patch
4+
---
5+
6+
- Prevent DOM-based captcha from hanging in React Native environments
7+
- Make `expo-auth-session` and `expo-web-browser` optional via dynamic imports
8+
- Add `SignedIn`, `SignedOut`, and `RedirectToTasks` convenience components

packages/clerk-js/src/utils/captcha/CaptchaChallenge.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,9 @@ export class CaptchaChallenge {
8181
* managed by clerk-js itself.
8282
*/
8383
public async managedInModal(opts?: Partial<CaptchaOptions>) {
84+
if (typeof document === 'undefined') {
85+
return { captchaError: 'modal_component_not_ready' };
86+
}
8487
return this.managedOrInvisible({
8588
modalWrapperQuerySelector: '#cl-modal-captcha-wrapper',
8689
modalContainerQuerySelector: '#cl-modal-captcha-container',

packages/expo/package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@
139139
"expo-apple-authentication": {
140140
"optional": true
141141
},
142+
"expo-auth-session": {
143+
"optional": true
144+
},
142145
"expo-constants": {
143146
"optional": true
144147
},
@@ -150,6 +153,9 @@
150153
},
151154
"expo-secure-store": {
152155
"optional": true
156+
},
157+
"expo-web-browser": {
158+
"optional": true
153159
}
154160
},
155161
"engines": {
Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,22 @@
1-
export { ClerkLoaded, ClerkLoading, Show } from '@clerk/react';
1+
// Re-export control components from @clerk/react
2+
// These provide conditional rendering based on auth state
3+
export { ClerkLoaded, ClerkLoading, RedirectToTasks, Show } from '@clerk/react';
4+
5+
import { Show } from '@clerk/react';
6+
import type { PropsWithChildren, ReactNode } from 'react';
7+
8+
/**
9+
* Render children only when the user is signed in.
10+
* A convenience wrapper around `<Show when="signed-in">`.
11+
*/
12+
export function SignedIn({ children }: PropsWithChildren): ReactNode {
13+
return <Show when='signed-in'>{children}</Show>;
14+
}
15+
16+
/**
17+
* Render children only when the user is signed out.
18+
* A convenience wrapper around `<Show when="signed-out">`.
19+
*/
20+
export function SignedOut({ children }: PropsWithChildren): ReactNode {
21+
return <Show when='signed-out'>{children}</Show>;
22+
}

packages/expo/src/hooks/useOAuth.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useSignIn, useSignUp } from '@clerk/react/legacy';
22
import type { OAuthStrategy, SetActive, SignInResource, SignUpResource } from '@clerk/shared/types';
3-
import * as AuthSession from 'expo-auth-session';
4-
import * as WebBrowser from 'expo-web-browser';
3+
import type * as WebBrowser from 'expo-web-browser';
54

65
import { errorThrower } from '../utils/errors';
76

@@ -46,6 +45,20 @@ export function useOAuth(useOAuthParams: UseOAuthFlowParams) {
4645
};
4746
}
4847

48+
// Dynamically import expo-auth-session and expo-web-browser only when needed
49+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- dynamic import of optional dependency
50+
let AuthSession: typeof import('expo-auth-session');
51+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- dynamic import of optional dependency
52+
let WebBrowserModule: typeof import('expo-web-browser');
53+
try {
54+
[AuthSession, WebBrowserModule] = await Promise.all([import('expo-auth-session'), import('expo-web-browser')]);
55+
} catch {
56+
return errorThrower.throw(
57+
'expo-auth-session and expo-web-browser are required for OAuth. ' +
58+
'Install them: npx expo install expo-auth-session expo-web-browser',
59+
);
60+
}
61+
4962
// Create a redirect url for the current platform and environment.
5063
//
5164
// This redirect URL needs to be whitelisted for your Clerk production instance via
@@ -64,7 +77,7 @@ export function useOAuth(useOAuthParams: UseOAuthFlowParams) {
6477

6578
const { externalVerificationRedirectURL } = signIn.firstFactorVerification;
6679

67-
const authSessionResult = await WebBrowser.openAuthSessionAsync(
80+
const authSessionResult = await WebBrowserModule.openAuthSessionAsync(
6881
// @ts-ignore
6982
externalVerificationRedirectURL.toString(),
7083
oauthRedirectUrl,

packages/expo/src/hooks/useSSO.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import type {
66
SignInResource,
77
SignUpResource,
88
} from '@clerk/shared/types';
9-
import * as AuthSession from 'expo-auth-session';
10-
import * as WebBrowser from 'expo-web-browser';
9+
import type * as WebBrowser from 'expo-web-browser';
1110

1211
import { errorThrower } from '../utils/errors';
1312

@@ -48,6 +47,20 @@ export function useSSO() {
4847
};
4948
}
5049

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
52+
let AuthSession: typeof import('expo-auth-session');
53+
// eslint-disable-next-line @typescript-eslint/consistent-type-imports -- dynamic import of optional dependency
54+
let WebBrowserModule: typeof import('expo-web-browser');
55+
try {
56+
[AuthSession, WebBrowserModule] = await Promise.all([import('expo-auth-session'), import('expo-web-browser')]);
57+
} catch {
58+
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',
61+
);
62+
}
63+
5164
const { strategy, unsafeMetadata, authSessionOptions } = startSSOFlowParams ?? {};
5265

5366
/**
@@ -73,7 +86,7 @@ export function useSSO() {
7386
return errorThrower.throw('Missing external verification redirect URL for SSO flow');
7487
}
7588

76-
const authSessionResult = await WebBrowser.openAuthSessionAsync(
89+
const authSessionResult = await WebBrowserModule.openAuthSessionAsync(
7790
externalVerificationRedirectURL.toString(),
7891
redirectUrl,
7992
authSessionOptions,

packages/expo/src/provider/singleton/createClerkInstance.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { type Clerk, isClerkRuntimeError } from '@clerk/clerk-js';
1+
import { type Clerk } from '@clerk/clerk-js';
22
import type { BrowserClerk, HeadlessBrowserClerk } from '@clerk/react';
3-
import { is4xxError } from '@clerk/shared/error';
3+
import { is4xxError, isClerkRuntimeError } from '@clerk/shared/error';
44
import type {
55
ClientJSONSnapshot,
66
EnvironmentJSONSnapshot,
@@ -62,7 +62,7 @@ export function createClerkInstance(ClerkClass: typeof Clerk) {
6262

6363
const getToken = tokenCache.getToken;
6464
const saveToken = tokenCache.saveToken;
65-
__internal_clerk = new ClerkClass(publishableKey);
65+
__internal_clerk = new ClerkClass(publishableKey) as unknown as BrowserClerk;
6666

6767
if (Platform.OS === 'ios' || Platform.OS === 'android') {
6868
// @ts-expect-error - This is an internal API
@@ -104,11 +104,13 @@ export function createClerkInstance(ClerkClass: typeof Clerk) {
104104
};
105105

106106
if (createResourceCache) {
107+
const isClerkNetworkError = (err: unknown): boolean =>
108+
isClerkRuntimeError(err) && err.code === 'network_error';
109+
107110
const retryInitilizeResourcesFromFAPI = async () => {
108-
const isClerkNetworkError = (err: unknown) => isClerkRuntimeError(err) && err.code === 'network_error';
109111
try {
110112
await __internal_clerk?.__internal_reloadInitialResources();
111-
} catch (err) {
113+
} catch (err: unknown) {
112114
// Retry after 3 seconds if the error is a network error or a 5xx error
113115
if (isClerkNetworkError(err) || !is4xxError(err)) {
114116
// Retry after 2 seconds if the error is a network error
@@ -123,9 +125,12 @@ export function createClerkInstance(ClerkClass: typeof Clerk) {
123125
ClientResourceCache.init({ publishableKey, storage: createResourceCache });
124126
SessionJWTCache.init({ publishableKey, storage: createResourceCache });
125127

126-
__internal_clerk.addListener(({ client }) => {
128+
// At this point __internal_clerk is guaranteed to be defined (just created above)
129+
130+
const clerk = __internal_clerk;
131+
clerk.addListener(({ client }) => {
127132
// @ts-expect-error - This is an internal API
128-
const environment = __internal_clerk?.__internal_environment as EnvironmentResource;
133+
const environment = clerk?.__internal_environment as EnvironmentResource;
129134
if (environment) {
130135
void EnvironmentResourceCache.save(environment.__internal_toSnapshot());
131136
}
@@ -144,7 +149,7 @@ export function createClerkInstance(ClerkClass: typeof Clerk) {
144149
}
145150
});
146151

147-
__internal_clerk.__internal_getCachedResources = async (): Promise<{
152+
clerk.__internal_getCachedResources = async (): Promise<{
148153
client: ClientJSONSnapshot | null;
149154
environment: EnvironmentJSONSnapshot | null;
150155
}> => {

packages/expo/src/provider/singleton/singleton.web.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@ import type { BrowserClerk, HeadlessBrowserClerk } from '@clerk/react';
22

33
import type { BuildClerkOptions } from './types';
44

5+
// Augment the global Window type to include Clerk
6+
declare global {
7+
interface Window {
8+
Clerk?: HeadlessBrowserClerk | BrowserClerk;
9+
}
10+
}
11+
512
/**
613
* Access the existing Clerk instance from `window.Clerk` on the web.
714
* Unlike the native implementation, this does not create a new instance—it only returns the existing one set by ClerkProvider.

0 commit comments

Comments
 (0)