@umituz/react-native-onboarding 2.10.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/package.json +4 -4
- 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/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/presentation/components/OnboardingHeader.tsx +8 -7
- package/src/presentation/components/OnboardingSlide.tsx +16 -8
- 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
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Store Actions
|
|
3
|
+
* Single Responsibility: Async store actions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
storageRepository,
|
|
8
|
+
StorageKey,
|
|
9
|
+
unwrap,
|
|
10
|
+
} from "@umituz/react-native-storage";
|
|
11
|
+
import type { OnboardingUserData } from "../../domain/entities/OnboardingUserData";
|
|
12
|
+
import type { OnboardingStoreState } from "./OnboardingStoreState";
|
|
13
|
+
|
|
14
|
+
const DEFAULT_STORAGE_KEY = StorageKey.ONBOARDING_COMPLETED;
|
|
15
|
+
const USER_DATA_STORAGE_KEY = "@onboarding_user_data";
|
|
16
|
+
|
|
17
|
+
export interface OnboardingStoreActions {
|
|
18
|
+
initialize: (storageKey?: string) => Promise<void>;
|
|
19
|
+
complete: (storageKey?: string) => Promise<void>;
|
|
20
|
+
skip: (storageKey?: string) => Promise<void>;
|
|
21
|
+
reset: (storageKey?: string) => Promise<void>;
|
|
22
|
+
saveAnswer: (questionId: string, answer: any) => Promise<void>;
|
|
23
|
+
setUserData: (data: OnboardingUserData) => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createOnboardingStoreActions(
|
|
27
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
28
|
+
get: () => OnboardingStoreState
|
|
29
|
+
): OnboardingStoreActions {
|
|
30
|
+
return {
|
|
31
|
+
initialize: async (storageKey = DEFAULT_STORAGE_KEY) => {
|
|
32
|
+
try {
|
|
33
|
+
set({ loading: true, error: null });
|
|
34
|
+
|
|
35
|
+
const completionResult = await storageRepository.getString(storageKey, "false");
|
|
36
|
+
const isComplete = unwrap(completionResult, "false") === "true";
|
|
37
|
+
|
|
38
|
+
const userDataResult = await storageRepository.getObject<OnboardingUserData>(
|
|
39
|
+
USER_DATA_STORAGE_KEY,
|
|
40
|
+
{ answers: {} }
|
|
41
|
+
);
|
|
42
|
+
const userData = unwrap(userDataResult, { answers: {} });
|
|
43
|
+
|
|
44
|
+
set({
|
|
45
|
+
isOnboardingComplete: isComplete,
|
|
46
|
+
userData,
|
|
47
|
+
loading: false,
|
|
48
|
+
error: completionResult.success ? null : "Failed to load onboarding status",
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (__DEV__) {
|
|
52
|
+
console.log('[OnboardingStore] Initialized with completion status:', isComplete);
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
set({
|
|
56
|
+
loading: false,
|
|
57
|
+
error: error instanceof Error ? error.message : "Failed to initialize onboarding",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (__DEV__) {
|
|
61
|
+
console.error('[OnboardingStore] Initialization error:', error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
complete: async (storageKey = DEFAULT_STORAGE_KEY) => {
|
|
67
|
+
try {
|
|
68
|
+
set({ loading: true, error: null });
|
|
69
|
+
|
|
70
|
+
const result = await storageRepository.setString(storageKey, "true");
|
|
71
|
+
|
|
72
|
+
const userData = { ...get().userData };
|
|
73
|
+
userData.completedAt = new Date().toISOString();
|
|
74
|
+
|
|
75
|
+
await storageRepository.setObject(USER_DATA_STORAGE_KEY, userData);
|
|
76
|
+
|
|
77
|
+
set({
|
|
78
|
+
isOnboardingComplete: result.success,
|
|
79
|
+
userData,
|
|
80
|
+
loading: false,
|
|
81
|
+
error: result.success ? null : "Failed to complete onboarding",
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (__DEV__) {
|
|
85
|
+
console.log('[OnboardingStore] Onboarding completed successfully');
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
set({
|
|
89
|
+
loading: false,
|
|
90
|
+
error: error instanceof Error ? error.message : "Failed to complete onboarding",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
if (__DEV__) {
|
|
94
|
+
console.error('[OnboardingStore] Completion error:', error);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
skip: async (storageKey = DEFAULT_STORAGE_KEY) => {
|
|
100
|
+
try {
|
|
101
|
+
set({ loading: true, error: null });
|
|
102
|
+
|
|
103
|
+
const result = await storageRepository.setString(storageKey, "true");
|
|
104
|
+
|
|
105
|
+
const userData = { ...get().userData };
|
|
106
|
+
userData.skipped = true;
|
|
107
|
+
userData.completedAt = new Date().toISOString();
|
|
108
|
+
await storageRepository.setObject(USER_DATA_STORAGE_KEY, userData);
|
|
109
|
+
|
|
110
|
+
set({
|
|
111
|
+
isOnboardingComplete: result.success,
|
|
112
|
+
userData,
|
|
113
|
+
loading: false,
|
|
114
|
+
error: result.success ? null : "Failed to skip onboarding",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (__DEV__) {
|
|
118
|
+
console.log('[OnboardingStore] Onboarding skipped successfully');
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
set({
|
|
122
|
+
loading: false,
|
|
123
|
+
error: error instanceof Error ? error.message : "Failed to skip onboarding",
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (__DEV__) {
|
|
127
|
+
console.error('[OnboardingStore] Skip error:', error);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
reset: async (storageKey = DEFAULT_STORAGE_KEY) => {
|
|
133
|
+
try {
|
|
134
|
+
set({ loading: true, error: null });
|
|
135
|
+
|
|
136
|
+
const result = await storageRepository.removeItem(storageKey);
|
|
137
|
+
await storageRepository.removeItem(USER_DATA_STORAGE_KEY);
|
|
138
|
+
|
|
139
|
+
set({
|
|
140
|
+
isOnboardingComplete: false,
|
|
141
|
+
currentStep: 0,
|
|
142
|
+
userData: { answers: {} },
|
|
143
|
+
loading: false,
|
|
144
|
+
error: result.success ? null : "Failed to reset onboarding",
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (__DEV__) {
|
|
148
|
+
console.log('[OnboardingStore] Onboarding reset successfully');
|
|
149
|
+
}
|
|
150
|
+
} catch (error) {
|
|
151
|
+
set({
|
|
152
|
+
loading: false,
|
|
153
|
+
error: error instanceof Error ? error.message : "Failed to reset onboarding",
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (__DEV__) {
|
|
157
|
+
console.error('[OnboardingStore] Reset error:', error);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
saveAnswer: async (questionId: string, answer: any) => {
|
|
163
|
+
try {
|
|
164
|
+
const userData = { ...get().userData };
|
|
165
|
+
userData.answers[questionId] = answer;
|
|
166
|
+
|
|
167
|
+
await storageRepository.setObject(USER_DATA_STORAGE_KEY, userData);
|
|
168
|
+
set({ userData });
|
|
169
|
+
|
|
170
|
+
if (__DEV__) {
|
|
171
|
+
console.log('[OnboardingStore] Answer saved for question:', questionId);
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (__DEV__) {
|
|
175
|
+
console.error('[OnboardingStore] Failed to save answer:', error);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
setUserData: async (data: OnboardingUserData) => {
|
|
181
|
+
try {
|
|
182
|
+
await storageRepository.setObject(USER_DATA_STORAGE_KEY, data);
|
|
183
|
+
set({ userData: data });
|
|
184
|
+
|
|
185
|
+
if (__DEV__) {
|
|
186
|
+
console.log('[OnboardingStore] User data updated successfully');
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if (__DEV__) {
|
|
190
|
+
console.error('[OnboardingStore] Failed to set user data:', error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Store Selectors
|
|
3
|
+
* Single Responsibility: Store state selectors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingStoreState } from "./OnboardingStoreState";
|
|
7
|
+
|
|
8
|
+
export interface OnboardingStoreSelectors {
|
|
9
|
+
getAnswer: (questionId: string) => any;
|
|
10
|
+
getUserData: () => OnboardingStoreState['userData'];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createOnboardingStoreSelectors(
|
|
14
|
+
get: () => OnboardingStoreState
|
|
15
|
+
): OnboardingStoreSelectors {
|
|
16
|
+
return {
|
|
17
|
+
getAnswer: (questionId: string) => {
|
|
18
|
+
return get().userData.answers[questionId];
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
getUserData: () => {
|
|
22
|
+
return get().userData;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Store State
|
|
3
|
+
* Single Responsibility: Store state interface and initial state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingUserData } from "../../domain/entities/OnboardingUserData";
|
|
7
|
+
|
|
8
|
+
export interface OnboardingStoreState {
|
|
9
|
+
isOnboardingComplete: boolean;
|
|
10
|
+
currentStep: number;
|
|
11
|
+
loading: boolean;
|
|
12
|
+
error: string | null;
|
|
13
|
+
userData: OnboardingUserData;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const initialOnboardingState: OnboardingStoreState = {
|
|
17
|
+
isOnboardingComplete: false,
|
|
18
|
+
currentStep: 0,
|
|
19
|
+
loading: true,
|
|
20
|
+
error: null,
|
|
21
|
+
userData: { answers: {} },
|
|
22
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OnboardingStore Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook, act } from '@testing-library/react-native';
|
|
6
|
+
import { useOnboardingStore, useOnboarding } from '../OnboardingStore';
|
|
7
|
+
|
|
8
|
+
// Mock storage repository
|
|
9
|
+
jest.mock('@umituz/react-native-storage', () => ({
|
|
10
|
+
storageRepository: {
|
|
11
|
+
getString: jest.fn(),
|
|
12
|
+
setString: jest.fn(),
|
|
13
|
+
getObject: jest.fn(),
|
|
14
|
+
setObject: jest.fn(),
|
|
15
|
+
removeItem: jest.fn(),
|
|
16
|
+
},
|
|
17
|
+
StorageKey: {
|
|
18
|
+
ONBOARDING_COMPLETED: '@onboarding_completed',
|
|
19
|
+
},
|
|
20
|
+
unwrap: jest.fn((result, defaultValue) => result.success ? result.data : defaultValue),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe('OnboardingStore', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('useOnboardingStore', () => {
|
|
29
|
+
it('should have initial state', () => {
|
|
30
|
+
const { result } = renderHook(() => useOnboardingStore());
|
|
31
|
+
|
|
32
|
+
expect(result.current.isOnboardingComplete).toBe(false);
|
|
33
|
+
expect(result.current.currentStep).toBe(0);
|
|
34
|
+
expect(result.current.loading).toBe(true);
|
|
35
|
+
expect(result.current.error).toBe(null);
|
|
36
|
+
expect(result.current.userData).toEqual({ answers: {} });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should set current step', () => {
|
|
40
|
+
const { result } = renderHook(() => useOnboardingStore());
|
|
41
|
+
|
|
42
|
+
act(() => {
|
|
43
|
+
result.current.setCurrentStep(5);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(result.current.currentStep).toBe(5);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should set loading state', () => {
|
|
50
|
+
const { result } = renderHook(() => useOnboardingStore());
|
|
51
|
+
|
|
52
|
+
act(() => {
|
|
53
|
+
result.current.setLoading(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.current.loading).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should set error state', () => {
|
|
60
|
+
const { result } = renderHook(() => useOnboardingStore());
|
|
61
|
+
const errorMessage = 'Test error';
|
|
62
|
+
|
|
63
|
+
act(() => {
|
|
64
|
+
result.current.setError(errorMessage);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result.current.error).toBe(errorMessage);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('useOnboarding', () => {
|
|
72
|
+
it('should return all store properties and methods', () => {
|
|
73
|
+
const { result } = renderHook(() => useOnboarding());
|
|
74
|
+
|
|
75
|
+
expect(typeof result.current.initialize).toBe('function');
|
|
76
|
+
expect(typeof result.current.complete).toBe('function');
|
|
77
|
+
expect(typeof result.current.skip).toBe('function');
|
|
78
|
+
expect(typeof result.current.reset).toBe('function');
|
|
79
|
+
expect(typeof result.current.saveAnswer).toBe('function');
|
|
80
|
+
expect(typeof result.current.getAnswer).toBe('function');
|
|
81
|
+
expect(typeof result.current.getUserData).toBe('function');
|
|
82
|
+
expect(typeof result.current.setUserData).toBe('function');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import React, { useMemo } from "react";
|
|
8
8
|
import { View, TouchableOpacity, Text, StyleSheet } from "react-native";
|
|
9
|
-
import {
|
|
9
|
+
import { Feather } from "@expo/vector-icons";
|
|
10
10
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
11
11
|
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
12
12
|
|
|
@@ -58,9 +58,10 @@ export const OnboardingHeader: React.FC<OnboardingHeaderProps> = ({
|
|
|
58
58
|
activeOpacity={0.7}
|
|
59
59
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
|
60
60
|
>
|
|
61
|
-
<
|
|
61
|
+
<Feather
|
|
62
|
+
name="arrow-left"
|
|
62
63
|
size={20}
|
|
63
|
-
{
|
|
64
|
+
color={useGradient ? "#FFFFFF" : tokens.colors.textPrimary}
|
|
64
65
|
/>
|
|
65
66
|
</TouchableOpacity>
|
|
66
67
|
) : (
|
|
@@ -92,14 +93,14 @@ const getStyles = (
|
|
|
92
93
|
width: 40,
|
|
93
94
|
height: 40,
|
|
94
95
|
borderRadius: 20,
|
|
95
|
-
backgroundColor: useGradient
|
|
96
|
-
? "rgba(255, 255, 255, 0.2)"
|
|
96
|
+
backgroundColor: useGradient
|
|
97
|
+
? "rgba(255, 255, 255, 0.2)"
|
|
97
98
|
: tokens.colors.surface,
|
|
98
99
|
alignItems: "center",
|
|
99
100
|
justifyContent: "center",
|
|
100
101
|
borderWidth: 1,
|
|
101
|
-
borderColor: useGradient
|
|
102
|
-
? "rgba(255, 255, 255, 0.3)"
|
|
102
|
+
borderColor: useGradient
|
|
103
|
+
? "rgba(255, 255, 255, 0.3)"
|
|
103
104
|
: tokens.colors.borderLight,
|
|
104
105
|
},
|
|
105
106
|
headerButtonDisabled: {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import React, { useMemo } from "react";
|
|
8
8
|
import { View, Text, StyleSheet, ScrollView } from "react-native";
|
|
9
|
-
import
|
|
9
|
+
import { Feather } from "@expo/vector-icons";
|
|
10
10
|
import { useAppDesignTokens, withAlpha } from "@umituz/react-native-design-system-theme";
|
|
11
11
|
import type { OnboardingSlide as OnboardingSlideType } from "../../domain/entities/OnboardingSlide";
|
|
12
12
|
|
|
@@ -19,11 +19,11 @@ export const OnboardingSlide: React.FC<OnboardingSlideProps> = ({ slide, useGrad
|
|
|
19
19
|
const tokens = useAppDesignTokens();
|
|
20
20
|
const styles = useMemo(() => getStyles(tokens, useGradient), [tokens, useGradient]);
|
|
21
21
|
|
|
22
|
-
// Check if icon is an emoji (contains emoji characters)
|
|
22
|
+
// Check if icon is an emoji (contains emoji characters)
|
|
23
23
|
const isEmoji = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u.test(slide.icon);
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
const
|
|
25
|
+
// Validate if it's a valid Feather icon name
|
|
26
|
+
const isValidFeatherIcon = !isEmoji && typeof slide.icon === 'string';
|
|
27
27
|
|
|
28
28
|
return (
|
|
29
29
|
<ScrollView
|
|
@@ -36,8 +36,12 @@ export const OnboardingSlide: React.FC<OnboardingSlideProps> = ({ slide, useGrad
|
|
|
36
36
|
<View style={styles.iconContainer}>
|
|
37
37
|
{isEmoji ? (
|
|
38
38
|
<Text style={styles.icon}>{slide.icon}</Text>
|
|
39
|
-
) :
|
|
40
|
-
<
|
|
39
|
+
) : isValidFeatherIcon ? (
|
|
40
|
+
<Feather
|
|
41
|
+
name={slide.icon as any}
|
|
42
|
+
size={60}
|
|
43
|
+
color="#FFFFFF"
|
|
44
|
+
/>
|
|
41
45
|
) : (
|
|
42
46
|
<Text style={styles.icon}>📱</Text>
|
|
43
47
|
)}
|
|
@@ -61,8 +65,12 @@ export const OnboardingSlide: React.FC<OnboardingSlideProps> = ({ slide, useGrad
|
|
|
61
65
|
<View style={styles.iconContainer}>
|
|
62
66
|
{isEmoji ? (
|
|
63
67
|
<Text style={styles.icon}>{slide.icon}</Text>
|
|
64
|
-
) :
|
|
65
|
-
<
|
|
68
|
+
) : isValidFeatherIcon ? (
|
|
69
|
+
<Feather
|
|
70
|
+
name={slide.icon as any}
|
|
71
|
+
size={60}
|
|
72
|
+
color={tokens.colors.textPrimary}
|
|
73
|
+
/>
|
|
66
74
|
) : (
|
|
67
75
|
<Text style={styles.icon}>📱</Text>
|
|
68
76
|
)}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import React from "react";
|
|
8
8
|
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
|
|
9
9
|
import { AtomicIcon } from "@umituz/react-native-design-system-atoms";
|
|
10
|
+
import { useTheme } from "@umituz/react-native-design-system-theme";
|
|
10
11
|
import type { OnboardingQuestion, QuestionOption } from "../../../domain/entities/OnboardingQuestion";
|
|
11
12
|
|
|
12
13
|
export interface MultipleChoiceQuestionProps {
|
|
@@ -20,6 +21,8 @@ export const MultipleChoiceQuestion: React.FC<MultipleChoiceQuestionProps> = ({
|
|
|
20
21
|
value = [],
|
|
21
22
|
onChange,
|
|
22
23
|
}) => {
|
|
24
|
+
const { tokens } = useTheme() as any;
|
|
25
|
+
|
|
23
26
|
const isEmoji = (icon: string) =>
|
|
24
27
|
/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u.test(icon);
|
|
25
28
|
|
|
@@ -57,17 +60,28 @@ export const MultipleChoiceQuestion: React.FC<MultipleChoiceQuestionProps> = ({
|
|
|
57
60
|
<AtomicIcon
|
|
58
61
|
name={option.icon as any}
|
|
59
62
|
customSize={24}
|
|
60
|
-
customColor={isSelected ?
|
|
63
|
+
customColor={isSelected ? tokens.colors.textPrimary : tokens.colors.textSecondary}
|
|
61
64
|
/>
|
|
62
65
|
)}
|
|
63
66
|
</View>
|
|
64
67
|
)}
|
|
65
|
-
<Text style={[
|
|
68
|
+
<Text style={[
|
|
69
|
+
styles.optionLabel,
|
|
70
|
+
isSelected && styles.optionLabelSelected,
|
|
71
|
+
{ color: isSelected ? tokens.colors.textPrimary : tokens.colors.textSecondary }
|
|
72
|
+
]}>
|
|
66
73
|
{option.label}
|
|
67
74
|
</Text>
|
|
68
|
-
<View style={[
|
|
75
|
+
<View style={[
|
|
76
|
+
styles.checkbox,
|
|
77
|
+
isSelected && { borderWidth: 3 },
|
|
78
|
+
{
|
|
79
|
+
borderColor: isSelected ? tokens.colors.borderPrimary : tokens.colors.borderSecondary,
|
|
80
|
+
backgroundColor: isSelected ? tokens.colors.backgroundSecondary : 'transparent'
|
|
81
|
+
}
|
|
82
|
+
]}>
|
|
69
83
|
{isSelected && (
|
|
70
|
-
<AtomicIcon name="Check" customSize={16} customColor=
|
|
84
|
+
<AtomicIcon name="Check" customSize={16} customColor={tokens.colors.textPrimary} />
|
|
71
85
|
)}
|
|
72
86
|
</View>
|
|
73
87
|
</TouchableOpacity>
|
|
@@ -78,7 +92,7 @@ export const MultipleChoiceQuestion: React.FC<MultipleChoiceQuestionProps> = ({
|
|
|
78
92
|
<View style={styles.container}>
|
|
79
93
|
{question.options?.map(renderOption)}
|
|
80
94
|
{question.validation?.maxSelections && (
|
|
81
|
-
<Text style={styles.hint}>
|
|
95
|
+
<Text style={[styles.hint, { color: tokens.colors.textSecondary }]}>
|
|
82
96
|
Select up to {question.validation.maxSelections} options
|
|
83
97
|
</Text>
|
|
84
98
|
)}
|
|
@@ -94,15 +108,12 @@ const styles = StyleSheet.create({
|
|
|
94
108
|
option: {
|
|
95
109
|
flexDirection: "row",
|
|
96
110
|
alignItems: "center",
|
|
97
|
-
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
|
98
111
|
borderRadius: 12,
|
|
99
112
|
padding: 16,
|
|
100
113
|
borderWidth: 2,
|
|
101
|
-
borderColor: "rgba(255, 255, 255, 0.2)",
|
|
102
114
|
},
|
|
103
115
|
optionSelected: {
|
|
104
|
-
|
|
105
|
-
borderColor: "rgba(255, 255, 255, 0.5)",
|
|
116
|
+
borderWidth: 3,
|
|
106
117
|
},
|
|
107
118
|
optionIcon: {
|
|
108
119
|
marginRight: 12,
|
|
@@ -113,11 +124,9 @@ const styles = StyleSheet.create({
|
|
|
113
124
|
optionLabel: {
|
|
114
125
|
flex: 1,
|
|
115
126
|
fontSize: 16,
|
|
116
|
-
color: "rgba(255, 255, 255, 0.9)",
|
|
117
127
|
fontWeight: "500",
|
|
118
128
|
},
|
|
119
129
|
optionLabelSelected: {
|
|
120
|
-
color: "#FFFFFF",
|
|
121
130
|
fontWeight: "600",
|
|
122
131
|
},
|
|
123
132
|
checkbox: {
|
|
@@ -125,17 +134,11 @@ const styles = StyleSheet.create({
|
|
|
125
134
|
height: 24,
|
|
126
135
|
borderRadius: 6,
|
|
127
136
|
borderWidth: 2,
|
|
128
|
-
borderColor: "rgba(255, 255, 255, 0.5)",
|
|
129
137
|
alignItems: "center",
|
|
130
138
|
justifyContent: "center",
|
|
131
139
|
},
|
|
132
|
-
checkboxSelected: {
|
|
133
|
-
borderColor: "#FFFFFF",
|
|
134
|
-
backgroundColor: "rgba(255, 255, 255, 0.3)",
|
|
135
|
-
},
|
|
136
140
|
hint: {
|
|
137
141
|
fontSize: 13,
|
|
138
|
-
color: "rgba(255, 255, 255, 0.7)",
|
|
139
142
|
textAlign: "center",
|
|
140
143
|
marginTop: 4,
|
|
141
144
|
},
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import React from "react";
|
|
8
8
|
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
|
|
9
9
|
import { AtomicIcon } from "@umituz/react-native-design-system-atoms";
|
|
10
|
+
import { useTheme } from "@umituz/react-native-design-system-theme";
|
|
10
11
|
import type { OnboardingQuestion } from "../../../domain/entities/OnboardingQuestion";
|
|
11
12
|
|
|
12
13
|
export interface RatingQuestionProps {
|
|
@@ -20,6 +21,7 @@ export const RatingQuestion: React.FC<RatingQuestionProps> = ({
|
|
|
20
21
|
value = 0,
|
|
21
22
|
onChange,
|
|
22
23
|
}) => {
|
|
24
|
+
const { tokens } = useTheme() as any;
|
|
23
25
|
const { validation } = question;
|
|
24
26
|
const max = validation?.max ?? 5;
|
|
25
27
|
|
|
@@ -36,7 +38,7 @@ export const RatingQuestion: React.FC<RatingQuestionProps> = ({
|
|
|
36
38
|
<AtomicIcon
|
|
37
39
|
name={isFilled ? "Star" : "Star"}
|
|
38
40
|
customSize={48}
|
|
39
|
-
customColor={isFilled ?
|
|
41
|
+
customColor={isFilled ? tokens.colors.warning : tokens.colors.textSecondary}
|
|
40
42
|
/>
|
|
41
43
|
</TouchableOpacity>
|
|
42
44
|
);
|
|
@@ -48,7 +50,7 @@ export const RatingQuestion: React.FC<RatingQuestionProps> = ({
|
|
|
48
50
|
{Array.from({ length: max }, (_, i) => renderStar(i))}
|
|
49
51
|
</View>
|
|
50
52
|
{value > 0 && (
|
|
51
|
-
<Text style={styles.valueText}>
|
|
53
|
+
<Text style={[styles.valueText, { color: tokens.colors.textPrimary }]}>
|
|
52
54
|
{value} / {max}
|
|
53
55
|
</Text>
|
|
54
56
|
)}
|
|
@@ -71,7 +73,6 @@ const styles = StyleSheet.create({
|
|
|
71
73
|
},
|
|
72
74
|
valueText: {
|
|
73
75
|
fontSize: 18,
|
|
74
|
-
color: "rgba(255, 255, 255, 0.9)",
|
|
75
76
|
fontWeight: "600",
|
|
76
77
|
},
|
|
77
78
|
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import React from "react";
|
|
8
8
|
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
|
|
9
9
|
import { AtomicIcon } from "@umituz/react-native-design-system-atoms";
|
|
10
|
+
import { useTheme } from "@umituz/react-native-design-system-theme";
|
|
10
11
|
import type { OnboardingQuestion, QuestionOption } from "../../../domain/entities/OnboardingQuestion";
|
|
11
12
|
|
|
12
13
|
export interface SingleChoiceQuestionProps {
|
|
@@ -20,6 +21,8 @@ export const SingleChoiceQuestion: React.FC<SingleChoiceQuestionProps> = ({
|
|
|
20
21
|
value,
|
|
21
22
|
onChange,
|
|
22
23
|
}) => {
|
|
24
|
+
const { tokens } = useTheme() as any;
|
|
25
|
+
|
|
23
26
|
const isEmoji = (icon: string) =>
|
|
24
27
|
/[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u.test(icon);
|
|
25
28
|
|
|
@@ -41,16 +44,24 @@ export const SingleChoiceQuestion: React.FC<SingleChoiceQuestionProps> = ({
|
|
|
41
44
|
<AtomicIcon
|
|
42
45
|
name={option.icon as any}
|
|
43
46
|
customSize={24}
|
|
44
|
-
customColor={isSelected ?
|
|
47
|
+
customColor={isSelected ? tokens.colors.textPrimary : tokens.colors.textSecondary}
|
|
45
48
|
/>
|
|
46
49
|
)}
|
|
47
50
|
</View>
|
|
48
51
|
)}
|
|
49
|
-
<Text style={[
|
|
52
|
+
<Text style={[
|
|
53
|
+
styles.optionLabel,
|
|
54
|
+
isSelected && styles.optionLabelSelected,
|
|
55
|
+
{ color: isSelected ? tokens.colors.textPrimary : tokens.colors.textSecondary }
|
|
56
|
+
]}>
|
|
50
57
|
{option.label}
|
|
51
58
|
</Text>
|
|
52
|
-
<View style={[
|
|
53
|
-
|
|
59
|
+
<View style={[
|
|
60
|
+
styles.radio,
|
|
61
|
+
isSelected && { borderWidth: 3 },
|
|
62
|
+
{ borderColor: isSelected ? tokens.colors.borderPrimary : tokens.colors.borderSecondary }
|
|
63
|
+
]}>
|
|
64
|
+
{isSelected && <View style={[styles.radioInner, { backgroundColor: tokens.colors.textPrimary }]} />}
|
|
54
65
|
</View>
|
|
55
66
|
</TouchableOpacity>
|
|
56
67
|
);
|
|
@@ -71,15 +82,12 @@ const styles = StyleSheet.create({
|
|
|
71
82
|
option: {
|
|
72
83
|
flexDirection: "row",
|
|
73
84
|
alignItems: "center",
|
|
74
|
-
backgroundColor: "rgba(255, 255, 255, 0.1)",
|
|
75
85
|
borderRadius: 12,
|
|
76
86
|
padding: 16,
|
|
77
87
|
borderWidth: 2,
|
|
78
|
-
borderColor: "rgba(255, 255, 255, 0.2)",
|
|
79
88
|
},
|
|
80
89
|
optionSelected: {
|
|
81
|
-
|
|
82
|
-
borderColor: "rgba(255, 255, 255, 0.5)",
|
|
90
|
+
borderWidth: 3,
|
|
83
91
|
},
|
|
84
92
|
optionIcon: {
|
|
85
93
|
marginRight: 12,
|
|
@@ -90,11 +98,9 @@ const styles = StyleSheet.create({
|
|
|
90
98
|
optionLabel: {
|
|
91
99
|
flex: 1,
|
|
92
100
|
fontSize: 16,
|
|
93
|
-
color: "rgba(255, 255, 255, 0.9)",
|
|
94
101
|
fontWeight: "500",
|
|
95
102
|
},
|
|
96
103
|
optionLabelSelected: {
|
|
97
|
-
color: "#FFFFFF",
|
|
98
104
|
fontWeight: "600",
|
|
99
105
|
},
|
|
100
106
|
radio: {
|
|
@@ -102,18 +108,13 @@ const styles = StyleSheet.create({
|
|
|
102
108
|
height: 24,
|
|
103
109
|
borderRadius: 12,
|
|
104
110
|
borderWidth: 2,
|
|
105
|
-
borderColor: "rgba(255, 255, 255, 0.5)",
|
|
106
111
|
alignItems: "center",
|
|
107
112
|
justifyContent: "center",
|
|
108
113
|
},
|
|
109
|
-
radioSelected: {
|
|
110
|
-
borderColor: "#FFFFFF",
|
|
111
|
-
},
|
|
112
114
|
radioInner: {
|
|
113
115
|
width: 12,
|
|
114
116
|
height: 12,
|
|
115
117
|
borderRadius: 6,
|
|
116
|
-
backgroundColor: "#FFFFFF",
|
|
117
118
|
},
|
|
118
119
|
});
|
|
119
120
|
|