diff --git a/EXAMPLES.md b/EXAMPLES.md
index f55ac098..e05b4a35 100644
--- a/EXAMPLES.md
+++ b/EXAMPLES.md
@@ -145,7 +145,9 @@ If you already have credentials (e.g. from `webAuth.authorize()` or `credentials
import Auth0, { parseIdToken } from 'react-native-auth0';
const auth0 = new Auth0({ domain, clientId });
-const credentials = await auth0.webAuth.authorize({ scope: 'openid profile email' });
+const credentials = await auth0.webAuth.authorize({
+ scope: 'openid profile email',
+});
const user = parseIdToken(credentials.idToken);
// user.sub, user.name, user.email, etc.
```
@@ -1916,6 +1918,62 @@ If you want to support multiple domains, you would have to pass an array of obje
You can skip sending the `customScheme` property if you do not want to customize it.
+#### Switching tenants at runtime
+
+The configuration above is **build-time** setup: it registers the redirect callback for every domain you intend to use. Switching the _active_ tenant while the app is running is done in JavaScript by changing the `domain`/`clientId` you pass to the SDK.
+
+When you change the `domain` or `clientId` prop on `Auth0Provider`, the provider rebuilds its underlying client so subsequent calls target the newly selected tenant. Keep the props in state and update them to switch:
+
+```jsx
+import React, { useState } from 'react';
+import { Auth0Provider, useAuth0 } from 'react-native-auth0';
+
+const TENANTS = [
+ { domain: 'tenant-a.us.auth0.com', clientId: 'CLIENT_ID_A' },
+ { domain: 'tenant-b.us.auth0.com', clientId: 'CLIENT_ID_B' },
+];
+
+const App = () => {
+ const [index, setIndex] = useState(0);
+ const tenant = TENANTS[index];
+
+ return (
+ // Changing domain/clientId recreates the client for the new tenant.
+
+
+ );
+};
+```
+
+After switching, the next `authorize()` call opens the login page for the newly selected tenant and the redirect resolves correctly, provided that tenant's domain/scheme was registered using the build-time configuration shown above.
+
+> Note: Switching tenants does not immediately clear the displayed auth state. The provider re-runs its initialization for the new tenant and updates `user` once that check completes, so the previously shown user may briefly remain until then. Persisted credentials are stored per tenant, so a user already logged in to the target tenant is restored on initialization; otherwise `user` becomes `null`.
+
+If you are using the `Auth0` class directly instead of the hooks, simply create (or reuse) an instance per tenant and call the one matching the active tenant:
+
+```js
+import Auth0 from 'react-native-auth0';
+
+const clients = {
+ tenantA: new Auth0({
+ domain: 'tenant-a.us.auth0.com',
+ clientId: 'CLIENT_ID_A',
+ }),
+ tenantB: new Auth0({
+ domain: 'tenant-b.us.auth0.com',
+ clientId: 'CLIENT_ID_B',
+ }),
+};
+
+// Use whichever client corresponds to the active tenant.
+const credentials = await clients[activeTenant].webAuth.authorize();
+```
+
## Allowed Browsers (Android)
On Android, some browsers do not correctly handle App Link redirects. For example, Firefox renders the callback URL as a web page instead of handing the redirect back to your app, causing the authentication flow to fail silently.
diff --git a/src/hooks/Auth0Provider.tsx b/src/hooks/Auth0Provider.tsx
index 313d7c9e..64ed1713 100644
--- a/src/hooks/Auth0Provider.tsx
+++ b/src/hooks/Auth0Provider.tsx
@@ -45,8 +45,18 @@ export const Auth0Provider = ({
children,
...options
}: PropsWithChildren) => {
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const client = useMemo(() => new Auth0(options), []);
+ // Recreate the client when the tenant configuration changes so that
+ // `domain`/`clientId` prop updates take effect (e.g. domain switching).
+ // The factory caches clients per domain|clientId, so unchanged configs
+ // still reuse the same underlying instance and its in-flight state.
+ const client = useMemo(
+ () => new Auth0(options),
+ // Intentionally key only on the tenant identity. `options` is a fresh
+ // object every render, so depending on it would recreate the client
+ // (and reset auth state) on every render.
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [options.domain, options.clientId]
+ );
const [state, dispatch] = useReducer(reducer, {
user: null,
error: null,
diff --git a/src/platforms/native/adapters/NativeAuth0Client.ts b/src/platforms/native/adapters/NativeAuth0Client.ts
index a89479c7..ae6c3ad3 100644
--- a/src/platforms/native/adapters/NativeAuth0Client.ts
+++ b/src/platforms/native/adapters/NativeAuth0Client.ts
@@ -35,12 +35,15 @@ export class NativeAuth0Client implements IAuth0Client {
private readonly tokenType: TokenType;
private readonly bridge: INativeBridge;
private readonly baseUrl: string;
+ private readonly options: NativeAuth0Options;
+ private syncLock: Promise = Promise.resolve();
private guardedBridge!: INativeBridge;
private readonly getDPoPHeadersForOrchestrator?: (
params: DPoPHeadersParams
) => Promise>;
constructor(options: NativeAuth0Options) {
+ this.options = options;
const baseUrl = `https://${options.domain}`;
this.baseUrl = baseUrl;
const useDPoP = options.useDPoP ?? true;
@@ -108,6 +111,25 @@ export class NativeAuth0Client implements IAuth0Client {
}
}
+ /**
+ * Re-points the native singleton at this client's configuration.
+ *
+ * The native module (iOS/Android) keeps a single active Auth0 instance, but
+ * the JS factory caches one client per domain|clientId. When multiple clients
+ * coexist (or a client is reused after another was initialized), the native
+ * instance may belong to a sibling client, so bridge calls would otherwise
+ * target the wrong domain/clientId. Re-initializing only happens when the
+ * native config has drifted, so the common single-client path stays a cheap
+ * `hasValidInstance` check. Serialized via `syncLock` to avoid interleaving
+ * re-initializations from concurrent calls.
+ */
+ private syncNativeConfig(): Promise {
+ this.syncLock = this.syncLock
+ .catch(() => undefined)
+ .then(() => this.initialize(this.bridge, this.options));
+ return this.syncLock;
+ }
+
users(token: string, tokenType?: TokenType): IUsersClient {
// Use provided tokenType or fall back to client's default
const effectiveTokenType = tokenType ?? this.tokenType;
@@ -165,6 +187,10 @@ export class NativeAuth0Client implements IAuth0Client {
guarded[methodName] = async (...args: any[]) => {
// This is the "guard": wait for the initialization promise to resolve.
await this.ready;
+ // Re-point the native singleton at this client's config in case a
+ // sibling client (different domain/clientId) overwrote it. No-op when
+ // the native instance already matches.
+ await this.syncNativeConfig();
// Call the original method with the correct 'this' context.
return originalMethod.apply(bridge, args);
};
diff --git a/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts b/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts
index cdca5462..bbc6217c 100644
--- a/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts
+++ b/src/platforms/native/adapters/__tests__/NativeAuth0Client.spec.ts
@@ -560,4 +560,49 @@ describe('NativeAuth0Client', () => {
).rejects.toBeInstanceOf(PasskeyError);
});
});
+
+ describe('native config re-sync (multi-tenant)', () => {
+ it('re-points the native singleton when its config has drifted to a sibling client', async () => {
+ // Simulate the native singleton currently belonging to a different
+ // domain/clientId (overwritten by a sibling Auth0 instance).
+ mockBridgeInstance.hasValidInstance.mockResolvedValue(false);
+
+ const client = new NativeAuth0Client(options);
+ await new Promise(process.nextTick);
+
+ // Constructor initialization re-pointed the native side once.
+ expect(mockBridgeInstance.initialize).toHaveBeenCalledTimes(1);
+ const initCallsAfterConstruct =
+ mockBridgeInstance.initialize.mock.calls.length;
+
+ await client.webAuth.authorize();
+
+ // The guarded path re-asserts this client's config before dispatching to
+ // the native module, so a drifted singleton gets re-initialized to the
+ // correct tenant rather than authorizing against the wrong domain.
+ expect(mockBridgeInstance.initialize.mock.calls.length).toBeGreaterThan(
+ initCallsAfterConstruct
+ );
+ expect(mockBridgeInstance.initialize).toHaveBeenLastCalledWith(
+ options.clientId,
+ options.domain,
+ undefined,
+ true,
+ undefined
+ );
+ expect(mockBridgeInstance.authorize).toHaveBeenCalledTimes(1);
+ });
+
+ it('does NOT re-initialize when the native singleton already matches', async () => {
+ // Default mock: hasValidInstance returns true (native already matches).
+ const client = new NativeAuth0Client(options);
+ await new Promise(process.nextTick);
+
+ await client.webAuth.authorize();
+
+ // No re-initialization needed; the common single-client path stays cheap.
+ expect(mockBridgeInstance.initialize).not.toHaveBeenCalled();
+ expect(mockBridgeInstance.authorize).toHaveBeenCalledTimes(1);
+ });
+ });
});