@jmruthers/pace-core 0.5.88 → 0.5.90

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 (159) hide show
  1. package/dist/{AuthService-DcTI5Ov4.d.ts → AuthService-DX15wM6y.d.ts} +8 -0
  2. package/dist/{DataTable-PWBMKMOG.js → DataTable-VIP44OB6.js} +7 -6
  3. package/dist/{UnifiedAuthProvider-5D3HEQND.js → UnifiedAuthProvider-6JRTOFPS.js} +4 -3
  4. package/dist/{chunk-BNXBJOGL.js → chunk-4DYK5KCK.js} +4 -4
  5. package/dist/{chunk-XXVM53P4.js → chunk-7NIERLC6.js} +8 -8
  6. package/dist/{chunk-XXVM53P4.js.map → chunk-7NIERLC6.js.map} +1 -1
  7. package/dist/{chunk-KTPG5VCH.js → chunk-7XBW2P7B.js} +2 -2
  8. package/dist/{chunk-DP5X5ORK.js → chunk-AIV3VYBQ.js} +82 -25
  9. package/dist/chunk-AIV3VYBQ.js.map +1 -0
  10. package/dist/{chunk-CJIZS3UE.js → chunk-EWMXLDIX.js} +19 -15
  11. package/dist/chunk-EWMXLDIX.js.map +1 -0
  12. package/dist/{chunk-YY4YYM3E.js → chunk-G2SCPUKC.js} +2 -2
  13. package/dist/{chunk-CXKMRKRF.js → chunk-G2YT64FA.js} +3 -3
  14. package/dist/{chunk-AQGF5OG7.js → chunk-GD3ENUKD.js} +3 -3
  15. package/dist/{chunk-H3P2RGKZ.js → chunk-JDPFQV3V.js} +55 -6
  16. package/dist/chunk-JDPFQV3V.js.map +1 -0
  17. package/dist/{chunk-3RZBKQ5Y.js → chunk-JQWSAYZC.js} +2 -2
  18. package/dist/{chunk-A6HBIY5P.js → chunk-SMJZMKYN.js} +11 -2
  19. package/dist/{chunk-A6HBIY5P.js.map → chunk-SMJZMKYN.js.map} +1 -1
  20. package/dist/{chunk-L3RV2ALE.js → chunk-VKOCWWVY.js} +6 -1
  21. package/dist/{chunk-L3RV2ALE.js.map → chunk-VKOCWWVY.js.map} +1 -1
  22. package/dist/{chunk-6UHXQH7P.js → chunk-XZHZYSAK.js} +5 -5
  23. package/dist/components.js +9 -8
  24. package/dist/components.js.map +1 -1
  25. package/dist/hooks.js +8 -8
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +13 -13
  28. package/dist/providers.d.ts +2 -2
  29. package/dist/providers.js +3 -2
  30. package/dist/rbac/index.js +8 -7
  31. package/dist/styles/index.js +2 -2
  32. package/dist/theming/runtime.js +3 -1
  33. package/dist/utils.js +1 -1
  34. package/docs/api/classes/ColumnFactory.md +1 -1
  35. package/docs/api/classes/ErrorBoundary.md +1 -1
  36. package/docs/api/classes/InvalidScopeError.md +1 -1
  37. package/docs/api/classes/MissingUserContextError.md +1 -1
  38. package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
  39. package/docs/api/classes/PermissionDeniedError.md +1 -1
  40. package/docs/api/classes/PublicErrorBoundary.md +1 -1
  41. package/docs/api/classes/RBACAuditManager.md +1 -1
  42. package/docs/api/classes/RBACCache.md +1 -1
  43. package/docs/api/classes/RBACEngine.md +1 -1
  44. package/docs/api/classes/RBACError.md +1 -1
  45. package/docs/api/classes/RBACNotInitializedError.md +1 -1
  46. package/docs/api/classes/SecureSupabaseClient.md +1 -1
  47. package/docs/api/classes/StorageUtils.md +1 -1
  48. package/docs/api/enums/FileCategory.md +1 -1
  49. package/docs/api/interfaces/AggregateConfig.md +1 -1
  50. package/docs/api/interfaces/ButtonProps.md +1 -1
  51. package/docs/api/interfaces/CardProps.md +1 -1
  52. package/docs/api/interfaces/ColorPalette.md +1 -1
  53. package/docs/api/interfaces/ColorShade.md +1 -1
  54. package/docs/api/interfaces/DataAccessRecord.md +1 -1
  55. package/docs/api/interfaces/DataRecord.md +1 -1
  56. package/docs/api/interfaces/DataTableAction.md +1 -1
  57. package/docs/api/interfaces/DataTableColumn.md +1 -1
  58. package/docs/api/interfaces/DataTableProps.md +1 -1
  59. package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
  60. package/docs/api/interfaces/EmptyStateConfig.md +1 -1
  61. package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
  62. package/docs/api/interfaces/EventLogoProps.md +1 -1
  63. package/docs/api/interfaces/FileDisplayProps.md +1 -1
  64. package/docs/api/interfaces/FileMetadata.md +1 -1
  65. package/docs/api/interfaces/FileReference.md +1 -1
  66. package/docs/api/interfaces/FileSizeLimits.md +1 -1
  67. package/docs/api/interfaces/FileUploadOptions.md +1 -1
  68. package/docs/api/interfaces/FileUploadProps.md +1 -1
  69. package/docs/api/interfaces/FooterProps.md +1 -1
  70. package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
  71. package/docs/api/interfaces/InputProps.md +1 -1
  72. package/docs/api/interfaces/LabelProps.md +1 -1
  73. package/docs/api/interfaces/LoginFormProps.md +1 -1
  74. package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
  75. package/docs/api/interfaces/NavigationContextType.md +1 -1
  76. package/docs/api/interfaces/NavigationGuardProps.md +1 -1
  77. package/docs/api/interfaces/NavigationItem.md +1 -1
  78. package/docs/api/interfaces/NavigationMenuProps.md +1 -1
  79. package/docs/api/interfaces/NavigationProviderProps.md +1 -1
  80. package/docs/api/interfaces/Organisation.md +1 -1
  81. package/docs/api/interfaces/OrganisationContextType.md +1 -1
  82. package/docs/api/interfaces/OrganisationMembership.md +1 -1
  83. package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
  84. package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
  85. package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
  86. package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
  87. package/docs/api/interfaces/PageAccessRecord.md +1 -1
  88. package/docs/api/interfaces/PagePermissionContextType.md +1 -1
  89. package/docs/api/interfaces/PagePermissionGuardProps.md +1 -1
  90. package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
  91. package/docs/api/interfaces/PaletteData.md +1 -1
  92. package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
  93. package/docs/api/interfaces/PublicErrorBoundaryProps.md +1 -1
  94. package/docs/api/interfaces/PublicErrorBoundaryState.md +1 -1
  95. package/docs/api/interfaces/PublicLoadingSpinnerProps.md +1 -1
  96. package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
  97. package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
  98. package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
  99. package/docs/api/interfaces/RBACConfig.md +1 -1
  100. package/docs/api/interfaces/RBACLogger.md +1 -1
  101. package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
  102. package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
  103. package/docs/api/interfaces/RouteAccessRecord.md +1 -1
  104. package/docs/api/interfaces/RouteConfig.md +1 -1
  105. package/docs/api/interfaces/SecureDataContextType.md +1 -1
  106. package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
  107. package/docs/api/interfaces/StorageConfig.md +1 -1
  108. package/docs/api/interfaces/StorageFileInfo.md +1 -1
  109. package/docs/api/interfaces/StorageFileMetadata.md +1 -1
  110. package/docs/api/interfaces/StorageListOptions.md +1 -1
  111. package/docs/api/interfaces/StorageListResult.md +1 -1
  112. package/docs/api/interfaces/StorageUploadOptions.md +1 -1
  113. package/docs/api/interfaces/StorageUploadResult.md +1 -1
  114. package/docs/api/interfaces/StorageUrlOptions.md +1 -1
  115. package/docs/api/interfaces/StyleImport.md +1 -1
  116. package/docs/api/interfaces/SwitchProps.md +1 -1
  117. package/docs/api/interfaces/ToastActionElement.md +1 -1
  118. package/docs/api/interfaces/ToastProps.md +1 -1
  119. package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
  120. package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
  121. package/docs/api/interfaces/UseEventLogoOptions.md +1 -1
  122. package/docs/api/interfaces/UseEventLogoReturn.md +1 -1
  123. package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
  124. package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
  125. package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
  126. package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
  127. package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
  128. package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
  129. package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
  130. package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
  131. package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
  132. package/docs/api/interfaces/UserEventAccess.md +1 -1
  133. package/docs/api/interfaces/UserMenuProps.md +1 -1
  134. package/docs/api/interfaces/UserProfile.md +1 -1
  135. package/docs/api/modules.md +9 -9
  136. package/docs/implementation-guides/public-pages-advanced.md +10 -0
  137. package/package.json +1 -1
  138. package/src/components/PaceLoginPage/PaceLoginPage.test.tsx +10 -0
  139. package/src/components/PaceLoginPage/PaceLoginPage.tsx +20 -14
  140. package/src/hooks/public/usePublicEvent.ts +0 -2
  141. package/src/services/AuthService.ts +29 -29
  142. package/src/services/EventService.ts +65 -0
  143. package/src/services/__tests__/AuthService.restoreSession.test.ts +35 -0
  144. package/src/services/__tests__/AuthService.test.ts +6 -7
  145. package/src/services/__tests__/EventService.eventColours.test.ts +72 -0
  146. package/src/utils/storage/helpers.test.ts +107 -3
  147. package/src/utils/storage/helpers.ts +80 -5
  148. package/dist/chunk-CJIZS3UE.js.map +0 -1
  149. package/dist/chunk-DP5X5ORK.js.map +0 -1
  150. package/dist/chunk-H3P2RGKZ.js.map +0 -1
  151. /package/dist/{DataTable-PWBMKMOG.js.map → DataTable-VIP44OB6.js.map} +0 -0
  152. /package/dist/{UnifiedAuthProvider-5D3HEQND.js.map → UnifiedAuthProvider-6JRTOFPS.js.map} +0 -0
  153. /package/dist/{chunk-BNXBJOGL.js.map → chunk-4DYK5KCK.js.map} +0 -0
  154. /package/dist/{chunk-KTPG5VCH.js.map → chunk-7XBW2P7B.js.map} +0 -0
  155. /package/dist/{chunk-YY4YYM3E.js.map → chunk-G2SCPUKC.js.map} +0 -0
  156. /package/dist/{chunk-CXKMRKRF.js.map → chunk-G2YT64FA.js.map} +0 -0
  157. /package/dist/{chunk-AQGF5OG7.js.map → chunk-GD3ENUKD.js.map} +0 -0
  158. /package/dist/{chunk-3RZBKQ5Y.js.map → chunk-JQWSAYZC.js.map} +0 -0
  159. /package/dist/{chunk-6UHXQH7P.js.map → chunk-XZHZYSAK.js.map} +0 -0
