@explorins/pers-sdk-react-native 2.3.6 → 2.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1266 -1244
  3. package/dist/__tests__/PersSDKProvider.test.js +33 -0
  4. package/dist/hooks/useBusiness.d.ts +2 -2
  5. package/dist/hooks/useBusiness.d.ts.map +1 -1
  6. package/dist/hooks/useBusiness.js +2 -2
  7. package/dist/hooks/useIPFSUrl.d.ts +3 -3
  8. package/dist/hooks/useIPFSUrl.d.ts.map +1 -1
  9. package/dist/hooks/useIPFSUrl.js +7 -7
  10. package/dist/hooks/useTransactions.d.ts.map +1 -1
  11. package/dist/hooks/useTransactions.js +13 -5
  12. package/dist/hooks/useWeb3.js +4 -4
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2412 -492
  16. package/dist/index.js.map +1 -1
  17. package/dist/providers/PersSDKProvider.d.ts +4 -0
  18. package/dist/providers/PersSDKProvider.d.ts.map +1 -1
  19. package/dist/providers/PersSDKProvider.js +14 -0
  20. package/dist/providers/react-native-http-client.d.ts +2 -0
  21. package/dist/providers/react-native-http-client.d.ts.map +1 -1
  22. package/dist/providers/react-native-http-client.js +5 -2
  23. package/dist/storage/rn-secure-storage.d.ts.map +1 -1
  24. package/dist/storage/rn-secure-storage.js +21 -8
  25. package/package.json +3 -3
  26. package/src/__tests__/PersSDKProvider.test.tsx +484 -420
  27. package/src/__tests__/setup.ts +40 -40
  28. package/src/hooks/index.ts +60 -60
  29. package/src/hooks/useAnalytics.ts +327 -327
  30. package/src/hooks/useAuth.ts +265 -265
  31. package/src/hooks/useBookings.ts +191 -191
  32. package/src/hooks/useBusiness.ts +251 -251
  33. package/src/hooks/useCampaigns.ts +500 -500
  34. package/src/hooks/useDonations.ts +66 -66
  35. package/src/hooks/useEvents.ts +372 -372
  36. package/src/hooks/useFiles.ts +155 -155
  37. package/src/hooks/useIPFSUrl.ts +192 -192
  38. package/src/hooks/usePurchases.ts +65 -65
  39. package/src/hooks/useRedemptions.ts +467 -467
  40. package/src/hooks/useTenants.ts +72 -72
  41. package/src/hooks/useTokenBalances.ts +385 -385
  42. package/src/hooks/useTokens.ts +313 -313
  43. package/src/hooks/useTransactionSigner.ts +611 -611
  44. package/src/hooks/useTransactions.ts +299 -290
  45. package/src/hooks/useTriggerSources.ts +301 -301
  46. package/src/hooks/useUserStatus.ts +61 -61
  47. package/src/hooks/useUsers.ts +167 -167
  48. package/src/hooks/useWeb3.ts +355 -355
  49. package/src/index.ts +777 -774
  50. package/src/polyfills/index.ts +160 -160
  51. package/src/polyfills/web3-polyfills.ts +97 -97
  52. package/src/providers/PersSDKProvider.tsx +393 -374
  53. package/src/providers/react-native-auth-provider.ts +110 -110
  54. package/src/providers/react-native-http-client.ts +150 -145
  55. package/src/providers/rn-dpop-provider.ts +190 -190
  56. package/src/storage/async-storage-token-storage.ts +185 -185
  57. package/src/storage/rn-secure-storage.ts +182 -170
  58. package/src/types/react-native-globals.d.ts +45 -45
  59. package/src/utils/index.ts +8 -8
  60. package/src/utils/platform-detection.ts +86 -86
  61. package/dist/index-CzLxqePZ.js +0 -7717
  62. package/dist/index-CzLxqePZ.js.map +0 -1
