@umituz/web-dashboard 6.0.0 → 6.2.0

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 (133) hide show
  1. package/README.md +47 -73
  2. package/package.json +15 -7
  3. package/src/config/DashboardConfig.ts +19 -68
  4. package/src/domains/analytics/components/index.ts +0 -6
  5. package/src/domains/analytics/hooks/index.ts +0 -6
  6. package/src/domains/analytics/hooks/useAnalytics.ts +2 -20
  7. package/src/domains/analytics/index.ts +0 -10
  8. package/src/domains/analytics/services/AnalyticsEngineService.ts +7 -74
  9. package/src/domains/analytics/services/PerformanceService.ts +27 -138
  10. package/src/domains/analytics/services/index.ts +0 -4
  11. package/src/domains/analytics/types/analytics.ts +100 -270
  12. package/src/domains/analytics/types/index.ts +0 -6
  13. package/src/domains/analytics/utils/analytics.test.ts +2 -9
  14. package/src/domains/analytics/utils/analytics.ts +1 -22
  15. package/src/domains/analytics/utils/index.ts +0 -6
  16. package/src/domains/calendar/config.ts +0 -7
  17. package/src/domains/calendar/hooks/index.ts +0 -4
  18. package/src/domains/calendar/hooks/useCalendar.ts +7 -27
  19. package/src/domains/calendar/index.ts +0 -4
  20. package/src/domains/calendar/services/CalendarService.ts +0 -21
  21. package/src/domains/calendar/services/index.ts +0 -4
  22. package/src/domains/calendar/types/calendar.types.ts +9 -53
  23. package/src/domains/calendar/utils/contentItemMapper.test.ts +0 -4
  24. package/src/domains/calendar/utils/contentItemMapper.ts +0 -31
  25. package/src/domains/calendar/utils/index.ts +0 -26
  26. package/src/domains/layouts/components/index.ts +0 -4
  27. package/src/domains/layouts/hooks/dashboard.ts +0 -21
  28. package/src/domains/layouts/hooks/index.ts +0 -4
  29. package/src/domains/layouts/index.ts +0 -6
  30. package/src/domains/layouts/theme/default.ts +0 -12
  31. package/src/domains/layouts/theme/index.ts +0 -4
  32. package/src/domains/layouts/theme/presets.ts +0 -9
  33. package/src/domains/layouts/theme/utils.ts +0 -15
  34. package/src/domains/layouts/types/index.ts +0 -4
  35. package/src/domains/layouts/types/layout.ts +18 -56
  36. package/src/domains/layouts/types/notification.ts +4 -17
  37. package/src/domains/layouts/types/sidebar.ts +8 -28
  38. package/src/domains/layouts/types/theme.ts +23 -59
  39. package/src/domains/layouts/types/user.ts +9 -30
  40. package/src/domains/layouts/utils/dashboard.ts +4 -48
  41. package/src/domains/layouts/utils/index.ts +0 -4
  42. package/src/domains/modules/build.tsx +2 -16
  43. package/src/domains/modules/types.ts +8 -23
  44. package/src/domains/onboarding/calc/calc.test.ts +173 -0
  45. package/src/domains/onboarding/calc/card-stack-motion-curve.ts +27 -0
  46. package/src/domains/onboarding/calc/count-with-locale.ts +8 -0
  47. package/src/domains/onboarding/calc/duration-equal-split.ts +12 -0
  48. package/src/domains/onboarding/calc/elapsed-millis.ts +6 -0
  49. package/src/domains/onboarding/calc/index.ts +36 -0
  50. package/src/domains/onboarding/calc/progress-percentage.ts +27 -0
  51. package/src/domains/onboarding/calc/slider-value-bounds.ts +34 -0
  52. package/src/domains/onboarding/calc/step-index-clamp.ts +19 -0
  53. package/src/domains/onboarding/calc/swipe-gesture-curve.ts +54 -0
  54. package/src/domains/onboarding/components/AnimationWrapper.tsx +69 -0
  55. package/src/domains/onboarding/components/OnboardingErrorBoundary.tsx +61 -0
  56. package/src/domains/onboarding/components/OnboardingFlow.test.tsx +459 -0
  57. package/src/domains/onboarding/components/OnboardingFlow.tsx +313 -0
  58. package/src/domains/onboarding/components/OnboardingIntl.tsx +98 -0
  59. package/src/domains/onboarding/components/PrimaryButton.tsx +40 -0
  60. package/src/domains/onboarding/components/ProgressBar.tsx +32 -0
  61. package/src/domains/onboarding/components/QuestionWrapper.tsx +28 -0
  62. package/src/domains/onboarding/components/ScreenContainer.tsx +19 -0
  63. package/src/domains/onboarding/components/SwipeCard.test.tsx +49 -0
  64. package/src/domains/onboarding/components/SwipeCard.tsx +88 -0
  65. package/src/domains/onboarding/components/renderBundledIcon.tsx +7 -0
  66. package/src/domains/onboarding/components/screens/AccountGateScreen.tsx +79 -0
  67. package/src/domains/onboarding/components/screens/AppDemoScreen.tsx +33 -0
  68. package/src/domains/onboarding/components/screens/ComparisonScreen.tsx +49 -0
  69. package/src/domains/onboarding/components/screens/GoalQuestionScreen.tsx +59 -0
  70. package/src/domains/onboarding/components/screens/PainPointsScreen.tsx +65 -0
  71. package/src/domains/onboarding/components/screens/PaywallScreen.tsx +122 -0
  72. package/src/domains/onboarding/components/screens/PermissionPrimingScreen.tsx +98 -0
  73. package/src/domains/onboarding/components/screens/PersonalizedSolutionScreen.tsx +40 -0
  74. package/src/domains/onboarding/components/screens/PreferencesScreen.tsx +180 -0
  75. package/src/domains/onboarding/components/screens/ProcessingScreen.tsx +63 -0
  76. package/src/domains/onboarding/components/screens/SocialProofScreen.tsx +87 -0
  77. package/src/domains/onboarding/components/screens/TinderCardsScreen.tsx +91 -0
  78. package/src/domains/onboarding/components/screens/ValueDeliveryScreen.tsx +69 -0
  79. package/src/domains/onboarding/components/screens/WelcomeScreen.tsx +32 -0
  80. package/src/domains/onboarding/components/screens/index.ts +28 -0
  81. package/src/domains/onboarding/hooks/index.ts +7 -0
  82. package/src/domains/onboarding/hooks/useOnboardingAnalytics.test.tsx +74 -0
  83. package/src/domains/onboarding/hooks/useOnboardingAnalytics.ts +49 -0
  84. package/src/domains/onboarding/hooks/useOnboardingProgress.test.tsx +150 -0
  85. package/src/domains/onboarding/hooks/useOnboardingProgress.ts +249 -0
  86. package/src/domains/onboarding/hooks/usePermissionPriming.ts +45 -0
  87. package/src/domains/onboarding/hooks/useReducedMotion.ts +19 -0
  88. package/src/domains/onboarding/index.ts +153 -0
  89. package/src/domains/onboarding/predicates/has-handler.ts +21 -0
  90. package/src/domains/onboarding/predicates/icon-name-resolves.ts +97 -0
  91. package/src/domains/onboarding/predicates/index.ts +26 -0
  92. package/src/domains/onboarding/predicates/permission-granted.ts +21 -0
  93. package/src/domains/onboarding/predicates/permission-supports.ts +56 -0
  94. package/src/domains/onboarding/predicates/predicates.test.ts +188 -0
  95. package/src/domains/onboarding/predicates/screen-emits-data.ts +13 -0
  96. package/src/domains/onboarding/predicates/storage-available.ts +14 -0
  97. package/src/domains/onboarding/templates/TemplateDemoFallback.tsx +92 -0
  98. package/src/domains/onboarding/templates/art-onboarding.ts +71 -0
  99. package/src/domains/onboarding/templates/health-onboarding.ts +92 -0
  100. package/src/domains/onboarding/templates/index.ts +6 -0
  101. package/src/domains/onboarding/templates/platform-onboarding.ts +201 -0
  102. package/src/domains/onboarding/templates/video-onboarding.ts +95 -0
  103. package/src/domains/onboarding/test/setup.ts +23 -0
  104. package/src/domains/onboarding/types/config.ts +57 -0
  105. package/src/domains/onboarding/types/index.ts +10 -0
  106. package/src/domains/onboarding/types/labels.ts +49 -0
  107. package/src/domains/onboarding/types/screens.ts +185 -0
  108. package/src/domains/onboarding/utils/analytics-dispatch.test.ts +93 -0
  109. package/src/domains/onboarding/utils/analytics-dispatch.ts +73 -0
  110. package/src/domains/onboarding/utils/default-storage-key.ts +1 -0
  111. package/src/domains/onboarding/utils/index.ts +12 -0
  112. package/src/domains/onboarding/utils/local-storage-bridge.test.ts +93 -0
  113. package/src/domains/onboarding/utils/local-storage-bridge.ts +84 -0
  114. package/src/domains/onboarding/utils/permission-bridge.test.ts +64 -0
  115. package/src/domains/onboarding/utils/permission-bridge.ts +87 -0
  116. package/src/domains/onboarding/validation/flow-structure-check.ts +22 -0
  117. package/src/domains/onboarding/validation/index.ts +9 -0
  118. package/src/domains/onboarding/validation/permission-status-check.ts +18 -0
  119. package/src/domains/onboarding/validation/screen-config-check.ts +100 -0
  120. package/src/domains/onboarding/validation/stored-progress-check.ts +59 -0
  121. package/src/domains/onboarding/validation/validation.test.ts +170 -0
  122. package/src/domains/permissions/hasPermission.ts +0 -8
  123. package/src/domains/permissions/types.ts +0 -20
  124. package/src/domains/settings/components/index.ts +0 -4
  125. package/src/domains/settings/hooks/index.ts +0 -4
  126. package/src/domains/settings/hooks/useSettings.ts +1 -26
  127. package/src/domains/settings/index.ts +0 -6
  128. package/src/domains/settings/types/index.ts +0 -4
  129. package/src/domains/settings/types/settings.ts +23 -67
  130. package/src/domains/settings/utils/index.ts +0 -4
  131. package/src/domains/settings/utils/settings.ts +0 -45
  132. package/src/styles/dashboard.contract.test.ts +1 -0
  133. package/src/styles/dashboard.css +818 -1
