@jmruthers/pace-core 0.5.43 → 0.5.45

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 (140) hide show
  1. package/dist/{DataTable-BGK2YF7A.js → DataTable-PWLLTSP7.js} +5 -5
  2. package/dist/{UnifiedAuthProvider-DGQsy-vY.d.ts → UnifiedAuthProvider-CQNiemcB.d.ts} +2 -2
  3. package/dist/{chunk-BLZBTCBT.js → chunk-2T6QEWMI.js} +2 -2
  4. package/dist/{chunk-DNAASEYY.js → chunk-3FAB54BI.js} +4 -4
  5. package/dist/{chunk-37LRETMD.js → chunk-3PNBACK3.js} +2 -2
  6. package/dist/{chunk-ZSLTSF55.js → chunk-6AQ7X3EE.js} +5 -5
  7. package/dist/{chunk-X4Y4KT5T.js → chunk-D4X7PPGX.js} +3 -3
  8. package/dist/{chunk-SAB5UT2E.js → chunk-FZ7EBWOT.js} +3 -3
  9. package/dist/{chunk-B3MGS7VR.js → chunk-GIISFLMP.js} +3 -3
  10. package/dist/{chunk-RL267HOF.js → chunk-NYF3CUNC.js} +2 -2
  11. package/dist/{chunk-P27KH7XF.js → chunk-OQ6DTLZ6.js} +156 -196
  12. package/dist/chunk-OQ6DTLZ6.js.map +1 -0
  13. package/dist/{chunk-APHGXR2D.js → chunk-VCHXOYD5.js} +3 -3
  14. package/dist/components.d.ts +1 -1
  15. package/dist/components.js +7 -7
  16. package/dist/hooks.js +4 -4
  17. package/dist/index.d.ts +1 -1
  18. package/dist/index.js +10 -10
  19. package/dist/providers.d.ts +1 -1
  20. package/dist/providers.js +3 -3
  21. package/dist/rbac/index.js +5 -5
  22. package/dist/utils.js +1 -1
  23. package/docs/api/classes/ErrorBoundary.md +1 -1
  24. package/docs/api/classes/InvalidScopeError.md +1 -1
  25. package/docs/api/classes/MissingUserContextError.md +1 -1
  26. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  27. package/docs/api/classes/PermissionDeniedError.md +1 -1
  28. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  29. package/docs/api/classes/RBACAuditManager.md +1 -1
  30. package/docs/api/classes/RBACCache.md +1 -1
  31. package/docs/api/classes/RBACEngine.md +1 -1
  32. package/docs/api/classes/RBACError.md +1 -1
  33. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  34. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  35. package/docs/api/interfaces/AggregateConfig.md +1 -1
  36. package/docs/api/interfaces/ButtonProps.md +1 -1
  37. package/docs/api/interfaces/CardProps.md +1 -1
  38. package/docs/api/interfaces/ColorPalette.md +1 -1
  39. package/docs/api/interfaces/ColorShade.md +1 -1
  40. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  41. package/docs/api/interfaces/DataTableAction.md +1 -1
  42. package/docs/api/interfaces/DataTableColumn.md +1 -1
  43. package/docs/api/interfaces/DataTableProps.md +1 -1
  44. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  45. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  46. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  47. package/docs/api/interfaces/EventContextType.md +1 -1
  48. package/docs/api/interfaces/EventLogoProps.md +1 -1
  49. package/docs/api/interfaces/EventProviderProps.md +1 -1
  50. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  51. package/docs/api/interfaces/FileUploadProps.md +1 -1
  52. package/docs/api/interfaces/FooterProps.md +1 -1
  53. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  54. package/docs/api/interfaces/InputProps.md +1 -1
  55. package/docs/api/interfaces/LabelProps.md +1 -1
  56. package/docs/api/interfaces/LoginFormProps.md +1 -1
  57. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  58. package/docs/api/interfaces/NavigationContextType.md +1 -1
  59. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  60. package/docs/api/interfaces/NavigationItem.md +1 -1
  61. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  62. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  63. package/docs/api/interfaces/Organisation.md +1 -1
  64. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  65. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  66. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  67. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  68. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  69. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  70. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  71. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  72. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  73. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  74. package/docs/api/interfaces/PaletteData.md +1 -1
  75. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  76. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  77. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  78. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  79. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  80. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  81. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  82. package/docs/api/interfaces/RBACConfig.md +1 -1
  83. package/docs/api/interfaces/RBACContextType.md +1 -1
  84. package/docs/api/interfaces/RBACLogger.md +1 -1
  85. package/docs/api/interfaces/RBACProviderProps.md +1 -1
  86. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  87. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  88. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  89. package/docs/api/interfaces/RouteConfig.md +1 -1
  90. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  91. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  92. package/docs/api/interfaces/StorageConfig.md +1 -1
  93. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  94. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  95. package/docs/api/interfaces/StorageListOptions.md +1 -1
  96. package/docs/api/interfaces/StorageListResult.md +1 -1
  97. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  98. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  99. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  100. package/docs/api/interfaces/StyleImport.md +1 -1
  101. package/docs/api/interfaces/ToastActionElement.md +1 -1
  102. package/docs/api/interfaces/ToastProps.md +1 -1
  103. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  104. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  105. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  106. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  107. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  108. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  109. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  110. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  111. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  112. package/docs/api/interfaces/UserEventAccess.md +1 -1
  113. package/docs/api/interfaces/UserMenuProps.md +1 -1
  114. package/docs/api/interfaces/UserProfile.md +1 -1
  115. package/docs/api/modules.md +3 -3
  116. package/package.json +1 -1
  117. package/src/components/DataTable/__tests__/DataTable.comprehensive.test.tsx +756 -0
  118. package/src/components/DataTable/__tests__/DataTable.test.tsx +880 -0
  119. package/src/components/DataTable/__tests__/DataTableCore.test.tsx +702 -0
  120. package/src/components/PrintButton/__tests__/PrintButton.test.tsx +271 -0
  121. package/src/providers/AuthProvider.tsx +131 -230
  122. package/src/providers/OrganisationProvider.tsx +72 -10
  123. package/src/providers/__tests__/AuthProvider.test.tsx +619 -0
  124. package/src/providers/__tests__/EventProvider.test.tsx +190 -0
  125. package/src/providers/__tests__/InactivityProvider.test.tsx +645 -0
  126. package/src/providers/__tests__/OrganisationProvider.test.tsx +343 -0
  127. package/src/providers/__tests__/README.md +167 -0
  128. package/src/providers/__tests__/UnifiedAuthProvider.test.tsx +581 -0
  129. package/src/rbac/__tests__/rbac-core.test.tsx +277 -0
  130. package/dist/chunk-P27KH7XF.js.map +0 -1
  131. /package/dist/{DataTable-BGK2YF7A.js.map → DataTable-PWLLTSP7.js.map} +0 -0
  132. /package/dist/{chunk-BLZBTCBT.js.map → chunk-2T6QEWMI.js.map} +0 -0
  133. /package/dist/{chunk-DNAASEYY.js.map → chunk-3FAB54BI.js.map} +0 -0
  134. /package/dist/{chunk-37LRETMD.js.map → chunk-3PNBACK3.js.map} +0 -0
  135. /package/dist/{chunk-ZSLTSF55.js.map → chunk-6AQ7X3EE.js.map} +0 -0
  136. /package/dist/{chunk-X4Y4KT5T.js.map → chunk-D4X7PPGX.js.map} +0 -0
  137. /package/dist/{chunk-SAB5UT2E.js.map → chunk-FZ7EBWOT.js.map} +0 -0
  138. /package/dist/{chunk-B3MGS7VR.js.map → chunk-GIISFLMP.js.map} +0 -0
  139. /package/dist/{chunk-RL267HOF.js.map → chunk-NYF3CUNC.js.map} +0 -0
  140. /package/dist/{chunk-APHGXR2D.js.map → chunk-VCHXOYD5.js.map} +0 -0
