Skip to content

Commit 13ca919

Browse files
[FSSDK-12647] user id vuid adjustment (#336)
1 parent 9827c24 commit 13ca919

9 files changed

Lines changed: 383 additions & 80 deletions

README.md

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,12 +216,44 @@ _props_
216216
| Prop | Type | Required | Description |
217217
| --- | --- | --- | --- |
218218
| `client` | `Client` | Yes | Instance created from `createInstance`. |
219-
| `user` | `{ id?: string; attributes?: UserAttributes }` | No | User info object — `id` and `attributes` will be used to create the user context for all decisions and event tracking. |
219+
| `user` | `{ id?: string; attributes?: UserAttributes } \| null` | No | User info object — `id` and `attributes` will be used to create the user context for all decisions and event tracking. Pass `null`, `undefined`, or omit while user info is being fetched — hooks will return `{ isLoading: true }` until a resolved user is provided. For VUID-only mode (no user ID), pass `user={{}}`. |
220220
| `timeout` | `number` | No | Maximum time (in milliseconds) to wait for the SDK to become ready before hooks resolve with a loading state. Default: `30000`. |
221221
| `qualifiedSegments` | `string[]` | No | Pre-fetched ODP audience segments for the user. Use [`getQualifiedSegments`](#getqualifiedsegments) to obtain these segments server-side. |
222222
| `skipSegments` | `boolean` | No | When `true`, skips background ODP segment fetching. Default: `false`. |
223223

224-
> **Note:** Unless VUID is enabled, `<OptimizelyProvider>` requires user data. If user information must be fetched asynchronously, resolve the promise before rendering the Provider.
224+
> **Note:** If user information is not yet available, you can render `<OptimizelyProvider>` without it — hooks will return `{ isLoading: true }` until a resolved user is provided. For VUID-only mode (no user ID), pass `user={{}}`.
225+
226+
#### VUID-only example
227+
228+
```jsx
229+
import {
230+
createInstance,
231+
createPollingProjectConfigManager,
232+
createBatchEventProcessor,
233+
createOdpManager,
234+
createVuidManager,
235+
OptimizelyProvider,
236+
} from '@optimizely/react-sdk';
237+
238+
const optimizely = createInstance({
239+
projectConfigManager: createPollingProjectConfigManager({
240+
sdkKey: 'your-optimizely-sdk-key',
241+
}),
242+
eventProcessor: createBatchEventProcessor(),
243+
odpManager: createOdpManager(),
244+
vuidManager: createVuidManager({
245+
enableVuid: true,
246+
}),
247+
});
248+
249+
function App() {
250+
return (
251+
<OptimizelyProvider client={optimizely} user={{}}>
252+
<MyComponent />
253+
</OptimizelyProvider>
254+
);
255+
}
256+
```
225257

226258
### Readiness
227259

docs/nextjs-integration.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,12 +445,15 @@ export default function MyFeature() {
445445

446446
### User Promise not supported
447447

448-
User `Promise` is not supported. You must provide a resolved user object to `OptimizelyProvider`. If user information must be fetched asynchronously, resolve the promise before rendering the Provider:
448+
User `Promise` is not supported. You can pass `null`, `undefined`, or omit the `user` prop while user information is being fetched — hooks will return `{ isLoading: true }` until a resolved user object is provided. For VUID-only mode (no user ID), pass `user={{}}`.
449449

450450
```tsx
451451
// Supported
452452
<OptimizelyProvider client={optimizely} user={{ id: 'user123', attributes: { plan: 'premium' } }} />
453453

454+
// Supported — hooks return { isLoading: true } until user is provided
455+
<OptimizelyProvider client={optimizely} user={null} />
456+
454457
// NOT supported
455458
<OptimizelyProvider client={optimizely} user={fetchUserPromise} />
456459
```

src/provider/OptimizelyProvider.spec.tsx

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ describe('OptimizelyProvider', () => {
233233
let capturedContext: OptimizelyContextValue | null = null;
234234

235235
const { unmount } = render(
236-
<OptimizelyProvider client={mockClient}>
236+
<OptimizelyProvider client={mockClient} user={{ id: 'user-1' }}>
237237
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
238238
</OptimizelyProvider>
239239
);
@@ -488,7 +488,7 @@ describe('OptimizelyProvider', () => {
488488
expect(mockClient.createUserContext).toHaveBeenCalledTimes(1);
489489
});
490490

491-
it('should create user context without userId when user prop is not provided', async () => {
491+
it('should not create user context when user prop is not provided', async () => {
492492
const mockClient = createMockClient();
493493

494494
render(
@@ -497,7 +497,7 @@ describe('OptimizelyProvider', () => {
497497
</OptimizelyProvider>
498498
);
499499

500-
expect(mockClient.createUserContext).toHaveBeenCalledWith(undefined, undefined);
500+
expect(mockClient.createUserContext).not.toHaveBeenCalled();
501501
});
502502
});
503503

@@ -793,6 +793,78 @@ describe('OptimizelyProvider', () => {
793793
});
794794
});
795795

796+
describe('null user', () => {
797+
it('should not create user context when user is null', async () => {
798+
const mockClient = createMockClient();
799+
800+
render(
801+
<OptimizelyProvider client={mockClient} user={null}>
802+
<div>Child</div>
803+
</OptimizelyProvider>
804+
);
805+
806+
expect(mockClient.createUserContext).not.toHaveBeenCalled();
807+
});
808+
809+
it('should have null userContext in store when user is null', async () => {
810+
const mockClient = createMockClient();
811+
let capturedContext: OptimizelyContextValue | null = null;
812+
813+
render(
814+
<OptimizelyProvider client={mockClient} user={null}>
815+
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
816+
</OptimizelyProvider>
817+
);
818+
819+
expect(capturedContext).not.toBeNull();
820+
expect(capturedContext!.store.getState().userContext).toBeNull();
821+
});
822+
823+
it('should create context when user changes from null to valid', async () => {
824+
const mockClient = createMockClient();
825+
let capturedContext: OptimizelyContextValue | null = null;
826+
827+
const { rerender } = render(
828+
<OptimizelyProvider client={mockClient} user={null}>
829+
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
830+
</OptimizelyProvider>
831+
);
832+
833+
expect(mockClient.createUserContext).not.toHaveBeenCalled();
834+
expect(capturedContext!.store.getState().userContext).toBeNull();
835+
836+
rerender(
837+
<OptimizelyProvider client={mockClient} user={{ id: 'user-1' }}>
838+
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
839+
</OptimizelyProvider>
840+
);
841+
842+
expect(mockClient.createUserContext).toHaveBeenCalledWith('user-1', undefined);
843+
expect(capturedContext!.store.getState().userContext).not.toBeNull();
844+
});
845+
846+
it('should set store userContext to null when user changes from valid to null', async () => {
847+
const mockClient = createMockClient();
848+
let capturedContext: OptimizelyContextValue | null = null;
849+
850+
const { rerender } = render(
851+
<OptimizelyProvider client={mockClient} user={{ id: 'user-1' }}>
852+
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
853+
</OptimizelyProvider>
854+
);
855+
856+
expect(capturedContext!.store.getState().userContext).not.toBeNull();
857+
858+
rerender(
859+
<OptimizelyProvider client={mockClient} user={null}>
860+
<ContextConsumer onContext={(ctx) => (capturedContext = ctx)} />
861+
</OptimizelyProvider>
862+
);
863+
864+
expect(capturedContext!.store.getState().userContext).toBeNull();
865+
});
866+
});
867+
796868
describe('context reference identity', () => {
797869
it('should change context value reference when client changes', async () => {
798870
const mockClient1 = createMockClient();

src/provider/OptimizelyProvider.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export function OptimizelyProvider({
6060

6161
userManagerRef.current = new UserContextManager({
6262
client,
63-
onUserContextReady: (ctx) => store.setUserContext(ctx),
63+
onUserContextChange: (ctx) => store.setUserContext(ctx),
6464
onError: (error) => store.setError(error),
6565
});
6666

src/provider/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export interface OptimizelyProviderProps {
3838
/**
3939
* User information for decisions.
4040
*/
41-
user?: UserInfo;
41+
user?: UserInfo | null;
4242

4343
/**
4444
* Timeout in milliseconds to wait for the client to become ready.

0 commit comments

Comments
 (0)