@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.
@@ -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 { ArrowLeft } from "lucide-react-native";
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
- <ArrowLeft
61
+ <Feather
62
+ name="arrow-left"
62
63
  size={20}
63
- {...({ color: useGradient ? "#FFFFFF" : tokens.colors.textPrimary } as any)}
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 * as LucideIcons from "lucide-react-native";
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) or Lucide icon name
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
- // Get Lucide icon component
26
- const IconComponent = !isEmoji ? (LucideIcons as any)[slide.icon] : null;
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
- ) : IconComponent ? (
40
- <IconComponent size={60} color="#FFFFFF" />
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
- ) : IconComponent ? (
65
- <IconComponent size={60} color={tokens.colors.textPrimary} />
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 ? "#FFFFFF" : "rgba(255, 255, 255, 0.8)"}
63
+ customColor={isSelected ? tokens.colors.textPrimary : tokens.colors.textSecondary}
61
64
  />
62
65
  )}
63
66
  </View>
64
67
  )}
65
- <Text style={[styles.optionLabel, isSelected && styles.optionLabelSelected]}>
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={[styles.checkbox, isSelected && styles.checkboxSelected]}>
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="#FFFFFF" />
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
- backgroundColor: "rgba(255, 255, 255, 0.25)",
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 ? "#FFD700" : "rgba(255, 255, 255, 0.3)"}
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 ? "#FFFFFF" : "rgba(255, 255, 255, 0.8)"}
47
+ customColor={isSelected ? tokens.colors.textPrimary : tokens.colors.textSecondary}
45
48
  />
46
49
  )}
47
50
  </View>
48
51
  )}
49
- <Text style={[styles.optionLabel, isSelected && styles.optionLabelSelected]}>
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={[styles.radio, isSelected && styles.radioSelected]}>
53
- {isSelected && <View style={styles.radioInner} />}
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
- backgroundColor: "rgba(255, 255, 255, 0.25)",
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