@@ -34,14 +34,6 @@ export interface AuthContextType {
34
34
 
35
35
  const AuthContext = createContext<AuthContextType | undefined>(undefined);
36
36
 
37
- export const useAuth = () => {
38
- const context = useContext(AuthContext);
39
- if (!context) {
40
- throw new Error('useAuth must be used within an AuthProvider');
41
- }
42
- return context;
43
- };
44
-
45
37
  export interface AuthProviderProps {
46
38
  children: React.ReactNode;
47
39
  supabaseClient?: SupabaseClient;
@@ -83,312 +75,213 @@ export function AuthProvider({ children, supabaseClient }: AuthProviderProps) {
83
75
  };
84
76
  }, []);
85
77
 
86
- // Load initial user if no session but supabase client exists
78
+ // FIXED: Proper session restoration with better error handling
87
79
  useEffect(() => {
88
- if (!supabaseClient) return;
80
+ if (!supabaseClient) {
81
+ setAuthLoading(false);
82
+ return;
83
+ }
89
84
 
90
- const loadInitialUser = async (retryCount = 0) => {
85
+ let isMounted = true;
86
+
87
+ const initializeAuth = async () => {
91
88
  try {
92
- DebugLogger.log('AuthProvider', `Loading initial user (attempt ${retryCount + 1})...`);
93
- const response = await supabaseClient.auth.getUser();
94
- const { data: { user: initialUser }, error } = response || { data: { user: null }, error: null };
89
+ DebugLogger.log('AuthProvider', 'Initializing authentication...');
90
+
91
+ // First, try to get the current session
92
+ const { data: { session: currentSession }, error: sessionError } = await supabaseClient.auth.getSession();
95
93
 
96
- if (error) {
97
- console.warn('Failed to get initial user:', error);
98
- // Retry once if it's a network error
99
- if (retryCount === 0 && (error.message?.includes('network') || error.message?.includes('timeout'))) {
100
- console.log('AuthProvider: Retrying initial user load due to network error...');
101
- setTimeout(() => loadInitialUser(1), 1000);
102
- return;
94
+ if (sessionError) {
95
+ console.warn('[AuthProvider] Error getting current session:', sessionError);
96
+ // Don't treat session errors as fatal - user might not be logged in
97
+ }
98
+
99
+ if (currentSession && isMounted) {
100
+ DebugLogger.log('AuthProvider', 'Session restored from storage:', currentSession.user?.email);
101
+ setSession(currentSession);
102
+ setUser(currentSession.user);
103
+ setAuthError(null);
104
+ } else if (isMounted) {
105
+ DebugLogger.log('AuthProvider', 'No session found in storage');
106
+ // Try to get user anyway (in case session is expired but user exists)
107
+ const { data: { user: currentUser }, error: userError } = await supabaseClient.auth.getUser();
108
+
109
+ if (userError) {
110
+ DebugLogger.log('AuthProvider', 'No user found:', userError.message);
111
+ } else if (currentUser && isMounted) {
112
+ DebugLogger.log('AuthProvider', 'User found without session:', currentUser.email);
113
+ setUser(currentUser);
114
+ // Don't set session if it's null - this prevents issues
103
115
  }
104
- setAuthLoading(false);
105
- return;
106
116
  }
107
-
108
- if (initialUser) {
109
- DebugLogger.log('AuthProvider', 'Initial user loaded successfully:', initialUser.email);
110
- setUser(initialUser);
111
- } else {
112
- DebugLogger.log('AuthProvider', 'No initial user found');
117
+
118
+ if (isMounted) {
119
+ setAuthLoading(false);
113
120
  }
114
- setAuthLoading(false);
115
121
  } catch (error) {
116
- console.error('Error loading initial user:', error);
117
- // Retry once if it's a network error
118
- if (retryCount === 0 && (error instanceof Error && (error.message.includes('network') || error.message.includes('timeout')))) {
119
- console.log('AuthProvider: Retrying initial user load due to network error...');
120
- setTimeout(() => loadInitialUser(1), 1000);
121
- return;
122
+ console.error('[AuthProvider] Error during auth initialization:', error);
123
+ if (isMounted) {
124
+ setAuthLoading(false);
125
+ // Don't set auth error for initialization failures - user might not be logged in
122
126
  }
123
- setAuthLoading(false);
124
127
  }
125
128
  };
126
129
 
127
- // Only load initial user if we haven't set up the auth state change listener yet
128
- // This prevents race conditions between loadInitialUser and auth state change
129
- const timeoutId = setTimeout(() => {
130
- if (authLoading && !user && !session) {
131
- loadInitialUser();
132
- }
133
- }, 100);
134
-
135
- return () => clearTimeout(timeoutId);
136
- }, [supabaseClient, authLoading, user, session]);
137
-
138
- // Auth state change listener
139
- useEffect(() => {
140
- if (!supabaseClient) {
141
- setAuthLoading(false);
142
- return;
143
- }
144
-
145
- // Reasonable timeout to allow for auth state changes while preventing infinite hanging
146
- const timeoutId = setTimeout(() => {
147
- if (authLoading) {
148
- console.warn('AuthProvider: Auth loading timeout reached - this may indicate a network issue or slow auth response');
149
- setAuthLoading(false);
150
- setAuthError(new AuthError('Authentication timeout - please try refreshing the page'));
151
- }
152
- }, 10000); // Increased to 10 seconds to allow for proper auth flow
153
-
154
- try {
155
- DebugLogger.log('AuthProvider', 'Setting up auth state change listener...');
156
- const authStateChange = supabaseClient.auth.onAuthStateChange(
157
- (event, session) => {
158
- try {
159
- DebugLogger.log('AuthProvider', 'Auth state changed:', event, session?.user?.email);
160
-
161
- // Clear timeout immediately when we get an auth state change
162
- clearTimeout(timeoutId);
130
+ // Set up auth state change listener
131
+ const { data: { subscription } } = supabaseClient.auth.onAuthStateChange(
132
+ (event, session) => {
133
+ if (!isMounted) return;
134
+
135
+ try {
136
+ DebugLogger.log('AuthProvider', 'Auth state changed:', event, session?.user?.email);
137
+
138
+ // Handle different auth events
139
+ if (event === 'SIGNED_OUT') {
140
+ DebugLogger.log('AuthProvider', 'User signed out, clearing all state');
141
+ setSession(null);
142
+ setUser(null);
143
+ setAuthError(null);
144
+ } else if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
145
+ DebugLogger.log('AuthProvider', 'User signed in or token refreshed');
146
+ setSession(session);
147
+ setUser(session?.user ?? null);
163
148
 
164
- // Handle different auth events
165
- if (event === 'SIGNED_OUT') {
166
- DebugLogger.log('AuthProvider', 'User signed out, clearing all state');
167
- // Clear all state immediately and aggressively
168
- setSession(null);
169
- setUser(null);
170
- setAuthLoading(false);
149
+ // Only clear auth error if we have a valid session
150
+ if (session) {
171
151
  setAuthError(null);
172
- } else if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') {
173
- DebugLogger.log('AuthProvider', 'User signed in or token refreshed');
152
+ }
153
+ } else if (event === 'INITIAL_SESSION') {
154
+ DebugLogger.log('AuthProvider', 'Initial session event');
155
+ if (session) {
174
156
  setSession(session);
175
- setUser(session?.user ?? null);
176
- setAuthLoading(false);
177
-
178
- // Only clear auth error if we have a valid session
179
- if (session) {
180
- setAuthError(null);
181
- }
182
- } else {
183
- // Handle other events (INITIAL_SESSION, etc.)
184
- if (session) {
185
- setSession(session);
186
- setUser(session.user ?? null);
187
- setAuthLoading(false);
188
- if (session) {
189
- setAuthError(null);
190
- }
191
- }
157
+ setUser(session.user ?? null);
158
+ setAuthError(null);
192
159
  }
193
- } catch (error) {
194
- console.warn('[AuthProvider] Error in auth state change handler:', error);
195
- // Ensure loading is always set to false
160
+ }
161
+
162
+ // Always set loading to false after any auth state change
163
+ setAuthLoading(false);
164
+ } catch (error) {
165
+ console.warn('[AuthProvider] Error in auth state change handler:', error);
166
+ if (isMounted) {
196
167
  setAuthLoading(false);
197
168
  }
198
169
  }
199
- );
200
-
201
- const subscription = authStateChange?.data?.subscription || authStateChange;
202
-
203
- return () => {
204
- clearTimeout(timeoutId);
205
- if (subscription && typeof subscription.unsubscribe === 'function') {
206
- DebugLogger.log('AuthProvider', 'Cleaning up auth state listener');
207
- subscription.unsubscribe();
208
- }
209
- };
210
- } catch (error) {
211
- console.error('AuthProvider: Error setting up auth state change listener:', error);
212
-
213
- // Don't set authError for connection issues or auth session missing errors
214
- if (error instanceof Error &&
215
- !error.message.includes('No API key found') &&
216
- !error.message.includes('AuthSessionMissingError') &&
217
- !error.message.includes('Auth session missing')) {
218
- setAuthError(error as AuthError);
219
170
  }
220
- setAuthLoading(false);
221
- clearTimeout(timeoutId);
222
- return () => {};
223
- }
171
+ );
172
+
173
+ // Initialize auth
174
+ initializeAuth();
175
+
176
+ return () => {
177
+ isMounted = false;
178
+ subscription?.unsubscribe();
179
+ };
224
180
  }, [supabaseClient]);
225
181
 
226
182
  // Auth methods
227
183
  const signIn = useCallback(async (email: string, password?: string) => {
228
184
  if (!supabaseClient) {
229
- const error = new Error('No Supabase client provided') as AuthError;
230
- setAuthError(error);
231
- return { error };
185
+ return { error: new AuthError('Supabase client not available') };
232
186
  }
233
187
 
234
- setAuthLoading(true);
235
- setAuthError(null);
236
-
237
188
  try {
238
- const { error } = await supabaseClient.auth.signInWithPassword({ email, password: password || '' });
239
- if (error) {
240
- setAuthError(error);
241
- }
189
+ const { error } = await supabaseClient.auth.signInWithPassword({
190
+ email,
191
+ password: password || '',
192
+ });
242
193
  return { error };
243
- } catch (err: any) {
244
- const authError = err as AuthError;
245
- setAuthError(authError);
246
- return { error: authError };
247
- } finally {
248
- setAuthLoading(false);
194
+ } catch (error) {
195
+ return { error: error as AuthError };
249
196
  }
250
197
  }, [supabaseClient]);
251
198
 
252
199
  const signUp = useCallback(async (email: string, password: string) => {
253
200
  if (!supabaseClient) {
254
- const error = new Error('No Supabase client provided') as AuthError;
255
- setAuthError(error);
256
- return { error };
201
+ return { error: new AuthError('Supabase client not available') };
257
202
  }
258
203
 
259
- setAuthError(null);
260
204
  try {
261
- const { error } = await supabaseClient.auth.signUp({ email, password });
262
- if (error) {
263
- setAuthError(error);
264
- }
205
+ const { error } = await supabaseClient.auth.signUp({
206
+ email,
207
+ password,
208
+ });
265
209
  return { error };
266
- } catch (err: any) {
267
- const authError = err as AuthError;
268
- setAuthError(authError);
269
- return { error: authError };
210
+ } catch (error) {
211
+ return { error: error as AuthError };
270
212
  }
271
213
  }, [supabaseClient]);
272
214
 
273
215
  const signOut = useCallback(async () => {
274
- DebugLogger.log('AuthProvider', 'signOut called');
275
-
276
- // Immediately set loading to false to prevent timeout warnings
277
- setAuthLoading(false);
278
-
279
216
  if (!supabaseClient) {
280
- DebugLogger.log('AuthProvider', 'No supabase client, clearing state manually');
281
- setUser(null);
282
- setSession(null);
283
- setAuthError(null);
284
- return { error: null };
217
+ return { error: new AuthError('Supabase client not available') };
285
218
  }
286
-
287
- // Clear state immediately before calling Supabase signOut
288
- DebugLogger.log('AuthProvider', 'Pre-clearing state before signOut call');
289
- setUser(null);
290
- setSession(null);
291
- setAuthError(null);
292
-
219
+
293
220
  try {
294
- DebugLogger.log('AuthProvider', 'Calling supabase signOut');
295
-
296
- // Add a timeout to prevent hanging
297
- const signOutPromise = supabaseClient.auth.signOut();
298
- const timeoutPromise = new Promise((_, reject) =>
299
- setTimeout(() => reject(new Error('SignOut timeout')), 3000)
300
- );
301
-
302
- const { error } = await Promise.race([signOutPromise, timeoutPromise]) as any;
303
-
304
- if (error && !error.message?.includes('SignOut timeout')) {
305
- console.error('[AuthProvider] signOut error:', error);
306
- // Don't set auth error for logout - we've already cleared state
307
- }
308
-
309
- DebugLogger.log('AuthProvider', 'signOut process completed');
310
- return { error: null }; // Always return success for logout
311
- } catch (err) {
312
- console.warn('[AuthProvider] signOut exception (ignored):', err);
313
- // Ignore errors during logout - state is already cleared
314
- return { error: null };
221
+ const { error } = await supabaseClient.auth.signOut();
222
+ return { error };
223
+ } catch (error) {
224
+ return { error: error as AuthError };
315
225
  }
316
226
  }, [supabaseClient]);
317
227
 
318
228
  const resetPassword = useCallback(async (email: string) => {
319
229
  if (!supabaseClient) {
320
- const error = new Error('No Supabase client provided') as AuthError;
321
- setAuthError(error);
322
- return { error };
230
+ return { error: new AuthError('Supabase client not available') };
323
231
  }
324
232
 
325
- setAuthError(null);
326
233
  try {
327
234
  const { error } = await supabaseClient.auth.resetPasswordForEmail(email);
328
- if (error) {
329
- setAuthError(error);
330
- }
331
235
  return { error };
332
- } catch (err: any) {
333
- const authError = err as AuthError;
334
- setAuthError(authError);
335
- return { error: authError };
236
+ } catch (error) {
237
+ return { error: error as AuthError };
336
238
  }
337
239
  }, [supabaseClient]);
338
240
 
339
241
  const updatePassword = useCallback(async (password: string) => {
340
- if (!supabaseClient) return { error: new AuthError('Supabase client not available.', 500) };
341
- const { error } = await supabaseClient.auth.updateUser({ password });
342
- if (error) {
343
- setAuthError(error);
242
+ if (!supabaseClient) {
243
+ return { error: new AuthError('Supabase client not available') };
244
+ }
245
+
246
+ try {
247
+ const { error } = await supabaseClient.auth.updateUser({
248
+ password,
249
+ });
250
+ return { error };
251
+ } catch (error) {
252
+ return { error: error as AuthError };
344
253
  }
345
- return { error };
346
254
  }, [supabaseClient]);
347
255
 
348
256
  const refreshSession = useCallback(async () => {
349
257
  if (!supabaseClient) {
350
- const error = new Error('No Supabase client provided') as AuthError;
351
- setAuthError(error);
352
- return { error };
258
+ return { error: new AuthError('Supabase client not available') };
353
259
  }
354
260
 
355
- setAuthError(null);
356
261
  try {
357
262
  const { error } = await supabaseClient.auth.refreshSession();
358
- if (error) {
359
- setAuthError(error);
360
- }
361
263
  return { error };
362
- } catch (err: any) {
363
- const authError = err as AuthError;
364
- setAuthError(authError);
365
- return { error: authError };
264
+ } catch (error) {
265
+ return { error: error as AuthError };
366
266
  }
367
267
  }, [supabaseClient]);
368
268
 
369
- // Memoized derived values
370
- const isAuthenticated = !!user;
371
-
372
269
  // Memoized context value
373
- const contextValue = useMemo<AuthContextType>(() => ({
270
+ const contextValue = useMemo(() => ({
374
271
  user,
375
272
  session,
376
- isAuthenticated,
273
+ isAuthenticated: !!user && !!session,
377
274
  authLoading,
378
275
  authError,
379
276
  error: authError, // Alias for backward compatibility
380
- supabase: supabaseClient || null,
381
-
277
+ supabase: supabaseClient || null, // Ensure null instead of undefined
382
278
  signIn,
383
279
  signUp,
384
280
  signOut,
385
281
  resetPassword,
386
282
  updatePassword,
387
283
  refreshSession,
388
- }), [
389
- user, session, isAuthenticated, authLoading, authError, supabaseClient,
390
- signIn, signUp, signOut, resetPassword, updatePassword, refreshSession,
391
- ]);
284
+ }), [user, session, authLoading, authError, supabaseClient, signIn, signUp, signOut, resetPassword, updatePassword, refreshSession]);
392
285
 
393
286
  return (
394
287
  <AuthContext.Provider value={contextValue}>
@@ -396,3 +289,11 @@ export function AuthProvider({ children, supabaseClient }: AuthProviderProps) {
396
289
  </AuthContext.Provider>
397
290
  );
398
291
  }
292
+
293
+ export const useAuth = (): AuthContextType => {
294
+ const context = useContext(AuthContext);
295
+ if (!context) {
296
+ throw new Error('useAuth must be used within an AuthProvider');
297
+ }
298
+ return context;
299
+ };
@@ -166,8 +166,15 @@ export function OrganisationProvider({ children }: OrganisationProviderProps) {
166
166
  setError(null);
167
167
  return;
168
168
  }
169
- setIsLoading(true);
170
- setError(null);
169
+
170
+ // FIXED: Additional check to prevent loading during auth state changes
171
+ if (isLoading) {
172
+ DebugLogger.log("OrganisationProvider", "Already loading, skipping duplicate load");
173
+ return;
174
+ }
175
+
176
+ setIsLoading(true);
177
+ setError(null);
171
178
 
172
179
  try {
173
180
  DebugLogger.log("OrganisationProvider", "Loading organisations for user:", user.id);
@@ -214,15 +221,42 @@ export function OrganisationProvider({ children }: OrganisationProviderProps) {
214
221
  throw new Error('User has no active organisation memberships') as OrganisationSecurityError;
215
222
  }
216
223
 
224
+ // FIXED: Debug log to identify any problematic membership data
225
+ memberships.forEach((membership: any, index: number) => {
226
+ if (!membership.organisation_id || membership.organisation_id.trim() === '') {
227
+ console.warn(`[OrganisationProvider] Membership ${index} has invalid organisation_id:`, membership);
228
+ }
229
+ });
230
+
217
231
  // Get organisation details for the memberships
218
232
  const organisationIds = memberships
219
233
  .map((m: any) => m.organisation_id)
220
- .filter((id: string) => id && id.trim() !== '' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id));
234
+ .filter((id: string) => {
235
+ // FIXED: Better validation to prevent empty string UUID errors
236
+ if (!id || typeof id !== 'string') {
237
+ console.warn("[OrganisationProvider] Invalid organisation ID (not string):", id);
238
+ return false;
239
+ }
240
+ const trimmedId = id.trim();
241
+ if (trimmedId === '') {
242
+ console.warn("[OrganisationProvider] Empty organisation ID found");
243
+ return false;
244
+ }
245
+ // Validate UUID format
246
+ const isValidUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(trimmedId);
247
+ if (!isValidUuid) {
248
+ console.warn("[OrganisationProvider] Invalid UUID format:", trimmedId);
249
+ }
250
+ return isValidUuid;
251
+ });
221
252
 
222
253
  if (organisationIds.length === 0) {
254
+ console.warn("[OrganisationProvider] No valid organisation IDs found in memberships:", memberships);
223
255
  throw new Error('No valid organisation IDs found in memberships') as OrganisationSecurityError;
224
256
  }
225
257
 
258
+ DebugLogger.log("OrganisationProvider", "Valid organisation IDs:", organisationIds);
259
+
226
260
  const { data: organisations, error: orgError } = await supabase
227
261
  .from('organisations')
228
262
  .select('id, name, display_name, subscription_tier, settings, is_active, parent_id, created_at, updated_at')
@@ -264,14 +298,25 @@ export function OrganisationProvider({ children }: OrganisationProviderProps) {
264
298
  const persistedOrgString = localStorage.getItem(STORAGE_KEYS.SELECTED_ORGANISATION);
265
299
  if (persistedOrgString) {
266
300
  const persistedOrg = JSON.parse(persistedOrgString) as Organisation;
267
- const validPersistedOrg = activeOrgs.find(org => org.id === persistedOrg.id);
268
- if (validPersistedOrg) {
269
- initialOrg = validPersistedOrg;
270
- DebugLogger.log("OrganisationProvider", "Restored persisted organisation:", initialOrg.display_name);
301
+ // FIXED: Validate persisted org ID before using it
302
+ if (persistedOrg.id && typeof persistedOrg.id === 'string' && persistedOrg.id.trim() !== '') {
303
+ const validPersistedOrg = activeOrgs.find(org => org.id === persistedOrg.id);
304
+ if (validPersistedOrg) {
305
+ initialOrg = validPersistedOrg;
306
+ DebugLogger.log("OrganisationProvider", "Restored persisted organisation:", initialOrg.display_name);
307
+ } else {
308
+ console.warn("[OrganisationProvider] Persisted organisation not found in active orgs, clearing cache");
309
+ localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
310
+ }
311
+ } else {
312
+ console.warn("[OrganisationProvider] Invalid persisted organisation ID, clearing cache");
313
+ localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
271
314
  }
272
315
  }
273
316
  } catch (storageError) {
274
317
  console.warn("[OrganisationProvider] Failed to restore persisted organisation:", storageError);
318
+ // Clear potentially corrupted cache
319
+ localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
275
320
  }
276
321
 
277
322
  // 2. Fall back to org_admin role organisation (highest privilege)
@@ -320,10 +365,27 @@ export function OrganisationProvider({ children }: OrganisationProviderProps) {
320
365
  }
321
366
  }, [user, session, supabase]);
322
367
 
323
- // Load organisations when dependencies change
368
+ // FIXED: Load organisations only when authentication is complete and stable
324
369
  useEffect(() => {
325
- loadUserOrganisations();
326
- }, [loadUserOrganisations]);
370
+ // Only load organizations if we have a valid user and session
371
+ // and we're not in the middle of authentication loading
372
+ if (user && session && supabase && !isLoading) {
373
+ DebugLogger.log("OrganisationProvider", "Authentication stable, loading organizations...");
374
+ loadUserOrganisations();
375
+ } else if (!user && !session) {
376
+ // Clear state if no authentication
377
+ DebugLogger.log("OrganisationProvider", "No authentication, clearing organization state");
378
+ setSelectedOrganisation(null);
379
+ setOrganisations([]);
380
+ setUserMemberships([]);
381
+ setRoleMapState(new Map());
382
+ setIsLoading(false);
383
+ setError(null);
384
+ // FIXED: Clear localStorage when no authentication to prevent stale data
385
+ localStorage.removeItem(STORAGE_KEYS.SELECTED_ORGANISATION);
386
+ localStorage.removeItem(STORAGE_KEYS.ORGANISATION_CONTEXT);
387
+ }
388
+ }, [user, session, supabase, isLoading, loadUserOrganisations]);
327
389
 
328
390
  // Handle logout and redirect to login
329
391
  const handleLogoutAndRedirect = useCallback(async () => {