diff --git a/EXAMPLES.md b/EXAMPLES.md index 20992283..9850cba2 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -24,6 +24,7 @@ - [Session Expiry from Upstream IdP (IPSIE)](#session-expiry-from-upstream-idp-ipsie) - [Use Suspense for loading state (React 19+)](#use-suspense-for-loading-state-react-19) - [Enterprise Connect](#enterprise-connect) +- [Forcing an Experiment Center variant](#forcing-an-experiment-center-variant) ## Use with a Class Component @@ -2112,4 +2113,71 @@ await logout({ ``` The `returnTo` URL must be registered in the application's **Allowed Logout -URLs** in the Auth0 Dashboard, or the logout redirect will be rejected. \ No newline at end of file +URLs** in the Auth0 Dashboard, or the logout redirect will be rejected. + +## Forcing an Experiment Center variant + +> [!NOTE] +> [Experiment Center](https://auth0.com/docs/customize/experiment-center/overview) support via SDKs is currently in Early Access. To request access to this feature, contact your Auth0 representative. + +Experiment Center lets you A/B test your login flow. To force a specific variant - for testing or to apply a decision from a feature-flag service - pass `experiment_id` and `variation_id` via `authorizationParams`. Auth0 will use them instead of its server-side deterministic assignment. Both IDs are obtained from your Auth0 Dashboard or the Management API. You can also pass the optional `segment_id` when the experiment uses segment targeting. + +```jsx +import { useAuth0 } from '@auth0/auth0-react'; + +function LoginButton() { + const { loginWithRedirect } = useAuth0(); + + return ( + + ); +} +``` + +The same params work with `loginWithPopup`: + +```jsx +import { useAuth0 } from '@auth0/auth0-react'; + +function LoginButton() { + const { loginWithPopup } = useAuth0(); + + return ( + + ); +} +``` + +> [!IMPORTANT] +> Pass these parameters per call on `loginWithRedirect` (or `loginWithPopup`), not on `Auth0Provider`'s `authorizationParams`. Setting them on the provider pins every login including silent `prompt=none` token-renewal calls - to the same variation, which cancels the A/B test. Experiment Center does not run on silent checks. + +- **Testing:** drive the IDs from test automation (e.g. Cypress/Playwright) using values from a CI environment variable against a staging tenant. Do not hard-code them in shipped app code. +- **Production:** pass the variant decision from a feature-flag tool (e.g. LaunchDarkly) that has already decided which variant the user should see for this request. + +The override applies only to this request; the next login without these params reverts to normal server-side assignment. diff --git a/README.md b/README.md index 09b3a7c5..091671aa 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,13 @@ function App() { export default App; ``` +### Experiment Center + +> [!NOTE] +> [Experiment Center](https://auth0.com/docs/customize/experiment-center) support via SDKs is currently in Early Access. To request access to this feature, contact your Auth0 representative. + +You can also force a specific Experiment Center variant per login by passing `experiment_id`, `variation_id`, and the optional `segment_id` in `authorizationParams` - see the [Experiment Center example](https://github.com/auth0/auth0-react/blob/main/EXAMPLES.md#forcing-an-experiment-center-variant). + For more code samples on how to integrate **auth0-react** SDK in your **React** application, have a look at our [examples](https://github.com/auth0/auth0-react/blob/main/EXAMPLES.md). ## API reference diff --git a/__tests__/auth-provider.test.tsx b/__tests__/auth-provider.test.tsx index d6775f63..74e63e34 100644 --- a/__tests__/auth-provider.test.tsx +++ b/__tests__/auth-provider.test.tsx @@ -491,6 +491,110 @@ describe('Auth0Provider', () => { }); }); + it('should forward Experiment Center params through loginWithRedirect', async () => { + const wrapper = createWrapper(); + const { result } = renderHook( + () => useContext(Auth0Context), + { wrapper } + ); + await waitFor(() => { + expect(result.current.loginWithRedirect).toBeInstanceOf(Function); + }); + await result.current.loginWithRedirect({ + authorizationParams: { + redirect_uri: '__redirect_uri__', + experiment_id: '__experiment_id__', + variation_id: '__variation_id__', + segment_id: '__segment_id__', + }, + }); + expect(clientMock.loginWithRedirect).toHaveBeenCalledWith({ + authorizationParams: { + redirect_uri: '__redirect_uri__', + experiment_id: '__experiment_id__', + variation_id: '__variation_id__', + segment_id: '__segment_id__', + }, + }); + }); + + it('should forward Experiment Center params through loginWithRedirect when segment_id is omitted', async () => { + const wrapper = createWrapper(); + const { result } = renderHook( + () => useContext(Auth0Context), + { wrapper } + ); + await waitFor(() => { + expect(result.current.loginWithRedirect).toBeInstanceOf(Function); + }); + await result.current.loginWithRedirect({ + authorizationParams: { + experiment_id: '__experiment_id__', + variation_id: '__variation_id__', + }, + }); + expect(clientMock.loginWithRedirect).toHaveBeenCalledWith({ + authorizationParams: { + experiment_id: '__experiment_id__', + variation_id: '__variation_id__', + }, + }); + }); + + it('should forward Experiment Center params through loginWithPopup', async () => { + const wrapper = createWrapper(); + const { result } = renderHook( + () => useContext(Auth0Context), + { wrapper } + ); + await waitFor(() => { + expect(result.current.loginWithPopup).toBeInstanceOf(Function); + }); + await result.current.loginWithPopup({ + authorizationParams: { + experiment_id: '__experiment_id__', + variation_id: '__variation_id__', + segment_id: '__segment_id__', + }, + }); + expect(clientMock.loginWithPopup).toHaveBeenCalledWith( + { + authorizationParams: { + experiment_id: '__experiment_id__', + variation_id: '__variation_id__', + segment_id: '__segment_id__', + }, + }, + undefined + ); + }); + + it('should forward Experiment Center params through loginWithPopup when segment_id is omitted', async () => { + const wrapper = createWrapper(); + const { result } = renderHook( + () => useContext(Auth0Context), + { wrapper } + ); + await waitFor(() => { + expect(result.current.loginWithPopup).toBeInstanceOf(Function); + }); + await result.current.loginWithPopup({ + authorizationParams: { + experiment_id: '__experiment_id__', + variation_id: '__variation_id__', + }, + }); + expect(clientMock.loginWithPopup).toHaveBeenCalledWith( + { + authorizationParams: { + experiment_id: '__experiment_id__', + variation_id: '__variation_id__', + }, + }, + undefined + ); + }); + it('should provide a login method supporting redirectUri', async () => { const warn = jest.spyOn(console, "warn").mockImplementation(() => undefined); const wrapper = createWrapper();