@@ -1,6 +1,6 @@
1
- [@jmruthers/pace-core - v0.5.88](README.md) / Exports
1
+ [@jmruthers/pace-core - v0.5.90](README.md) / Exports
2
2
 
3
- # @jmruthers/pace-core - v0.5.88
3
+ # @jmruthers/pace-core - v0.5.90
4
4
 
5
5
  **`File`**
6
6
 
@@ -3800,7 +3800,7 @@ Useful for testing or when you need to force refresh all data
3800
3800
 
3801
3801
  #### Defined in
3802
3802
 
3803
- [packages/core/src/hooks/public/usePublicEvent.ts:412](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/hooks/public/usePublicEvent.ts#L412)
3803
+ [packages/core/src/hooks/public/usePublicEvent.ts:410](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/hooks/public/usePublicEvent.ts#L410)
3804
3804
 
3805
3805
  ___
3806
3806
 
@@ -3821,7 +3821,7 @@ Get cache statistics for debugging
3821
3821
 
3822
3822
  #### Defined in
3823
3823
 
3824
- [packages/core/src/hooks/public/usePublicEvent.ts:423](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/hooks/public/usePublicEvent.ts#L423)
3824
+ [packages/core/src/hooks/public/usePublicEvent.ts:421](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/hooks/public/usePublicEvent.ts#L421)
3825
3825
 
3826
3826
  ___
3827
3827
 
@@ -6988,7 +6988,7 @@ Signed URL with expiration timestamp, or null if failed
6988
6988
 
6989
6989
  #### Defined in
6990
6990
 
6991
- [packages/core/src/utils/storage/helpers.ts:234](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L234)
6991
+ [packages/core/src/utils/storage/helpers.ts:309](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L309)
6992
6992
 
6993
6993
  ___
6994
6994
 
@@ -7014,7 +7014,7 @@ Success status and optional error message
7014
7014
 
7015
7015
  #### Defined in
7016
7016
 
7017
- [packages/core/src/utils/storage/helpers.ts:269](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L269)
7017
+ [packages/core/src/utils/storage/helpers.ts:344](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L344)
7018
7018
 
7019
7019
  ___
7020
7020
 
@@ -7039,7 +7039,7 @@ List of files with metadata
7039
7039
 
7040
7040
  #### Defined in
7041
7041
 
7042
- [packages/core/src/utils/storage/helpers.ts:303](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L303)
7042
+ [packages/core/src/utils/storage/helpers.ts:378](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L378)
7043
7043
 
7044
7044
  ___
7045
7045
 
@@ -7065,7 +7065,7 @@ File blob with metadata, or null if failed
7065
7065
 
7066
7066
  #### Defined in
7067
7067
 
7068
- [packages/core/src/utils/storage/helpers.ts:363](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L363)
7068
+ [packages/core/src/utils/storage/helpers.ts:438](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L438)
7069
7069
 
7070
7070
  ___
7071
7071
 
@@ -7092,4 +7092,4 @@ Move a file to archived location (soft delete)
7092
7092
 
7093
7093
  #### Defined in
7094
7094
 
7095
- [packages/core/src/utils/storage/helpers.ts:416](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L416)
7095
+ [packages/core/src/utils/storage/helpers.ts:491](https://github.com/jmruthers/pace-core/blob/main/packages/core/src/utils/storage/helpers.ts#L491)
@@ -407,6 +407,16 @@ interface LogoSize {
407
407
 
408
408
  #### Usage Examples
409
409
 
410
+ > **Storage path formats**
411
+ >
412
+ > When configuring `event_logo` for an event, Pace supports multiple formats:
413
+ >
414
+ > - A fully qualified `https://` URL will be used directly.
415
+ > - A storage path like `public-files/org-123/logo.png` automatically resolves against the standard buckets configured via `pace-core`.
416
+ > - If your deployment stores logos in a different bucket, prefix the path with the bucket name using `custombucket/org-123/logo.png` (alphanumeric names) or `custom-bucket::org-123/logo.png` (bucket names containing hyphens).
417
+ >
418
+ > The double colon form helps when your folders start with numbers (for example `2024/`), ensuring they are not mistaken for bucket names.
419
+
410
420
  **Basic Logo Loading:**
411
421
  ```tsx
412
422
  import { usePublicEventLogo } from '@jmruthers/pace-core';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jmruthers/pace-core",
3
- "version": "0.5.88",
3
+ "version": "0.5.90",
4
4
  "description": "Clean, modern React component library with Tailwind v4 styling and native utilities",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -174,6 +174,16 @@ describe('PaceLoginPage Component', () => {
174
174
  expect(errorElement).toHaveClass('mt-4', 'text-destructive', 'text-center');
175
175
  expect(errorElement.tagName).toBe('EM');
176
176
  });
177
+
178
+ it('does not show benign AuthSessionMissingError on login page', () => {
179
+ const authErr = new Error('Auth session missing!');
180
+ authErr.name = 'AuthSessionMissingError';
181
+ mockAuthContext.authError = authErr as any;
182
+
183
+ renderWithProviders(<PaceLoginPage appName="Test App" />);
184
+
185
+ expect(screen.queryByText(/Auth session missing/i)).toBeNull();
186
+ });
177
187
  });
178
188
 
179
189
  // Role-based redirection tests
@@ -292,15 +292,18 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
292
292
  try {
293
293
  const { error } = await signIn(data.email, data.password);
294
294
 
295
- if (!error) {
296
- // Navigation will be handled by the useEffect that checks app access
297
- // Don't navigate here if requireAppAccess is true
298
- if (!requireAppAccess) {
299
- try {
300
- navigate(onSuccessRedirectPath, { replace: true });
301
- } catch (navError) {
302
- console.error('Navigation error after sign-in:', navError);
303
- }
295
+ if (error) {
296
+ // Throw error so LoginForm can catch and display it
297
+ throw error;
298
+ }
299
+
300
+ // Navigation will be handled by the useEffect that checks app access
301
+ // Don't navigate here if requireAppAccess is true
302
+ if (!requireAppAccess) {
303
+ try {
304
+ navigate(onSuccessRedirectPath, { replace: true });
305
+ } catch (navError) {
306
+ console.error('Navigation error after sign-in:', navError);
304
307
  }
305
308
  }
306
309
  } finally {
@@ -325,11 +328,14 @@ export const PaceLoginPage: React.FC<PaceLoginPageProps> = ({
325
328
  console.error('Login error:', error);
326
329
  }}
327
330
  />
328
- {authError && (
329
- <em className="mt-4 text-destructive text-center">
330
- {authError.message}
331
- </em>
332
- )}
331
+ {(() => {
332
+ const benign = !!(authError && (
333
+ authError.name === 'AuthSessionMissingError' || /Auth session missing/i.test(authError.message)
334
+ ));
335
+ return authError && !benign ? (
336
+ <em className="mt-4 text-destructive text-center">{authError.message}</em>
337
+ ) : null;
338
+ })()}
333
339
  {accessError && (
334
340
  <em className="mt-4 text-destructive text-center">
335
341
  {accessError}
@@ -224,7 +224,6 @@ export function usePublicEvent(
224
224
  event_catering_email,
225
225
  event_news,
226
226
  event_billing,
227
- event_footer,
228
227
  event_email
229
228
  `)
230
229
  .eq('event_code', eventCode)
@@ -301,7 +300,6 @@ export function usePublicEvent(
301
300
  event_catering_email,
302
301
  event_news,
303
302
  event_billing,
304
- event_footer,
305
303
  event_email
306
304
  `)
307
305
  .eq('event_code', eventCode)
@@ -453,42 +453,42 @@ export class AuthService extends BaseService implements IAuthService {
453
453
  // Record error but continue to attempt getUser to satisfy edge cases
454
454
  console.debug('[AuthService] getSession returned error, attempting to fetch user anyway');
455
455
  this.authError = sessionError;
456
+
457
+ // Attempt getUser as fallback when getSession fails
458
+ try {
459
+ const getUserFn = (this.supabaseClient.auth as any)?.getUser as (() => Promise<{ data?: { user?: User | null }, error?: AuthError | null }>) | undefined;
460
+ if (typeof getUserFn === 'function') {
461
+ const userResult = await getUserFn();
462
+ const currentUser = userResult?.data?.user ?? null;
463
+ const userError = userResult?.error ?? null;
464
+
465
+ if (currentUser) {
466
+ this.user = currentUser;
467
+ // If we got a user but no session, we still don't have a valid session
468
+ this.session = null;
469
+ }
470
+ if (userError && !this.authError) {
471
+ this.authError = userError;
472
+ }
473
+ }
474
+ } catch (getUserError) {
475
+ // If getUser also fails, we've already recorded the sessionError
476
+ console.debug('[AuthService] getUser also failed:', getUserError);
477
+ }
456
478
  }
457
479
 
458
480
  if (currentSession) {
459
481
  this.session = currentSession;
460
482
  this.user = currentSession.user;
461
483
  this.authError = null;
462
- } else {
463
- console.debug('[AuthService] No active session found, checking for existing user');
484
+ } else if (!sessionError) {
485
+ // Only skip getUser if we didn't already attempt it due to a sessionError
486
+ // Treat missing session as a normal cold-start state on public pages (e.g., login)
487
+ // Do not call getUser() which can raise AuthSessionMissingError and surface a noisy banner
488
+ console.debug('[AuthService] No active session found; treating as normal unauthenticated state');
464
489
  this.session = null;
465
- // Safely call getUser without destructuring
466
- let currentUser: User | null = null;
467
- let userError: AuthError | null = null;
468
- const getUserFn = (this.supabaseClient.auth as any)?.getUser as (() => Promise<{ data?: { user?: User | null }, error?: AuthError | null }>) | undefined;
469
- if (typeof getUserFn === 'function') {
470
- const userResult = await getUserFn();
471
- currentUser = userResult?.data?.user ?? null;
472
- userError = userResult?.error ?? null;
473
- }
474
-
475
- if (userError) {
476
- console.debug('[AuthService] getUser returned error during restoration');
477
- this.authError = userError;
478
- }
479
-
480
- if (currentUser) {
481
- this.user = currentUser;
482
- console.debug('[AuthService] Found user without active session during restoration');
483
- } else {
484
- this.user = null;
485
- this.session = null;
486
- }
487
-
488
- // Only clear authError if we successfully got user or had no errors
489
- if (!userError && !sessionError) {
490
- this.authError = null;
491
- }
490
+ this.user = null;
491
+ this.authError = null;
492
492
  }
493
493
 
494
494
  // Finish successfully even if earlier calls reported an error, to avoid noisy warnings in benign cases
@@ -14,6 +14,7 @@ import { IEventService } from './interfaces/IEventService';
14
14
  import { Event } from '../types/unified';
15
15
  import { Organisation } from '../types/organisation';
16
16
  import { DebugLogger } from '../utils/debugLogger';
17
+ import { applyPalette, clearPalette } from '../theming/runtime';
17
18
 
18
19
  export class EventService extends BaseService implements IEventService {
19
20
  private events: Event[] = [];
@@ -135,6 +136,8 @@ export class EventService extends BaseService implements IEventService {
135
136
  this.persistEventSelection(event.event_id);
136
137
  // Reset the user cleared flag when selecting an event
137
138
  this.userClearedEventRef = false;
139
+ // Apply theme for the newly selected event
140
+ this.updateThemeForSelectedEvent();
138
141
  } else {
139
142
  this.selectedEvent = null;
140
143
  this.setSelectedEventId?.(null);
@@ -145,6 +148,8 @@ export class EventService extends BaseService implements IEventService {
145
148
  this.hasAutoSelectedRef = false;
146
149
  // Mark that user explicitly cleared the event to prevent auto-selection
147
150
  this.userClearedEventRef = true;
151
+ // Clear theme when event is cleared
152
+ this.updateThemeForSelectedEvent();
148
153
  }
149
154
  this.notify();
150
155
  }
@@ -175,6 +180,8 @@ export class EventService extends BaseService implements IEventService {
175
180
  if (persistedEvent) {
176
181
  this.selectedEvent = persistedEvent;
177
182
  this.setSelectedEventId?.(persistedEventId);
183
+ // Apply theme for persisted event
184
+ this.updateThemeForSelectedEvent();
178
185
  return true;
179
186
  } else {
180
187
  // Clear invalid persisted event
@@ -218,6 +225,8 @@ export class EventService extends BaseService implements IEventService {
218
225
  this.selectedEvent = nextEvent;
219
226
  this.setSelectedEventId?.(nextEvent.event_id);
220
227
  this.persistEventSelection(nextEvent.event_id);
228
+ // Apply theme for auto-selected event
229
+ this.updateThemeForSelectedEvent();
221
230
  }
222
231
  }
223
232
 
@@ -338,6 +347,62 @@ export class EventService extends BaseService implements IEventService {
338
347
  }
339
348
  }
340
349
 
350
+ /**
351
+ * Parse and normalize event_colours to PaletteData (supports ev-* keys and string JSON)
352
+ */
353
+ private parseAndNormalizeEventColours(input: unknown): { main: any; sec: any; acc: any } | null {
354
+ try {
355
+ if (!input) return null;
356
+ let obj: any = input;
357
+ if (typeof input === 'string') {
358
+ try {
359
+ obj = JSON.parse(input);
360
+ } catch {
361
+ return null;
362
+ }
363
+ } else if (typeof input !== 'object') {
364
+ return null;
365
+ }
366
+
367
+ const pick = (o: any, pref: string, plain: string) => (o?.[pref] ?? o?.[plain]) || null;
368
+ const main = pick(obj, 'ev-main', 'main');
369
+ const sec = pick(obj, 'ev-sec', 'sec');
370
+ const acc = pick(obj, 'ev-acc', 'acc');
371
+ if (!main && !sec && !acc) return null;
372
+
373
+ // Fill helper: ensure all TW shades exist using raw or 500 as fallback
374
+ const shades = ['50','100','200','300','400','500','600','700','800','900','950'];
375
+ const fill = (p: any) => {
376
+ if (!p) return {} as any;
377
+ const out: any = {};
378
+ for (const s of shades) out[s] = p[s] || p?.raw || p?.['500'];
379
+ if (p?.raw) out.raw = p.raw;
380
+ return out;
381
+ };
382
+
383
+ return { main: fill(main), sec: fill(sec), acc: fill(acc) };
384
+ } catch (error) {
385
+ console.warn('[EventService] Failed to parse/normalize event colours:', error);
386
+ return null;
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Apply or clear runtime theme variables based on the current selected event's colours
392
+ */
393
+ private updateThemeForSelectedEvent(): void {
394
+ try {
395
+ const normalized = this.parseAndNormalizeEventColours(this.selectedEvent?.event_colours);
396
+ if (normalized) {
397
+ applyPalette(normalized);
398
+ } else {
399
+ clearPalette();
400
+ }
401
+ } catch (error) {
402
+ console.warn('[EventService] Failed to update theme from event colours:', error);
403
+ }
404
+ }
405
+
341
406
  getNextEventByDate(events?: Event[]): Event | null {
342
407
  const eventsToUse = events || this.events;
343
408
  if (!eventsToUse || eventsToUse.length === 0) {
@@ -0,0 +1,35 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { AuthService } from '../../services/AuthService';
3
+
4
+ // Minimal Supabase auth mock sufficient for restoreSession/initialize paths
5
+ function createSupabaseMock() {
6
+ const auth = {
7
+ getSession: vi.fn().mockResolvedValue({ data: { session: null }, error: null }),
8
+ getUser: vi.fn(), // should not be called when session is null
9
+ onAuthStateChange: vi.fn().mockImplementation((cb: any) => {
10
+ // Provide a subscription-like object
11
+ return { unsubscribe: () => {} } as any;
12
+ }),
13
+ } as any;
14
+
15
+ return { auth } as any;
16
+ }
17
+
18
+ describe('AuthService.restoreSession (no session startup)', () => {
19
+ it('does not call getUser() and does not set authError when session is null', async () => {
20
+ const supabase = createSupabaseMock();
21
+ const service = new AuthService(supabase);
22
+
23
+ // initialize() triggers setup listener and restoreSession()
24
+ await service.initialize();
25
+
26
+ // Assertions
27
+ expect(supabase.auth.getSession).toHaveBeenCalledTimes(1);
28
+ expect(supabase.auth.getUser).not.toHaveBeenCalled();
29
+ expect(service.getError()).toBeNull();
30
+ expect(service.getUser()).toBeNull();
31
+ expect(service.getSession()).toBeNull();
32
+ });
33
+ });
34
+
35
+
@@ -518,23 +518,22 @@ describe('AuthService', () => {
518
518
  expect(mockSupabase.auth.getUser).toHaveBeenCalled();
519
519
  });
520
520
 
521
- it('should handle initialization with getUser error', async () => {
522
- const userError = new AuthError('User error');
521
+ it('should not call getUser when getSession succeeds with no session (prevents AuthSessionMissingError)', async () => {
523
522
  mockSupabase.auth.getSession.mockResolvedValue({
524
523
  data: { session: null },
525
524
  error: null
526
525
  });
527
- mockSupabase.auth.getUser.mockResolvedValue({
528
- data: { user: null },
529
- error: userError
530
- });
531
526
  mockSupabase.auth.onAuthStateChange.mockReturnValue({
532
527
  data: { subscription: { unsubscribe: vi.fn() } }
533
528
  });
534
529
 
535
530
  await authService.initialize();
536
531
 
537
- expect(mockSupabase.auth.getUser).toHaveBeenCalled();
532
+ // When getSession succeeds with no session, getUser is NOT called to prevent
533
+ // AuthSessionMissingError from being raised on public pages (e.g., login page)
534
+ // This is intentional behavior to avoid noisy error banners for benign unauthenticated states
535
+ expect(mockSupabase.auth.getSession).toHaveBeenCalled();
536
+ expect(mockSupabase.auth.getUser).not.toHaveBeenCalled();
538
537
  });
539
538
 
540
539
  it('should restore session from storage during initialization', async () => {
@@ -0,0 +1,72 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { EventService } from '../../services/EventService';
3
+
4
+ function createSupabaseMock() {
5
+ const auth = {
6
+ onAuthStateChange: vi.fn().mockReturnValue({ unsubscribe: () => {} }),
7
+ } as any;
8
+ return { auth } as any;
9
+ }
10
+
11
+ const baseEvent = {
12
+ event_id: 'e1',
13
+ event_name: 'Test',
14
+ organisation_id: 'org1',
15
+ };
16
+
17
+ describe('EventService event_colours normalization', () => {
18
+ it('applies palettes when event_colours uses ev-* keys (object)', async () => {
19
+ const supabase = createSupabaseMock();
20
+ const svc = new EventService(supabase, null, null, 'APP', { id: 'org1' } as any, () => {});
21
+ // @ts-ignore access private for test by casting
22
+ (svc as any).selectedEvent = {
23
+ ...baseEvent,
24
+ event_colours: {
25
+ 'ev-main': { raw: { L: 0.75, C: 0.1, H: 200 }, '500': { L: 0.75, C: 0.1, H: 200 } },
26
+ 'ev-sec': { raw: { L: 0.7, C: 0.12, H: 260 }, '500': { L: 0.7, C: 0.12, H: 260 } },
27
+ 'ev-acc': { raw: { L: 0.65, C: 0.14, H: 20 }, '500': { L: 0.65, C: 0.14, H: 20 } },
28
+ },
29
+ };
30
+
31
+ // Spy on document style updates by calling the method and ensuring no throw
32
+ expect(() => (svc as any).updateThemeForSelectedEvent()).not.toThrow();
33
+ });
34
+
35
+ it('parses string JSON with ev-* keys', () => {
36
+ const supabase = createSupabaseMock();
37
+ const svc = new EventService(supabase, null, null, 'APP', { id: 'org1' } as any, () => {});
38
+ const payload = JSON.stringify({
39
+ 'ev-main': { '500': { L: 0.75, C: 0.1, H: 200 }, raw: { L: 0.75, C: 0.1, H: 200 } },
40
+ 'ev-sec': { '500': { L: 0.7, C: 0.12, H: 260 }, raw: { L: 0.7, C: 0.12, H: 260 } },
41
+ 'ev-acc': { '500': { L: 0.65, C: 0.14, H: 20 }, raw: { L: 0.65, C: 0.14, H: 20 } },
42
+ });
43
+ // @ts-ignore private
44
+ (svc as any).selectedEvent = { ...baseEvent, event_colours: payload };
45
+ expect(() => (svc as any).updateThemeForSelectedEvent()).not.toThrow();
46
+ });
47
+
48
+ it('accepts plain main/sec/acc', () => {
49
+ const supabase = createSupabaseMock();
50
+ const svc = new EventService(supabase, null, null, 'APP', { id: 'org1' } as any, () => {});
51
+ // @ts-ignore private
52
+ (svc as any).selectedEvent = {
53
+ ...baseEvent,
54
+ event_colours: {
55
+ main: { '500': { L: 0.75, C: 0.1, H: 200 } },
56
+ sec: { '500': { L: 0.7, C: 0.12, H: 260 } },
57
+ acc: { '500': { L: 0.65, C: 0.14, H: 20 } },
58
+ },
59
+ };
60
+ expect(() => (svc as any).updateThemeForSelectedEvent()).not.toThrow();
61
+ });
62
+
63
+ it('clears when invalid structure', () => {
64
+ const supabase = createSupabaseMock();
65
+ const svc = new EventService(supabase, null, null, 'APP', { id: 'org1' } as any, () => {});
66
+ // @ts-ignore private
67
+ (svc as any).selectedEvent = { ...baseEvent, event_colours: { something: {} } };
68
+ expect(() => (svc as any).updateThemeForSelectedEvent()).not.toThrow();
69
+ });
70
+ });
71
+
72
+
@@ -354,7 +354,7 @@ describe('[utility] Storage Helpers', () => {
354
354
  describe('getPublicUrl', () => {
355
355
  it('generates public URL for public bucket', () => {
356
356
  const filePath = 'org-123/logos/logo.png';
357
-
357
+
358
358
  const mockGetPublicUrl = vi.fn().mockReturnValue({
359
359
  data: { publicUrl: 'https://example.com/public/logo.png' }
360
360
  });
@@ -370,9 +370,23 @@ describe('[utility] Storage Helpers', () => {
370
370
  expect(result).toBe('https://example.com/public/logo.png');
371
371
  });
372
372
 
373
+ it('returns direct URLs without calling storage', () => {
374
+ const directUrl = 'https://cdn.example.com/logo.png';
375
+
376
+ const localSupabase = createMockSupabaseClient();
377
+ localSupabase.storage = {
378
+ from: vi.fn()
379
+ };
380
+
381
+ const result = getPublicUrl(localSupabase as any, directUrl, true);
382
+
383
+ expect(localSupabase.storage.from).not.toHaveBeenCalled();
384
+ expect(result).toBe(directUrl);
385
+ });
386
+
373
387
  it('generates public URL for private bucket', () => {
374
388
  const filePath = 'org-123/documents/test.pdf';
375
-
389
+
376
390
  const mockGetPublicUrl = vi.fn().mockReturnValue({
377
391
  data: { publicUrl: 'https://example.com/files/test.pdf' }
378
392
  });
@@ -387,10 +401,100 @@ describe('[utility] Storage Helpers', () => {
387
401
  expect(result).toBe('https://example.com/files/test.pdf');
388
402
  });
389
403
 
404
+ it('uses explicit bucket prefix when provided', () => {
405
+ const filePath = 'public-files/org-123/logo.png';
406
+
407
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
408
+ data: { publicUrl: 'https://example.com/public/logo.png' }
409
+ });
410
+
411
+ mockSupabase.storage = {
412
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
413
+ };
414
+
415
+ const result = getPublicUrl(mockSupabase, filePath, true);
416
+
417
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
418
+ expect(mockGetPublicUrl).toHaveBeenCalledWith('org-123/logo.png');
419
+ expect(result).toBe('https://example.com/public/logo.png');
420
+ });
421
+
422
+ it('supports custom bucket hints without hyphen characters', () => {
423
+ const filePath = 'branding/org-123/logo.png';
424
+
425
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
426
+ data: { publicUrl: 'https://example.com/branding/logo.png' }
427
+ });
428
+
429
+ mockSupabase.storage = {
430
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
431
+ };
432
+
433
+ const result = getPublicUrl(mockSupabase, filePath, true);
434
+
435
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('branding');
436
+ expect(mockGetPublicUrl).toHaveBeenCalledWith('org-123/logo.png');
437
+ expect(result).toBe('https://example.com/branding/logo.png');
438
+ });
439
+
440
+ it('supports double colon bucket hints to avoid path ambiguity', () => {
441
+ const filePath = 'brand-assets::org-123/logo.png';
442
+
443
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
444
+ data: { publicUrl: 'https://example.com/brand/logo.png' }
445
+ });
446
+
447
+ mockSupabase.storage = {
448
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
449
+ };
450
+
451
+ const result = getPublicUrl(mockSupabase, filePath, true);
452
+
453
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('brand-assets');
454
+ expect(mockGetPublicUrl).toHaveBeenCalledWith('org-123/logo.png');
455
+ expect(result).toBe('https://example.com/brand/logo.png');
456
+ });
457
+
458
+ it('treats numeric leading path segments as directories rather than buckets', () => {
459
+ const filePath = '2024/05/logo.png';
460
+
461
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
462
+ data: { publicUrl: 'https://example.com/public/logo.png' }
463
+ });
464
+
465
+ mockSupabase.storage = {
466
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
467
+ };
468
+
469
+ const result = getPublicUrl(mockSupabase, filePath, true);
470
+
471
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
472
+ expect(mockGetPublicUrl).toHaveBeenCalledWith(filePath);
473
+ expect(result).toBe('https://example.com/public/logo.png');
474
+ });
475
+
476
+ it('ignores bucket prefix when path starts with UUID', () => {
477
+ const filePath = '123e4567-e89b-12d3-a456-426614174000/event_logos/logo.png';
478
+
479
+ const mockGetPublicUrl = vi.fn().mockReturnValue({
480
+ data: { publicUrl: 'https://example.com/public/logo.png' }
481
+ });
482
+
483
+ mockSupabase.storage = {
484
+ from: vi.fn(() => ({ getPublicUrl: mockGetPublicUrl }))
485
+ };
486
+
487
+ const result = getPublicUrl(mockSupabase, filePath, true);
488
+
489
+ expect(mockSupabase.storage.from).toHaveBeenCalledWith('public-files');
490
+ expect(mockGetPublicUrl).toHaveBeenCalledWith(filePath);
491
+ expect(result).toBe('https://example.com/public/logo.png');
492
+ });
493
+
390
494
  it('validates required parameters', () => {
391
495
  expect(() => getPublicUrl(null as any, 'path', false))
392
496
  .toThrow();
393
-
497
+
394
498
  expect(() => getPublicUrl(mockSupabase, '', false))
395
499
  .toThrow();
396
500
  });