@jmruthers/pace-core 0.5.165 → 0.5.167

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 (141) hide show
  1. package/dist/{DataTable-HAATGNJY.js → DataTable-QMER2BPR.js} +3 -3
  2. package/dist/{chunk-23YYDN24.js → chunk-4AJ2ZWQW.js} +2 -2
  3. package/dist/{chunk-DDB2M4XO.js → chunk-AXFP27TJ.js} +2 -2
  4. package/dist/{chunk-Q4NUEMNV.js → chunk-OX3NRR4N.js} +9 -20
  5. package/dist/chunk-OX3NRR4N.js.map +1 -0
  6. package/dist/{chunk-GFSYFEVZ.js → chunk-PKYRTRZA.js} +35 -24
  7. package/dist/chunk-PKYRTRZA.js.map +1 -0
  8. package/dist/components.js +3 -3
  9. package/dist/index.js +4 -4
  10. package/dist/rbac/index.d.ts +10 -3
  11. package/dist/rbac/index.js +2 -2
  12. package/dist/utils.js +1 -1
  13. package/docs/api/classes/ColumnFactory.md +1 -1
  14. package/docs/api/classes/ErrorBoundary.md +1 -1
  15. package/docs/api/classes/InvalidScopeError.md +1 -1
  16. package/docs/api/classes/MissingUserContextError.md +1 -1
  17. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  18. package/docs/api/classes/PermissionDeniedError.md +1 -1
  19. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  20. package/docs/api/classes/RBACAuditManager.md +1 -1
  21. package/docs/api/classes/RBACCache.md +1 -1
  22. package/docs/api/classes/RBACEngine.md +1 -1
  23. package/docs/api/classes/RBACError.md +1 -1
  24. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  25. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  26. package/docs/api/classes/StorageUtils.md +1 -1
  27. package/docs/api/enums/FileCategory.md +1 -1
  28. package/docs/api/interfaces/AggregateConfig.md +1 -1
  29. package/docs/api/interfaces/BadgeProps.md +1 -1
  30. package/docs/api/interfaces/ButtonProps.md +1 -1
  31. package/docs/api/interfaces/CalendarProps.md +1 -1
  32. package/docs/api/interfaces/CardProps.md +1 -1
  33. package/docs/api/interfaces/ColorPalette.md +1 -1
  34. package/docs/api/interfaces/ColorShade.md +1 -1
  35. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  36. package/docs/api/interfaces/DataRecord.md +1 -1
  37. package/docs/api/interfaces/DataTableAction.md +1 -1
  38. package/docs/api/interfaces/DataTableColumn.md +1 -1
  39. package/docs/api/interfaces/DataTableProps.md +1 -1
  40. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  41. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  42. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  43. package/docs/api/interfaces/EventAppRoleData.md +1 -1
  44. package/docs/api/interfaces/EventLogoProps.md +1 -1
  45. package/docs/api/interfaces/ExportColumn.md +1 -1
  46. package/docs/api/interfaces/ExportOptions.md +1 -1
  47. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  48. package/docs/api/interfaces/FileMetadata.md +1 -1
  49. package/docs/api/interfaces/FileReference.md +1 -1
  50. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  51. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  52. package/docs/api/interfaces/FileUploadProps.md +1 -1
  53. package/docs/api/interfaces/FooterProps.md +1 -1
  54. package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
  55. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  56. package/docs/api/interfaces/InputProps.md +1 -1
  57. package/docs/api/interfaces/LabelProps.md +1 -1
  58. package/docs/api/interfaces/LoginFormProps.md +1 -1
  59. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  60. package/docs/api/interfaces/NavigationContextType.md +1 -1
  61. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  62. package/docs/api/interfaces/NavigationItem.md +1 -1
  63. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  64. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  65. package/docs/api/interfaces/Organisation.md +1 -1
  66. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  67. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  68. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  69. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  70. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  71. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  72. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  73. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  74. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  75. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  76. package/docs/api/interfaces/PaletteData.md +1 -1
  77. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  78. package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
  79. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  80. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  81. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  82. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  83. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  84. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  85. package/docs/api/interfaces/RBACConfig.md +1 -1
  86. package/docs/api/interfaces/RBACLogger.md +1 -1
  87. package/docs/api/interfaces/ResourcePermissions.md +1 -1
  88. package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
  89. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  90. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  91. package/docs/api/interfaces/RoleManagementResult.md +1 -1
  92. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  93. package/docs/api/interfaces/RouteConfig.md +1 -1
  94. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  95. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  96. package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
  97. package/docs/api/interfaces/StorageConfig.md +1 -1
  98. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  99. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  100. package/docs/api/interfaces/StorageListOptions.md +1 -1
  101. package/docs/api/interfaces/StorageListResult.md +1 -1
  102. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  103. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  104. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  105. package/docs/api/interfaces/StyleImport.md +1 -1
  106. package/docs/api/interfaces/SwitchProps.md +1 -1
  107. package/docs/api/interfaces/TabsContentProps.md +1 -1
  108. package/docs/api/interfaces/TabsListProps.md +1 -1
  109. package/docs/api/interfaces/TabsProps.md +1 -1
  110. package/docs/api/interfaces/TabsTriggerProps.md +1 -1
  111. package/docs/api/interfaces/TextareaProps.md +1 -1
  112. package/docs/api/interfaces/ToastActionElement.md +1 -1
  113. package/docs/api/interfaces/ToastProps.md +1 -1
  114. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  115. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  116. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  117. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  118. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  119. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  120. package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
  121. package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
  122. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  123. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  124. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  125. package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
  126. package/docs/api/interfaces/UserEventAccess.md +1 -1
  127. package/docs/api/interfaces/UserMenuProps.md +1 -1
  128. package/docs/api/interfaces/UserProfile.md +1 -1
  129. package/docs/api/modules.md +19 -12
  130. package/package.json +1 -1
  131. package/src/__tests__/hooks/usePermissions.test.ts +12 -9
  132. package/src/components/NavigationMenu/NavigationMenu.tsx +9 -21
  133. package/src/hooks/__tests__/ServiceHooks.test.tsx +2 -2
  134. package/src/rbac/hooks/__tests__/usePermissions.integration.test.ts +32 -34
  135. package/src/rbac/hooks/usePermissions.test.ts +34 -32
  136. package/src/rbac/hooks/usePermissions.ts +56 -28
  137. package/dist/chunk-GFSYFEVZ.js.map +0 -1
  138. package/dist/chunk-Q4NUEMNV.js.map +0 -1
  139. /package/dist/{DataTable-HAATGNJY.js.map → DataTable-QMER2BPR.js.map} +0 -0
  140. /package/dist/{chunk-23YYDN24.js.map → chunk-4AJ2ZWQW.js.map} +0 -0
  141. /package/dist/{chunk-DDB2M4XO.js.map → chunk-AXFP27TJ.js.map} +0 -0