@@ -1,420 +1,484 @@
1
- import React from 'react';
2
- import { render, act, waitFor } from '@testing-library/react';
3
- import '@testing-library/jest-dom';
4
- import { PersSDKProvider, usePersSDK, PersSDKContext } from '../providers/PersSDKProvider';
5
-
6
- // Store SDK instances for event emission
7
- let mockSdkInstance: any = null;
8
-
9
- // Mock the SDK core module BEFORE any imports use it
10
- jest.mock('@explorins/pers-sdk/core', () => {
11
- const createMockEventEmitter = () => {
12
- const subscribers: Array<{ callback: (event: any) => void; options?: any }> = [];
13
-
14
- return {
15
- subscribe: jest.fn((callback: (event: any) => void, options?: any) => {
16
- subscribers.push({ callback, options });
17
- return () => {
18
- const index = subscribers.findIndex(s => s.callback === callback);
19
- if (index > -1) subscribers.splice(index, 1);
20
- };
21
- }),
22
- emit: (event: any) => {
23
- subscribers.forEach(({ callback, options }) => {
24
- if (!options?.domains || options.domains.includes(event.domain)) {
25
- callback(event);
26
- }
27
- });
28
- },
29
- _subscribers: subscribers
30
- };
31
- };
32
-
33
- const MockPersSDK = jest.fn().mockImplementation(() => {
34
- const events = createMockEventEmitter();
35
- mockSdkInstance = {
36
- events,
37
- auth: {
38
- ensureValidToken: jest.fn().mockResolvedValue(true)
39
- },
40
- users: {
41
- getCurrentUser: jest.fn().mockResolvedValue({ id: 'refreshed-user', email: 'refreshed@test.com' })
42
- },
43
- restoreSession: jest.fn().mockResolvedValue(null)
44
- };
45
- return mockSdkInstance;
46
- });
47
-
48
- return {
49
- PersSDK: MockPersSDK,
50
- DefaultAuthProvider: jest.fn().mockImplementation(() => ({
51
- getToken: jest.fn().mockResolvedValue('mock-token')
52
- }))
53
- };
54
- });
55
-
56
- // Mock platform-specific providers
57
- jest.mock('../providers/react-native-http-client', () => ({
58
- ReactNativeHttpClient: jest.fn().mockImplementation(() => ({}))
59
- }));
60
-
61
- jest.mock('../providers/react-native-auth-provider', () => ({
62
- createReactNativeAuthProvider: jest.fn().mockReturnValue({
63
- getToken: jest.fn().mockResolvedValue('mock-token')
64
- })
65
- }));
66
-
67
- jest.mock('../providers/rn-dpop-provider', () => ({
68
- ReactNativeDPoPProvider: jest.fn().mockImplementation(() => ({}))
69
- }));
70
-
71
- // Helper component to access context
72
- const TestConsumer: React.FC<{ onContext: (ctx: PersSDKContext) => void }> = ({ onContext }) => {
73
- const context = usePersSDK();
74
- React.useEffect(() => {
75
- onContext(context);
76
- }, [context, onContext]);
77
- return <div data-testid="consumer">Consumer</div>;
78
- };
79
-
80
- // Helper to emit events on the mock SDK
81
- const emitAuthEvent = (type: string, details: any = {}) => {
82
- if (mockSdkInstance?.events) {
83
- mockSdkInstance.events.emit({
84
- type,
85
- domain: 'authentication',
86
- details
87
- });
88
- }
89
- };
90
-
91
- describe('PersSDKProvider', () => {
92
- beforeEach(() => {
93
- jest.clearAllMocks();
94
- mockSdkInstance = null;
95
- });
96
-
97
- describe('initialization', () => {
98
- it('should initialize with null SDK when no config provided', async () => {
99
- let capturedContext: PersSDKContext | null = null;
100
-
101
- render(
102
- <PersSDKProvider>
103
- <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
104
- </PersSDKProvider>
105
- );
106
-
107
- await waitFor(() => {
108
- expect(capturedContext).not.toBeNull();
109
- });
110
-
111
- expect(capturedContext!.sdk).toBeNull();
112
- expect(capturedContext!.isInitialized).toBe(false);
113
- expect(capturedContext!.isAuthenticated).toBe(false);
114
- expect(capturedContext!.user).toBeNull();
115
- });
116
-
117
- it('should auto-initialize when config is provided', async () => {
118
- let capturedContext: PersSDKContext | null = null;
119
-
120
- render(
121
- <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
122
- <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
123
- </PersSDKProvider>
124
- );
125
-
126
- await waitFor(() => {
127
- expect(capturedContext?.isInitialized).toBe(true);
128
- });
129
-
130
- expect(capturedContext!.sdk).not.toBeNull();
131
- });
132
-
133
- it('should prevent multiple initializations', async () => {
134
- const { PersSDK } = require('@explorins/pers-sdk/core');
135
-
136
- let capturedContext: PersSDKContext | null = null;
137
-
138
- render(
139
- <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
140
- <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
141
- </PersSDKProvider>
142
- );
143
-
144
- await waitFor(() => {
145
- expect(capturedContext?.isInitialized).toBe(true);
146
- });
147
-
148
- // Try to initialize again - should be ignored
149
- await act(async () => {
150
- await capturedContext!.initialize({ apiProjectKey: 'different-project' });
151
- });
152
-
153
- // Should only have been called once
154
- expect(PersSDK).toHaveBeenCalledTimes(1);
155
- });
156
- });
157
-
158
- describe('event handling', () => {
159
- it('should set user on session_restored event', async () => {
160
- let capturedContext: PersSDKContext | null = null;
161
- const mockUser = { id: 'user-123', email: 'test@example.com' };
162
-
163
- render(
164
- <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
165
- <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
166
- </PersSDKProvider>
167
- );
168
-
169
- await waitFor(() => {
170
- expect(capturedContext?.sdk).not.toBeNull();
171
- });
172
-
173
- // Emit session_restored event
174
- await act(async () => {
175
- emitAuthEvent('session_restored', { user: mockUser });
176
- });
177
-
178
- await waitFor(() => {
179
- expect(capturedContext?.isAuthenticated).toBe(true);
180
- expect(capturedContext?.user).toEqual(mockUser);
181
- });
182
- });
183
-
184
- it('should set user on login_success event', async () => {
185
- let capturedContext: PersSDKContext | null = null;
186
- const mockUser = { id: 'user-456', email: 'login@example.com' };
187
-
188
- render(
189
- <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
190
- <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
191
- </PersSDKProvider>
192
- );
193
-
194
- await waitFor(() => {
195
- expect(capturedContext?.sdk).not.toBeNull();
196
- });
197
-
198
- await act(async () => {
199
- emitAuthEvent('login_success', { user: mockUser });
200
- });
201
-
202
- await waitFor(() => {
203
- expect(capturedContext?.isAuthenticated).toBe(true);
204
- expect(capturedContext?.user).toEqual(mockUser);
205
- });
206
- });
207
-
208
- it('should clear user on logout_success event', async () => {
209
- let capturedContext: PersSDKContext | null = null;
210
- const mockUser = { id: 'user-789', email: 'logout@example.com' };
211
-
212
- render(
213
- <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
214
- <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
215
- </PersSDKProvider>
216
- );
217
-
218
- await waitFor(() => {
219
- expect(capturedContext?.sdk).not.toBeNull();
220
- });
221
-
222
- // First, log in
223
- await act(async () => {
224
- emitAuthEvent('login_success', { user: mockUser });
225
- });
226
-
227
- await waitFor(() => {
228
- expect(capturedContext?.isAuthenticated).toBe(true);
229
- });
230
-
231
- // Then logout
232
- await act(async () => {
233
- emitAuthEvent('logout_success', {});
234
- });
235
-
236
- await waitFor(() => {
237
- expect(capturedContext?.isAuthenticated).toBe(false);
238
- expect(capturedContext?.user).toBeNull();
239
- });
240
- });
241
-
242
- it('should clear user on session_restoration_failed event', async () => {
243
- let capturedContext: PersSDKContext | null = null;
244
-
245
- render(
246
- <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
247
- <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
248
- </PersSDKProvider>
249
- );
250
-
251
- await waitFor(() => {
252
- expect(capturedContext?.sdk).not.toBeNull();
253
- });
254
-
255
- // Set initial authenticated state
256
- await act(async () => {
257
- capturedContext!.setAuthenticationState({ id: 'temp-user' } as any, true);
258
- });
259
-
260
- await waitFor(() => {
261
- expect(capturedContext?.isAuthenticated).toBe(true);
262
- });
263
-
264
- // Emit session_restoration_failed
265
- await act(async () => {
266
- emitAuthEvent('session_restoration_failed', { error: 'Token expired' });
267
- });
268
-
269
- await waitFor(() => {
270
- expect(capturedContext?.isAuthenticated).toBe(false);
271
- expect(capturedContext?.user).toBeNull();
272
- });
273
- });
274
-
275
- it('should NOT respond to AUTH_FAILED code (dead code was removed)', async () => {
276
- // This test verifies that we no longer check for event.code === 'AUTH_FAILED'
277
- // which was dead code that never triggered
278
- let capturedContext: PersSDKContext | null = null;
279
- const mockUser = { id: 'user-stay', email: 'stay@example.com' };
280
-
281
- render(
282
- <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
283
- <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
284
- </PersSDKProvider>
285
- );
286
-
287
- await waitFor(() => {
288
- expect(capturedContext?.sdk).not.toBeNull();
289
- });
290
-
291
- // Log in first
292
- await act(async () => {
293
- emitAuthEvent('login_success', { user: mockUser });
294
- });
295
-
296
- await waitFor(() => {
297
- expect(capturedContext?.isAuthenticated).toBe(true);
298
- });
299
-
300
- // Emit an event with code: 'AUTH_FAILED' - this should NOT clear user
301
- // because we removed the dead code that checked for this
302
- await act(async () => {
303
- if (mockSdkInstance?.events) {
304
- mockSdkInstance.events.emit({
305
- type: 'some_other_event',
306
- code: 'AUTH_FAILED', // This was the dead code path we removed
307
- domain: 'authentication',
308
- details: {}
309
- });
310
- }
311
- });
312
-
313
- // User should STILL be authenticated - the code: 'AUTH_FAILED' check is gone
314
- expect(capturedContext!.isAuthenticated).toBe(true);
315
- expect(capturedContext!.user).toEqual(mockUser);
316
- });
317
- });
318
-
319
- describe('usePersSDK hook', () => {
320
- it('should throw error when used outside provider', () => {
321
- // Suppress the expected error
322
- const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
323
-
324
- const TestComponent = () => {
325
- usePersSDK();
326
- return null;
327
- };
328
-
329
- expect(() => render(<TestComponent />)).toThrow(
330
- 'usePersSDK must be used within a PersSDKProvider'
331
- );
332
-
333
- spy.mockRestore();
334
- });
335
- });
336
-
337
- describe('context methods', () => {
338
- it('should allow manual authentication state updates', async () => {
339
- let capturedContext: PersSDKContext | null = null;
340
- const mockUser = { id: 'manual-user', email: 'manual@example.com' };
341
-
342
- render(
343
- <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
344
- <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
345
- </PersSDKProvider>
346
- );
347
-
348
- await waitFor(() => {
349
- expect(capturedContext?.isInitialized).toBe(true);
350
- });
351
-
352
- await act(async () => {
353
- capturedContext!.setAuthenticationState(mockUser as any, true);
354
- });
355
-
356
- await waitFor(() => {
357
- expect(capturedContext?.isAuthenticated).toBe(true);
358
- expect(capturedContext?.user).toEqual(mockUser);
359
- });
360
- });
361
-
362
- it('should refresh user data from SDK', async () => {
363
- let capturedContext: PersSDKContext | null = null;
364
-
365
- render(
366
- <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
367
- <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
368
- </PersSDKProvider>
369
- );
370
-
371
- await waitFor(() => {
372
- expect(capturedContext?.isInitialized).toBe(true);
373
- });
374
-
375
- await act(async () => {
376
- await capturedContext!.refreshUserData();
377
- });
378
-
379
- await waitFor(() => {
380
- expect(capturedContext?.user).toEqual({ id: 'refreshed-user', email: 'refreshed@test.com' });
381
- });
382
- });
383
-
384
- it('should throw error when refreshUserData called before init', async () => {
385
- let capturedContext: PersSDKContext | null = null;
386
-
387
- render(
388
- <PersSDKProvider>
389
- <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
390
- </PersSDKProvider>
391
- );
392
-
393
- await waitFor(() => {
394
- expect(capturedContext).not.toBeNull();
395
- });
396
-
397
- await expect(capturedContext!.refreshUserData()).rejects.toThrow(
398
- 'SDK not initialized'
399
- );
400
- });
401
-
402
- it('should throw error when restoreSession called before init', async () => {
403
- let capturedContext: PersSDKContext | null = null;
404
-
405
- render(
406
- <PersSDKProvider>
407
- <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
408
- </PersSDKProvider>
409
- );
410
-
411
- await waitFor(() => {
412
- expect(capturedContext).not.toBeNull();
413
- });
414
-
415
- await expect(capturedContext!.restoreSession()).rejects.toThrow(
416
- 'SDK not initialized'
417
- );
418
- });
419
- });
420
- });
1
+ import React from 'react';
2
+ import { render, act, waitFor } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { PersSDKProvider, usePersSDK, PersSDKContext } from '../providers/PersSDKProvider';
5
+
6
+ // Store SDK instances for event emission
7
+ let mockSdkInstance: any = null;
8
+
9
+ // Mock the SDK core module BEFORE any imports use it
10
+ jest.mock('@explorins/pers-sdk/core', () => {
11
+ const createMockEventEmitter = () => {
12
+ const subscribers: Array<{ callback: (event: any) => void; options?: any }> = [];
13
+
14
+ return {
15
+ subscribe: jest.fn((callback: (event: any) => void, options?: any) => {
16
+ subscribers.push({ callback, options });
17
+ return () => {
18
+ const index = subscribers.findIndex(s => s.callback === callback);
19
+ if (index > -1) subscribers.splice(index, 1);
20
+ };
21
+ }),
22
+ emit: (event: any) => {
23
+ subscribers.forEach(({ callback, options }) => {
24
+ if (!options?.domains || options.domains.includes(event.domain)) {
25
+ callback(event);
26
+ }
27
+ });
28
+ },
29
+ _subscribers: subscribers
30
+ };
31
+ };
32
+
33
+ const MockPersSDK = jest.fn().mockImplementation(() => {
34
+ const events = createMockEventEmitter();
35
+ let currentLocale: string | null = null;
36
+ mockSdkInstance = {
37
+ events,
38
+ getLocale: jest.fn(() => currentLocale),
39
+ setLocale: jest.fn((next: string | null) => { currentLocale = next ? next.toLowerCase() : null; }),
40
+ auth: {
41
+ ensureValidToken: jest.fn().mockResolvedValue(true)
42
+ },
43
+ users: {
44
+ getCurrentUser: jest.fn().mockResolvedValue({ id: 'refreshed-user', email: 'refreshed@test.com' })
45
+ },
46
+ restoreSession: jest.fn().mockResolvedValue(null)
47
+ };
48
+ return mockSdkInstance;
49
+ });
50
+
51
+ return {
52
+ PersSDK: MockPersSDK,
53
+ DefaultAuthProvider: jest.fn().mockImplementation(() => ({
54
+ getToken: jest.fn().mockResolvedValue('mock-token')
55
+ }))
56
+ };
57
+ });
58
+
59
+ // Mock platform-specific providers
60
+ jest.mock('../providers/react-native-http-client', () => ({
61
+ ReactNativeHttpClient: jest.fn().mockImplementation(() => ({}))
62
+ }));
63
+
64
+ jest.mock('../providers/react-native-auth-provider', () => ({
65
+ createReactNativeAuthProvider: jest.fn().mockReturnValue({
66
+ getToken: jest.fn().mockResolvedValue('mock-token')
67
+ })
68
+ }));
69
+
70
+ jest.mock('../providers/rn-dpop-provider', () => ({
71
+ ReactNativeDPoPProvider: jest.fn().mockImplementation(() => ({}))
72
+ }));
73
+
74
+ // Helper component to access context
75
+ const TestConsumer: React.FC<{ onContext: (ctx: PersSDKContext) => void }> = ({ onContext }) => {
76
+ const context = usePersSDK();
77
+ React.useEffect(() => {
78
+ onContext(context);
79
+ }, [context, onContext]);
80
+ return <div data-testid="consumer">Consumer</div>;
81
+ };
82
+
83
+ // Helper to emit events on the mock SDK
84
+ const emitAuthEvent = (type: string, details: any = {}) => {
85
+ if (mockSdkInstance?.events) {
86
+ mockSdkInstance.events.emit({
87
+ type,
88
+ domain: 'authentication',
89
+ details
90
+ });
91
+ }
92
+ };
93
+
94
+ describe('PersSDKProvider', () => {
95
+ beforeEach(() => {
96
+ jest.clearAllMocks();
97
+ mockSdkInstance = null;
98
+ });
99
+
100
+ describe('initialization', () => {
101
+ it('should initialize with null SDK when no config provided', async () => {
102
+ let capturedContext: PersSDKContext | null = null;
103
+
104
+ render(
105
+ <PersSDKProvider>
106
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
107
+ </PersSDKProvider>
108
+ );
109
+
110
+ await waitFor(() => {
111
+ expect(capturedContext).not.toBeNull();
112
+ });
113
+
114
+ expect(capturedContext!.sdk).toBeNull();
115
+ expect(capturedContext!.isInitialized).toBe(false);
116
+ expect(capturedContext!.isAuthenticated).toBe(false);
117
+ expect(capturedContext!.user).toBeNull();
118
+ });
119
+
120
+ it('should auto-initialize when config is provided', async () => {
121
+ let capturedContext: PersSDKContext | null = null;
122
+
123
+ render(
124
+ <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
125
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
126
+ </PersSDKProvider>
127
+ );
128
+
129
+ await waitFor(() => {
130
+ expect(capturedContext?.isInitialized).toBe(true);
131
+ });
132
+
133
+ expect(capturedContext!.sdk).not.toBeNull();
134
+ });
135
+
136
+ it('should prevent multiple initializations', async () => {
137
+ const { PersSDK } = require('@explorins/pers-sdk/core');
138
+
139
+ let capturedContext: PersSDKContext | null = null;
140
+
141
+ render(
142
+ <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
143
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
144
+ </PersSDKProvider>
145
+ );
146
+
147
+ await waitFor(() => {
148
+ expect(capturedContext?.isInitialized).toBe(true);
149
+ });
150
+
151
+ // Try to initialize again - should be ignored
152
+ await act(async () => {
153
+ await capturedContext!.initialize({ apiProjectKey: 'different-project' });
154
+ });
155
+
156
+ // Should only have been called once
157
+ expect(PersSDK).toHaveBeenCalledTimes(1);
158
+ });
159
+ });
160
+
161
+ describe('locale', () => {
162
+ it('exposes a null locale until one is set', async () => {
163
+ let capturedContext: PersSDKContext | null = null;
164
+
165
+ render(
166
+ <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
167
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
168
+ </PersSDKProvider>
169
+ );
170
+
171
+ await waitFor(() => expect(capturedContext?.isInitialized).toBe(true));
172
+ expect(capturedContext!.locale).toBeNull();
173
+ });
174
+
175
+ it('passes the configured locale through to the SDK config', async () => {
176
+ const { PersSDK } = require('@explorins/pers-sdk/core');
177
+
178
+ render(
179
+ <PersSDKProvider config={{ apiProjectKey: 'test-project', locale: 'auto' }}>
180
+ <div />
181
+ </PersSDKProvider>
182
+ );
183
+
184
+ await waitFor(() => expect(PersSDK).toHaveBeenCalledTimes(1));
185
+ expect(PersSDK.mock.calls[0][1]).toMatchObject({ locale: 'auto' });
186
+ });
187
+
188
+ it('setLocale updates the SDK and the context so consumers re-render', async () => {
189
+ let capturedContext: PersSDKContext | null = null;
190
+
191
+ render(
192
+ <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
193
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
194
+ </PersSDKProvider>
195
+ );
196
+
197
+ await waitFor(() => expect(capturedContext?.isInitialized).toBe(true));
198
+
199
+ act(() => { capturedContext!.setLocale('ar-SA'); });
200
+
201
+ await waitFor(() => expect(capturedContext?.locale).toBe('ar-sa'));
202
+ expect(mockSdkInstance.setLocale).toHaveBeenCalledWith('ar-SA');
203
+
204
+ act(() => { capturedContext!.setLocale(null); });
205
+ await waitFor(() => expect(capturedContext?.locale).toBeNull());
206
+ });
207
+
208
+ it('setLocale throws before the SDK is initialized', async () => {
209
+ let capturedContext: PersSDKContext | null = null;
210
+
211
+ render(
212
+ <PersSDKProvider>
213
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
214
+ </PersSDKProvider>
215
+ );
216
+
217
+ await waitFor(() => expect(capturedContext).not.toBeNull());
218
+ expect(() => capturedContext!.setLocale('de')).toThrow('SDK not initialized');
219
+ });
220
+ });
221
+
222
+ describe('event handling', () => {
223
+ it('should set user on session_restored event', async () => {
224
+ let capturedContext: PersSDKContext | null = null;
225
+ const mockUser = { id: 'user-123', email: 'test@example.com' };
226
+
227
+ render(
228
+ <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
229
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
230
+ </PersSDKProvider>
231
+ );
232
+
233
+ await waitFor(() => {
234
+ expect(capturedContext?.sdk).not.toBeNull();
235
+ });
236
+
237
+ // Emit session_restored event
238
+ await act(async () => {
239
+ emitAuthEvent('session_restored', { user: mockUser });
240
+ });
241
+
242
+ await waitFor(() => {
243
+ expect(capturedContext?.isAuthenticated).toBe(true);
244
+ expect(capturedContext?.user).toEqual(mockUser);
245
+ });
246
+ });
247
+
248
+ it('should set user on login_success event', async () => {
249
+ let capturedContext: PersSDKContext | null = null;
250
+ const mockUser = { id: 'user-456', email: 'login@example.com' };
251
+
252
+ render(
253
+ <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
254
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
255
+ </PersSDKProvider>
256
+ );
257
+
258
+ await waitFor(() => {
259
+ expect(capturedContext?.sdk).not.toBeNull();
260
+ });
261
+
262
+ await act(async () => {
263
+ emitAuthEvent('login_success', { user: mockUser });
264
+ });
265
+
266
+ await waitFor(() => {
267
+ expect(capturedContext?.isAuthenticated).toBe(true);
268
+ expect(capturedContext?.user).toEqual(mockUser);
269
+ });
270
+ });
271
+
272
+ it('should clear user on logout_success event', async () => {
273
+ let capturedContext: PersSDKContext | null = null;
274
+ const mockUser = { id: 'user-789', email: 'logout@example.com' };
275
+
276
+ render(
277
+ <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
278
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
279
+ </PersSDKProvider>
280
+ );
281
+
282
+ await waitFor(() => {
283
+ expect(capturedContext?.sdk).not.toBeNull();
284
+ });
285
+
286
+ // First, log in
287
+ await act(async () => {
288
+ emitAuthEvent('login_success', { user: mockUser });
289
+ });
290
+
291
+ await waitFor(() => {
292
+ expect(capturedContext?.isAuthenticated).toBe(true);
293
+ });
294
+
295
+ // Then logout
296
+ await act(async () => {
297
+ emitAuthEvent('logout_success', {});
298
+ });
299
+
300
+ await waitFor(() => {
301
+ expect(capturedContext?.isAuthenticated).toBe(false);
302
+ expect(capturedContext?.user).toBeNull();
303
+ });
304
+ });
305
+
306
+ it('should clear user on session_restoration_failed event', async () => {
307
+ let capturedContext: PersSDKContext | null = null;
308
+
309
+ render(
310
+ <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
311
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
312
+ </PersSDKProvider>
313
+ );
314
+
315
+ await waitFor(() => {
316
+ expect(capturedContext?.sdk).not.toBeNull();
317
+ });
318
+
319
+ // Set initial authenticated state
320
+ await act(async () => {
321
+ capturedContext!.setAuthenticationState({ id: 'temp-user' } as any, true);
322
+ });
323
+
324
+ await waitFor(() => {
325
+ expect(capturedContext?.isAuthenticated).toBe(true);
326
+ });
327
+
328
+ // Emit session_restoration_failed
329
+ await act(async () => {
330
+ emitAuthEvent('session_restoration_failed', { error: 'Token expired' });
331
+ });
332
+
333
+ await waitFor(() => {
334
+ expect(capturedContext?.isAuthenticated).toBe(false);
335
+ expect(capturedContext?.user).toBeNull();
336
+ });
337
+ });
338
+
339
+ it('should NOT respond to AUTH_FAILED code (dead code was removed)', async () => {
340
+ // This test verifies that we no longer check for event.code === 'AUTH_FAILED'
341
+ // which was dead code that never triggered
342
+ let capturedContext: PersSDKContext | null = null;
343
+ const mockUser = { id: 'user-stay', email: 'stay@example.com' };
344
+
345
+ render(
346
+ <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
347
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
348
+ </PersSDKProvider>
349
+ );
350
+
351
+ await waitFor(() => {
352
+ expect(capturedContext?.sdk).not.toBeNull();
353
+ });
354
+
355
+ // Log in first
356
+ await act(async () => {
357
+ emitAuthEvent('login_success', { user: mockUser });
358
+ });
359
+
360
+ await waitFor(() => {
361
+ expect(capturedContext?.isAuthenticated).toBe(true);
362
+ });
363
+
364
+ // Emit an event with code: 'AUTH_FAILED' - this should NOT clear user
365
+ // because we removed the dead code that checked for this
366
+ await act(async () => {
367
+ if (mockSdkInstance?.events) {
368
+ mockSdkInstance.events.emit({
369
+ type: 'some_other_event',
370
+ code: 'AUTH_FAILED', // This was the dead code path we removed
371
+ domain: 'authentication',
372
+ details: {}
373
+ });
374
+ }
375
+ });
376
+
377
+ // User should STILL be authenticated - the code: 'AUTH_FAILED' check is gone
378
+ expect(capturedContext!.isAuthenticated).toBe(true);
379
+ expect(capturedContext!.user).toEqual(mockUser);
380
+ });
381
+ });
382
+
383
+ describe('usePersSDK hook', () => {
384
+ it('should throw error when used outside provider', () => {
385
+ // Suppress the expected error
386
+ const spy = jest.spyOn(console, 'error').mockImplementation(() => {});
387
+
388
+ const TestComponent = () => {
389
+ usePersSDK();
390
+ return null;
391
+ };
392
+
393
+ expect(() => render(<TestComponent />)).toThrow(
394
+ 'usePersSDK must be used within a PersSDKProvider'
395
+ );
396
+
397
+ spy.mockRestore();
398
+ });
399
+ });
400
+
401
+ describe('context methods', () => {
402
+ it('should allow manual authentication state updates', async () => {
403
+ let capturedContext: PersSDKContext | null = null;
404
+ const mockUser = { id: 'manual-user', email: 'manual@example.com' };
405
+
406
+ render(
407
+ <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
408
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
409
+ </PersSDKProvider>
410
+ );
411
+
412
+ await waitFor(() => {
413
+ expect(capturedContext?.isInitialized).toBe(true);
414
+ });
415
+
416
+ await act(async () => {
417
+ capturedContext!.setAuthenticationState(mockUser as any, true);
418
+ });
419
+
420
+ await waitFor(() => {
421
+ expect(capturedContext?.isAuthenticated).toBe(true);
422
+ expect(capturedContext?.user).toEqual(mockUser);
423
+ });
424
+ });
425
+
426
+ it('should refresh user data from SDK', async () => {
427
+ let capturedContext: PersSDKContext | null = null;
428
+
429
+ render(
430
+ <PersSDKProvider config={{ apiProjectKey: 'test-project' }}>
431
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
432
+ </PersSDKProvider>
433
+ );
434
+
435
+ await waitFor(() => {
436
+ expect(capturedContext?.isInitialized).toBe(true);
437
+ });
438
+
439
+ await act(async () => {
440
+ await capturedContext!.refreshUserData();
441
+ });
442
+
443
+ await waitFor(() => {
444
+ expect(capturedContext?.user).toEqual({ id: 'refreshed-user', email: 'refreshed@test.com' });
445
+ });
446
+ });
447
+
448
+ it('should throw error when refreshUserData called before init', async () => {
449
+ let capturedContext: PersSDKContext | null = null;
450
+
451
+ render(
452
+ <PersSDKProvider>
453
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
454
+ </PersSDKProvider>
455
+ );
456
+
457
+ await waitFor(() => {
458
+ expect(capturedContext).not.toBeNull();
459
+ });
460
+
461
+ await expect(capturedContext!.refreshUserData()).rejects.toThrow(
462
+ 'SDK not initialized'
463
+ );
464
+ });
465
+
466
+ it('should throw error when restoreSession called before init', async () => {
467
+ let capturedContext: PersSDKContext | null = null;
468
+
469
+ render(
470
+ <PersSDKProvider>
471
+ <TestConsumer onContext={(ctx) => { capturedContext = ctx; }} />
472
+ </PersSDKProvider>
473
+ );
474
+
475
+ await waitFor(() => {
476
+ expect(capturedContext).not.toBeNull();
477
+ });
478
+
479
+ await expect(capturedContext!.restoreSession()).rejects.toThrow(
480
+ 'SDK not initialized'
481
+ );
482
+ });
483
+ });
484
+ });