@micha.bigler/ui-core-micha 3.14.4 → 4.0.1
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.
- package/CHANGELOG.md +29 -0
- package/README.md +7 -0
- package/dist/chat/kitSxRegistry.js +9 -0
- package/dist/components/charts/kitSxRegistry.js +6 -0
- package/dist/entries/charts.js +6 -0
- package/dist/entries/chat.js +8 -0
- package/dist/entries/markdown.js +1 -0
- package/dist/index.js +0 -14
- package/dist/pages/AccountPage.js +1 -2
- package/dist/pages/LoginPage.js +1 -2
- package/dist/pages/PasswordChangePage.js +1 -2
- package/dist/pages/PasswordInvitePage.js +1 -2
- package/dist/pages/PasswordResetRequestPage.js +1 -2
- package/dist/pages/SignUpPage.js +1 -2
- package/dist/pages/SignupConfirmPage.js +1 -2
- package/dist/theme/kitSxEntry.js +11 -0
- package/dist/theme/kitSxRegistry.js +2 -15
- package/package.json +31 -7
- package/.github/workflows/ci.yml +0 -35
- package/.github/workflows/publish.yml +0 -126
- package/DESIGN.md +0 -60
- package/WORK_ORDERS.md +0 -150
- package/dev/entries.jsx +0 -721
- package/dev/fixtures.js +0 -26
- package/dev/main.jsx +0 -75
- package/dev/mockTransport.jsx +0 -128
- package/docs/CHART-LAYOUT.md +0 -157
- package/docs/DARK-TOKENS.md +0 -140
- package/docs/TYPE-SCALE.md +0 -111
- package/docs/messaging-deviations.md +0 -118
- package/index.html +0 -12
- package/renovate.json +0 -10
- package/scripts/chart-api-census.mjs +0 -415
- package/src/auth/AuthContext.jsx +0 -158
- package/src/auth/ReauthModal.jsx +0 -82
- package/src/auth/UserMenu.jsx +0 -121
- package/src/auth/apiClient.jsx +0 -232
- package/src/auth/authApi.jsx +0 -539
- package/src/auth/authConfig.jsx +0 -34
- package/src/auth/reauth.js +0 -47
- package/src/chat/ChatComposer.jsx +0 -36
- package/src/chat/ChatList.jsx +0 -87
- package/src/chat/ChatThread.jsx +0 -128
- package/src/chat/ChatWorkspace.jsx +0 -244
- package/src/chat/MarkdownReply.jsx +0 -22
- package/src/chat/ModelPicker.jsx +0 -85
- package/src/chat/api.js +0 -18
- package/src/chat/useChatSocket.js +0 -32
- package/src/chat/wire.js +0 -7
- package/src/components/AccessCodeManager.jsx +0 -288
- package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
- package/src/components/AllowedEmailDomainsManager.jsx +0 -92
- package/src/components/AuthFactorRequirementCard.jsx +0 -95
- package/src/components/BulkInviteCsvTab.jsx +0 -198
- package/src/components/EmptyState.jsx +0 -38
- package/src/components/LoginForm.jsx +0 -159
- package/src/components/MFAComponent.jsx +0 -350
- package/src/components/MfaLoginComponent.jsx +0 -252
- package/src/components/MobileBottomNav.jsx +0 -82
- package/src/components/PasskeysComponent.jsx +0 -252
- package/src/components/PasswordChangeForm.jsx +0 -87
- package/src/components/PasswordResetRequestForm.jsx +0 -43
- package/src/components/PasswordSetForm.jsx +0 -97
- package/src/components/ProfileComponent.jsx +0 -348
- package/src/components/QrSignupManager.jsx +0 -404
- package/src/components/QrSignupValidityManager.jsx +0 -106
- package/src/components/RegistrationMethodsManager.jsx +0 -150
- package/src/components/SecurityComponent.jsx +0 -141
- package/src/components/SocialLoginButtons.jsx +0 -83
- package/src/components/SoftChip.jsx +0 -93
- package/src/components/StatTile.jsx +0 -39
- package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
- package/src/components/TurnstileWidget.jsx +0 -99
- package/src/components/UserInviteComponent.jsx +0 -76
- package/src/components/UserListComponent.jsx +0 -618
- package/src/components/charts/BarChart.jsx +0 -102
- package/src/components/charts/ChartFrame.jsx +0 -214
- package/src/components/charts/LineChart.jsx +0 -126
- package/src/components/charts/ScatterChart.jsx +0 -299
- package/src/components/charts/TimeSeriesChart.jsx +0 -300
- package/src/components/charts/chartDefaults.js +0 -644
- package/src/components/charts/chartLabels.js +0 -36
- package/src/components/charts/exportChart.js +0 -258
- package/src/components/charts/formatters.js +0 -39
- package/src/components/charts/palette.js +0 -33
- package/src/components/charts/yearTickInterval.js +0 -7
- package/src/i18n/authTranslations.ts +0 -2651
- package/src/i18n/chartsTranslations.ts +0 -91
- package/src/i18n/chatTranslations.ts +0 -55
- package/src/i18n/messagingTranslations.ts +0 -120
- package/src/i18n/notificationsTranslations.ts +0 -32
- package/src/i18n/onboardingTranslations.ts +0 -29
- package/src/i18n/sectionNavTranslations.ts +0 -20
- package/src/i18n/uiCoreTranslations.js +0 -29
- package/src/i18n/userMenuTranslations.ts +0 -5
- package/src/index.js +0 -158
- package/src/layout/PageLayout.jsx +0 -36
- package/src/layout/SectionNav.jsx +0 -320
- package/src/messaging/AttachmentList.jsx +0 -145
- package/src/messaging/Composer.jsx +0 -197
- package/src/messaging/ConversationLaunchers.jsx +0 -51
- package/src/messaging/ConversationList.jsx +0 -109
- package/src/messaging/DirectMessageLauncher.jsx +0 -112
- package/src/messaging/MessageBubble.jsx +0 -183
- package/src/messaging/MessagingProvider.jsx +0 -0
- package/src/messaging/MessagingScopeConfig.jsx +0 -24
- package/src/messaging/PollCard.jsx +0 -163
- package/src/messaging/ReactionBar.jsx +0 -43
- package/src/messaging/ReadTicks.jsx +0 -82
- package/src/messaging/Thread.jsx +0 -195
- package/src/messaging/api.js +0 -48
- package/src/messaging/composerImageCompression.js +0 -35
- package/src/messaging/linkifyText.jsx +0 -37
- package/src/notifications/NotificationBell.jsx +0 -84
- package/src/notifications/NotificationSettings.jsx +0 -330
- package/src/notifications/NotificationsProvider.jsx +0 -224
- package/src/notifications/PopupSurface.jsx +0 -105
- package/src/notifications/api.js +0 -55
- package/src/notifications/feedApi.js +0 -18
- package/src/notifications/realtime.jsx +0 -138
- package/src/notifications/serviceWorker/sw.js +0 -50
- package/src/onboarding/OnboardingProvider.jsx +0 -326
- package/src/onboarding/OnboardingWizard.jsx +0 -61
- package/src/onboarding/WizardDialogShell.jsx +0 -51
- package/src/onboarding/api.js +0 -13
- package/src/onboarding/stepSelection.js +0 -16
- package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
- package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
- package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
- package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
- package/src/pages/AccountPage.jsx +0 -394
- package/src/pages/LoginPage.jsx +0 -343
- package/src/pages/PasswordChangePage.jsx +0 -62
- package/src/pages/PasswordInvitePage.jsx +0 -177
- package/src/pages/PasswordResetRequestPage.jsx +0 -68
- package/src/pages/SignUpPage.jsx +0 -268
- package/src/pages/SignupConfirmPage.jsx +0 -155
- package/src/testing/checkKitIntegration.js +0 -233
- package/src/theme/createAppTheme.js +0 -423
- package/src/theme/fonts.js +0 -3
- package/src/theme/index.js +0 -21
- package/src/theme/kitSxRegistry.js +0 -285
- package/src/theme/themeCompleteness.js +0 -1045
- package/src/theme/tokens.js +0 -478
- package/src/utils/auth-errors.js +0 -76
- package/src/utils/authService.js +0 -223
- package/src/utils/resolveErrorText.js +0 -28
- package/src/utils/webauthn.js +0 -51
- package/tests/AccountPage.profileStatements.test.jsx +0 -162
- package/tests/AccountPage.test.jsx +0 -200
- package/tests/AuthContext.test.jsx +0 -129
- package/tests/BarChart.test.jsx +0 -112
- package/tests/BrowserPushStep.test.jsx +0 -110
- package/tests/ChartFrame.test.jsx +0 -336
- package/tests/ConversationList.test.jsx +0 -144
- package/tests/CookieConsentStep.test.jsx +0 -69
- package/tests/EmptyState.test.jsx +0 -62
- package/tests/LineChart.test.jsx +0 -80
- package/tests/MessagingProvider.test.jsx +0 -50
- package/tests/MobileBottomNav.test.jsx +0 -319
- package/tests/NotificationBell.test.jsx +0 -60
- package/tests/NotificationSettings.test.jsx +0 -276
- package/tests/NotificationsProvider.test.jsx +0 -240
- package/tests/OnboardingProvider.test.jsx +0 -294
- package/tests/OnboardingWizard.test.jsx +0 -143
- package/tests/PasswordChangeForm.test.jsx +0 -70
- package/tests/PasswordInvitePage.test.jsx +0 -142
- package/tests/PasswordSetForm.test.jsx +0 -70
- package/tests/PopupSurface.test.jsx +0 -142
- package/tests/ProfileComponent.refresh.test.jsx +0 -194
- package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
- package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
- package/tests/ScatterChart.test.jsx +0 -270
- package/tests/SectionNav.test.jsx +0 -363
- package/tests/SignUpPage.test.jsx +0 -208
- package/tests/SoftChip.test.jsx +0 -105
- package/tests/StatTile.test.jsx +0 -57
- package/tests/Thread.test.jsx +0 -151
- package/tests/TimeSeriesChart.test.jsx +0 -367
- package/tests/UserListComponent.test.jsx +0 -192
- package/tests/UserMenu.test.jsx +0 -171
- package/tests/apiClientLanguage.test.js +0 -33
- package/tests/apiClientPublicPaths.test.js +0 -178
- package/tests/authApiLogin.test.js +0 -42
- package/tests/authApiRegistration.test.js +0 -31
- package/tests/authErrorCodeRendering.test.jsx +0 -77
- package/tests/authErrors.test.js +0 -74
- package/tests/authTranslations.test.js +0 -74
- package/tests/chart-api-census.test.js +0 -366
- package/tests/chartAxisTickBand.test.jsx +0 -95
- package/tests/chartDefaults.test.js +0 -902
- package/tests/chartLabels.test.js +0 -50
- package/tests/chartsPalette.test.js +0 -99
- package/tests/chatApi.test.js +0 -48
- package/tests/chatDriverNames.test.jsx +0 -71
- package/tests/chatExports.test.js +0 -3
- package/tests/chatList.test.jsx +0 -84
- package/tests/chatPicker.test.jsx +0 -82
- package/tests/chatSocket.test.jsx +0 -26
- package/tests/chatThread.test.jsx +0 -116
- package/tests/chatTranslations.test.js +0 -47
- package/tests/chatWorkspace.test.jsx +0 -234
- package/tests/checkKitIntegration.test.jsx +0 -219
- package/tests/createAppTheme.test.js +0 -132
- package/tests/createAppThemeDark.test.js +0 -127
- package/tests/createAppThemeLightGuard.test.js +0 -40
- package/tests/createAppThemePageContrast.test.js +0 -108
- package/tests/createAppThemeTypography.test.js +0 -53
- package/tests/errorCallSiteGuard.test.js +0 -32
- package/tests/exportChart.test.js +0 -313
- package/tests/feedApi.test.js +0 -46
- package/tests/fixtures/light-theme-baseline.json +0 -2234
- package/tests/i18nAggregate.test.js +0 -120
- package/tests/i18nGermanVariant.test.js +0 -65
- package/tests/loginPageErrorResolution.test.jsx +0 -102
- package/tests/messagingApi.test.js +0 -32
- package/tests/messagingChunk6.test.jsx +0 -116
- package/tests/messagingCompactBubble.test.jsx +0 -64
- package/tests/messagingComposer.test.jsx +0 -136
- package/tests/messagingComposerChunk5.test.jsx +0 -113
- package/tests/messagingContractConformance.test.js +0 -69
- package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
- package/tests/messagingExports.test.js +0 -21
- package/tests/messagingInteractions.test.jsx +0 -136
- package/tests/messagingLastMessagePreview.test.jsx +0 -93
- package/tests/messagingLinkify.test.jsx +0 -51
- package/tests/messagingMessageActions.test.jsx +0 -163
- package/tests/messagingMsg6.test.jsx +0 -105
- package/tests/messagingMsg6f.test.jsx +0 -155
- package/tests/messagingMsg6g.test.jsx +0 -166
- package/tests/messagingMsg6h.test.jsx +0 -136
- package/tests/messagingMsg6i.test.jsx +0 -129
- package/tests/messagingPollRendering.test.jsx +0 -142
- package/tests/messagingReactionMerge.test.jsx +0 -77
- package/tests/messagingReadTicksTooltip.test.jsx +0 -36
- package/tests/messagingThreadReceipts.test.jsx +0 -178
- package/tests/messagingThreadingQuoting.test.jsx +0 -87
- package/tests/messagingUnreadLifecycle.test.jsx +0 -78
- package/tests/notificationsApi.test.js +0 -41
- package/tests/notificationsExports.test.js +0 -34
- package/tests/onboardingDescriptors.test.js +0 -132
- package/tests/packageTreeShaking.test.js +0 -126
- package/tests/realtime.test.jsx +0 -219
- package/tests/resolveErrorText.test.js +0 -72
- package/tests/stepSelection.test.js +0 -53
- package/tests/themeCompleteness.test.js +0 -626
- package/tests/timeSeriesLegend.test.js +0 -99
- package/tests/userScreenTranslations.test.jsx +0 -218
- package/tsconfig.build.json +0 -18
- package/vite.config.mjs +0 -6
- package/work-orders/AUTH-1.md +0 -74
- package/work-orders/AUTH-2.md +0 -74
- package/work-orders/AUTH-4.md +0 -183
- package/work-orders/AUTH-5.md +0 -154
- package/work-orders/AUTH-7.md +0 -181
- package/work-orders/AUTH-8.md +0 -184
- package/work-orders/CHART-1.md +0 -285
- package/work-orders/CHART-2.md +0 -222
- package/work-orders/CHART-3.md +0 -163
- package/work-orders/CHART-4.md +0 -203
- package/work-orders/CHART-5.md +0 -226
- package/work-orders/CHART-6.md +0 -428
- package/work-orders/CHART-7.md +0 -175
- package/work-orders/DX-1.md +0 -169
- package/work-orders/DX-2.md +0 -188
- package/work-orders/DX-3.md +0 -195
- package/work-orders/I18N-1.md +0 -254
- package/work-orders/I18N-3.md +0 -207
- package/work-orders/MSG-3.md +0 -375
- package/work-orders/MSG-3b.md +0 -1002
- package/work-orders/MSG-3c.md +0 -378
- package/work-orders/MSG-3d.md +0 -268
- package/work-orders/MSG-3e.md +0 -164
- package/work-orders/MSG-3f.md +0 -391
- package/work-orders/MSG-6e.md +0 -308
- package/work-orders/MSG-6f.md +0 -214
- package/work-orders/MSG-6g.md +0 -244
- package/work-orders/MSG-6h.md +0 -238
- package/work-orders/MSG-6i.md +0 -271
- package/work-orders/MSG-6j.md +0 -117
- package/work-orders/NOTIF-12.md +0 -214
- package/work-orders/NOTIF-13.md +0 -142
- package/work-orders/NOTIF-14.md +0 -151
- package/work-orders/PRIM-1.md +0 -177
- package/work-orders/SHELL-1.md +0 -255
- package/work-orders/SHELL-2.md +0 -197
- package/work-orders/SHELL-3.md +0 -359
- package/work-orders/SHELL-4.md +0 -351
- package/work-orders/SHELL-5.md +0 -345
- package/work-orders/SHELL-6.md +0 -210
- package/work-orders/THEME-1.md +0 -587
- package/work-orders/THEME-10.md +0 -303
- package/work-orders/THEME-11.md +0 -208
- package/work-orders/THEME-2.md +0 -339
- package/work-orders/THEME-3.md +0 -210
- package/work-orders/THEME-4.md +0 -271
- package/work-orders/THEME-5.md +0 -341
- package/work-orders/THEME-6.md +0 -247
- package/work-orders/THEME-7.md +0 -82
- package/work-orders/THEME-8.md +0 -211
- package/work-orders/THEME-9.md +0 -194
- package/work-orders/UCM-AUTH-10.md +0 -490
- package/work-orders/UCM-AUTH-11.md +0 -185
- package/work-orders/UCM-AUTH-12.md +0 -98
- package/work-orders/UCM-AUTH-13.md +0 -47
- package/work-orders/UCM-AUTH-9.md +0 -207
- package/work-orders/UCM-CHART-10.md +0 -150
- package/work-orders/UCM-CHART-11.md +0 -132
- package/work-orders/UCM-CHART-12.md +0 -348
- package/work-orders/UCM-CHART-13.md +0 -122
- package/work-orders/UCM-CHART-14.md +0 -198
- package/work-orders/UCM-CHART-15.md +0 -187
- package/work-orders/UCM-CHART-16.md +0 -169
- package/work-orders/UCM-CHART-17.md +0 -195
- package/work-orders/UCM-CHART-18.md +0 -200
- package/work-orders/UCM-CHART-8.md +0 -147
- package/work-orders/UCM-CHART-9.md +0 -265
- package/work-orders/UCM-CHAT-1.md +0 -190
- package/work-orders/UCM-CHAT-2.md +0 -75
- package/work-orders/UCM-CI-1.md +0 -65
- package/work-orders/UCM-DEP-1.md +0 -63
- package/work-orders/UCM-I18N-4.md +0 -226
- package/work-orders/UCM-I18N-5.md +0 -211
- package/work-orders/UCM-MSG-20.md +0 -192
- package/work-orders/UCM-PRIM-2.md +0 -114
- package/work-orders/UCM-SHELL-7.md +0 -134
- package/work-orders/UCM-SHELL-8.md +0 -125
- package/work-orders/UCM-TEST-1.md +0 -304
- package/work-orders/UCM-THEME-13.md +0 -69
- package/work-orders/UCM-THEME-14.md +0 -312
- package/work-orders/UCM-THEME-15.md +0 -356
- package/work-orders/UCM-THEME-16.md +0 -80
- package/work-orders/UCM-THEME-17.md +0 -111
- package/work-orders/UCM-THEME-18.md +0 -184
- package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
- package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
- package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
- package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
- package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
- package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
- package/work-orders/assets/UCM-THEME-17-type-scale.html +0 -391
package/src/notifications/api.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import apiClient from '../auth/apiClient';
|
|
2
|
-
|
|
3
|
-
const PREFERENCES_URL = '/api/notifications/preferences/';
|
|
4
|
-
const PUSH_SUBSCRIPTION_URL = `${PREFERENCES_URL}push-subscription/`;
|
|
5
|
-
const SUBSCRIPTIONS_URL = `${PREFERENCES_URL}subscriptions/`;
|
|
6
|
-
|
|
7
|
-
export async function getNotificationPreferences() {
|
|
8
|
-
const response = await apiClient.get(PREFERENCES_URL);
|
|
9
|
-
return response.data;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function patchNotificationPreferences(patch) {
|
|
13
|
-
const response = await apiClient.patch(PREFERENCES_URL, patch);
|
|
14
|
-
return response.data;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export async function setNotificationCategorySubscription(category, subscribed) {
|
|
18
|
-
const response = await apiClient.post(SUBSCRIPTIONS_URL, { category, subscribed });
|
|
19
|
-
return response.data;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function getVapidPublicKey() {
|
|
23
|
-
const response = await apiClient.get(`${PREFERENCES_URL}vapid-public-key/`);
|
|
24
|
-
return response.data?.vapidPublicKey;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export async function listPushSubscriptions() {
|
|
28
|
-
const response = await apiClient.get(PUSH_SUBSCRIPTION_URL);
|
|
29
|
-
return response.data;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function savePushSubscription(subscription, ua) {
|
|
33
|
-
const response = await apiClient.post(PUSH_SUBSCRIPTION_URL, { subscription, ua });
|
|
34
|
-
return response.data;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export async function removePushSubscription(subscriptionIdentifier) {
|
|
38
|
-
const response = await apiClient.delete(PUSH_SUBSCRIPTION_URL, { data: subscriptionIdentifier });
|
|
39
|
-
return response.data;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function urlBase64ToUint8Array(base64String) {
|
|
43
|
-
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
|
|
44
|
-
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
|
|
45
|
-
const decoder = typeof window !== 'undefined' && typeof window.atob === 'function'
|
|
46
|
-
? window.atob
|
|
47
|
-
: typeof atob === 'function'
|
|
48
|
-
? atob
|
|
49
|
-
: null;
|
|
50
|
-
|
|
51
|
-
if (!decoder) throw new Error('Base64 decoding is unavailable in this environment.');
|
|
52
|
-
|
|
53
|
-
const raw = decoder(base64);
|
|
54
|
-
return Uint8Array.from([...raw].map((character) => character.charCodeAt(0)));
|
|
55
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import apiClient from '../auth/apiClient';
|
|
2
|
-
|
|
3
|
-
export async function getNotificationFeed({ status } = {}) {
|
|
4
|
-
const response = await apiClient.get('/api/notifications/feed/', {
|
|
5
|
-
params: status ? { status } : undefined,
|
|
6
|
-
});
|
|
7
|
-
return response.data;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export async function getUnreadCount() {
|
|
11
|
-
const response = await apiClient.get('/api/notifications/feed/unread-count/');
|
|
12
|
-
return response.data;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function markNotifications({ action, ids }) {
|
|
16
|
-
const response = await apiClient.post('/api/notifications/feed/mark/', { action, ids });
|
|
17
|
-
return response.data;
|
|
18
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import { createContext, useCallback, useContext, useEffect, useRef } from 'react';
|
|
2
|
-
|
|
3
|
-
const INITIAL_BACKOFF_MS = 1_000;
|
|
4
|
-
const MAX_BACKOFF_MS = 30_000;
|
|
5
|
-
|
|
6
|
-
// Envelope discriminator for the notifications domain. A message with no
|
|
7
|
-
// `envelope` field (pre-NOTIF-13 dcm) is treated as this default so an
|
|
8
|
-
// un-bumped backend keeps working unchanged (backward-compat).
|
|
9
|
-
export const DEFAULT_ENVELOPE = 'notification';
|
|
10
|
-
|
|
11
|
-
const RealtimeContext = createContext(null);
|
|
12
|
-
|
|
13
|
-
export function useRealtime() {
|
|
14
|
-
const context = useContext(RealtimeContext);
|
|
15
|
-
if (!context) {
|
|
16
|
-
throw new Error('useRealtime must be used within a NotificationsProvider');
|
|
17
|
-
}
|
|
18
|
-
return context;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export { RealtimeContext };
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Owns the single WebSocket connection and an envelope-keyed subscriber
|
|
25
|
-
* registry. Internal — consumed only by NotificationsProvider, which
|
|
26
|
-
* re-exposes `subscribe` via RealtimeContext so useRealtime() works without
|
|
27
|
-
* a separate provider apps have to mount.
|
|
28
|
-
*
|
|
29
|
-
* Backoff, reconnect, and cleanup semantics are unchanged from the prior
|
|
30
|
-
* single-purpose socket owner. `active`/`wsUrl` are primitives, so the
|
|
31
|
-
* effect does not re-run (and does not reconnect) on unrelated re-renders.
|
|
32
|
-
*/
|
|
33
|
-
export function useRealtimeCore({ active, wsUrl }) {
|
|
34
|
-
const socketRef = useRef(null);
|
|
35
|
-
const subscribersRef = useRef(new Map());
|
|
36
|
-
const reconnectSubscribersRef = useRef(new Set());
|
|
37
|
-
|
|
38
|
-
const subscribe = useCallback((envelope, handler) => {
|
|
39
|
-
const subscribers = subscribersRef.current;
|
|
40
|
-
if (!subscribers.has(envelope)) {
|
|
41
|
-
subscribers.set(envelope, new Set());
|
|
42
|
-
}
|
|
43
|
-
subscribers.get(envelope).add(handler);
|
|
44
|
-
return () => {
|
|
45
|
-
const handlers = subscribers.get(envelope);
|
|
46
|
-
if (!handlers) return;
|
|
47
|
-
handlers.delete(handler);
|
|
48
|
-
if (handlers.size === 0) {
|
|
49
|
-
subscribers.delete(envelope);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
}, []);
|
|
53
|
-
|
|
54
|
-
// Additive lifecycle signal for domains whose REST projections must be
|
|
55
|
-
// refreshed after the one shared transport reconnects. It deliberately does
|
|
56
|
-
// not alter the socket/backoff ownership or the envelope subscriber API.
|
|
57
|
-
const onReconnect = useCallback((handler) => {
|
|
58
|
-
reconnectSubscribersRef.current.add(handler);
|
|
59
|
-
return () => reconnectSubscribersRef.current.delete(handler);
|
|
60
|
-
}, []);
|
|
61
|
-
|
|
62
|
-
useEffect(() => {
|
|
63
|
-
if (!active) return undefined;
|
|
64
|
-
|
|
65
|
-
let isActive = true;
|
|
66
|
-
let reconnectTimer = null;
|
|
67
|
-
let backoffMs = INITIAL_BACKOFF_MS;
|
|
68
|
-
let hasConnected = false;
|
|
69
|
-
|
|
70
|
-
const handleMessage = (event) => {
|
|
71
|
-
let data;
|
|
72
|
-
try {
|
|
73
|
-
data = JSON.parse(event.data);
|
|
74
|
-
} catch {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const envelope = data.envelope ?? DEFAULT_ENVELOPE;
|
|
79
|
-
const handlers = subscribersRef.current.get(envelope);
|
|
80
|
-
if (!handlers || handlers.size === 0) return;
|
|
81
|
-
handlers.forEach((handler) => {
|
|
82
|
-
try {
|
|
83
|
-
handler(data);
|
|
84
|
-
} catch (error) {
|
|
85
|
-
console.error('Realtime subscriber error', error);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
const connect = () => {
|
|
91
|
-
if (!isActive || socketRef.current) return;
|
|
92
|
-
|
|
93
|
-
const socket = new WebSocket(wsUrl);
|
|
94
|
-
socketRef.current = socket;
|
|
95
|
-
|
|
96
|
-
socket.onopen = () => {
|
|
97
|
-
backoffMs = INITIAL_BACKOFF_MS;
|
|
98
|
-
if (hasConnected) {
|
|
99
|
-
reconnectSubscribersRef.current.forEach((handler) => {
|
|
100
|
-
try {
|
|
101
|
-
handler();
|
|
102
|
-
} catch (error) {
|
|
103
|
-
console.error('Realtime reconnect subscriber error', error);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
hasConnected = true;
|
|
108
|
-
};
|
|
109
|
-
socket.onmessage = handleMessage;
|
|
110
|
-
socket.onerror = () => {
|
|
111
|
-
socket.close();
|
|
112
|
-
};
|
|
113
|
-
socket.onclose = () => {
|
|
114
|
-
if (socketRef.current === socket) {
|
|
115
|
-
socketRef.current = null;
|
|
116
|
-
}
|
|
117
|
-
if (!isActive || reconnectTimer) return;
|
|
118
|
-
reconnectTimer = setTimeout(() => {
|
|
119
|
-
reconnectTimer = null;
|
|
120
|
-
connect();
|
|
121
|
-
}, backoffMs);
|
|
122
|
-
backoffMs = Math.min(backoffMs * 2, MAX_BACKOFF_MS);
|
|
123
|
-
};
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
connect();
|
|
127
|
-
|
|
128
|
-
return () => {
|
|
129
|
-
isActive = false;
|
|
130
|
-
clearTimeout(reconnectTimer);
|
|
131
|
-
const socket = socketRef.current;
|
|
132
|
-
socketRef.current = null;
|
|
133
|
-
if (socket) socket.close();
|
|
134
|
-
};
|
|
135
|
-
}, [active, wsUrl]);
|
|
136
|
-
|
|
137
|
-
return { subscribe, onReconnect };
|
|
138
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Canonical service-worker source. Copy this file verbatim to each consuming
|
|
3
|
-
* application's public/sw.js so it is served from that application's origin root.
|
|
4
|
-
*/
|
|
5
|
-
const resolveSameOriginUrl = (url) => {
|
|
6
|
-
try {
|
|
7
|
-
const resolvedUrl = new URL(url || '/', self.location.origin);
|
|
8
|
-
if (resolvedUrl.origin !== self.location.origin) return '/';
|
|
9
|
-
|
|
10
|
-
return `${resolvedUrl.pathname}${resolvedUrl.search}${resolvedUrl.hash}`;
|
|
11
|
-
} catch {
|
|
12
|
-
return '/';
|
|
13
|
-
}
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
self.addEventListener('push', (event) => {
|
|
17
|
-
if (!event.data) return;
|
|
18
|
-
|
|
19
|
-
let payload;
|
|
20
|
-
try {
|
|
21
|
-
payload = event.data.json();
|
|
22
|
-
} catch {
|
|
23
|
-
payload = { body: event.data.text() };
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const options = {
|
|
27
|
-
body: payload.body || '',
|
|
28
|
-
data: { url: resolveSameOriginUrl(payload.url) },
|
|
29
|
-
};
|
|
30
|
-
if (payload.icon) options.icon = payload.icon;
|
|
31
|
-
if (payload.badge) options.badge = payload.badge;
|
|
32
|
-
|
|
33
|
-
event.waitUntil(self.registration.showNotification(payload.title || 'Notification', options));
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
self.addEventListener('notificationclick', (event) => {
|
|
37
|
-
event.notification.close();
|
|
38
|
-
const url = resolveSameOriginUrl(event.notification.data?.url);
|
|
39
|
-
|
|
40
|
-
event.waitUntil(
|
|
41
|
-
clients.matchAll({ type: 'window', includeUncontrolled: true }).then((clientList) => {
|
|
42
|
-
for (const client of clientList) {
|
|
43
|
-
if (client.url.includes(self.location.origin) && 'focus' in client) {
|
|
44
|
-
return Promise.resolve(client.navigate(url)).then(() => client.focus());
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return clients.openWindow ? clients.openWindow(url) : undefined;
|
|
48
|
-
}),
|
|
49
|
-
);
|
|
50
|
-
});
|
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
createContext,
|
|
3
|
-
useCallback,
|
|
4
|
-
useContext,
|
|
5
|
-
useEffect,
|
|
6
|
-
useMemo,
|
|
7
|
-
useRef,
|
|
8
|
-
useState,
|
|
9
|
-
} from 'react';
|
|
10
|
-
import { AuthContext } from '../auth/AuthContext';
|
|
11
|
-
import { getNotificationPreferences } from '../notifications/api';
|
|
12
|
-
import { getOnboardingStepConfig } from './api';
|
|
13
|
-
import { selectActiveSteps } from './stepSelection';
|
|
14
|
-
import CookieConsentStep from './steps/CookieConsentStep';
|
|
15
|
-
import CompleteNameStep from './steps/CompleteNameStep';
|
|
16
|
-
import BrowserPushStep from './steps/BrowserPushStep';
|
|
17
|
-
import PwaInstallStep from './steps/PwaInstallStep';
|
|
18
|
-
|
|
19
|
-
export const UNIVERSAL_STEP_DESCRIPTORS = [
|
|
20
|
-
{
|
|
21
|
-
id: 'cookie_consent',
|
|
22
|
-
condition: (ctx) => Boolean(ctx.user && !ctx.user.accepted_privacy_statement),
|
|
23
|
-
blocking: true,
|
|
24
|
-
skipable: false,
|
|
25
|
-
persistDismissed: false,
|
|
26
|
-
titleKey: 'Onboarding.PRIVACY_COOKIES_TITLE',
|
|
27
|
-
Component: CookieConsentStep,
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
id: 'complete_name',
|
|
31
|
-
condition: (ctx) => Boolean(ctx.user && (!ctx.user.first_name?.trim() || !ctx.user.last_name?.trim())),
|
|
32
|
-
blocking: true,
|
|
33
|
-
skipable: false,
|
|
34
|
-
persistDismissed: false,
|
|
35
|
-
titleKey: 'Onboarding.COMPLETE_NAME_TITLE',
|
|
36
|
-
Component: CompleteNameStep,
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
id: 'browser_push',
|
|
40
|
-
condition: (ctx) => {
|
|
41
|
-
const { nagUntil = 'any-channel', showOnce = false } = ctx.browserPush || {};
|
|
42
|
-
if (showOnce && ctx.seenSteps?.has('browser_push')) return false;
|
|
43
|
-
if (nagUntil === 'off') return false;
|
|
44
|
-
const supported = ctx.pushState?.supported;
|
|
45
|
-
if (!supported) return !ctx.emailOptedIn;
|
|
46
|
-
if (nagUntil === 'all-channels') return !ctx.pushState.subscribed || !ctx.emailOptedIn;
|
|
47
|
-
return !ctx.pushState.subscribed && !ctx.emailOptedIn; // 'any-channel' (default)
|
|
48
|
-
},
|
|
49
|
-
blocking: false,
|
|
50
|
-
skipable: true,
|
|
51
|
-
persistDismissed: true,
|
|
52
|
-
titleKey: 'Onboarding.NOTIFICATIONS_TITLE',
|
|
53
|
-
Component: BrowserPushStep,
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
id: 'pwa_install',
|
|
57
|
-
// App-level opt-in (ctx.pwaInstallEnabled) gates this on top of device
|
|
58
|
-
// capability — a step being "universal" in the library means available
|
|
59
|
-
// to every app, not automatically shown by every app. Apps that haven't
|
|
60
|
-
// verified their PWA manifest/icons are actually installable must not
|
|
61
|
-
// pass pwaInstallEnabled, or this would show an iOS "Add to Home Screen"
|
|
62
|
-
// hint for an app that isn't really installable.
|
|
63
|
-
condition: (ctx) => Boolean(
|
|
64
|
-
ctx.pwaInstallEnabled
|
|
65
|
-
&& !ctx.pwaInstall?.isStandalone
|
|
66
|
-
&& (Boolean(ctx.pwaInstall?.deferredPrompt) || Boolean(ctx.pwaInstall?.isIos)),
|
|
67
|
-
),
|
|
68
|
-
blocking: false,
|
|
69
|
-
skipable: true,
|
|
70
|
-
persistDismissed: true,
|
|
71
|
-
titleKey: 'Onboarding.PWA_INSTALL_TITLE',
|
|
72
|
-
Component: PwaInstallStep,
|
|
73
|
-
},
|
|
74
|
-
];
|
|
75
|
-
|
|
76
|
-
export const OnboardingContext = createContext(null);
|
|
77
|
-
|
|
78
|
-
export function useOnboarding() {
|
|
79
|
-
return useContext(OnboardingContext);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function loadDismissedSteps() {
|
|
83
|
-
if (typeof window === 'undefined') return new Set();
|
|
84
|
-
try {
|
|
85
|
-
return new Set(JSON.parse(window.localStorage.getItem('onboarding_dismissed')) || []);
|
|
86
|
-
} catch {
|
|
87
|
-
return new Set();
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function loadSeenSteps() {
|
|
92
|
-
if (typeof window === 'undefined') return new Set();
|
|
93
|
-
try {
|
|
94
|
-
return new Set(JSON.parse(window.localStorage.getItem('onboarding_seen')) || []);
|
|
95
|
-
} catch {
|
|
96
|
-
return new Set();
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function persistSeenStep(id) {
|
|
101
|
-
if (typeof window === 'undefined') return;
|
|
102
|
-
try {
|
|
103
|
-
const seenSteps = loadSeenSteps();
|
|
104
|
-
seenSteps.add(id);
|
|
105
|
-
window.localStorage.setItem('onboarding_seen', JSON.stringify([...seenSteps]));
|
|
106
|
-
} catch {
|
|
107
|
-
// Persistence is optional; the current session remains unaffected.
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function getInitialPushState() {
|
|
112
|
-
const supported = typeof navigator !== 'undefined'
|
|
113
|
-
&& typeof window !== 'undefined'
|
|
114
|
-
&& 'serviceWorker' in navigator
|
|
115
|
-
&& 'PushManager' in window;
|
|
116
|
-
return supported ? null : { supported: false, subscribed: false };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function getInitialPwaInstallState() {
|
|
120
|
-
if (typeof window === 'undefined' || typeof navigator === 'undefined') {
|
|
121
|
-
return { isStandalone: false, isIos: false, deferredPrompt: null };
|
|
122
|
-
}
|
|
123
|
-
const isStandalone = Boolean(
|
|
124
|
-
window.matchMedia?.('(display-mode: standalone)').matches || navigator.standalone,
|
|
125
|
-
);
|
|
126
|
-
// iPadOS 13+ reports a desktop Safari/Mac user agent by default, so the
|
|
127
|
-
// classic /ipad/i UA sniff misses it — the standard workaround is
|
|
128
|
-
// detecting touch support on a reported Mac platform (real Macs report
|
|
129
|
-
// maxTouchPoints 0).
|
|
130
|
-
const isIos = /iphone|ipad|ipod/i.test(navigator.userAgent || '')
|
|
131
|
-
|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
132
|
-
return { isStandalone, isIos, deferredPrompt: null };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Stable empty-object default — an inline `{}` default would create a new
|
|
136
|
-
// reference every render, invalidating the `ctx` memo below for every
|
|
137
|
-
// consumer even when no extraContext is passed.
|
|
138
|
-
const EMPTY_EXTRA_CONTEXT = {};
|
|
139
|
-
const EMPTY_BROWSER_PUSH = {};
|
|
140
|
-
|
|
141
|
-
export function OnboardingProvider({
|
|
142
|
-
children,
|
|
143
|
-
extraSteps = [],
|
|
144
|
-
extraContext = EMPTY_EXTRA_CONTEXT,
|
|
145
|
-
pwaInstallEnabled = false,
|
|
146
|
-
browserPush = EMPTY_BROWSER_PUSH,
|
|
147
|
-
}) {
|
|
148
|
-
const { user } = useContext(AuthContext);
|
|
149
|
-
const [configMap, setConfigMap] = useState({});
|
|
150
|
-
const [configLoaded, setConfigLoaded] = useState(false);
|
|
151
|
-
const [pushState, setPushState] = useState(getInitialPushState);
|
|
152
|
-
const [emailOptedIn, setEmailOptedIn] = useState(false);
|
|
153
|
-
const [pwaInstall, setPwaInstall] = useState(getInitialPwaInstallState);
|
|
154
|
-
const [dismissedSet, setDismissedSet] = useState(loadDismissedSteps);
|
|
155
|
-
const seenAtMountRef = useRef(loadSeenSteps());
|
|
156
|
-
|
|
157
|
-
const resolvedBrowserPush = useMemo(
|
|
158
|
-
() => ({ nagUntil: 'any-channel', showOnce: false, ...browserPush }),
|
|
159
|
-
[browserPush],
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
const descriptors = useMemo(
|
|
163
|
-
() => [...UNIVERSAL_STEP_DESCRIPTORS, ...extraSteps],
|
|
164
|
-
[extraSteps],
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
useEffect(() => {
|
|
168
|
-
let cancelled = false;
|
|
169
|
-
if (!user) {
|
|
170
|
-
setConfigMap({});
|
|
171
|
-
setConfigLoaded(false);
|
|
172
|
-
return undefined;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
getOnboardingStepConfig()
|
|
176
|
-
.then((data) => {
|
|
177
|
-
if (cancelled) return;
|
|
178
|
-
const nextConfigMap = {};
|
|
179
|
-
(data || []).forEach((item) => { nextConfigMap[item.key] = item.enabled; });
|
|
180
|
-
setConfigMap(nextConfigMap);
|
|
181
|
-
})
|
|
182
|
-
.catch(() => {
|
|
183
|
-
if (!cancelled) setConfigMap({});
|
|
184
|
-
})
|
|
185
|
-
.finally(() => {
|
|
186
|
-
if (!cancelled) setConfigLoaded(true);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
return () => { cancelled = true; };
|
|
190
|
-
}, [user]);
|
|
191
|
-
|
|
192
|
-
useEffect(() => {
|
|
193
|
-
let cancelled = false;
|
|
194
|
-
if (typeof navigator === 'undefined' || typeof window === 'undefined'
|
|
195
|
-
|| !('serviceWorker' in navigator) || !('PushManager' in window)) {
|
|
196
|
-
setPushState({ supported: false, subscribed: false });
|
|
197
|
-
return undefined;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
navigator.serviceWorker.ready
|
|
201
|
-
.then((registration) => registration.pushManager.getSubscription())
|
|
202
|
-
.then((subscription) => {
|
|
203
|
-
if (!cancelled) setPushState({ supported: true, subscribed: Boolean(subscription) });
|
|
204
|
-
})
|
|
205
|
-
.catch(() => {
|
|
206
|
-
if (!cancelled) setPushState({ supported: true, subscribed: false });
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
return () => { cancelled = true; };
|
|
210
|
-
}, []);
|
|
211
|
-
|
|
212
|
-
// Capture beforeinstallprompt as early as possible (at provider mount, not
|
|
213
|
-
// when the wizard step happens to render) — Chrome fires it once, early,
|
|
214
|
-
// and calling preventDefault() late means losing the deferred prompt
|
|
215
|
-
// reference for good. Gated behind pwaInstallEnabled: calling
|
|
216
|
-
// preventDefault() unconditionally would suppress the browser's own native
|
|
217
|
-
// install UI for every app using this library, even ones that never opted
|
|
218
|
-
// in and have no replacement UI to show instead.
|
|
219
|
-
useEffect(() => {
|
|
220
|
-
if (typeof window === 'undefined' || !pwaInstallEnabled) return undefined;
|
|
221
|
-
|
|
222
|
-
const mediaQuery = window.matchMedia?.('(display-mode: standalone)');
|
|
223
|
-
|
|
224
|
-
const handleDisplayModeChange = (event) => {
|
|
225
|
-
if (event.matches) setPwaInstall((prev) => ({ ...prev, isStandalone: true }));
|
|
226
|
-
};
|
|
227
|
-
const handleBeforeInstallPrompt = (event) => {
|
|
228
|
-
event.preventDefault();
|
|
229
|
-
setPwaInstall((prev) => ({ ...prev, deferredPrompt: event }));
|
|
230
|
-
};
|
|
231
|
-
const handleAppInstalled = () => {
|
|
232
|
-
setPwaInstall((prev) => ({ ...prev, isStandalone: true, deferredPrompt: null }));
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
if (mediaQuery?.addEventListener) {
|
|
236
|
-
mediaQuery.addEventListener('change', handleDisplayModeChange);
|
|
237
|
-
} else if (mediaQuery?.addListener) {
|
|
238
|
-
mediaQuery.addListener(handleDisplayModeChange);
|
|
239
|
-
}
|
|
240
|
-
window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
|
241
|
-
window.addEventListener('appinstalled', handleAppInstalled);
|
|
242
|
-
|
|
243
|
-
return () => {
|
|
244
|
-
if (mediaQuery?.removeEventListener) {
|
|
245
|
-
mediaQuery.removeEventListener('change', handleDisplayModeChange);
|
|
246
|
-
} else if (mediaQuery?.removeListener) {
|
|
247
|
-
mediaQuery.removeListener(handleDisplayModeChange);
|
|
248
|
-
}
|
|
249
|
-
window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt);
|
|
250
|
-
window.removeEventListener('appinstalled', handleAppInstalled);
|
|
251
|
-
};
|
|
252
|
-
}, [pwaInstallEnabled]);
|
|
253
|
-
|
|
254
|
-
useEffect(() => {
|
|
255
|
-
let cancelled = false;
|
|
256
|
-
if (!user) {
|
|
257
|
-
setEmailOptedIn(false);
|
|
258
|
-
return undefined;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
setEmailOptedIn(false);
|
|
262
|
-
getNotificationPreferences()
|
|
263
|
-
.then((data) => {
|
|
264
|
-
if (!cancelled) setEmailOptedIn(Boolean(data?.email_opt_in));
|
|
265
|
-
})
|
|
266
|
-
.catch(() => {
|
|
267
|
-
if (!cancelled) setEmailOptedIn(false);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
return () => { cancelled = true; };
|
|
271
|
-
}, [user]);
|
|
272
|
-
|
|
273
|
-
const dismissStep = useCallback((id) => {
|
|
274
|
-
setDismissedSet((previous) => {
|
|
275
|
-
const next = new Set(previous);
|
|
276
|
-
next.add(id);
|
|
277
|
-
if (typeof window !== 'undefined') {
|
|
278
|
-
try {
|
|
279
|
-
window.localStorage.setItem('onboarding_dismissed', JSON.stringify([...next]));
|
|
280
|
-
} catch {
|
|
281
|
-
// Persistence is optional; the in-memory dismissal still applies.
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return next;
|
|
285
|
-
});
|
|
286
|
-
}, []);
|
|
287
|
-
|
|
288
|
-
const markStepSeen = useCallback((id) => {
|
|
289
|
-
persistSeenStep(id);
|
|
290
|
-
}, []);
|
|
291
|
-
|
|
292
|
-
// extraContext is spread last: duplicate core keys intentionally override core values, so apps should use distinct names.
|
|
293
|
-
const ctx = useMemo(
|
|
294
|
-
() => ({
|
|
295
|
-
user,
|
|
296
|
-
pushState,
|
|
297
|
-
emailOptedIn,
|
|
298
|
-
pwaInstall,
|
|
299
|
-
pwaInstallEnabled,
|
|
300
|
-
browserPush: resolvedBrowserPush,
|
|
301
|
-
seenSteps: seenAtMountRef.current,
|
|
302
|
-
...extraContext,
|
|
303
|
-
}),
|
|
304
|
-
[
|
|
305
|
-
user,
|
|
306
|
-
pushState,
|
|
307
|
-
emailOptedIn,
|
|
308
|
-
pwaInstall,
|
|
309
|
-
pwaInstallEnabled,
|
|
310
|
-
resolvedBrowserPush,
|
|
311
|
-
seenAtMountRef.current,
|
|
312
|
-
extraContext,
|
|
313
|
-
],
|
|
314
|
-
);
|
|
315
|
-
const activeSteps = useMemo(() => {
|
|
316
|
-
if (!configLoaded || !user) return [];
|
|
317
|
-
return selectActiveSteps(descriptors, configMap, ctx, dismissedSet);
|
|
318
|
-
}, [configLoaded, user, descriptors, configMap, ctx, dismissedSet]);
|
|
319
|
-
|
|
320
|
-
const value = useMemo(
|
|
321
|
-
() => ({ activeSteps, dismissStep, markStepSeen, ctx, configMap, setConfigMap }),
|
|
322
|
-
[activeSteps, dismissStep, markStepSeen, ctx, configMap],
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
return <OnboardingContext.Provider value={value}>{children}</OnboardingContext.Provider>;
|
|
326
|
-
}
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import React, { useContext, useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { AuthContext } from '../auth/AuthContext';
|
|
3
|
-
import { useOnboarding } from './OnboardingProvider';
|
|
4
|
-
import { WizardDialogShell } from './WizardDialogShell';
|
|
5
|
-
|
|
6
|
-
export function OnboardingWizard() {
|
|
7
|
-
const onboarding = useOnboarding();
|
|
8
|
-
const authContext = useContext(AuthContext);
|
|
9
|
-
const [sessionDismissed, setSessionDismissed] = useState(() => new Set());
|
|
10
|
-
const totalRef = useRef(null);
|
|
11
|
-
const [completed, setCompleted] = useState(0);
|
|
12
|
-
const activeSteps = onboarding?.activeSteps || [];
|
|
13
|
-
const visibleSteps = activeSteps.filter((step) => !sessionDismissed.has(step.id));
|
|
14
|
-
const currentStep = visibleSteps[0];
|
|
15
|
-
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
if (visibleSteps.length > 0 && totalRef.current === null) {
|
|
18
|
-
totalRef.current = visibleSteps.length;
|
|
19
|
-
setCompleted(0);
|
|
20
|
-
}
|
|
21
|
-
if (visibleSteps.length === 0) {
|
|
22
|
-
totalRef.current = null;
|
|
23
|
-
setCompleted(0);
|
|
24
|
-
}
|
|
25
|
-
}, [visibleSteps.length]);
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
if (onboarding && currentStep) onboarding.markStepSeen(currentStep.id);
|
|
29
|
-
}, [onboarding, currentStep?.id]);
|
|
30
|
-
|
|
31
|
-
if (!onboarding || visibleSteps.length === 0) return null;
|
|
32
|
-
|
|
33
|
-
const { dismissStep, ctx } = onboarding;
|
|
34
|
-
const total = totalRef.current || visibleSteps.length;
|
|
35
|
-
|
|
36
|
-
const completeCurrentStep = () => {
|
|
37
|
-
setSessionDismissed((previous) => new Set([...previous, currentStep.id]));
|
|
38
|
-
setCompleted((count) => count + 1);
|
|
39
|
-
authContext?.refreshUser?.()?.catch(() => {
|
|
40
|
-
// Best-effort refresh; the step itself already persisted successfully.
|
|
41
|
-
});
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const dismissCurrentStep = () => {
|
|
45
|
-
if (currentStep.persistDismissed) dismissStep(currentStep.id);
|
|
46
|
-
completeCurrentStep();
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<WizardDialogShell
|
|
51
|
-
step={currentStep}
|
|
52
|
-
stepIndex={completed}
|
|
53
|
-
total={total}
|
|
54
|
-
onComplete={completeCurrentStep}
|
|
55
|
-
onDismiss={dismissCurrentStep}
|
|
56
|
-
ctx={ctx}
|
|
57
|
-
/>
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export default OnboardingWizard;
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import Box from '@mui/material/Box';
|
|
4
|
-
import Dialog from '@mui/material/Dialog';
|
|
5
|
-
import DialogContent from '@mui/material/DialogContent';
|
|
6
|
-
import DialogTitle from '@mui/material/DialogTitle';
|
|
7
|
-
import LinearProgress from '@mui/material/LinearProgress';
|
|
8
|
-
import Typography from '@mui/material/Typography';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Presentational dialog shell shared by OnboardingWizard and the popup
|
|
12
|
-
* notification surface (NOTIF-12). Owns only the dialog frame, the
|
|
13
|
-
* counter/progress header, and the onComplete/onDismiss/ctx child contract —
|
|
14
|
-
* step selection, dismissal persistence, and any progress store stay with
|
|
15
|
-
* the caller.
|
|
16
|
-
*/
|
|
17
|
-
export function WizardDialogShell({ step, stepIndex, total, onComplete, onDismiss, ctx, title }) {
|
|
18
|
-
const { t } = useTranslation();
|
|
19
|
-
const progress = total > 1 ? Math.round((stepIndex / total) * 100) : 0;
|
|
20
|
-
const StepComponent = step.Component;
|
|
21
|
-
// `title` lets a non-onboarding caller (e.g. the popup surface) supply its
|
|
22
|
-
// own header instead of the onboarding-specific step counter/"Setup" copy.
|
|
23
|
-
// Omitted by OnboardingWizard, so its header text is unchanged.
|
|
24
|
-
const headerLabel = title ?? (total > 1
|
|
25
|
-
? t('Onboarding.STEP_COUNTER', { current: stepIndex + 1, total })
|
|
26
|
-
: t('Onboarding.SETUP'));
|
|
27
|
-
|
|
28
|
-
return (
|
|
29
|
-
<Dialog
|
|
30
|
-
open
|
|
31
|
-
fullWidth
|
|
32
|
-
maxWidth="sm"
|
|
33
|
-
disableEscapeKeyDown={step.blocking}
|
|
34
|
-
onClose={step.blocking ? undefined : onDismiss}
|
|
35
|
-
>
|
|
36
|
-
<DialogTitle sx={{ pb: 0 }}>
|
|
37
|
-
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
|
38
|
-
<Typography variant="subtitle2" color="text.secondary">
|
|
39
|
-
{headerLabel}
|
|
40
|
-
</Typography>
|
|
41
|
-
</Box>
|
|
42
|
-
{total > 1 && <LinearProgress variant="determinate" value={progress} sx={{ mt: 1, borderRadius: 1 }} />}
|
|
43
|
-
</DialogTitle>
|
|
44
|
-
<DialogContent sx={{ pt: 2 }}>
|
|
45
|
-
<StepComponent onComplete={onComplete} onDismiss={onDismiss} ctx={ctx} />
|
|
46
|
-
</DialogContent>
|
|
47
|
-
</Dialog>
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export default WizardDialogShell;
|