@umituz/react-native-onboarding 3.6.20 → 3.6.21
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/package.json +9 -9
- package/src/infrastructure/storage/OnboardingStoreActions.ts +24 -190
- package/src/infrastructure/storage/actions/answerActions.ts +47 -0
- package/src/infrastructure/storage/actions/completeAction.ts +45 -0
- package/src/infrastructure/storage/actions/index.ts +22 -0
- package/src/infrastructure/storage/actions/initializeAction.ts +40 -0
- package/src/infrastructure/storage/actions/resetAction.ts +37 -0
- package/src/infrastructure/storage/actions/skipAction.ts +46 -0
- package/src/infrastructure/storage/actions/storageHelpers.ts +60 -0
- package/src/presentation/hooks/useOnboardingScreenHandlers.ts +114 -0
- package/src/presentation/hooks/useOnboardingScreenState.ts +23 -71
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-onboarding",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.21",
|
|
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",
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"url": "https://github.com/umituz/react-native-onboarding"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@expo/vector-icons": ">=
|
|
32
|
-
"@umituz/react-native-design-system": "
|
|
33
|
-
"@umituz/react-native-localization": "
|
|
34
|
-
"@umituz/react-native-storage": "
|
|
31
|
+
"@expo/vector-icons": ">=15.0.0",
|
|
32
|
+
"@umituz/react-native-design-system": ">=2.0.0",
|
|
33
|
+
"@umituz/react-native-localization": ">=1.0.0",
|
|
34
|
+
"@umituz/react-native-storage": ">=2.6.0",
|
|
35
35
|
"expo-image": ">=2.0.0",
|
|
36
36
|
"expo-linear-gradient": ">=13.0.0",
|
|
37
37
|
"expo-video": ">=1.0.0",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"zustand": ">=4.5.2"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@expo/vector-icons": "^
|
|
44
|
+
"@expo/vector-icons": "^15.0.0",
|
|
45
45
|
"@react-native-async-storage/async-storage": "^2.1.2",
|
|
46
46
|
"@react-native-community/datetimepicker": "^8.2.0",
|
|
47
47
|
"@react-native/eslint-config": "^0.83.1",
|
|
@@ -49,9 +49,9 @@
|
|
|
49
49
|
"@types/react-native": "^0.72.8",
|
|
50
50
|
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
|
51
51
|
"@typescript-eslint/parser": "^8.50.1",
|
|
52
|
-
"@umituz/react-native-design-system": "
|
|
53
|
-
"@umituz/react-native-localization": "
|
|
54
|
-
"@umituz/react-native-storage": "
|
|
52
|
+
"@umituz/react-native-design-system": "^2.1.0",
|
|
53
|
+
"@umituz/react-native-localization": "^1.9.0",
|
|
54
|
+
"@umituz/react-native-storage": "^2.6.21",
|
|
55
55
|
"eslint": "^9.39.2",
|
|
56
56
|
"eslint-plugin-react": "^7.37.5",
|
|
57
57
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Onboarding Store Actions
|
|
3
|
-
* Single Responsibility: Async store actions
|
|
3
|
+
* Single Responsibility: Async store actions interface and factory
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { storageRepository, unwrap } from "@umituz/react-native-storage";
|
|
7
6
|
import type { OnboardingUserData } from "../../domain/entities/OnboardingUserData";
|
|
8
7
|
import type { OnboardingStoreState } from "./OnboardingStoreState";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
import {
|
|
9
|
+
initializeAction,
|
|
10
|
+
completeAction,
|
|
11
|
+
skipAction,
|
|
12
|
+
resetAction,
|
|
13
|
+
saveAnswerAction,
|
|
14
|
+
setUserDataAction,
|
|
15
|
+
DEFAULT_STORAGE_KEY,
|
|
16
|
+
} from "./actions";
|
|
12
17
|
|
|
13
18
|
export interface OnboardingStoreActions {
|
|
14
19
|
initialize: (storageKey?: string) => Promise<void>;
|
|
15
20
|
complete: (storageKey?: string) => Promise<void>;
|
|
16
21
|
skip: (storageKey?: string) => Promise<void>;
|
|
17
22
|
reset: (storageKey?: string) => Promise<void>;
|
|
18
|
-
saveAnswer: (questionId: string, answer:
|
|
23
|
+
saveAnswer: (questionId: string, answer: unknown) => Promise<void>;
|
|
19
24
|
setUserData: (data: OnboardingUserData) => Promise<void>;
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -24,193 +29,22 @@ export function createOnboardingStoreActions(
|
|
|
24
29
|
get: () => OnboardingStoreState
|
|
25
30
|
): OnboardingStoreActions {
|
|
26
31
|
return {
|
|
27
|
-
initialize:
|
|
28
|
-
|
|
29
|
-
set({ loading: true, error: null });
|
|
30
|
-
|
|
31
|
-
const completionResult = await storageRepository.getString(storageKey, "false");
|
|
32
|
-
const isComplete = unwrap(completionResult, "false") === "true";
|
|
33
|
-
|
|
34
|
-
const userDataResult = await storageRepository.getItem<OnboardingUserData>(
|
|
35
|
-
USER_DATA_STORAGE_KEY,
|
|
36
|
-
{ answers: {} }
|
|
37
|
-
);
|
|
38
|
-
const userData = unwrap(userDataResult, { answers: {} });
|
|
39
|
-
|
|
40
|
-
set({
|
|
41
|
-
isOnboardingComplete: isComplete,
|
|
42
|
-
userData,
|
|
43
|
-
loading: false,
|
|
44
|
-
error: completionResult.success ? null : "Failed to load onboarding status",
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
if (__DEV__) {
|
|
48
|
-
console.log('[OnboardingStore] Initialized with completion status:', isComplete);
|
|
49
|
-
}
|
|
50
|
-
} catch (error) {
|
|
51
|
-
set({
|
|
52
|
-
loading: false,
|
|
53
|
-
error: error instanceof Error ? error.message : "Failed to initialize onboarding",
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
if (__DEV__) {
|
|
57
|
-
console.error('[OnboardingStore] Initialization error:', error);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
complete: async (storageKey = DEFAULT_STORAGE_KEY) => {
|
|
63
|
-
try {
|
|
64
|
-
set({ loading: true, error: null });
|
|
65
|
-
|
|
66
|
-
// Ensure storage write completes before proceeding
|
|
67
|
-
const result = await storageRepository.setString(storageKey, "true");
|
|
68
|
-
|
|
69
|
-
if (!result.success) {
|
|
70
|
-
throw new Error("Failed to save completion status to storage");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const userData = { ...get().userData };
|
|
74
|
-
userData.completedAt = new Date().toISOString();
|
|
75
|
-
|
|
76
|
-
const userDataResult = await storageRepository.setItem(USER_DATA_STORAGE_KEY, userData);
|
|
77
|
-
|
|
78
|
-
if (!userDataResult.success) {
|
|
79
|
-
throw new Error("Failed to save user data to storage");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Only update state after storage write is confirmed
|
|
83
|
-
set({
|
|
84
|
-
isOnboardingComplete: true,
|
|
85
|
-
userData,
|
|
86
|
-
loading: false,
|
|
87
|
-
error: null,
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
if (__DEV__) {
|
|
91
|
-
console.log('[OnboardingStore] Onboarding completed and persisted successfully');
|
|
92
|
-
}
|
|
93
|
-
} catch (error) {
|
|
94
|
-
set({
|
|
95
|
-
loading: false,
|
|
96
|
-
error: error instanceof Error ? error.message : "Failed to complete onboarding",
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
if (__DEV__) {
|
|
100
|
-
console.error('[OnboardingStore] Completion error:', error);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
throw error;
|
|
104
|
-
}
|
|
105
|
-
},
|
|
32
|
+
initialize: (storageKey = DEFAULT_STORAGE_KEY) =>
|
|
33
|
+
initializeAction(set, storageKey),
|
|
106
34
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
set({ loading: true, error: null });
|
|
35
|
+
complete: (storageKey = DEFAULT_STORAGE_KEY) =>
|
|
36
|
+
completeAction(set, get, storageKey),
|
|
110
37
|
|
|
111
|
-
|
|
112
|
-
|
|
38
|
+
skip: (storageKey = DEFAULT_STORAGE_KEY) =>
|
|
39
|
+
skipAction(set, get, storageKey),
|
|
113
40
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
41
|
+
reset: (storageKey = DEFAULT_STORAGE_KEY) =>
|
|
42
|
+
resetAction(set, storageKey),
|
|
117
43
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
userData.completedAt = new Date().toISOString();
|
|
44
|
+
saveAnswer: (questionId: string, answer: unknown) =>
|
|
45
|
+
saveAnswerAction(set, get, questionId, answer),
|
|
121
46
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (!userDataResult.success) {
|
|
125
|
-
throw new Error("Failed to save user data to storage");
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Only update state after storage write is confirmed
|
|
129
|
-
set({
|
|
130
|
-
isOnboardingComplete: true,
|
|
131
|
-
userData,
|
|
132
|
-
loading: false,
|
|
133
|
-
error: null,
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
if (__DEV__) {
|
|
137
|
-
console.log('[OnboardingStore] Onboarding skipped and persisted successfully');
|
|
138
|
-
}
|
|
139
|
-
} catch (error) {
|
|
140
|
-
set({
|
|
141
|
-
loading: false,
|
|
142
|
-
error: error instanceof Error ? error.message : "Failed to skip onboarding",
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
if (__DEV__) {
|
|
146
|
-
console.error('[OnboardingStore] Skip error:', error);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
throw error;
|
|
150
|
-
}
|
|
151
|
-
},
|
|
152
|
-
|
|
153
|
-
reset: async (storageKey = DEFAULT_STORAGE_KEY) => {
|
|
154
|
-
try {
|
|
155
|
-
set({ loading: true, error: null });
|
|
156
|
-
|
|
157
|
-
const result = await storageRepository.removeItem(storageKey);
|
|
158
|
-
await storageRepository.removeItem(USER_DATA_STORAGE_KEY);
|
|
159
|
-
|
|
160
|
-
set({
|
|
161
|
-
isOnboardingComplete: false,
|
|
162
|
-
currentStep: 0,
|
|
163
|
-
userData: { answers: {} },
|
|
164
|
-
loading: false,
|
|
165
|
-
error: result.success ? null : "Failed to reset onboarding",
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
if (__DEV__) {
|
|
169
|
-
console.log('[OnboardingStore] Onboarding reset successfully');
|
|
170
|
-
}
|
|
171
|
-
} catch (error) {
|
|
172
|
-
set({
|
|
173
|
-
loading: false,
|
|
174
|
-
error: error instanceof Error ? error.message : "Failed to reset onboarding",
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
if (__DEV__) {
|
|
178
|
-
console.error('[OnboardingStore] Reset error:', error);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
},
|
|
182
|
-
|
|
183
|
-
saveAnswer: async (questionId: string, answer: any) => {
|
|
184
|
-
try {
|
|
185
|
-
const userData = { ...get().userData };
|
|
186
|
-
userData.answers[questionId] = answer;
|
|
187
|
-
|
|
188
|
-
await storageRepository.setItem(USER_DATA_STORAGE_KEY, userData);
|
|
189
|
-
set({ userData });
|
|
190
|
-
|
|
191
|
-
if (__DEV__) {
|
|
192
|
-
console.log('[OnboardingStore] Answer saved for question:', questionId);
|
|
193
|
-
}
|
|
194
|
-
} catch (error) {
|
|
195
|
-
if (__DEV__) {
|
|
196
|
-
console.error('[OnboardingStore] Failed to save answer:', error);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
},
|
|
200
|
-
|
|
201
|
-
setUserData: async (data: OnboardingUserData) => {
|
|
202
|
-
try {
|
|
203
|
-
await storageRepository.setItem(USER_DATA_STORAGE_KEY, data);
|
|
204
|
-
set({ userData: data });
|
|
205
|
-
|
|
206
|
-
if (__DEV__) {
|
|
207
|
-
console.log('[OnboardingStore] User data updated successfully');
|
|
208
|
-
}
|
|
209
|
-
} catch (error) {
|
|
210
|
-
if (__DEV__) {
|
|
211
|
-
console.error('[OnboardingStore] Failed to set user data:', error);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
},
|
|
47
|
+
setUserData: (data: OnboardingUserData) =>
|
|
48
|
+
setUserDataAction(set, data),
|
|
215
49
|
};
|
|
216
|
-
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Answer Actions
|
|
3
|
+
* Single Responsibility: Save and update user answers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { storageRepository } from "@umituz/react-native-storage";
|
|
7
|
+
import type { OnboardingUserData } from "../../../domain/entities/OnboardingUserData";
|
|
8
|
+
import type { OnboardingStoreState } from "../OnboardingStoreState";
|
|
9
|
+
import { USER_DATA_STORAGE_KEY, handleError, logSuccess } from "./storageHelpers";
|
|
10
|
+
|
|
11
|
+
export async function saveAnswerAction(
|
|
12
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
13
|
+
get: () => OnboardingStoreState,
|
|
14
|
+
questionId: string,
|
|
15
|
+
answer: unknown
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
const userData: OnboardingUserData = {
|
|
19
|
+
...get().userData,
|
|
20
|
+
answers: {
|
|
21
|
+
...get().userData.answers,
|
|
22
|
+
[questionId]: answer,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
await storageRepository.setItem(USER_DATA_STORAGE_KEY, userData);
|
|
27
|
+
set({ userData });
|
|
28
|
+
|
|
29
|
+
logSuccess(`Answer saved for question: ${questionId}`);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
handleError(error, "save answer");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function setUserDataAction(
|
|
36
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
37
|
+
data: OnboardingUserData
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
await storageRepository.setItem(USER_DATA_STORAGE_KEY, data);
|
|
41
|
+
set({ userData: data });
|
|
42
|
+
|
|
43
|
+
logSuccess("User data updated successfully");
|
|
44
|
+
} catch (error) {
|
|
45
|
+
handleError(error, "set user data");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complete Action
|
|
3
|
+
* Single Responsibility: Mark onboarding as completed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingUserData } from "../../../domain/entities/OnboardingUserData";
|
|
7
|
+
import type { OnboardingStoreState } from "../OnboardingStoreState";
|
|
8
|
+
import {
|
|
9
|
+
saveCompletionStatus,
|
|
10
|
+
saveUserData,
|
|
11
|
+
handleError,
|
|
12
|
+
logSuccess,
|
|
13
|
+
} from "./storageHelpers";
|
|
14
|
+
|
|
15
|
+
export async function completeAction(
|
|
16
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
17
|
+
get: () => OnboardingStoreState,
|
|
18
|
+
storageKey: string
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
try {
|
|
21
|
+
set({ loading: true, error: null });
|
|
22
|
+
|
|
23
|
+
await saveCompletionStatus(storageKey);
|
|
24
|
+
|
|
25
|
+
const userData: OnboardingUserData = {
|
|
26
|
+
...get().userData,
|
|
27
|
+
completedAt: new Date().toISOString(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
await saveUserData(userData);
|
|
31
|
+
|
|
32
|
+
set({
|
|
33
|
+
isOnboardingComplete: true,
|
|
34
|
+
userData,
|
|
35
|
+
loading: false,
|
|
36
|
+
error: null,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
logSuccess("Onboarding completed and persisted successfully");
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const errorMessage = handleError(error, "complete onboarding");
|
|
42
|
+
set({ loading: false, error: errorMessage });
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actions Index
|
|
3
|
+
* Single Responsibility: Export all action functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
loadCompletionStatus,
|
|
8
|
+
loadUserData,
|
|
9
|
+
saveCompletionStatus,
|
|
10
|
+
saveUserData,
|
|
11
|
+
removeStorageKeys,
|
|
12
|
+
handleError,
|
|
13
|
+
logSuccess,
|
|
14
|
+
DEFAULT_STORAGE_KEY,
|
|
15
|
+
USER_DATA_STORAGE_KEY,
|
|
16
|
+
} from "./storageHelpers";
|
|
17
|
+
|
|
18
|
+
export { initializeAction } from "./initializeAction";
|
|
19
|
+
export { completeAction } from "./completeAction";
|
|
20
|
+
export { skipAction } from "./skipAction";
|
|
21
|
+
export { resetAction } from "./resetAction";
|
|
22
|
+
export { saveAnswerAction, setUserDataAction } from "./answerActions";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialize Action
|
|
3
|
+
* Single Responsibility: Load initial onboarding state from storage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingUserData } from "../../../domain/entities/OnboardingUserData";
|
|
7
|
+
import type { OnboardingStoreState } from "../OnboardingStoreState";
|
|
8
|
+
import {
|
|
9
|
+
loadCompletionStatus,
|
|
10
|
+
loadUserData,
|
|
11
|
+
handleError,
|
|
12
|
+
logSuccess,
|
|
13
|
+
} from "./storageHelpers";
|
|
14
|
+
|
|
15
|
+
export async function initializeAction(
|
|
16
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
17
|
+
storageKey: string
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
set({ loading: true, error: null });
|
|
21
|
+
|
|
22
|
+
const isComplete = await loadCompletionStatus(storageKey);
|
|
23
|
+
const defaultData: OnboardingUserData = { answers: {} };
|
|
24
|
+
const userData = await loadUserData(defaultData);
|
|
25
|
+
|
|
26
|
+
set({
|
|
27
|
+
isOnboardingComplete: isComplete,
|
|
28
|
+
userData,
|
|
29
|
+
loading: false,
|
|
30
|
+
error: null,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
logSuccess(`Initialized with completion status: ${isComplete}`);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
set({
|
|
36
|
+
loading: false,
|
|
37
|
+
error: handleError(error, "initialize onboarding"),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reset Action
|
|
3
|
+
* Single Responsibility: Reset onboarding state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingStoreState } from "../OnboardingStoreState";
|
|
7
|
+
import {
|
|
8
|
+
removeStorageKeys,
|
|
9
|
+
handleError,
|
|
10
|
+
logSuccess,
|
|
11
|
+
} from "./storageHelpers";
|
|
12
|
+
|
|
13
|
+
export async function resetAction(
|
|
14
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
15
|
+
storageKey: string
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
set({ loading: true, error: null });
|
|
19
|
+
|
|
20
|
+
await removeStorageKeys(storageKey);
|
|
21
|
+
|
|
22
|
+
set({
|
|
23
|
+
isOnboardingComplete: false,
|
|
24
|
+
currentStep: 0,
|
|
25
|
+
userData: { answers: {} },
|
|
26
|
+
loading: false,
|
|
27
|
+
error: null,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
logSuccess("Onboarding reset successfully");
|
|
31
|
+
} catch (error) {
|
|
32
|
+
set({
|
|
33
|
+
loading: false,
|
|
34
|
+
error: handleError(error, "reset onboarding"),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skip Action
|
|
3
|
+
* Single Responsibility: Mark onboarding as skipped
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingUserData } from "../../../domain/entities/OnboardingUserData";
|
|
7
|
+
import type { OnboardingStoreState } from "../OnboardingStoreState";
|
|
8
|
+
import {
|
|
9
|
+
saveCompletionStatus,
|
|
10
|
+
saveUserData,
|
|
11
|
+
handleError,
|
|
12
|
+
logSuccess,
|
|
13
|
+
} from "./storageHelpers";
|
|
14
|
+
|
|
15
|
+
export async function skipAction(
|
|
16
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
17
|
+
get: () => OnboardingStoreState,
|
|
18
|
+
storageKey: string
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
try {
|
|
21
|
+
set({ loading: true, error: null });
|
|
22
|
+
|
|
23
|
+
await saveCompletionStatus(storageKey);
|
|
24
|
+
|
|
25
|
+
const userData: OnboardingUserData = {
|
|
26
|
+
...get().userData,
|
|
27
|
+
skipped: true,
|
|
28
|
+
completedAt: new Date().toISOString(),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await saveUserData(userData);
|
|
32
|
+
|
|
33
|
+
set({
|
|
34
|
+
isOnboardingComplete: true,
|
|
35
|
+
userData,
|
|
36
|
+
loading: false,
|
|
37
|
+
error: null,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
logSuccess("Onboarding skipped and persisted successfully");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const errorMessage = handleError(error, "skip onboarding");
|
|
43
|
+
set({ loading: false, error: errorMessage });
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Helpers
|
|
3
|
+
* Single Responsibility: Common storage operations and error handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { storageRepository, unwrap } from "@umituz/react-native-storage";
|
|
7
|
+
import type { OnboardingUserData } from "../../../domain/entities/OnboardingUserData";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_STORAGE_KEY = "@onboarding:completed";
|
|
10
|
+
export const USER_DATA_STORAGE_KEY = "@onboarding:user_data";
|
|
11
|
+
|
|
12
|
+
export async function loadCompletionStatus(storageKey: string): Promise<boolean> {
|
|
13
|
+
const result = await storageRepository.getString(storageKey, "false");
|
|
14
|
+
return unwrap(result, "false") === "true";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadUserData(
|
|
18
|
+
defaultData: OnboardingUserData
|
|
19
|
+
): Promise<OnboardingUserData> {
|
|
20
|
+
const result = await storageRepository.getItem<OnboardingUserData>(
|
|
21
|
+
USER_DATA_STORAGE_KEY,
|
|
22
|
+
defaultData
|
|
23
|
+
);
|
|
24
|
+
return unwrap(result, defaultData);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function saveCompletionStatus(storageKey: string): Promise<void> {
|
|
28
|
+
const result = await storageRepository.setString(storageKey, "true");
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
throw new Error("Failed to save completion status to storage");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function saveUserData(data: OnboardingUserData): Promise<void> {
|
|
35
|
+
const result = await storageRepository.setItem(USER_DATA_STORAGE_KEY, data);
|
|
36
|
+
if (!result.success) {
|
|
37
|
+
throw new Error("Failed to save user data to storage");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function removeStorageKeys(storageKey: string): Promise<void> {
|
|
42
|
+
await storageRepository.removeItem(storageKey);
|
|
43
|
+
await storageRepository.removeItem(USER_DATA_STORAGE_KEY);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function handleError(error: unknown, context: string): string {
|
|
47
|
+
const message = error instanceof Error ? error.message : `Failed to ${context}`;
|
|
48
|
+
|
|
49
|
+
if (__DEV__) {
|
|
50
|
+
console.error(`[OnboardingStore] ${context} error:`, error);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return message;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function logSuccess(message: string): void {
|
|
57
|
+
if (__DEV__) {
|
|
58
|
+
console.log(`[OnboardingStore] ${message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOnboardingScreenHandlers Hook
|
|
3
|
+
* Single Responsibility: Handle onboarding screen user interactions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
8
|
+
import { SlideManager } from "../../infrastructure/services/SlideManager";
|
|
9
|
+
|
|
10
|
+
export interface UseOnboardingScreenHandlersProps {
|
|
11
|
+
filteredSlides: OnboardingSlide[];
|
|
12
|
+
currentSlide: OnboardingSlide | undefined;
|
|
13
|
+
currentIndex: number;
|
|
14
|
+
isLastSlide: boolean;
|
|
15
|
+
saveCurrentAnswer: (slide: OnboardingSlide) => Promise<void>;
|
|
16
|
+
completeOnboarding: () => Promise<void>;
|
|
17
|
+
goToNext: () => void;
|
|
18
|
+
goToPrevious: () => void;
|
|
19
|
+
skipOnboarding: () => Promise<void>;
|
|
20
|
+
loadAnswerForSlide: (slide: OnboardingSlide) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseOnboardingScreenHandlersReturn {
|
|
24
|
+
handleNext: () => Promise<void>;
|
|
25
|
+
handlePrevious: () => void;
|
|
26
|
+
handleSkip: () => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useOnboardingScreenHandlers({
|
|
30
|
+
filteredSlides,
|
|
31
|
+
currentSlide,
|
|
32
|
+
currentIndex,
|
|
33
|
+
isLastSlide,
|
|
34
|
+
saveCurrentAnswer,
|
|
35
|
+
completeOnboarding,
|
|
36
|
+
goToNext,
|
|
37
|
+
goToPrevious,
|
|
38
|
+
skipOnboarding,
|
|
39
|
+
loadAnswerForSlide,
|
|
40
|
+
}: UseOnboardingScreenHandlersProps): UseOnboardingScreenHandlersReturn {
|
|
41
|
+
const handleNext = useCallback(async () => {
|
|
42
|
+
if (!currentSlide) return;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await saveCurrentAnswer(currentSlide);
|
|
46
|
+
|
|
47
|
+
if (isLastSlide) {
|
|
48
|
+
await completeOnboarding();
|
|
49
|
+
} else {
|
|
50
|
+
goToNext();
|
|
51
|
+
|
|
52
|
+
const nextSlide = SlideManager.getSlideAtIndex(
|
|
53
|
+
filteredSlides,
|
|
54
|
+
currentIndex + 1
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (nextSlide) {
|
|
58
|
+
loadAnswerForSlide(nextSlide);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (__DEV__) {
|
|
63
|
+
console.error("[useOnboardingScreenHandlers] Error in handleNext:", error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, [
|
|
67
|
+
currentSlide,
|
|
68
|
+
isLastSlide,
|
|
69
|
+
saveCurrentAnswer,
|
|
70
|
+
completeOnboarding,
|
|
71
|
+
goToNext,
|
|
72
|
+
filteredSlides,
|
|
73
|
+
currentIndex,
|
|
74
|
+
loadAnswerForSlide,
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const handlePrevious = useCallback(() => {
|
|
78
|
+
try {
|
|
79
|
+
goToPrevious();
|
|
80
|
+
|
|
81
|
+
const prevSlide = SlideManager.getSlideAtIndex(
|
|
82
|
+
filteredSlides,
|
|
83
|
+
currentIndex - 1
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (prevSlide) {
|
|
87
|
+
loadAnswerForSlide(prevSlide);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (__DEV__) {
|
|
91
|
+
console.error(
|
|
92
|
+
"[useOnboardingScreenHandlers] Error in handlePrevious:",
|
|
93
|
+
error
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}, [goToPrevious, filteredSlides, currentIndex, loadAnswerForSlide]);
|
|
98
|
+
|
|
99
|
+
const handleSkip = useCallback(async () => {
|
|
100
|
+
try {
|
|
101
|
+
await skipOnboarding();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (__DEV__) {
|
|
104
|
+
console.error("[useOnboardingScreenHandlers] Error in handleSkip:", error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}, [skipOnboarding]);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
handleNext,
|
|
111
|
+
handlePrevious,
|
|
112
|
+
handleSkip,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useOnboardingScreenState Hook
|
|
3
|
-
* Single Responsibility: Coordinate onboarding screen state
|
|
3
|
+
* Single Responsibility: Coordinate onboarding screen state
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useMemo,
|
|
6
|
+
import { useMemo, useEffect } from "react";
|
|
7
7
|
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
8
8
|
import { useOnboarding } from "../../infrastructure/storage/OnboardingStore";
|
|
9
9
|
import { useOnboardingNavigation } from "../../infrastructure/hooks/useOnboardingNavigation";
|
|
10
10
|
import { useOnboardingAnswers } from "../../infrastructure/hooks/useOnboardingAnswers";
|
|
11
11
|
import { useOnboardingContainerStyle } from "./useOnboardingContainerStyle";
|
|
12
|
+
import { useOnboardingScreenHandlers } from "./useOnboardingScreenHandlers";
|
|
12
13
|
import { SlideManager } from "../../infrastructure/services/SlideManager";
|
|
13
14
|
import { ValidationManager } from "../../infrastructure/services/ValidationManager";
|
|
14
15
|
import { shouldUseGradient } from "../../infrastructure/utils/gradientUtils";
|
|
@@ -27,14 +28,14 @@ export interface UseOnboardingScreenStateReturn {
|
|
|
27
28
|
currentIndex: number;
|
|
28
29
|
isFirstSlide: boolean;
|
|
29
30
|
isLastSlide: boolean;
|
|
30
|
-
currentAnswer:
|
|
31
|
+
currentAnswer: unknown;
|
|
31
32
|
isAnswerValid: boolean;
|
|
32
33
|
useGradient: boolean;
|
|
33
|
-
containerStyle:
|
|
34
|
+
containerStyle: unknown;
|
|
34
35
|
handleNext: () => Promise<void>;
|
|
35
36
|
handlePrevious: () => void;
|
|
36
37
|
handleSkip: () => Promise<void>;
|
|
37
|
-
setCurrentAnswer: (value:
|
|
38
|
+
setCurrentAnswer: (value: unknown) => void;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export function useOnboardingScreenState({
|
|
@@ -46,7 +47,6 @@ export function useOnboardingScreenState({
|
|
|
46
47
|
}: UseOnboardingScreenStateProps): UseOnboardingScreenStateReturn {
|
|
47
48
|
const onboardingStore = useOnboarding();
|
|
48
49
|
|
|
49
|
-
// Filter slides using service
|
|
50
50
|
const filteredSlides = useMemo(() => {
|
|
51
51
|
if (!slides || !Array.isArray(slides) || slides.length === 0) {
|
|
52
52
|
return [];
|
|
@@ -55,7 +55,6 @@ export function useOnboardingScreenState({
|
|
|
55
55
|
return SlideManager.filterSlides(slides, userData);
|
|
56
56
|
}, [slides, onboardingStore.userData]);
|
|
57
57
|
|
|
58
|
-
// Navigation hook
|
|
59
58
|
const {
|
|
60
59
|
currentIndex,
|
|
61
60
|
goToNext,
|
|
@@ -77,16 +76,14 @@ export function useOnboardingScreenState({
|
|
|
77
76
|
if (onSkip) {
|
|
78
77
|
await onSkip();
|
|
79
78
|
}
|
|
80
|
-
}
|
|
79
|
+
}
|
|
81
80
|
);
|
|
82
81
|
|
|
83
|
-
// Get current slide
|
|
84
82
|
const currentSlide = useMemo(
|
|
85
83
|
() => SlideManager.getSlideAtIndex(filteredSlides, currentIndex),
|
|
86
|
-
[filteredSlides, currentIndex]
|
|
84
|
+
[filteredSlides, currentIndex]
|
|
87
85
|
);
|
|
88
86
|
|
|
89
|
-
// Answer management hook
|
|
90
87
|
const {
|
|
91
88
|
currentAnswer,
|
|
92
89
|
setCurrentAnswer,
|
|
@@ -94,83 +91,39 @@ export function useOnboardingScreenState({
|
|
|
94
91
|
saveCurrentAnswer,
|
|
95
92
|
} = useOnboardingAnswers(currentSlide);
|
|
96
93
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
currentIndex + 1,
|
|
110
|
-
);
|
|
111
|
-
if (nextSlide) {
|
|
112
|
-
loadAnswerForSlide(nextSlide);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
} catch (error) {
|
|
116
|
-
if (__DEV__) {
|
|
117
|
-
console.error('[useOnboardingScreenState] Error in handleNext:', error);
|
|
118
|
-
}
|
|
94
|
+
const { handleNext, handlePrevious, handleSkip } = useOnboardingScreenHandlers(
|
|
95
|
+
{
|
|
96
|
+
filteredSlides,
|
|
97
|
+
currentSlide,
|
|
98
|
+
currentIndex,
|
|
99
|
+
isLastSlide,
|
|
100
|
+
saveCurrentAnswer,
|
|
101
|
+
completeOnboarding,
|
|
102
|
+
goToNext,
|
|
103
|
+
goToPrevious,
|
|
104
|
+
skipOnboarding,
|
|
105
|
+
loadAnswerForSlide,
|
|
119
106
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
// Handle previous slide with useCallback for performance
|
|
123
|
-
const handlePrevious = useCallback(() => {
|
|
124
|
-
try {
|
|
125
|
-
goToPrevious();
|
|
126
|
-
const prevSlide = SlideManager.getSlideAtIndex(
|
|
127
|
-
filteredSlides,
|
|
128
|
-
currentIndex - 1,
|
|
129
|
-
);
|
|
130
|
-
if (prevSlide) {
|
|
131
|
-
loadAnswerForSlide(prevSlide);
|
|
132
|
-
}
|
|
133
|
-
} catch (error) {
|
|
134
|
-
if (__DEV__) {
|
|
135
|
-
console.error('[useOnboardingScreenState] Error in handlePrevious:', error);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}, [goToPrevious, filteredSlides, currentIndex, loadAnswerForSlide]);
|
|
139
|
-
|
|
140
|
-
// Handle skip with useCallback for performance
|
|
141
|
-
const handleSkip = useCallback(async () => {
|
|
142
|
-
try {
|
|
143
|
-
await skipOnboarding();
|
|
144
|
-
} catch (error) {
|
|
145
|
-
if (__DEV__) {
|
|
146
|
-
console.error('[useOnboardingScreenState] Error in handleSkip:', error);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}, [skipOnboarding]);
|
|
107
|
+
);
|
|
150
108
|
|
|
151
|
-
// Check if gradient should be used
|
|
152
109
|
const useGradient = shouldUseGradient(currentSlide, globalUseGradient);
|
|
153
110
|
|
|
154
|
-
// Validate answer using service
|
|
155
111
|
const isAnswerValid = useMemo(() => {
|
|
156
112
|
if (!currentSlide?.question) {
|
|
157
113
|
return true;
|
|
158
114
|
}
|
|
159
115
|
return ValidationManager.validateAnswer(
|
|
160
116
|
currentSlide.question,
|
|
161
|
-
currentAnswer
|
|
117
|
+
currentAnswer
|
|
162
118
|
);
|
|
163
119
|
}, [currentSlide, currentAnswer]);
|
|
164
120
|
|
|
165
|
-
// Container style using dedicated hook
|
|
166
121
|
const { containerStyle } = useOnboardingContainerStyle({ useGradient });
|
|
167
122
|
|
|
168
|
-
// Cleanup effect to prevent memory leaks
|
|
169
123
|
useEffect(() => {
|
|
170
124
|
return () => {
|
|
171
|
-
// Cleanup any pending operations or subscriptions
|
|
172
125
|
if (__DEV__) {
|
|
173
|
-
console.log(
|
|
126
|
+
console.log("[useOnboardingScreenState] Cleanup completed");
|
|
174
127
|
}
|
|
175
128
|
};
|
|
176
129
|
}, []);
|
|
@@ -191,4 +144,3 @@ export function useOnboardingScreenState({
|
|
|
191
144
|
setCurrentAnswer,
|
|
192
145
|
};
|
|
193
146
|
}
|
|
194
|
-
|