@@ -0,0 +1,49 @@
1
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import type {
3
+ AnalyticsConfig,
4
+ OnboardingAnalyticsEvent,
5
+ } from '../types/config';
6
+ import { trackOnboardingEvent } from '../utils/analytics-dispatch';
7
+
8
+ export interface UseOnboardingAnalyticsReturn {
9
+ trackEvent: (event: OnboardingAnalyticsEvent) => void;
10
+ trackFunnel: (event: OnboardingAnalyticsEvent) => void;
11
+ enabled: boolean;
12
+ }
13
+
14
+ export function useOnboardingAnalytics(config: AnalyticsConfig | undefined): UseOnboardingAnalyticsReturn {
15
+ const configRef = useRef(config);
16
+ useEffect(() => {
17
+ configRef.current = config;
18
+ }, [config]);
19
+
20
+ const enabled = config?.trackEvents === true;
21
+
22
+ const dispatch = useCallback(
23
+ (event: OnboardingAnalyticsEvent) => {
24
+ if (!enabled) return;
25
+ const current = configRef.current;
26
+ if (!current) return;
27
+ trackOnboardingEvent(event, {
28
+ provider: current.provider,
29
+ customTracker: current.customTracker,
30
+ });
31
+ },
32
+ [enabled],
33
+ );
34
+
35
+ const trackEvent = useCallback(
36
+ (event: OnboardingAnalyticsEvent) => dispatch(event),
37
+ [dispatch],
38
+ );
39
+
40
+ const trackFunnel = useCallback(
41
+ (event: OnboardingAnalyticsEvent) => dispatch(event),
42
+ [dispatch],
43
+ );
44
+
45
+ return useMemo(
46
+ () => ({ trackEvent, trackFunnel, enabled }),
47
+ [trackEvent, trackFunnel, enabled],
48
+ );
49
+ }
@@ -0,0 +1,150 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { act, renderHook } from '@testing-library/react';
3
+ import { useOnboardingProgress } from './useOnboardingProgress';
4
+ import { defaultStorageKey } from '../utils/default-storage-key';
5
+ import type { OnboardingScreen } from '../types/screens';
6
+
7
+ function makeScreens(count: number): OnboardingScreen[] {
8
+ return Array.from({ length: count }, (_, i) => ({
9
+ id: `s${i}`,
10
+ type: 'welcome',
11
+ title: `Step ${i}`,
12
+ }));
13
+ }
14
+
15
+ beforeEach(() => {
16
+ window.localStorage.clear();
17
+ });
18
+
19
+ describe('useOnboardingProgress — state machine (§1.1, §1.2)', () => {
20
+ it('starts on the first screen', () => {
21
+ const { result } = renderHook(() =>
22
+ useOnboardingProgress({ screens: makeScreens(3) }),
23
+ );
24
+ expect(result.current.currentStep).toBe(0);
25
+ expect(result.current.isFirst).toBe(true);
26
+ expect(result.current.isLast).toBe(false);
27
+ expect(result.current.isFinished).toBe(false);
28
+ expect(result.current.totalSteps).toBe(3);
29
+ });
30
+
31
+ it('advances, records completion, and collects data', () => {
32
+ const { result } = renderHook(() =>
33
+ useOnboardingProgress({ screens: makeScreens(3), ephemeral: true }),
34
+ );
35
+ act(() => result.current.next({ selected: ['x'] }));
36
+ expect(result.current.currentStep).toBe(1);
37
+ expect(result.current.completedSteps).toEqual(['s0']);
38
+ expect(result.current.collected).toEqual({ s0: { selected: ['x'] } });
39
+ expect(result.current.isFirst).toBe(false);
40
+ });
41
+
42
+ it('retreats with prev and stops at the first screen', () => {
43
+ const { result } = renderHook(() =>
44
+ useOnboardingProgress({ screens: makeScreens(3), ephemeral: true }),
45
+ );
46
+ act(() => result.current.next());
47
+ act(() => result.current.next());
48
+ act(() => result.current.prev());
49
+ expect(result.current.currentStep).toBe(1);
50
+ act(() => result.current.prev());
51
+ act(() => result.current.prev()); // below zero — must clamp
52
+ expect(result.current.currentStep).toBe(0);
53
+ });
54
+
55
+ it('finish() marks the flow finished and returns a result', () => {
56
+ const { result } = renderHook(() =>
57
+ useOnboardingProgress({ screens: makeScreens(2), ephemeral: true }),
58
+ );
59
+ act(() => result.current.next());
60
+ let returned: ReturnType<typeof result.current.finish> | undefined;
61
+ act(() => {
62
+ returned = result.current.finish({ final: true });
63
+ });
64
+ expect(result.current.isFinished).toBe(true);
65
+ expect(result.current.result).not.toBeNull();
66
+ expect(result.current.result?.durationMs).toBeGreaterThanOrEqual(0);
67
+ expect(returned?.collected).toEqual({ s1: { final: true } });
68
+ });
69
+
70
+ it('next() on the last screen finishes the flow', () => {
71
+ const { result } = renderHook(() =>
72
+ useOnboardingProgress({ screens: makeScreens(2), ephemeral: true }),
73
+ );
74
+ act(() => result.current.next());
75
+ act(() => result.current.next()); // last screen -> finish
76
+ expect(result.current.isFinished).toBe(true);
77
+ expect(result.current.completedSteps).toEqual(['s0', 's1']);
78
+ });
79
+
80
+ it('reset() rewinds and clears stored progress (§16.1)', () => {
81
+ const { result } = renderHook(() =>
82
+ useOnboardingProgress({ screens: makeScreens(3), storageKey: 'k' }),
83
+ );
84
+ act(() => result.current.next({ selected: ['x'] }));
85
+ expect(JSON.parse(localStorage.getItem('k') ?? '{}').currentStep).toBe(1);
86
+ act(() => result.current.reset());
87
+ expect(result.current.currentStep).toBe(0);
88
+ expect(result.current.completedSteps).toEqual([]);
89
+ expect(result.current.collected).toEqual({});
90
+ // The stale checkpoint is gone; the persist effect re-saves a fresh one.
91
+ const stored = JSON.parse(localStorage.getItem('k') ?? 'null');
92
+ expect(stored).not.toBeNull();
93
+ expect(stored.currentStep).toBe(0);
94
+ expect(stored.completedSteps).toEqual([]);
95
+ });
96
+
97
+ it('goTo() clamps out-of-range indices', () => {
98
+ const { result } = renderHook(() =>
99
+ useOnboardingProgress({ screens: makeScreens(3), ephemeral: true }),
100
+ );
101
+ act(() => result.current.goTo(99));
102
+ expect(result.current.currentStep).toBe(2);
103
+ act(() => result.current.goTo(-5));
104
+ expect(result.current.currentStep).toBe(0);
105
+ });
106
+ });
107
+
108
+ describe('useOnboardingProgress — persistence (§1.4, §1.5, §1.6)', () => {
109
+ it('persists after each transition and rehydrates on a fresh mount', () => {
110
+ const screens = makeScreens(4);
111
+ const first = renderHook(() => useOnboardingProgress({ screens, storageKey: 'resume' }));
112
+ act(() => first.result.current.next({ selected: ['a'] }));
113
+ act(() => first.result.current.next());
114
+ first.unmount();
115
+ expect(JSON.parse(localStorage.getItem('resume') ?? '{}').currentStep).toBe(2);
116
+
117
+ const second = renderHook(() => useOnboardingProgress({ screens, storageKey: 'resume' }));
118
+ expect(second.result.current.currentStep).toBe(2);
119
+ expect(second.result.current.collected).toEqual({ s0: { selected: ['a'] } });
120
+ second.unmount();
121
+ });
122
+
123
+ it('never touches storage in ephemeral mode', () => {
124
+ const { result } = renderHook(() =>
125
+ useOnboardingProgress({ screens: makeScreens(2), ephemeral: true }),
126
+ );
127
+ act(() => result.current.next());
128
+ expect(localStorage.getItem(defaultStorageKey)).toBeNull();
129
+ });
130
+
131
+ it('rehydration past the end of a shortened flow starts fresh', () => {
132
+ // Progress stored for 4 screens; the app now ships 2. currentStep=3 is
133
+ // out of bounds and must not be restored.
134
+ localStorage.setItem(
135
+ 'short',
136
+ JSON.stringify({
137
+ version: 1,
138
+ currentStep: 3,
139
+ completedSteps: ['s0', 's1', 's2'],
140
+ collected: {},
141
+ startedAt: new Date().toISOString(),
142
+ lastUpdated: new Date().toISOString(),
143
+ }),
144
+ );
145
+ const { result } = renderHook(() =>
146
+ useOnboardingProgress({ screens: makeScreens(2), storageKey: 'short' }),
147
+ );
148
+ expect(result.current.currentStep).toBe(0);
149
+ });
150
+ });
@@ -0,0 +1,249 @@
1
+ import { useCallback, useEffect, useMemo, useReducer } from 'react';
2
+ import type { OnboardingScreen } from '../types/screens';
3
+ import type { OnboardingResult } from '../types/config';
4
+ import {
5
+ advanceStepIndex,
6
+ clampStepIndex,
7
+ retreatStepIndex,
8
+ } from '../calc/step-index-clamp';
9
+ import {
10
+ computeProgressFraction,
11
+ computeRemainingSteps,
12
+ } from '../calc/progress-percentage';
13
+ import {
14
+ buildOnboardingResult,
15
+ clearOnboardingProgress,
16
+ loadOnboardingProgress,
17
+ saveOnboardingProgress,
18
+ } from '../utils/local-storage-bridge';
19
+
20
+ interface ProgressState {
21
+ currentStep: number;
22
+ totalSteps: number;
23
+ completedSteps: ReadonlyArray<string>;
24
+ collected: Record<string, unknown>;
25
+ startedAt: Date;
26
+ lastUpdated: Date;
27
+ finishedAt: Date | null;
28
+ }
29
+
30
+ type ProgressAction =
31
+ | { type: 'next'; currentScreen: OnboardingScreen; data?: unknown }
32
+ | { type: 'previous' }
33
+ | { type: 'goto'; index: number }
34
+ | { type: 'reset' }
35
+ | { type: 'finish'; currentScreen: OnboardingScreen; data?: unknown }
36
+ | { type: 'hydrate'; state: ProgressState };
37
+
38
+ function reducer(state: ProgressState, action: ProgressAction): ProgressState {
39
+ switch (action.type) {
40
+ case 'next': {
41
+ const completed = state.completedSteps.includes(action.currentScreen.id)
42
+ ? state.completedSteps
43
+ : [...state.completedSteps, action.currentScreen.id];
44
+ const collected =
45
+ action.data !== undefined
46
+ ? { ...state.collected, [action.currentScreen.id]: action.data }
47
+ : state.collected;
48
+ return {
49
+ ...state,
50
+ currentStep: advanceStepIndex(state.currentStep, state.totalSteps),
51
+ completedSteps: completed,
52
+ collected,
53
+ lastUpdated: new Date(),
54
+ };
55
+ }
56
+ case 'previous':
57
+ return { ...state, currentStep: retreatStepIndex(state.currentStep), lastUpdated: new Date() };
58
+ case 'goto':
59
+ return {
60
+ ...state,
61
+ currentStep: clampStepIndex(action.index, state.totalSteps),
62
+ lastUpdated: new Date(),
63
+ };
64
+ case 'finish': {
65
+ const completed = state.completedSteps.includes(action.currentScreen.id)
66
+ ? state.completedSteps
67
+ : [...state.completedSteps, action.currentScreen.id];
68
+ const collected =
69
+ action.data !== undefined
70
+ ? { ...state.collected, [action.currentScreen.id]: action.data }
71
+ : state.collected;
72
+ return {
73
+ ...state,
74
+ completedSteps: completed,
75
+ collected,
76
+ finishedAt: new Date(),
77
+ lastUpdated: new Date(),
78
+ };
79
+ }
80
+ case 'reset':
81
+ return {
82
+ ...state,
83
+ currentStep: 0,
84
+ completedSteps: [],
85
+ collected: {},
86
+ finishedAt: null,
87
+ lastUpdated: new Date(),
88
+ startedAt: new Date(),
89
+ };
90
+ case 'hydrate':
91
+ return action.state;
92
+ default:
93
+ return state;
94
+ }
95
+ }
96
+
97
+ interface UseOnboardingProgressOptions {
98
+ screens: ReadonlyArray<OnboardingScreen>;
99
+ storageKey?: string;
100
+ ephemeral?: boolean;
101
+ }
102
+
103
+ export interface UseOnboardingProgressReturn {
104
+ currentStep: number;
105
+ totalSteps: number;
106
+ progress: number; completedSteps: ReadonlyArray<string>;
107
+ collected: Record<string, unknown>;
108
+ remainingSteps: number;
109
+ isFirst: boolean;
110
+ isLast: boolean;
111
+ isFinished: boolean;
112
+ startedAt: Date;
113
+ next: (data?: unknown) => void;
114
+ prev: () => void;
115
+ goTo: (index: number) => void;
116
+ reset: () => void;
117
+ finish: (data?: unknown) => OnboardingResult;
118
+ result: OnboardingResult | null;
119
+ }
120
+
121
+ export function useOnboardingProgress(
122
+ options: UseOnboardingProgressOptions,
123
+ ): UseOnboardingProgressReturn {
124
+ const { screens, storageKey, ephemeral = false } = options;
125
+ const totalSteps = screens.length;
126
+
127
+ const initialState = useMemo<ProgressState>(() => {
128
+ const now = new Date();
129
+ if (!ephemeral) {
130
+ const restored = loadOnboardingProgress(storageKey);
131
+ if (restored && restored.currentStep < totalSteps) {
132
+ return {
133
+ currentStep: restored.currentStep,
134
+ totalSteps,
135
+ completedSteps: restored.completedSteps,
136
+ collected: restored.collected,
137
+ startedAt: restored.startedAt,
138
+ lastUpdated: restored.lastUpdated,
139
+ finishedAt: null,
140
+ };
141
+ }
142
+ }
143
+ return {
144
+ currentStep: 0,
145
+ totalSteps,
146
+ completedSteps: [],
147
+ collected: {},
148
+ startedAt: now,
149
+ lastUpdated: now,
150
+ finishedAt: null,
151
+ };
152
+ }, [totalSteps, storageKey, ephemeral]);
153
+
154
+ const [state, dispatch] = useReducer(reducer, initialState);
155
+
156
+ useEffect(() => {
157
+ if (ephemeral) return;
158
+ saveOnboardingProgress(storageKey, {
159
+ currentStep: state.currentStep,
160
+ completedSteps: state.completedSteps,
161
+ collected: state.collected,
162
+ startedAt: state.startedAt,
163
+ lastUpdated: state.lastUpdated,
164
+ });
165
+ }, [
166
+ ephemeral,
167
+ storageKey,
168
+ state.currentStep,
169
+ state.completedSteps,
170
+ state.collected,
171
+ state.startedAt,
172
+ state.lastUpdated,
173
+ ]);
174
+
175
+ const next = useCallback(
176
+ (data?: unknown) => {
177
+ const currentScreen = screens[state.currentStep];
178
+ if (!currentScreen) return;
179
+ if (state.currentStep === state.totalSteps - 1) {
180
+ dispatch({ type: 'finish', currentScreen, data });
181
+ } else {
182
+ dispatch({ type: 'next', currentScreen, data });
183
+ }
184
+ },
185
+ [screens, state.currentStep, state.totalSteps],
186
+ );
187
+
188
+ const prev = useCallback(() => dispatch({ type: 'previous' }), []);
189
+ const goTo = useCallback((index: number) => dispatch({ type: 'goto', index }), []);
190
+ const reset = useCallback(() => {
191
+ if (!ephemeral) clearOnboardingProgress(storageKey);
192
+ dispatch({ type: 'reset' });
193
+ }, [ephemeral, storageKey]);
194
+
195
+ const finish = useCallback(
196
+ (data?: unknown) => {
197
+ const currentScreen = screens[state.currentStep];
198
+ if (currentScreen) dispatch({ type: 'finish', currentScreen, data });
199
+ const finishedAt = new Date();
200
+ return buildOnboardingResult({
201
+ completedSteps:
202
+ currentScreen && !state.completedSteps.includes(currentScreen.id)
203
+ ? [...state.completedSteps, currentScreen.id]
204
+ : state.completedSteps,
205
+ collected:
206
+ currentScreen && data !== undefined
207
+ ? { ...state.collected, [currentScreen.id]: data }
208
+ : state.collected,
209
+ startedAt: state.startedAt,
210
+ finishedAt,
211
+ });
212
+ },
213
+ [screens, state.currentStep, state.completedSteps, state.collected, state.startedAt],
214
+ );
215
+
216
+ const progress = computeProgressFraction(state.currentStep, state.totalSteps);
217
+ const remainingSteps = computeRemainingSteps(state.currentStep, state.totalSteps);
218
+ const isFirst = state.currentStep === 0;
219
+ const isLast = state.currentStep === state.totalSteps - 1;
220
+ const isFinished = state.finishedAt !== null;
221
+ const result =
222
+ state.finishedAt !== null
223
+ ? buildOnboardingResult({
224
+ completedSteps: state.completedSteps,
225
+ collected: state.collected,
226
+ startedAt: state.startedAt,
227
+ finishedAt: state.finishedAt,
228
+ })
229
+ : null;
230
+
231
+ return {
232
+ currentStep: state.currentStep,
233
+ totalSteps,
234
+ progress,
235
+ completedSteps: state.completedSteps,
236
+ collected: state.collected,
237
+ remainingSteps,
238
+ isFirst,
239
+ isLast,
240
+ isFinished,
241
+ startedAt: state.startedAt,
242
+ next,
243
+ prev,
244
+ goTo,
245
+ reset,
246
+ finish,
247
+ result,
248
+ };
249
+ }
@@ -0,0 +1,45 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import type { PermissionStatus, PermissionType } from '../types/screens';
3
+ import {
4
+ isPermissionDecided,
5
+ isPermissionUnsupported,
6
+ } from '../predicates/permission-granted';
7
+ import { queryPermissionStatus, requestPermission } from '../utils/permission-bridge';
8
+
9
+ export interface UsePermissionPrimingReturn {
10
+ status: PermissionStatus;
11
+ isRequesting: boolean;
12
+ requestPermission: () => Promise<PermissionStatus>;
13
+ reset: () => void;
14
+ }
15
+
16
+ export function usePermissionPriming(permission: PermissionType): UsePermissionPrimingReturn {
17
+ const [status, setStatus] = useState<PermissionStatus>('prompt');
18
+ const [isRequesting, setIsRequesting] = useState(false);
19
+
20
+ useEffect(() => {
21
+ let cancelled = false;
22
+ queryPermissionStatus(permission).then((initial) => {
23
+ if (!cancelled) setStatus(initial);
24
+ });
25
+ return () => {
26
+ cancelled = true;
27
+ };
28
+ }, [permission]);
29
+
30
+ const request = useCallback(async (): Promise<PermissionStatus> => {
31
+ if (isPermissionDecided(status) || isPermissionUnsupported(status)) return status;
32
+ setIsRequesting(true);
33
+ try {
34
+ const next = await requestPermission(permission);
35
+ setStatus(next);
36
+ return next;
37
+ } finally {
38
+ setIsRequesting(false);
39
+ }
40
+ }, [permission, status]);
41
+
42
+ const reset = useCallback(() => setStatus('prompt'), []);
43
+
44
+ return { status, isRequesting, requestPermission: request, reset };
45
+ }
@@ -0,0 +1,19 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export function useReducedMotion(): boolean {
4
+ const getInitial = (): boolean => {
5
+ if (typeof window === 'undefined' || !window.matchMedia) return false;
6
+ return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
7
+ };
8
+ const [reduced, setReduced] = useState<boolean>(getInitial);
9
+
10
+ useEffect(() => {
11
+ if (typeof window === 'undefined' || !window.matchMedia) return;
12
+ const mql = window.matchMedia('(prefers-reduced-motion: reduce)');
13
+ const handler = (e: MediaQueryListEvent) => setReduced(e.matches);
14
+ mql.addEventListener('change', handler);
15
+ return () => mql.removeEventListener('change', handler);
16
+ }, []);
17
+
18
+ return reduced;
19
+ }
@@ -0,0 +1,153 @@
1
+ export { OnboardingFlow } from './components/OnboardingFlow';
2
+ export type { OnboardingFlowProps } from './components/OnboardingFlow';
3
+
4
+ export {
5
+ OnboardingIntlProvider,
6
+ OnboardingIntlContext,
7
+ useOnboardingIntl,
8
+ defaultOnboardingLabels,
9
+ formatLabel,
10
+ } from './components/OnboardingIntl';
11
+ export type { OnboardingIntl, OnboardingIntlProviderProps } from './components/OnboardingIntl';
12
+
13
+ export {
14
+ WelcomeScreen,
15
+ GoalQuestionScreen,
16
+ PainPointsScreen,
17
+ SocialProofScreen,
18
+ TinderCardsScreen,
19
+ PersonalizedSolutionScreen,
20
+ ComparisonScreen,
21
+ PreferencesScreen,
22
+ PermissionPrimingScreen,
23
+ ProcessingScreen,
24
+ AppDemoScreen,
25
+ ValueDeliveryScreen,
26
+ AccountGateScreen,
27
+ PaywallScreen,
28
+ } from './components/screens';
29
+
30
+ export { ProgressBar } from './components/ProgressBar';
31
+ export type { ProgressBarProps } from './components/ProgressBar';
32
+ export { QuestionWrapper } from './components/QuestionWrapper';
33
+ export type { QuestionWrapperProps } from './components/QuestionWrapper';
34
+ export { SwipeCard } from './components/SwipeCard';
35
+ export type { SwipeCardProps } from './components/SwipeCard';
36
+ export { AnimationWrapper } from './components/AnimationWrapper';
37
+ export type { AnimationWrapperProps, ScreenTransition } from './components/AnimationWrapper';
38
+ export { ScreenContainer } from './components/ScreenContainer';
39
+ export type { ScreenContainerProps } from './components/ScreenContainer';
40
+ export { PrimaryButton } from './components/PrimaryButton';
41
+ export type { PrimaryButtonProps } from './components/PrimaryButton';
42
+ export { OnboardingErrorBoundary } from './components/OnboardingErrorBoundary';
43
+ export type { OnboardingErrorBoundaryProps } from './components/OnboardingErrorBoundary';
44
+
45
+ export {
46
+ useOnboardingProgress,
47
+ usePermissionPriming,
48
+ useOnboardingAnalytics,
49
+ useReducedMotion,
50
+ } from './hooks';
51
+ export type {
52
+ UseOnboardingProgressReturn,
53
+ UsePermissionPrimingReturn,
54
+ UseOnboardingAnalyticsReturn,
55
+ } from './hooks';
56
+
57
+ export {
58
+ healthOnboardingTemplate,
59
+ artOnboardingTemplate,
60
+ videoOnboardingTemplate,
61
+ platformOnboardingTemplate,
62
+ withDefaultDemo,
63
+ defaultDemoResult,
64
+ TemplateDemoFallback,
65
+ } from './templates';
66
+ export type { TemplateDemoResult } from './templates';
67
+
68
+ export {
69
+ computeProgressFraction,
70
+ computeProgressPercentage,
71
+ computeRemainingSteps,
72
+ clampStepIndex,
73
+ advanceStepIndex,
74
+ retreatStepIndex,
75
+ SWIPE_DECISION_THRESHOLD_PX,
76
+ SWIPE_ROTATION_RANGE,
77
+ SWIPE_ROTATION_VALUES,
78
+ SWIPE_LEFT_OPACITY_INPUT,
79
+ SWIPE_LEFT_OPACITY_VALUES,
80
+ SWIPE_RIGHT_OPACITY_INPUT,
81
+ SWIPE_RIGHT_OPACITY_VALUES,
82
+ didCrossRightSwipeThreshold,
83
+ didCrossLeftSwipeThreshold,
84
+ getCardEnterInitial,
85
+ getCardEnterAnimate,
86
+ getCardEnterExit,
87
+ getCardTransitionDurationMs,
88
+ resolveSliderBounds,
89
+ areSliderBoundsValid,
90
+ clampValueToBounds,
91
+ splitTotalDuration,
92
+ getDefaultStepDurationMs,
93
+ formatCountWithLocale,
94
+ elapsedMillis,
95
+ } from './calc';
96
+ export type { CardMotionState } from './calc';
97
+
98
+ export {
99
+ checkFlowStructure,
100
+ checkScreenConfig,
101
+ checkStoredProgress,
102
+ getRecognizedScreenTypes,
103
+ isRecognizedPermissionStatus,
104
+ normalizePermissionStatus,
105
+ } from './validation';
106
+ export type { ValidationOutcome, StoredProgressShape, StoredProgressOutcome } from './validation';
107
+
108
+ export {
109
+ isPermissionGranted,
110
+ isPermissionDenied,
111
+ isPermissionPrompt,
112
+ isPermissionUnsupported,
113
+ isPermissionDecided,
114
+ doesBrowserSupportNotifications,
115
+ doesBrowserSupportMediaCapture,
116
+ doesBrowserSupportGeolocation,
117
+ isRuntimeSupportMissing,
118
+ isLocalStorageAvailable,
119
+ doesIconNameResolve,
120
+ getRegisteredIconNames,
121
+ resolveIconByName,
122
+ hasOnRequest,
123
+ hasOnAccept,
124
+ hasOnSignIn,
125
+ accountGateAllowsSkip,
126
+ screenEmitsData,
127
+ } from './predicates';
128
+
129
+ export {
130
+ trackOnboardingEvent,
131
+ dispatchAnalyticsEvent,
132
+ saveOnboardingProgress,
133
+ loadOnboardingProgress,
134
+ clearOnboardingProgress,
135
+ buildOnboardingResult,
136
+ queryPermissionStatus,
137
+ requestPermission,
138
+ defaultStorageKey,
139
+ } from './utils';
140
+ export type { RehydratedProgress } from './utils';
141
+
142
+ export type {
143
+ OnboardingScreen,
144
+ OnboardingScreenType,
145
+ OnboardingOption,
146
+ OnboardingConfig,
147
+ OnboardingCategory,
148
+ OnboardingLabels,
149
+ AnalyticsConfig,
150
+ AnalyticsProvider,
151
+ OnboardingAnalyticsEvent,
152
+ OnboardingResult,
153
+ } from './types';
@@ -0,0 +1,21 @@
1
+ import type {
2
+ AccountGateScreenConfig,
3
+ PaywallScreenConfig,
4
+ PermissionPrimingScreenConfig,
5
+ } from '../types/screens';
6
+
7
+ export function hasOnRequest(config: PermissionPrimingScreenConfig): boolean {
8
+ return typeof config.onRequest === 'function';
9
+ }
10
+
11
+ export function hasOnAccept(config: PaywallScreenConfig): boolean {
12
+ return typeof config.onAccept === 'function';
13
+ }
14
+
15
+ export function hasOnSignIn(config: AccountGateScreenConfig): boolean {
16
+ return typeof config.onSignIn === 'function';
17
+ }
18
+
19
+ export function accountGateAllowsSkip(config: AccountGateScreenConfig): boolean {
20
+ return config.optional === true;
21
+ }