@umituz/react-native-onboarding 3.6.20 → 3.6.22

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-onboarding",
3
- "version": "3.6.20",
3
+ "version": "3.6.22",
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": ">=14.0.0",
32
- "@umituz/react-native-design-system": "latest",
33
- "@umituz/react-native-localization": "latest",
34
- "@umituz/react-native-storage": "latest",
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": "^14.0.0",
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": "latest",
53
- "@umituz/react-native-localization": "latest",
54
- "@umituz/react-native-storage": "latest",
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
- const DEFAULT_STORAGE_KEY = "@onboarding:completed";
11
- const USER_DATA_STORAGE_KEY = "@onboarding:user_data";
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: any) => Promise<void>;
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: async (storageKey = DEFAULT_STORAGE_KEY) => {
28
- try {
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
- skip: async (storageKey = DEFAULT_STORAGE_KEY) => {
108
- try {
109
- set({ loading: true, error: null });
35
+ complete: (storageKey = DEFAULT_STORAGE_KEY) =>
36
+ completeAction(set, get, storageKey),
110
37
 
111
- // Ensure storage write completes before proceeding
112
- const result = await storageRepository.setString(storageKey, "true");
38
+ skip: (storageKey = DEFAULT_STORAGE_KEY) =>
39
+ skipAction(set, get, storageKey),
113
40
 
114
- if (!result.success) {
115
- throw new Error("Failed to save skip status to storage");
116
- }
41
+ reset: (storageKey = DEFAULT_STORAGE_KEY) =>
42
+ resetAction(set, storageKey),
117
43
 
118
- const userData = { ...get().userData };
119
- userData.skipped = true;
120
- userData.completedAt = new Date().toISOString();
44
+ saveAnswer: (questionId: string, answer: unknown) =>
45
+ saveAnswerAction(set, get, questionId, answer),
121
46
 
122
- const userDataResult = await storageRepository.setItem(USER_DATA_STORAGE_KEY, userData);
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
+ }
@@ -1,9 +1,15 @@
1
+ /**
2
+ * Multiple Choice Question Component
3
+ * Single Responsibility: Render multiple choice question with options
4
+ */
5
+
1
6
  import React from "react";
2
- import { View, TouchableOpacity, StyleSheet } from "react-native";
3
- import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { AtomicText } from "@umituz/react-native-design-system";
4
9
  import { useOnboardingProvider } from "../../providers/OnboardingProvider";
5
10
  import { ensureArray } from "../../../infrastructure/utils/arrayUtils";
6
- import type { OnboardingQuestion, QuestionOption } from "../../../domain/entities/OnboardingQuestion";
11
+ import type { OnboardingQuestion } from "../../../domain/entities/OnboardingQuestion";
12
+ import { QuestionOptionItem } from "./QuestionOptionItem";
7
13
 
8
14
  export interface MultipleChoiceQuestionProps {
9
15
  question: OnboardingQuestion;
@@ -32,64 +38,23 @@ export const MultipleChoiceQuestion = ({
32
38
  }
33
39
  onChange(newValue);
34
40
  };
35
- const renderOption = (option: QuestionOption) => {
36
- const isSelected = safeValue.includes(option.id);
37
- const isEmoji = option.iconType === 'emoji';
38
-
39
- return (
40
- <TouchableOpacity
41
- key={option.id}
42
- style={[
43
- styles.option,
44
- {
45
- backgroundColor: isSelected ? colors.iconBg : colors.featureItemBg,
46
- borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
47
- borderWidth: isSelected ? 2 : 1,
48
- }
49
- ]}
50
- onPress={() => handleToggle(option.id)}
51
- activeOpacity={0.8}
52
- >
53
- {option.icon && (
54
- <View style={[
55
- styles.optionIcon,
56
- { backgroundColor: isSelected ? colors.iconColor : colors.featureItemBg }
57
- ]}>
58
- {isEmoji ? (
59
- <AtomicText style={{ fontSize: 24 }}>{option.icon}</AtomicText>
60
- ) : (
61
- <AtomicIcon
62
- name={option.icon as any}
63
- customSize={20}
64
- customColor={isSelected ? colors.buttonTextColor : colors.subTextColor}
65
- />
66
- )}
67
- </View>
68
- )}
69
- <AtomicText type="bodyLarge" style={[styles.optionLabel, { color: isSelected ? colors.textColor : colors.subTextColor, fontWeight: isSelected ? '700' : '500' }]}>
70
- {option.label}
71
- </AtomicText>
72
- <View style={[
73
- styles.checkbox,
74
- {
75
- borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
76
- backgroundColor: isSelected ? colors.iconColor : 'transparent',
77
- borderWidth: isSelected ? 0 : 2,
78
- }
79
- ]}>
80
- {isSelected && (
81
- <AtomicIcon name="checkmark" customSize={16} customColor={colors.buttonTextColor} />
82
- )}
83
- </View>
84
- </TouchableOpacity>
85
- );
86
- };
87
41
 
88
42
  return (
89
43
  <View style={styles.container}>
90
- {question.options?.map(renderOption)}
44
+ {question.options?.map((option) => (
45
+ <QuestionOptionItem
46
+ key={option.id}
47
+ option={option}
48
+ isSelected={safeValue.includes(option.id)}
49
+ onPress={() => handleToggle(option.id)}
50
+ colors={colors}
51
+ />
52
+ ))}
91
53
  {question.validation?.maxSelections && (
92
- <AtomicText type="labelSmall" style={[styles.hint, { color: colors.subTextColor }]}>
54
+ <AtomicText
55
+ type="labelSmall"
56
+ style={[styles.hint, { color: colors.subTextColor }]}
57
+ >
93
58
  Select up to {question.validation.maxSelections} options
94
59
  </AtomicText>
95
60
  )}
@@ -102,36 +67,8 @@ const styles = StyleSheet.create({
102
67
  width: "100%",
103
68
  gap: 12,
104
69
  },
105
- option: {
106
- flexDirection: "row",
107
- alignItems: "center",
108
- borderRadius: 20,
109
- padding: 16,
110
- marginBottom: 8,
111
- },
112
- optionIcon: {
113
- width: 40,
114
- height: 40,
115
- borderRadius: 20,
116
- alignItems: 'center',
117
- justifyContent: 'center',
118
- marginRight: 16,
119
- },
120
- optionLabel: {
121
- flex: 1,
122
- fontSize: 16,
123
- },
124
- checkbox: {
125
- width: 24,
126
- height: 24,
127
- borderRadius: 8,
128
- alignItems: "center",
129
- justifyContent: "center",
130
- },
131
70
  hint: {
132
71
  textAlign: "center",
133
72
  marginTop: 8,
134
73
  },
135
74
  });
136
-
137
-
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Question Option Item Component
3
+ * Single Responsibility: Render a single selectable option
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
8
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
9
+ import type { QuestionOption } from "../../../domain/entities/OnboardingQuestion";
10
+ import type { OnboardingColors } from "../../types/OnboardingTheme";
11
+
12
+ export interface QuestionOptionItemProps {
13
+ option: QuestionOption;
14
+ isSelected: boolean;
15
+ onPress: () => void;
16
+ colors: OnboardingColors;
17
+ }
18
+
19
+ export const QuestionOptionItem = ({
20
+ option,
21
+ isSelected,
22
+ onPress,
23
+ colors,
24
+ }: QuestionOptionItemProps) => {
25
+ const isEmoji = option.iconType === 'emoji';
26
+
27
+ return (
28
+ <TouchableOpacity
29
+ style={[
30
+ styles.option,
31
+ {
32
+ backgroundColor: isSelected ? colors.iconBg : colors.featureItemBg,
33
+ borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
34
+ borderWidth: isSelected ? 2 : 1,
35
+ }
36
+ ]}
37
+ onPress={onPress}
38
+ activeOpacity={0.8}
39
+ >
40
+ {option.icon && (
41
+ <View style={[
42
+ styles.optionIcon,
43
+ { backgroundColor: isSelected ? colors.iconColor : colors.featureItemBg }
44
+ ]}>
45
+ {isEmoji ? (
46
+ <AtomicText style={{ fontSize: 24 }}>{option.icon}</AtomicText>
47
+ ) : (
48
+ <AtomicIcon
49
+ name={option.icon as any}
50
+ customSize={20}
51
+ customColor={isSelected ? colors.buttonTextColor : colors.subTextColor}
52
+ />
53
+ )}
54
+ </View>
55
+ )}
56
+ <AtomicText
57
+ type="bodyLarge"
58
+ style={[
59
+ styles.optionLabel,
60
+ {
61
+ color: isSelected ? colors.textColor : colors.subTextColor,
62
+ fontWeight: isSelected ? '700' : '500'
63
+ }
64
+ ]}
65
+ >
66
+ {option.label}
67
+ </AtomicText>
68
+ <View style={[
69
+ styles.checkbox,
70
+ {
71
+ borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
72
+ backgroundColor: isSelected ? colors.iconColor : 'transparent',
73
+ borderWidth: isSelected ? 0 : 2,
74
+ }
75
+ ]}>
76
+ {isSelected && (
77
+ <AtomicIcon
78
+ name="checkmark"
79
+ customSize={16}
80
+ customColor={colors.buttonTextColor}
81
+ />
82
+ )}
83
+ </View>
84
+ </TouchableOpacity>
85
+ );
86
+ };
87
+
88
+ const styles = StyleSheet.create({
89
+ option: {
90
+ flexDirection: "row",
91
+ alignItems: "center",
92
+ borderRadius: 20,
93
+ padding: 16,
94
+ marginBottom: 8,
95
+ },
96
+ optionIcon: {
97
+ width: 40,
98
+ height: 40,
99
+ borderRadius: 20,
100
+ alignItems: 'center',
101
+ justifyContent: 'center',
102
+ marginRight: 16,
103
+ },
104
+ optionLabel: {
105
+ flex: 1,
106
+ fontSize: 16,
107
+ },
108
+ checkbox: {
109
+ width: 24,
110
+ height: 24,
111
+ borderRadius: 8,
112
+ alignItems: "center",
113
+ justifyContent: "center",
114
+ },
115
+ });
@@ -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 and handlers
3
+ * Single Responsibility: Coordinate onboarding screen state
4
4
  */
5
5
 
6
- import { useMemo, useCallback, useEffect } from "react";
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: any;
31
+ currentAnswer: unknown;
31
32
  isAnswerValid: boolean;
32
33
  useGradient: boolean;
33
- containerStyle: any;
34
+ containerStyle: unknown;
34
35
  handleNext: () => Promise<void>;
35
36
  handlePrevious: () => void;
36
37
  handleSkip: () => Promise<void>;
37
- setCurrentAnswer: (value: any) => void;
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
- // Handle next slide with useCallback for performance
98
- const handleNext = useCallback(async () => {
99
- if (!currentSlide) return;
100
-
101
- try {
102
- await saveCurrentAnswer(currentSlide);
103
- if (isLastSlide) {
104
- await completeOnboarding();
105
- } else {
106
- goToNext();
107
- const nextSlide = SlideManager.getSlideAtIndex(
108
- filteredSlides,
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
- }, [currentSlide, isLastSlide, saveCurrentAnswer, completeOnboarding, goToNext, filteredSlides, currentIndex, loadAnswerForSlide]);
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('[useOnboardingScreenState] Cleanup completed');
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
-