@@ -23,9 +23,13 @@ import { getPermissionMap } from '../api';
23
23
 
24
24
  // Mock data
25
25
  const mockUserId = 'user-123';
26
+ const mockOrgId = 'org-123';
27
+ const mockEventId = 'event-123';
28
+ const mockAppId = 'app-123';
26
29
  const mockScope = {
27
- organisationId: 'org-123',
28
- eventId: 'event-123'
30
+ organisationId: mockOrgId,
31
+ eventId: mockEventId,
32
+ appId: mockAppId
29
33
  };
30
34
 
31
35
  const mockPermissionMap = {
@@ -47,7 +51,7 @@ describe('usePermissions Hook', () => {
47
51
  describe('Permission Fetching', () => {
48
52
  it('fetches permissions for valid user', async () => {
49
53
 
50
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
54
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
51
55
 
52
56
  expect(result.current.isLoading).toBe(true);
53
57
  expect(result.current.permissions).toEqual({});
@@ -63,7 +67,7 @@ describe('usePermissions Hook', () => {
63
67
  const error = new Error('Failed to fetch permissions');
64
68
  mockGetPermissionMap.mockRejectedValue(error);
65
69
 
66
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
70
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
67
71
 
68
72
  await waitFor(() => {
69
73
  expect(result.current.isLoading).toBe(false);
@@ -73,7 +77,7 @@ describe('usePermissions Hook', () => {
73
77
  });
74
78
 
75
79
  it('returns empty permissions for invalid user', async () => {
76
- const { result } = renderHook(() => usePermissions('', mockScope));
80
+ const { result } = renderHook(() => usePermissions('', mockOrgId, mockEventId, mockAppId));
77
81
 
78
82
  await waitFor(() => {
79
83
  expect(result.current.isLoading).toBe(false);
@@ -112,7 +116,7 @@ describe('usePermissions Hook', () => {
112
116
  });
113
117
 
114
118
  it('hasPermission returns correct boolean', async () => {
115
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
119
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
116
120
 
117
121
  await waitFor(() => {
118
122
  expect(result.current.isLoading).toBe(false);
@@ -126,7 +130,7 @@ describe('usePermissions Hook', () => {
126
130
  });
127
131
 
128
132
  it('hasAnyPermission works with multiple permissions', async () => {
129
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
133
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
130
134
 
131
135
  await waitFor(() => {
132
136
  expect(result.current.isLoading).toBe(false);
@@ -138,7 +142,7 @@ describe('usePermissions Hook', () => {
138
142
  });
139
143
 
140
144
  it('hasAllPermissions works with multiple permissions', async () => {
141
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
145
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
142
146
 
143
147
  await waitFor(() => {
144
148
  expect(result.current.isLoading).toBe(false);
@@ -150,7 +154,7 @@ describe('usePermissions Hook', () => {
150
154
  });
151
155
 
152
156
  it('handles empty permission lists', async () => {
153
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
157
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
154
158
 
155
159
  await waitFor(() => {
156
160
  expect(result.current.isLoading).toBe(false);
@@ -165,7 +169,7 @@ describe('usePermissions Hook', () => {
165
169
  it('refetch function works correctly', async () => {
166
170
  mockGetPermissionMap.mockResolvedValue(mockPermissionMap);
167
171
 
168
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
172
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
169
173
 
170
174
  await waitFor(() => {
171
175
  expect(result.current.isLoading).toBe(false);
@@ -184,7 +188,7 @@ describe('usePermissions Hook', () => {
184
188
  .mockResolvedValueOnce(mockPermissionMap)
185
189
  .mockRejectedValueOnce(new Error('Refetch error'));
186
190
 
187
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
191
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
188
192
 
189
193
  await waitFor(() => {
190
194
  expect(result.current.isLoading).toBe(false);
@@ -203,7 +207,7 @@ describe('usePermissions Hook', () => {
203
207
  it('shows loading state during initial fetch', () => {
204
208
  mockGetPermissionMap.mockImplementation(() => new Promise(() => {})); // Never resolves
205
209
 
206
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
210
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
207
211
 
208
212
  expect(result.current.isLoading).toBe(true);
209
213
  expect(result.current.permissions).toEqual({});
@@ -213,7 +217,7 @@ describe('usePermissions Hook', () => {
213
217
  it('shows loading state during refetch', async () => {
214
218
  mockGetPermissionMap.mockResolvedValue(mockPermissionMap);
215
219
 
216
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
220
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
217
221
 
218
222
  await waitFor(() => {
219
223
  expect(result.current.isLoading).toBe(false);
@@ -233,7 +237,7 @@ describe('usePermissions Hook', () => {
233
237
  it('handles non-Error exceptions', async () => {
234
238
  mockGetPermissionMap.mockRejectedValue('String error');
235
239
 
236
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
240
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
237
241
 
238
242
  await waitFor(() => {
239
243
  expect(result.current.error).toBeInstanceOf(Error);
@@ -246,7 +250,7 @@ describe('usePermissions Hook', () => {
246
250
  .mockRejectedValueOnce(new Error('Initial error'))
247
251
  .mockResolvedValueOnce(mockPermissionMap);
248
252
 
249
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
253
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
250
254
 
251
255
  await waitFor(() => {
252
256
  expect(result.current.error).toBeDefined();
@@ -275,7 +279,7 @@ describe('usePermissions Hook', () => {
275
279
 
276
280
  mockGetPermissionMap.mockResolvedValue(superAdminPermissions);
277
281
 
278
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
282
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
279
283
 
280
284
  await waitFor(() => {
281
285
  expect(result.current.isLoading).toBe(false);
@@ -304,7 +308,7 @@ describe('usePermissions Hook', () => {
304
308
 
305
309
  mockGetPermissionMap.mockResolvedValue(orgAdminPermissions);
306
310
 
307
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
311
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
308
312
 
309
313
  await waitFor(() => {
310
314
  expect(result.current.isLoading).toBe(false);
@@ -333,7 +337,7 @@ describe('usePermissions Hook', () => {
333
337
 
334
338
  mockGetPermissionMap.mockResolvedValue(eventAdminPermissions);
335
339
 
336
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
340
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
337
341
 
338
342
  await waitFor(() => {
339
343
  expect(result.current.isLoading).toBe(false);
@@ -363,7 +367,7 @@ describe('usePermissions Hook', () => {
363
367
 
364
368
  mockGetPermissionMap.mockResolvedValue(memberPermissions);
365
369
 
366
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
370
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
367
371
 
368
372
  await waitFor(() => {
369
373
  expect(result.current.isLoading).toBe(false);
@@ -393,7 +397,7 @@ describe('usePermissions Hook', () => {
393
397
 
394
398
  mockGetPermissionMap.mockResolvedValue(participantPermissions);
395
399
 
396
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
400
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
397
401
 
398
402
  await waitFor(() => {
399
403
  expect(result.current.isLoading).toBe(false);
@@ -465,7 +469,7 @@ describe('usePermissions Hook', () => {
465
469
 
466
470
  describe('Edge Cases', () => {
467
471
  it('handles null userId', async () => {
468
- const { result } = renderHook(() => usePermissions(null as any, mockScope));
472
+ const { result } = renderHook(() => usePermissions(null as any, mockOrgId, mockEventId, mockAppId));
469
473
 
470
474
  await waitFor(() => {
471
475
  expect(result.current.isLoading).toBe(false);
@@ -475,7 +479,7 @@ describe('usePermissions Hook', () => {
475
479
  });
476
480
 
477
481
  it('handles undefined userId', async () => {
478
- const { result } = renderHook(() => usePermissions(undefined as any, mockScope));
482
+ const { result } = renderHook(() => usePermissions(undefined as any, mockOrgId, mockEventId, mockAppId));
479
483
 
480
484
  await waitFor(() => {
481
485
  expect(result.current.isLoading).toBe(false);
@@ -487,7 +491,7 @@ describe('usePermissions Hook', () => {
487
491
  it('handles empty scope', async () => {
488
492
  mockGetPermissionMap.mockResolvedValue(mockPermissionMap);
489
493
 
490
- const { result } = renderHook(() => usePermissions(mockUserId, {} as any));
494
+ const { result } = renderHook(() => usePermissions(mockUserId, '', undefined, undefined));
491
495
 
492
496
  // Wait for the hook to complete loading
493
497
  await waitFor(() => {
@@ -506,7 +510,7 @@ describe('usePermissions Hook', () => {
506
510
 
507
511
  mockGetPermissionMap.mockResolvedValue(largePermissionMap);
508
512
 
509
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
513
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
510
514
 
511
515
  await waitFor(() => {
512
516
  expect(result.current.isLoading).toBe(false);
@@ -517,7 +521,7 @@ describe('usePermissions Hook', () => {
517
521
  it('handles empty permission map', async () => {
518
522
  mockGetPermissionMap.mockResolvedValue({});
519
523
 
520
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
524
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
521
525
 
522
526
  await waitFor(() => {
523
527
  expect(result.current.isLoading).toBe(false);
@@ -532,9 +536,8 @@ describe('usePermissions Hook', () => {
532
536
  });
533
537
 
534
538
  it('handles scope with empty organisationId', async () => {
535
- const invalidScope = { organisationId: '', eventId: 'event-123' };
536
539
 
537
- const { result } = renderHook(() => usePermissions(mockUserId, invalidScope));
540
+ const { result } = renderHook(() => usePermissions(mockUserId, '', 'event-123', undefined));
538
541
 
539
542
  await waitFor(() => {
540
543
  expect(result.current.isLoading).toBe(true);
@@ -544,9 +547,8 @@ describe('usePermissions Hook', () => {
544
547
  });
545
548
 
546
549
  it('handles scope with whitespace-only organisationId', async () => {
547
- const invalidScope = { organisationId: ' ', eventId: 'event-123' };
548
550
 
549
- const { result } = renderHook(() => usePermissions(mockUserId, invalidScope));
551
+ const { result } = renderHook(() => usePermissions(mockUserId, ' ', 'event-123', undefined));
550
552
 
551
553
  await waitFor(() => {
552
554
  expect(result.current.isLoading).toBe(true);
@@ -560,7 +562,7 @@ describe('usePermissions Hook', () => {
560
562
  it('maintains stable references for same permissions', async () => {
561
563
  mockGetPermissionMap.mockResolvedValue(mockPermissionMap);
562
564
 
563
- const { result, rerender } = renderHook(() => usePermissions(mockUserId, mockScope));
565
+ const { result, rerender } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
564
566
 
565
567
  await waitFor(() => {
566
568
  expect(result.current.isLoading).toBe(false);
@@ -580,7 +582,7 @@ describe('usePermissions Hook', () => {
580
582
  it('handles rapid permission checks efficiently', async () => {
581
583
  mockGetPermissionMap.mockResolvedValue(mockPermissionMap);
582
584
 
583
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
585
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
584
586
 
585
587
  await waitFor(() => {
586
588
  expect(result.current.isLoading).toBe(false);
@@ -601,7 +603,7 @@ describe('usePermissions Hook', () => {
601
603
 
602
604
  describe('[integration] Cache Invalidation', () => {
603
605
  it('refetches after cache invalidation', async () => {
604
- const { result } = renderHook(() => usePermissions(mockUserId, mockScope));
606
+ const { result } = renderHook(() => usePermissions(mockUserId, mockOrgId, mockEventId, mockAppId));
605
607
 
606
608
  await waitFor(() => {
607
609
  expect(result.current.isLoading).toBe(false);
@@ -27,13 +27,20 @@ import { getRBACLogger } from '../config';
27
27
  * Hook to get user's permissions in a scope
28
28
  *
29
29
  * @param userId - User ID
30
- * @param scope - Scope for permission checking
30
+ * @param organisationId - Organisation ID
31
+ * @param eventId - Event ID (optional)
32
+ * @param appId - Application ID (optional)
31
33
  * @returns Permission state and methods
32
34
  *
33
35
  * @example
34
36
  * ```tsx
35
37
  * function MyComponent() {
36
- * const { permissions, isLoading, error } = usePermissions(userId, scope);
38
+ * const { permissions, isLoading, error } = usePermissions(
39
+ * userId,
40
+ * organisationId,
41
+ * eventId,
42
+ * appId
43
+ * );
37
44
  *
38
45
  * if (isLoading) return <div>Loading...</div>;
39
46
  * if (error) return <div>Error: {error.message}</div>;
@@ -47,29 +54,31 @@ import { getRBACLogger } from '../config';
47
54
  * }
48
55
  * ```
49
56
  */
50
- export function usePermissions(userId: UUID, scope: Scope) {
57
+ export function usePermissions(
58
+ userId: UUID,
59
+ organisationId: string | undefined,
60
+ eventId: string | undefined,
61
+ appId: string | undefined
62
+ ) {
51
63
  const [permissions, setPermissions] = useState<PermissionMap>({} as PermissionMap);
52
64
  const [isLoading, setIsLoading] = useState(true);
53
65
  const [error, setError] = useState<Error | null>(null);
54
66
  const isFetchingRef = useRef(false);
55
67
  const logger = getRBACLogger();
56
68
 
57
- // Extract individual values to ensure React detects changes
58
- // This is critical - React compares these values, not the object reference
59
- const orgId = scope.organisationId || '';
60
- const eventId = scope.eventId;
61
- const appId = scope.appId;
69
+ // Track previous appId to detect when it becomes available
70
+ const prevAppIdRef = useRef<string | undefined>(appId);
71
+
72
+ // Normalize organisationId to empty string if undefined
73
+ const orgId = organisationId || '';
62
74
 
63
75
  // Log when hook is called with new scope (every render)
64
76
  // This helps us see if React is calling the hook with updated scope
65
77
  logger.warn('[usePermissions] Hook called with scope', {
66
78
  userId,
67
- extractedOrgId: orgId,
68
- extractedEventId: eventId,
69
- extractedAppId: appId,
70
- scopeOrgId: scope.organisationId,
71
- scopeEventId: scope.eventId,
72
- scopeAppId: scope.appId,
79
+ organisationId: orgId,
80
+ eventId,
81
+ appId,
73
82
  hasAppId: !!appId,
74
83
  hasOrganisationId: !!orgId
75
84
  });
@@ -78,17 +87,17 @@ export function usePermissions(userId: UUID, scope: Scope) {
78
87
  React.useEffect(() => {
79
88
  logger.warn('[usePermissions] Scope changed (useEffect)', {
80
89
  userId,
81
- organisationId: scope.organisationId,
82
- eventId: scope.eventId,
83
- appId: scope.appId,
84
- hasAppId: !!scope.appId,
85
- hasOrganisationId: !!scope.organisationId
90
+ organisationId,
91
+ eventId,
92
+ appId,
93
+ hasAppId: !!appId,
94
+ hasOrganisationId: !!organisationId
86
95
  });
87
- }, [scope.organisationId, scope.eventId, scope.appId, userId]);
96
+ }, [userId, organisationId, eventId, appId]);
88
97
 
89
98
  // Add timeout for missing organisation context (3 seconds)
90
99
  useEffect(() => {
91
- if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
100
+ if (!orgId || orgId === null || (typeof orgId === 'string' && orgId.trim() === '')) {
92
101
  const timeoutId = setTimeout(() => {
93
102
  setError(new Error('Organisation context is required for permission checks'));
94
103
  setIsLoading(false);
@@ -100,16 +109,28 @@ export function usePermissions(userId: UUID, scope: Scope) {
100
109
  if (error?.message === 'Organisation context is required for permission checks') {
101
110
  setError(null);
102
111
  }
103
- }, [scope.organisationId, error]);
112
+ }, [organisationId, error]);
104
113
 
105
114
  useEffect(() => {
106
115
  const fetchPermissions = async () => {
116
+ // Log appId change detection
117
+ const appIdChanged = prevAppIdRef.current !== appId;
118
+ if (appIdChanged) {
119
+ logger.warn('[usePermissions] appId changed detected!', {
120
+ prevAppId: prevAppIdRef.current,
121
+ newAppId: appId,
122
+ hasAppId: !!appId
123
+ });
124
+ prevAppIdRef.current = appId;
125
+ }
126
+
107
127
  logger.warn('[usePermissions] Fetch useEffect triggered', {
108
128
  userId,
109
129
  orgId,
110
130
  eventId,
111
131
  appId,
112
132
  hasAppId: !!appId,
133
+ appIdChanged,
113
134
  isFetching: isFetchingRef.current
114
135
  });
115
136
 
@@ -176,15 +197,15 @@ export function usePermissions(userId: UUID, scope: Scope) {
176
197
  setIsLoading(true);
177
198
  setError(null);
178
199
 
179
- // Build scope object from extracted values
180
- const scopeForFetch: Scope = {
200
+ // Build scope object for API call
201
+ const scope: Scope = {
181
202
  organisationId: orgId,
182
203
  eventId: eventId,
183
204
  appId: appId
184
205
  };
185
206
 
186
207
  // Fetch new permissions - don't clear old ones until we have new ones
187
- const permissionMap = await getPermissionMap({ userId, scope: scopeForFetch });
208
+ const permissionMap = await getPermissionMap({ userId, scope });
188
209
 
189
210
  logger.warn('[usePermissions] Permissions fetched successfully', {
190
211
  permissionCount: Object.keys(permissionMap).length,
@@ -211,7 +232,7 @@ export function usePermissions(userId: UUID, scope: Scope) {
211
232
  };
212
233
 
213
234
  fetchPermissions();
214
- }, [userId, scope.organisationId, scope.eventId, scope.appId]);
235
+ }, [userId, organisationId, eventId, appId]);
215
236
 
216
237
  const hasPermission = useCallback((permission: Permission): boolean => {
217
238
  if (permissions['*']) {
@@ -248,7 +269,7 @@ export function usePermissions(userId: UUID, scope: Scope) {
248
269
 
249
270
  // Don't fetch permissions if scope is invalid (e.g., organisationId is null/empty)
250
271
  // IMPORTANT: Don't clear existing permissions - keep them until we have new ones
251
- if (!scope.organisationId || scope.organisationId === null || (typeof scope.organisationId === 'string' && scope.organisationId.trim() === '')) {
272
+ if (!orgId || orgId === null || (typeof orgId === 'string' && orgId.trim() === '')) {
252
273
  // Keep existing permissions, just mark as loading
253
274
  setIsLoading(true);
254
275
  setError(null);
@@ -260,6 +281,13 @@ export function usePermissions(userId: UUID, scope: Scope) {
260
281
  setIsLoading(true);
261
282
  setError(null);
262
283
 
284
+ // Build scope object for API call
285
+ const scope: Scope = {
286
+ organisationId: orgId,
287
+ eventId: eventId,
288
+ appId: appId
289
+ };
290
+
263
291
  // Fetch new permissions - don't clear old ones until we have new ones
264
292
  const permissionMap = await getPermissionMap({ userId, scope });
265
293
 
@@ -276,7 +304,7 @@ export function usePermissions(userId: UUID, scope: Scope) {
276
304
  setIsLoading(false);
277
305
  isFetchingRef.current = false;
278
306
  }
279
- }, [userId, scope.organisationId, scope.eventId, scope.appId]);
307
+ }, [userId, organisationId, eventId, appId]);
280
308
 
281
309
  // Memoize the return object to prevent unnecessary re-renders
282
310
  return useMemo(() => ({