diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index db377ee7cc..7dafdb12d6 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -118,6 +118,7 @@ import { EmailLinkError, EmailLinkErrorCode, Environment, + isClerkRuntimeError, Organization, Waitlist, } from './resources/internal'; @@ -190,6 +191,11 @@ export class Clerk implements ClerkInterface { #pageLifecycle: ReturnType | null = null; #touchThrottledUntil = 0; + public __internal_getEnvironment: (() => Promise) | undefined; + public __internal_getClient: (() => Promise) | undefined; + public __internal_setEnvironment: ((environment: EnvironmentResource) => Promise) | undefined; + public __internal_setClient: ((client: ClientResource) => Promise) | undefined; + public __internal_createPublicCredentials: | (( publicKey: PublicKeyCredentialCreationOptionsWithoutExtensions, @@ -1835,10 +1841,29 @@ export class Clerk implements ClerkInterface { }; #loadInNonStandardBrowser = async (): Promise => { - const [environment, client] = await Promise.all([ - Environment.getInstance().fetch({ touch: false }), - Client.getInstance().fetch(), - ]); + let environment, client; + try { + const [fetchedEnv, fetchedClient] = await Promise.all([ + Environment.getInstance().fetch({ touch: false }), + Client.getInstance().fetch(), + ]); + environment = fetchedEnv; + client = fetchedClient; + await this.__internal_setEnvironment?.(fetchedEnv); + await this.__internal_setClient?.(fetchedClient); + } catch (err) { + if (isClerkRuntimeError(err) && err.code === 'network_error') { + console.log('Clerk: using cached environment and client'); + environment = await this.__internal_getEnvironment?.(); + client = await this.__internal_getClient?.(); + } else { + throw err; + } + } + + if (!environment || !client) { + return false; + } this.updateClient(client); this.updateEnvironment(environment); diff --git a/packages/expo/package.json b/packages/expo/package.json index 782ca86349..79249d21ce 100644 --- a/packages/expo/package.json +++ b/packages/expo/package.json @@ -65,12 +65,14 @@ "@clerk/expo-passkeys": "workspace:*", "@clerk/shared": "workspace:*", "@clerk/types": "workspace:*", + "@react-native-async-storage/async-storage": "^2.1.0", "base-64": "^1.0.0", "react-native-url-polyfill": "2.0.0", "tslib": "2.4.1" }, "devDependencies": { "@clerk/eslint-config-custom": "workspace:*", + "@react-native-async-storage/async-storage": "^2.1.0", "@types/base-64": "^1.0.2", "@types/node": "^20.11.24", "@types/react": "18.3.12", @@ -83,6 +85,7 @@ "typescript": "*" }, "peerDependencies": { + "@react-native-async-storage/async-storage": ">=2", "expo-auth-session": ">=5", "expo-local-authentication": ">=13.5.0", "expo-secure-store": ">=12.4.0", diff --git a/packages/expo/src/provider/singleton/createClerkInstance.ts b/packages/expo/src/provider/singleton/createClerkInstance.ts index 006f2f15d5..7d860b21b6 100644 --- a/packages/expo/src/provider/singleton/createClerkInstance.ts +++ b/packages/expo/src/provider/singleton/createClerkInstance.ts @@ -5,6 +5,7 @@ import type { PublicKeyCredentialCreationOptionsWithoutExtensions, PublicKeyCredentialRequestOptionsWithoutExtensions, } from '@clerk/types'; +import AsyncStorage from '@react-native-async-storage/async-storage'; import { Platform } from 'react-native'; import { MemoryTokenCache } from '../../cache/MemoryTokenCache'; @@ -81,6 +82,28 @@ export function createClerkInstance(ClerkClass: typeof Clerk) { __internal_clerk.__internal_isWebAuthnPlatformAuthenticatorSupported = () => { return Promise.resolve(true); }; + + // @ts-expect-error - This is an internal API + __internal_clerk.__internal_setClient = async (client: any) => { + await AsyncStorage.setItem('clerk_client', JSON.stringify(client)); + }; + + // @ts-expect-error - This is an internal API + __internal_clerk.__internal_getClient = async () => { + const client = await AsyncStorage.getItem('clerk_client'); + return client ? JSON.parse(client) : null; + }; + + // @ts-expect-error - This is an internal API + __internal_clerk.__internal_setEnvironment = async (environment: any) => { + await AsyncStorage.setItem('clerk_environment', JSON.stringify(environment)); + }; + + // @ts-expect-error - This is an internal API + __internal_clerk.__internal_getEnvironment = async () => { + const environment = await AsyncStorage.getItem('clerk_environment'); + return environment ? JSON.parse(environment) : null; + }; } // @ts-expect-error - This is an internal API diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e1799e9e37..97d9ad330b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -576,6 +576,9 @@ importers: '@clerk/types': specifier: workspace:* version: link:../types + '@react-native-async-storage/async-storage': + specifier: ^2.1.0 + version: 2.1.0(react-native@0.73.9(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react@18.3.1)) base-64: specifier: ^1.0.0 version: 1.0.0 @@ -4351,6 +4354,11 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0 + '@react-native-async-storage/async-storage@2.1.0': + resolution: {integrity: sha512-eAGQGPTAuFNEoIQSB5j2Jh1zm5NPyBRTfjRMfCN0W1OakC5WIB5vsDyIQhUweKN9XOE2/V07lqTMGsL0dGXNkA==} + peerDependencies: + react-native: ^0.0.0-0 || >=0.65 <1.0 + '@react-native-community/cli-clean@12.3.7': resolution: {integrity: sha512-BCYW77QqyxfhiMEBOoHyciJRNV6Rhz1RvclReIKnCA9wAwmoJBeu4Mu+AwiECA2bUITX16fvPt3NwDsSd1jwfQ==} @@ -9824,6 +9832,10 @@ packages: resolution: {integrity: sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==} engines: {node: '>=0.10.0'} + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + is-plain-obj@3.0.0: resolution: {integrity: sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==} engines: {node: '>=10'} @@ -10868,6 +10880,10 @@ packages: merge-descriptors@1.0.3: resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + merge-options@3.0.4: + resolution: {integrity: sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==} + engines: {node: '>=10'} + merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -19303,6 +19319,11 @@ snapshots: '@swc/helpers': 0.5.13 react: 18.3.1 + '@react-native-async-storage/async-storage@2.1.0(react-native@0.73.9(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react@18.3.1))': + dependencies: + merge-options: 3.0.4 + react-native: 0.73.9(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))(react@18.3.1) + '@react-native-community/cli-clean@12.3.7': dependencies: '@react-native-community/cli-tools': 12.3.7 @@ -26505,6 +26526,8 @@ snapshots: is-plain-obj@1.1.0: {} + is-plain-obj@2.1.0: {} + is-plain-obj@3.0.0: {} is-plain-obj@4.1.0: {} @@ -28007,6 +28030,10 @@ snapshots: merge-descriptors@1.0.3: {} + merge-options@3.0.4: + dependencies: + is-plain-obj: 2.1.0 + merge-stream@2.0.0: {} merge2@1.4.1: {}