@umituz/react-native-onboarding 2.9.0 → 3.0.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.
- package/README.md +0 -0
- package/package.json +4 -4
- package/src/domain/entities/OnboardingOptions.ts +0 -0
- package/src/domain/entities/OnboardingQuestion.ts +0 -0
- package/src/domain/entities/OnboardingSlide.ts +0 -0
- package/src/domain/entities/OnboardingUserData.ts +0 -0
- package/src/index.ts +8 -0
- package/src/infrastructure/hooks/__tests__/useOnboardingAnswers.test.ts +163 -0
- package/src/infrastructure/hooks/__tests__/useOnboardingNavigation.test.ts +121 -0
- package/src/infrastructure/hooks/useOnboardingAnswers.ts +2 -2
- package/src/infrastructure/hooks/useOnboardingNavigation.ts +0 -0
- package/src/infrastructure/services/OnboardingSlideService.ts +0 -0
- package/src/infrastructure/services/OnboardingValidationService.ts +0 -0
- package/src/infrastructure/storage/OnboardingStore.ts +37 -162
- package/src/infrastructure/storage/OnboardingStoreActions.ts +195 -0
- package/src/infrastructure/storage/OnboardingStoreSelectors.ts +25 -0
- package/src/infrastructure/storage/OnboardingStoreState.ts +22 -0
- package/src/infrastructure/storage/__tests__/OnboardingStore.test.ts +85 -0
- package/src/infrastructure/utils/gradientUtils.ts +0 -0
- package/src/presentation/components/OnboardingFooter.tsx +0 -0
- package/src/presentation/components/OnboardingHeader.tsx +8 -7
- package/src/presentation/components/OnboardingScreenContent.tsx +0 -0
- package/src/presentation/components/OnboardingSlide.tsx +16 -8
- package/src/presentation/components/QuestionRenderer.tsx +0 -0
- package/src/presentation/components/QuestionSlide.tsx +0 -0
- package/src/presentation/components/QuestionSlideHeader.tsx +0 -0
- package/src/presentation/components/questions/MultipleChoiceQuestion.tsx +20 -17
- package/src/presentation/components/questions/RatingQuestion.tsx +4 -3
- package/src/presentation/components/questions/SingleChoiceQuestion.tsx +16 -15
- package/src/presentation/components/questions/TextInputQuestion.tsx +12 -7
- package/src/presentation/hooks/__tests__/useOnboardingContainerStyle.test.ts +96 -0
- package/src/presentation/hooks/useOnboardingContainerStyle.ts +37 -0
- package/src/presentation/hooks/useOnboardingScreenState.ts +69 -46
- package/src/presentation/screens/OnboardingScreen.tsx +5 -0
package/README.md
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-onboarding",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "Advanced onboarding flow for React Native apps with personalization questions, theme-aware colors, animations, and customizable slides. SOLID, DRY, KISS principles applied.",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"@umituz/react-native-design-system-theme": "latest",
|
|
37
37
|
"@umituz/react-native-design-system": "latest",
|
|
38
38
|
"@umituz/react-native-design-system-atoms": "latest",
|
|
39
|
+
"@expo/vector-icons": ">=14.0.0",
|
|
39
40
|
"expo-linear-gradient": "^15.0.0",
|
|
40
|
-
"lucide-react-native": "^0.344.0",
|
|
41
41
|
"react": ">=18.2.0",
|
|
42
42
|
"react-native": ">=0.74.0",
|
|
43
43
|
"react-native-safe-area-context": "^5.0.0",
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
"@umituz/react-native-localization": "latest",
|
|
51
51
|
"@umituz/react-native-design-system-theme": "latest",
|
|
52
52
|
"@umituz/react-native-design-system-atoms": "latest",
|
|
53
|
+
"@expo/vector-icons": ">=14.0.0",
|
|
53
54
|
"expo-linear-gradient": "^15.0.7",
|
|
54
|
-
"lucide-react-native": "^0.344.0",
|
|
55
55
|
"react": "^18.2.0",
|
|
56
56
|
"react-native": "^0.74.0",
|
|
57
57
|
"react-native-safe-area-context": "^5.6.0",
|
|
@@ -66,4 +66,4 @@
|
|
|
66
66
|
"README.md",
|
|
67
67
|
"LICENSE"
|
|
68
68
|
]
|
|
69
|
-
}
|
|
69
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/index.ts
CHANGED
|
@@ -48,10 +48,18 @@ export {
|
|
|
48
48
|
useOnboardingStore,
|
|
49
49
|
useOnboarding,
|
|
50
50
|
} from "./infrastructure/storage/OnboardingStore";
|
|
51
|
+
export type { OnboardingStoreState } from "./infrastructure/storage/OnboardingStoreState";
|
|
52
|
+
export type { OnboardingStoreActions } from "./infrastructure/storage/OnboardingStoreActions";
|
|
53
|
+
export type { OnboardingStoreSelectors } from "./infrastructure/storage/OnboardingStoreSelectors";
|
|
51
54
|
export {
|
|
52
55
|
useOnboardingNavigation,
|
|
53
56
|
type UseOnboardingNavigationReturn,
|
|
54
57
|
} from "./infrastructure/hooks/useOnboardingNavigation";
|
|
58
|
+
export {
|
|
59
|
+
useOnboardingContainerStyle,
|
|
60
|
+
type UseOnboardingContainerStyleProps,
|
|
61
|
+
type UseOnboardingContainerStyleReturn,
|
|
62
|
+
} from "./presentation/hooks/useOnboardingContainerStyle";
|
|
55
63
|
|
|
56
64
|
// =============================================================================
|
|
57
65
|
// PRESENTATION LAYER - Components and Screens
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OnboardingAnswers Hook Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook, act } from '@testing-library/react-native';
|
|
6
|
+
import { useOnboardingAnswers } from '../useOnboardingAnswers';
|
|
7
|
+
import { useOnboardingStore } from '../../storage/OnboardingStore';
|
|
8
|
+
|
|
9
|
+
// Mock the store
|
|
10
|
+
jest.mock('../../storage/OnboardingStore', () => ({
|
|
11
|
+
useOnboardingStore: jest.fn(),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const mockUseOnboardingStore = useOnboardingStore as jest.MockedFunction<typeof useOnboardingStore>;
|
|
15
|
+
|
|
16
|
+
describe('useOnboardingAnswers', () => {
|
|
17
|
+
const mockGetAnswer = jest.fn();
|
|
18
|
+
const mockSaveAnswer = jest.fn();
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
mockUseOnboardingStore.mockReturnValue({
|
|
23
|
+
getAnswer: mockGetAnswer,
|
|
24
|
+
saveAnswer: mockSaveAnswer,
|
|
25
|
+
} as any);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should initialize with undefined answer', () => {
|
|
29
|
+
const mockSlide = {
|
|
30
|
+
id: '1',
|
|
31
|
+
type: 'question',
|
|
32
|
+
question: {
|
|
33
|
+
id: 'q1',
|
|
34
|
+
type: 'single',
|
|
35
|
+
text: 'Test question',
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
|
|
40
|
+
|
|
41
|
+
expect(result.current.currentAnswer).toBeUndefined();
|
|
42
|
+
expect(mockGetAnswer).toHaveBeenCalledWith('q1');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should load saved answer for slide', () => {
|
|
46
|
+
const mockSlide = {
|
|
47
|
+
id: '1',
|
|
48
|
+
type: 'question',
|
|
49
|
+
question: {
|
|
50
|
+
id: 'q1',
|
|
51
|
+
type: 'single',
|
|
52
|
+
text: 'Test question',
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
mockGetAnswer.mockReturnValue('option1');
|
|
57
|
+
|
|
58
|
+
const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
|
|
59
|
+
|
|
60
|
+
expect(result.current.currentAnswer).toBe('option1');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should use default value when no saved answer', () => {
|
|
64
|
+
const mockSlide = {
|
|
65
|
+
id: '1',
|
|
66
|
+
type: 'question',
|
|
67
|
+
question: {
|
|
68
|
+
id: 'q1',
|
|
69
|
+
type: 'single',
|
|
70
|
+
text: 'Test question',
|
|
71
|
+
defaultValue: 'default',
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
mockGetAnswer.mockReturnValue(undefined);
|
|
76
|
+
|
|
77
|
+
const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
|
|
78
|
+
|
|
79
|
+
expect(result.current.currentAnswer).toBe('default');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should set current answer', () => {
|
|
83
|
+
const mockSlide = {
|
|
84
|
+
id: '1',
|
|
85
|
+
type: 'question',
|
|
86
|
+
question: {
|
|
87
|
+
id: 'q1',
|
|
88
|
+
type: 'single',
|
|
89
|
+
text: 'Test question',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
|
|
94
|
+
|
|
95
|
+
act(() => {
|
|
96
|
+
result.current.setCurrentAnswer('newAnswer');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(result.current.currentAnswer).toBe('newAnswer');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should save answer for slide', async () => {
|
|
103
|
+
const mockSlide = {
|
|
104
|
+
id: '1',
|
|
105
|
+
type: 'question',
|
|
106
|
+
question: {
|
|
107
|
+
id: 'q1',
|
|
108
|
+
type: 'single',
|
|
109
|
+
text: 'Test question',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
mockSaveAnswer.mockResolvedValue(undefined);
|
|
114
|
+
|
|
115
|
+
const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
|
|
116
|
+
|
|
117
|
+
act(() => {
|
|
118
|
+
result.current.setCurrentAnswer('answer1');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
await act(async () => {
|
|
122
|
+
await result.current.saveCurrentAnswer(mockSlide);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(mockSaveAnswer).toHaveBeenCalledWith('q1', 'answer1');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should not save answer for non-question slide', async () => {
|
|
129
|
+
const mockSlide = {
|
|
130
|
+
id: '1',
|
|
131
|
+
type: 'welcome',
|
|
132
|
+
title: 'Welcome',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
|
|
136
|
+
|
|
137
|
+
await act(async () => {
|
|
138
|
+
await result.current.saveCurrentAnswer(mockSlide);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(mockSaveAnswer).not.toHaveBeenCalled();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('should not save undefined answer', async () => {
|
|
145
|
+
const mockSlide = {
|
|
146
|
+
id: '1',
|
|
147
|
+
type: 'question',
|
|
148
|
+
question: {
|
|
149
|
+
id: 'q1',
|
|
150
|
+
type: 'single',
|
|
151
|
+
text: 'Test question',
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
|
|
156
|
+
|
|
157
|
+
await act(async () => {
|
|
158
|
+
await result.current.saveCurrentAnswer(mockSlide);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
expect(mockSaveAnswer).not.toHaveBeenCalled();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OnboardingNavigation Hook Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook, act } from '@testing-library/react-native';
|
|
6
|
+
import { useOnboardingNavigation } from '../useOnboardingNavigation';
|
|
7
|
+
|
|
8
|
+
describe('useOnboardingNavigation', () => {
|
|
9
|
+
const mockOnComplete = jest.fn();
|
|
10
|
+
const mockOnSkip = jest.fn();
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should initialize with first slide', () => {
|
|
17
|
+
const { result } = renderHook(() =>
|
|
18
|
+
useOnboardingNavigation(3, mockOnComplete, mockOnSkip)
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(result.current.currentIndex).toBe(0);
|
|
22
|
+
expect(result.current.isFirstSlide).toBe(true);
|
|
23
|
+
expect(result.current.isLastSlide).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should navigate to next slide', () => {
|
|
27
|
+
const { result } = renderHook(() =>
|
|
28
|
+
useOnboardingNavigation(3, mockOnComplete, mockOnSkip)
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
act(() => {
|
|
32
|
+
result.current.goToNext();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(result.current.currentIndex).toBe(1);
|
|
36
|
+
expect(result.current.isFirstSlide).toBe(false);
|
|
37
|
+
expect(result.current.isLastSlide).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should navigate to previous slide', () => {
|
|
41
|
+
const { result } = renderHook(() =>
|
|
42
|
+
useOnboardingNavigation(3, mockOnComplete, mockOnSkip)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
act(() => {
|
|
46
|
+
result.current.goToNext();
|
|
47
|
+
});
|
|
48
|
+
act(() => {
|
|
49
|
+
result.current.goToPrevious();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
expect(result.current.currentIndex).toBe(0);
|
|
53
|
+
expect(result.current.isFirstSlide).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should handle last slide correctly', () => {
|
|
57
|
+
const { result } = renderHook(() =>
|
|
58
|
+
useOnboardingNavigation(2, mockOnComplete, mockOnSkip)
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
act(() => {
|
|
62
|
+
result.current.goToNext();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
expect(result.current.currentIndex).toBe(1);
|
|
66
|
+
expect(result.current.isLastSlide).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should not go beyond last slide', () => {
|
|
70
|
+
const { result } = renderHook(() =>
|
|
71
|
+
useOnboardingNavigation(2, mockOnComplete, mockOnSkip)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
act(() => {
|
|
75
|
+
result.current.goToNext();
|
|
76
|
+
});
|
|
77
|
+
act(() => {
|
|
78
|
+
result.current.goToNext();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(result.current.currentIndex).toBe(1);
|
|
82
|
+
expect(result.current.isLastSlide).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should not go before first slide', () => {
|
|
86
|
+
const { result } = renderHook(() =>
|
|
87
|
+
useOnboardingNavigation(2, mockOnComplete, mockOnSkip)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
act(() => {
|
|
91
|
+
result.current.goToPrevious();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(result.current.currentIndex).toBe(0);
|
|
95
|
+
expect(result.current.isFirstSlide).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('should call onComplete when completing', async () => {
|
|
99
|
+
const { result } = renderHook(() =>
|
|
100
|
+
useOnboardingNavigation(1, mockOnComplete, mockOnSkip)
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
await act(async () => {
|
|
104
|
+
await result.current.complete();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
expect(mockOnComplete).toHaveBeenCalled();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should call onSkip when skipping', async () => {
|
|
111
|
+
const { result } = renderHook(() =>
|
|
112
|
+
useOnboardingNavigation(1, mockOnComplete, mockOnSkip)
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
await act(async () => {
|
|
116
|
+
await result.current.skip();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(mockOnSkip).toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { useState, useEffect, useCallback } from "react";
|
|
9
9
|
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
10
|
-
import {
|
|
10
|
+
import { useOnboarding } from "../storage/OnboardingStore";
|
|
11
11
|
|
|
12
12
|
export interface UseOnboardingAnswersReturn {
|
|
13
13
|
currentAnswer: any;
|
|
@@ -24,7 +24,7 @@ export interface UseOnboardingAnswersReturn {
|
|
|
24
24
|
export function useOnboardingAnswers(
|
|
25
25
|
currentSlide: OnboardingSlide | undefined,
|
|
26
26
|
): UseOnboardingAnswersReturn {
|
|
27
|
-
const onboardingStore =
|
|
27
|
+
const onboardingStore = useOnboarding();
|
|
28
28
|
const [currentAnswer, setCurrentAnswer] = useState<any>(undefined);
|
|
29
29
|
|
|
30
30
|
/**
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -6,187 +6,62 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { create } from "zustand";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from "@umituz/react-native-storage";
|
|
14
|
-
import type { OnboardingUserData } from "../../domain/entities/OnboardingUserData";
|
|
9
|
+
import type { OnboardingStoreState } from "./OnboardingStoreState";
|
|
10
|
+
import { initialOnboardingState } from "./OnboardingStoreState";
|
|
11
|
+
import { createOnboardingStoreActions } from "./OnboardingStoreActions";
|
|
12
|
+
import { createOnboardingStoreSelectors } from "./OnboardingStoreSelectors";
|
|
15
13
|
|
|
16
|
-
interface OnboardingStore {
|
|
17
|
-
//
|
|
18
|
-
isOnboardingComplete: boolean;
|
|
19
|
-
currentStep: number;
|
|
20
|
-
loading: boolean;
|
|
21
|
-
error: string | null;
|
|
22
|
-
userData: OnboardingUserData;
|
|
23
|
-
|
|
24
|
-
// Actions
|
|
25
|
-
initialize: (storageKey?: string) => Promise<void>;
|
|
26
|
-
complete: (storageKey?: string) => Promise<void>;
|
|
27
|
-
skip: (storageKey?: string) => Promise<void>;
|
|
14
|
+
interface OnboardingStore extends OnboardingStoreState {
|
|
15
|
+
// Simple actions
|
|
28
16
|
setCurrentStep: (step: number) => void;
|
|
29
|
-
reset: (storageKey?: string) => Promise<void>;
|
|
30
17
|
setLoading: (loading: boolean) => void;
|
|
31
18
|
setError: (error: string | null) => void;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
getUserData: () => OnboardingUserData;
|
|
35
|
-
setUserData: (data: OnboardingUserData) => Promise<void>;
|
|
19
|
+
setState: (state: Partial<OnboardingStoreState>) => void;
|
|
20
|
+
getState: () => OnboardingStoreState;
|
|
36
21
|
}
|
|
37
22
|
|
|
38
|
-
const DEFAULT_STORAGE_KEY = StorageKey.ONBOARDING_COMPLETED;
|
|
39
|
-
const USER_DATA_STORAGE_KEY = "@onboarding_user_data";
|
|
40
|
-
|
|
41
23
|
export const useOnboardingStore = create<OnboardingStore>((set, get) => ({
|
|
42
|
-
|
|
43
|
-
currentStep: 0,
|
|
44
|
-
loading: true,
|
|
45
|
-
error: null,
|
|
46
|
-
userData: { answers: {} },
|
|
47
|
-
|
|
48
|
-
initialize: async (storageKey = DEFAULT_STORAGE_KEY) => {
|
|
49
|
-
set({ loading: true, error: null });
|
|
50
|
-
|
|
51
|
-
// Load completion status
|
|
52
|
-
const completionResult = await storageRepository.getString(storageKey, "false");
|
|
53
|
-
const isComplete = unwrap(completionResult, "false") === "true";
|
|
54
|
-
|
|
55
|
-
// Load user data
|
|
56
|
-
const userDataResult = await storageRepository.getObject<OnboardingUserData>(
|
|
57
|
-
USER_DATA_STORAGE_KEY,
|
|
58
|
-
{ answers: {} }
|
|
59
|
-
);
|
|
60
|
-
const userData = unwrap(userDataResult, { answers: {} });
|
|
61
|
-
|
|
62
|
-
set({
|
|
63
|
-
isOnboardingComplete: isComplete,
|
|
64
|
-
userData,
|
|
65
|
-
loading: false,
|
|
66
|
-
error: completionResult.success ? null : "Failed to load onboarding status",
|
|
67
|
-
});
|
|
68
|
-
},
|
|
69
|
-
|
|
70
|
-
complete: async (storageKey = DEFAULT_STORAGE_KEY) => {
|
|
71
|
-
set({ loading: true, error: null });
|
|
72
|
-
|
|
73
|
-
const result = await storageRepository.setString(storageKey, "true");
|
|
74
|
-
|
|
75
|
-
// Update user data with completion timestamp
|
|
76
|
-
const userData = get().userData;
|
|
77
|
-
userData.completedAt = new Date().toISOString();
|
|
78
|
-
|
|
79
|
-
await storageRepository.setObject(USER_DATA_STORAGE_KEY, userData);
|
|
80
|
-
|
|
81
|
-
set({
|
|
82
|
-
isOnboardingComplete: result.success,
|
|
83
|
-
userData,
|
|
84
|
-
loading: false,
|
|
85
|
-
error: result.success ? null : "Failed to complete onboarding",
|
|
86
|
-
});
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
skip: async (storageKey = DEFAULT_STORAGE_KEY) => {
|
|
90
|
-
set({ loading: true, error: null });
|
|
91
|
-
|
|
92
|
-
const result = await storageRepository.setString(storageKey, "true");
|
|
93
|
-
|
|
94
|
-
// Update user data with skipped flag
|
|
95
|
-
const userData = get().userData;
|
|
96
|
-
userData.skipped = true;
|
|
97
|
-
userData.completedAt = new Date().toISOString();
|
|
98
|
-
await storageRepository.setObject(USER_DATA_STORAGE_KEY, userData);
|
|
99
|
-
|
|
100
|
-
set({
|
|
101
|
-
isOnboardingComplete: result.success,
|
|
102
|
-
userData,
|
|
103
|
-
loading: false,
|
|
104
|
-
error: result.success ? null : "Failed to skip onboarding",
|
|
105
|
-
});
|
|
106
|
-
},
|
|
24
|
+
...initialOnboardingState,
|
|
107
25
|
|
|
108
26
|
setCurrentStep: (step) => set({ currentStep: step }),
|
|
109
|
-
|
|
110
|
-
reset: async (storageKey = DEFAULT_STORAGE_KEY) => {
|
|
111
|
-
set({ loading: true, error: null });
|
|
112
|
-
|
|
113
|
-
const result = await storageRepository.removeItem(storageKey);
|
|
114
|
-
await storageRepository.removeItem(USER_DATA_STORAGE_KEY);
|
|
115
|
-
|
|
116
|
-
set({
|
|
117
|
-
isOnboardingComplete: false,
|
|
118
|
-
currentStep: 0,
|
|
119
|
-
userData: { answers: {} },
|
|
120
|
-
loading: false,
|
|
121
|
-
error: result.success ? null : "Failed to reset onboarding",
|
|
122
|
-
});
|
|
123
|
-
},
|
|
124
|
-
|
|
125
27
|
setLoading: (loading) => set({ loading }),
|
|
126
28
|
setError: (error) => set({ error }),
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const userData = get().userData;
|
|
130
|
-
userData.answers[questionId] = answer;
|
|
131
|
-
|
|
132
|
-
await storageRepository.setObject(USER_DATA_STORAGE_KEY, userData);
|
|
133
|
-
set({ userData: { ...userData } });
|
|
134
|
-
},
|
|
135
|
-
|
|
136
|
-
getAnswer: (questionId: string) => {
|
|
137
|
-
return get().userData.answers[questionId];
|
|
138
|
-
},
|
|
139
|
-
|
|
140
|
-
getUserData: () => {
|
|
141
|
-
return get().userData;
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
setUserData: async (data: OnboardingUserData) => {
|
|
145
|
-
await storageRepository.setObject(USER_DATA_STORAGE_KEY, data);
|
|
146
|
-
set({ userData: data });
|
|
147
|
-
},
|
|
29
|
+
setState: set,
|
|
30
|
+
getState: get,
|
|
148
31
|
}));
|
|
149
32
|
|
|
150
33
|
/**
|
|
151
34
|
* Hook for accessing onboarding state
|
|
152
35
|
*/
|
|
153
36
|
export const useOnboarding = () => {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
userData,
|
|
160
|
-
initialize,
|
|
161
|
-
complete,
|
|
162
|
-
skip,
|
|
163
|
-
setCurrentStep,
|
|
164
|
-
reset,
|
|
165
|
-
setLoading,
|
|
166
|
-
setError,
|
|
167
|
-
saveAnswer,
|
|
168
|
-
getAnswer,
|
|
169
|
-
getUserData,
|
|
170
|
-
setUserData,
|
|
171
|
-
} = useOnboardingStore();
|
|
37
|
+
const store = useOnboardingStore();
|
|
38
|
+
const setState = store.setState as any;
|
|
39
|
+
const getState = () => store;
|
|
40
|
+
const actions = createOnboardingStoreActions(setState, getState);
|
|
41
|
+
const selectors = createOnboardingStoreSelectors(getState);
|
|
172
42
|
|
|
173
43
|
return {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
44
|
+
// State
|
|
45
|
+
isOnboardingComplete: store.isOnboardingComplete,
|
|
46
|
+
currentStep: store.currentStep,
|
|
47
|
+
loading: store.loading,
|
|
48
|
+
error: store.error,
|
|
49
|
+
userData: store.userData,
|
|
50
|
+
|
|
51
|
+
// Actions
|
|
52
|
+
initialize: actions.initialize,
|
|
53
|
+
complete: actions.complete,
|
|
54
|
+
skip: actions.skip,
|
|
55
|
+
setCurrentStep: store.setCurrentStep,
|
|
56
|
+
reset: actions.reset,
|
|
57
|
+
setLoading: store.setLoading,
|
|
58
|
+
setError: store.setError,
|
|
59
|
+
saveAnswer: actions.saveAnswer,
|
|
60
|
+
setUserData: actions.setUserData,
|
|
61
|
+
|
|
62
|
+
// Selectors
|
|
63
|
+
getAnswer: selectors.getAnswer,
|
|
64
|
+
getUserData: selectors.getUserData,
|
|
190
65
|
};
|
|
191
66
|
};
|
|
192
67
|
|