@umituz/react-native-onboarding 3.5.3 → 3.5.4

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,15 +1,12 @@
1
1
  {
2
2
  "name": "@umituz/react-native-onboarding",
3
- "version": "3.5.3",
3
+ "version": "3.5.4",
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",
7
7
  "scripts": {
8
- "typecheck": "echo 'TypeScript validation passed'",
9
- "lint": "echo 'Lint passed'",
10
- "version:patch": "npm version patch -m 'chore: release v%s'",
11
- "version:minor": "npm version minor -m 'chore: release v%s'",
12
- "version:major": "npm version major -m 'chore: release v%s'"
8
+ "typecheck": "npx tsc --noEmit 2>&1",
9
+ "lint": "eslint . --fix"
13
10
  },
14
11
  "keywords": [
15
12
  "react-native",
@@ -31,10 +28,10 @@
31
28
  "url": "https://github.com/umituz/react-native-onboarding"
32
29
  },
33
30
  "peerDependencies": {
34
- "@umituz/react-native-storage": "latest",
35
- "@umituz/react-native-localization": "latest",
36
- "@umituz/react-native-design-system": "^2.1.0",
37
31
  "@expo/vector-icons": ">=14.0.0",
32
+ "@umituz/react-native-design-system": "^2.1.0",
33
+ "@umituz/react-native-localization": "latest",
34
+ "@umituz/react-native-storage": "latest",
38
35
  "expo-image": ">=2.0.0",
39
36
  "expo-linear-gradient": ">=13.0.0",
40
37
  "expo-video": ">=1.0.0",
@@ -44,19 +41,28 @@
44
41
  "zustand": "^4.5.0 || ^5.0.0"
45
42
  },
46
43
  "devDependencies": {
47
- "@umituz/react-native-storage": "latest",
48
- "@umituz/react-native-localization": "latest",
49
- "@umituz/react-native-design-system": "^2.1.0",
50
44
  "@expo/vector-icons": "^14.0.0",
45
+ "@react-native-async-storage/async-storage": "^2.2.0",
46
+ "@react-native-community/datetimepicker": "^8.5.1",
47
+ "@react-native/eslint-config": "^0.83.1",
48
+ "@types/react": "~19.1.10",
49
+ "@types/react-native": "^0.72.8",
50
+ "@typescript-eslint/eslint-plugin": "^8.50.1",
51
+ "@typescript-eslint/parser": "^8.50.1",
52
+ "@umituz/react-native-design-system": "^2.1.0",
53
+ "@umituz/react-native-localization": "latest",
54
+ "@umituz/react-native-storage": "latest",
55
+ "eslint": "^9.39.2",
56
+ "eslint-plugin-react": "^7.37.5",
57
+ "eslint-plugin-react-hooks": "^7.0.1",
51
58
  "expo-image": "~2.0.0",
52
59
  "expo-linear-gradient": "^15.0.7",
53
60
  "expo-video": "~2.0.0",
54
- "zustand": "^5.0.0",
55
- "@types/react": "~19.1.10",
56
61
  "react": "19.1.0",
57
62
  "react-native": "0.81.5",
58
63
  "react-native-safe-area-context": "^5.6.0",
59
- "typescript": "~5.9.2"
64
+ "typescript": "~5.9.2",
65
+ "zustand": "^5.0.0"
60
66
  },
61
67
  "publishConfig": {
62
68
  "access": "public"
@@ -43,6 +43,11 @@ export interface QuestionOption {
43
43
  * Optional value (if different from label)
44
44
  */
45
45
  value?: string;
46
+
47
+ /**
48
+ * Type of icon: 'emoji' or 'icon' (default: 'icon')
49
+ */
50
+ iconType?: 'emoji' | 'icon';
46
51
  }
47
52
 
48
53
  /**
@@ -145,6 +150,11 @@ export interface OnboardingQuestion {
145
150
  */
146
151
  icon?: string;
147
152
 
153
+ /**
154
+ * Type of icon: 'emoji' or 'icon' (default: 'icon')
155
+ */
156
+ iconType?: 'emoji' | 'icon';
157
+
148
158
  /**
149
159
  * Skip this question if condition is met
150
160
  * @param answers - Previous answers
@@ -49,7 +49,7 @@ export const useOnboardingNavigation = (
49
49
  await onComplete();
50
50
  }
51
51
  // Emit event for app-level handling
52
- /* eslint-disable-next-line no-console */
52
+
53
53
  if (__DEV__) console.log("[useOnboardingNavigation] Emitting onboarding-complete event");
54
54
  DeviceEventEmitter.emit("onboarding-complete");
55
55
  }, [onComplete]);
@@ -9,9 +9,9 @@ import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
9
9
  import type { OnboardingUserData } from "../../domain/entities/OnboardingUserData";
10
10
 
11
11
  /**
12
- * Service for managing onboarding slide operations
12
+ * SlideManager
13
13
  */
14
- export class OnboardingSlideService {
14
+ export class SlideManager {
15
15
  /**
16
16
  * Filter slides based on skipIf conditions
17
17
  * @param slides - All available slides
@@ -8,9 +8,9 @@
8
8
  import type { OnboardingQuestion } from "../../domain/entities/OnboardingQuestion";
9
9
 
10
10
  /**
11
- * Service for validating onboarding question answers
11
+ * ValidationManager
12
12
  */
13
- export class OnboardingValidationService {
13
+ export class ValidationManager {
14
14
  /**
15
15
  * Validate answer against question validation rules
16
16
  * @param question - The question to validate against
@@ -0,0 +1,41 @@
1
+ /**
2
+ * BaseSlide Component
3
+ * Single Responsibility: Provide a base layout for all onboarding slides
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet, ScrollView } from "react-native";
8
+
9
+ export interface BaseSlideProps {
10
+ children: React.ReactNode;
11
+ }
12
+
13
+ export const BaseSlide = ({ children }: BaseSlideProps) => {
14
+ return (
15
+ <ScrollView
16
+ style={styles.container}
17
+ contentContainerStyle={styles.content}
18
+ showsVerticalScrollIndicator={false}
19
+ bounces={false}
20
+ >
21
+ <View style={styles.slideContainer}>
22
+ {children}
23
+ </View>
24
+ </ScrollView>
25
+ );
26
+ };
27
+
28
+ const styles = StyleSheet.create({
29
+ container: {
30
+ flex: 1,
31
+ },
32
+ content: {
33
+ flexGrow: 1,
34
+ justifyContent: "center",
35
+ paddingVertical: 40,
36
+ },
37
+ slideContainer: {
38
+ paddingHorizontal: 24,
39
+ alignItems: "center",
40
+ },
41
+ });
@@ -35,8 +35,8 @@ export const OnboardingFooter = ({
35
35
  const { colors } = useOnboardingTheme();
36
36
 
37
37
  const buttonText = isLastSlide
38
- ? getStartedButtonText || t("onboarding.getStarted") || "Get Started"
39
- : nextButtonText || t("general.continue") || "Continue";
38
+ ? getStartedButtonText || t("onboarding.getStarted")
39
+ : nextButtonText || t("general.continue");
40
40
 
41
41
  const progressPercent = ((currentIndex + 1) / totalSlides) * 100;
42
42
 
@@ -84,7 +84,7 @@ export const OnboardingFooter = ({
84
84
  type="labelSmall"
85
85
  style={[styles.progressText, { color: colors.progressTextColor }]}
86
86
  >
87
- {currentIndex + 1} {t("general.of") || "of"} {totalSlides}
87
+ {currentIndex + 1} {t("general.of")} {totalSlides}
88
88
  </AtomicText>
89
89
  )}
90
90
  </View>
@@ -24,7 +24,7 @@ export const OnboardingHeader = ({
24
24
  const { t } = useLocalization();
25
25
  const { colors } = useOnboardingTheme();
26
26
 
27
- const skipText = skipButtonText || t("onboarding.skip") || "Skip";
27
+ const skipText = skipButtonText || t("onboarding.skip");
28
28
 
29
29
  return (
30
30
  <View style={styles.header}>
@@ -15,9 +15,9 @@ export interface OnboardingResetSettingProps {
15
15
 
16
16
  export const OnboardingResetSetting = ({
17
17
  onReset,
18
- title = "Reset Onboarding",
19
- description = "Show onboarding flow again",
20
- iconName = "refresh-outline",
18
+ title,
19
+ description,
20
+ iconName = "extension-puzzle",
21
21
  iconColor,
22
22
  titleColor,
23
23
  visible = __DEV__,
@@ -14,7 +14,6 @@ import { OnboardingSlide as OnboardingSlideComponent } from "./OnboardingSlide";
14
14
  import { QuestionSlide } from "./QuestionSlide";
15
15
  import { OnboardingFooter } from "./OnboardingFooter";
16
16
  import { BackgroundVideo } from "./BackgroundVideo";
17
- import { useOnboardingTheme } from "../providers/OnboardingThemeProvider";
18
17
 
19
18
  export interface OnboardingScreenContentProps {
20
19
  containerStyle?: any;
@@ -1,106 +1,121 @@
1
+ /**
2
+ * OnboardingSlide Component
3
+ * Single Responsibility: Render a single onboarding slide
4
+ */
5
+
1
6
  import React from "react";
2
- import { View, ScrollView, StyleSheet } from "react-native";
7
+ import { View, StyleSheet } from "react-native";
3
8
  import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
4
9
  import type { OnboardingSlide as OnboardingSlideType } from "../../domain/entities/OnboardingSlide";
5
- import type { OnboardingThemeVariant } from "../../domain/entities/OnboardingTheme";
10
+ import { BaseSlide } from "./BaseSlide";
6
11
  import { useOnboardingTheme } from "../providers/OnboardingThemeProvider";
7
12
 
8
13
  export interface OnboardingSlideProps {
9
14
  slide: OnboardingSlideType;
10
- variant?: OnboardingThemeVariant;
15
+ variant?: "default" | "card" | "minimal" | "fullscreen";
11
16
  }
12
17
 
13
18
  export const OnboardingSlide = ({
14
19
  slide,
15
- variant = "default"
20
+ variant = "default",
16
21
  }: OnboardingSlideProps) => {
17
22
  const { colors } = useOnboardingTheme();
18
23
 
24
+ const hasIcon = !!slide.icon;
19
25
  const isEmoji = slide.iconType === 'emoji';
20
- const hasIcon = slide.icon && slide.icon.length > 0;
21
26
  const iconSize = variant === "minimal" ? 80 : 72;
22
27
 
23
28
  return (
24
- <ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false} bounces={false}>
25
- <View style={styles.slideContainer}>
26
- {hasIcon && (
27
- <View style={styles.iconBox}>
28
- {isEmoji ? (
29
- <AtomicText style={{ fontSize: iconSize }}>{slide.icon}</AtomicText>
30
- ) : (
31
- <AtomicIcon name={slide.icon} customSize={iconSize} customColor={colors.iconColor} />
32
- )}
33
- </View>
34
- )}
29
+ <BaseSlide>
30
+ {hasIcon && (
31
+ <View style={styles.iconBox}>
32
+ {isEmoji ? (
33
+ <AtomicText style={{ fontSize: iconSize }}>{slide.icon}</AtomicText>
34
+ ) : (
35
+ <AtomicIcon
36
+ name={slide.icon as any}
37
+ customSize={iconSize}
38
+ customColor={colors.iconColor}
39
+ />
40
+ )}
41
+ </View>
42
+ )}
35
43
 
36
- <AtomicText type="headlineMedium" style={[styles.title, { color: colors.textColor }]}>
37
- {slide.title}
38
- </AtomicText>
44
+ <AtomicText
45
+ type="displaySmall"
46
+ style={[styles.title, { color: colors.textColor }]}
47
+ >
48
+ {slide.title}
49
+ </AtomicText>
39
50
 
40
- <AtomicText type="bodyLarge" style={[styles.description, { color: colors.subTextColor }]}>
51
+ {slide.description && (
52
+ <AtomicText
53
+ type="bodyLarge"
54
+ style={[styles.description, { color: colors.subTextColor }]}
55
+ >
41
56
  {slide.description}
42
57
  </AtomicText>
58
+ )}
43
59
 
44
- {slide.features && slide.features.length > 0 && (
45
- <View style={styles.featuresContainer}>
46
- {slide.features.map((feature, index) => (
47
- <View key={index} style={styles.featureItem}>
48
- <AtomicIcon name="checkmark-circle" size="sm" customColor={colors.iconColor} />
49
- <AtomicText type="bodyMedium" style={[styles.featureText, { color: colors.textColor }]}>
50
- {feature}
51
- </AtomicText>
52
- </View>
53
- ))}
54
- </View>
55
- )}
56
- </View>
57
- </ScrollView>
60
+ {slide.features && slide.features.length > 0 && (
61
+ <View style={styles.features}>
62
+ {slide.features.map((feature, index) => (
63
+ <View
64
+ key={index}
65
+ style={[
66
+ styles.featureItem,
67
+ { backgroundColor: colors.featureItemBg },
68
+ ]}
69
+ >
70
+ <AtomicIcon
71
+ name="checkmark-circle"
72
+ size="sm"
73
+ customColor={colors.iconColor}
74
+ />
75
+ <AtomicText
76
+ type="bodyMedium"
77
+ style={[styles.featureText, { color: colors.textColor }]}
78
+ >
79
+ {feature}
80
+ </AtomicText>
81
+ </View>
82
+ ))}
83
+ </View>
84
+ )}
85
+ </BaseSlide>
58
86
  );
59
87
  };
60
88
 
61
89
  const styles = StyleSheet.create({
62
- content: {
63
- flexGrow: 1,
64
- paddingTop: 40,
65
- },
66
- slideContainer: {
67
- paddingHorizontal: 30,
68
- alignItems: "center",
69
- },
70
90
  iconBox: {
71
- marginBottom: 40,
72
- height: 120,
91
+ marginBottom: 32,
92
+ height: 100,
73
93
  justifyContent: "center",
74
94
  alignItems: "center",
75
95
  },
76
96
  title: {
97
+ fontWeight: "800",
77
98
  textAlign: "center",
78
99
  marginBottom: 16,
79
- fontWeight: "800",
80
100
  },
81
101
  description: {
82
102
  textAlign: "center",
83
103
  lineHeight: 24,
84
104
  marginBottom: 32,
85
- opacity: 0.9,
86
105
  },
87
- featuresContainer: {
106
+ features: {
88
107
  width: "100%",
89
108
  gap: 12,
90
- marginTop: 8,
91
109
  },
92
110
  featureItem: {
93
111
  flexDirection: "row",
94
112
  alignItems: "center",
113
+ padding: 16,
114
+ borderRadius: 16,
95
115
  gap: 12,
96
- backgroundColor: "rgba(255, 255, 255, 0.1)",
97
- padding: 12,
98
- borderRadius: 12,
99
116
  },
100
117
  featureText: {
118
+ fontWeight: "600",
101
119
  flex: 1,
102
120
  },
103
121
  });
104
-
105
-
106
-
@@ -1,23 +1,29 @@
1
+ /**
2
+ * QuestionSlide Component
3
+ * Single Responsibility: Render a question-type slide
4
+ */
5
+
1
6
  import React from "react";
2
- import { View, ScrollView, StyleSheet } from "react-native";
7
+ import { View, StyleSheet } from "react-native";
3
8
  import { AtomicText } from "@umituz/react-native-design-system";
4
9
  import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
5
- import type { OnboardingThemeVariant } from "../../domain/entities/OnboardingTheme";
6
10
  import { QuestionSlideHeader } from "./QuestionSlideHeader";
7
11
  import { QuestionRenderer } from "./QuestionRenderer";
12
+ import { BaseSlide } from "./BaseSlide";
8
13
  import { useOnboardingTheme } from "../providers/OnboardingThemeProvider";
9
14
 
10
15
  export interface QuestionSlideProps {
11
16
  slide: OnboardingSlide;
12
17
  value: any;
13
18
  onChange: (value: any) => void;
14
- variant?: OnboardingThemeVariant;
19
+ variant?: "default" | "card" | "minimal" | "fullscreen";
15
20
  }
16
21
 
17
22
  export const QuestionSlide = ({
18
23
  slide,
19
24
  value,
20
25
  onChange,
26
+ variant: _variant = "default",
21
27
  }: QuestionSlideProps) => {
22
28
  const { colors } = useOnboardingTheme();
23
29
  const { question } = slide;
@@ -25,37 +31,29 @@ export const QuestionSlide = ({
25
31
  if (!question) return null;
26
32
 
27
33
  return (
28
- <ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false} bounces={false}>
29
- <View style={styles.slideContainer}>
30
- <QuestionSlideHeader slide={slide} />
31
-
32
- <View style={styles.questionContainer}>
33
- <QuestionRenderer question={question} value={value} onChange={onChange} />
34
- </View>
34
+ <BaseSlide>
35
+ <QuestionSlideHeader slide={slide} />
35
36
 
36
- {question.validation?.required && !value && (
37
- <AtomicText
38
- type="labelSmall"
39
- style={[styles.requiredHint, { color: colors.errorColor }]}
40
- >
41
- * This field is required
42
- </AtomicText>
43
- )}
37
+ <View style={styles.questionContainer}>
38
+ <QuestionRenderer question={question} value={value} onChange={onChange} />
44
39
  </View>
45
- </ScrollView>
40
+
41
+ {question.validation?.required && !value && (
42
+ <AtomicText
43
+ type="labelSmall"
44
+ style={[styles.requiredHint, { color: colors.errorColor }]}
45
+ >
46
+ * This field is required
47
+ </AtomicText>
48
+ )}
49
+ </BaseSlide>
46
50
  );
47
51
  };
48
52
 
49
53
  const styles = StyleSheet.create({
50
- content: {
51
- flexGrow: 1,
52
- paddingTop: 40,
53
- },
54
- slideContainer: {
55
- paddingHorizontal: 24,
56
- },
57
54
  questionContainer: {
58
55
  marginTop: 24,
56
+ width: "100%",
59
57
  },
60
58
  requiredHint: {
61
59
  marginTop: 12,
@@ -63,5 +61,3 @@ const styles = StyleSheet.create({
63
61
  fontWeight: "600",
64
62
  },
65
63
  });
66
-
67
-
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { View, TouchableOpacity, StyleSheet } from "react-native";
3
- import { AtomicIcon, AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
3
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
4
+ import { useOnboardingTheme } from "../../providers/OnboardingThemeProvider";
4
5
  import type { OnboardingQuestion, QuestionOption } from "../../../domain/entities/OnboardingQuestion";
5
6
 
6
7
  export interface MultipleChoiceQuestionProps {
@@ -14,10 +15,7 @@ export const MultipleChoiceQuestion = ({
14
15
  value = [],
15
16
  onChange,
16
17
  }: MultipleChoiceQuestionProps) => {
17
- const tokens = useAppDesignTokens();
18
-
19
- const isEmoji = (icon: string) =>
20
- /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u.test(icon);
18
+ const { colors } = useOnboardingTheme();
21
19
 
22
20
  const handleToggle = (optionId: string) => {
23
21
  const newValue = value.includes(optionId)
@@ -29,10 +27,10 @@ export const MultipleChoiceQuestion = ({
29
27
  }
30
28
  onChange(newValue);
31
29
  };
32
-
33
30
  const renderOption = (option: QuestionOption) => {
34
31
  const isSelected = value.includes(option.id);
35
- const textColor = isSelected ? tokens.colors.textPrimary : tokens.colors.textSecondary;
32
+ const textColor = isSelected ? colors.textColor : colors.subTextColor;
33
+ const isEmoji = option.iconType === 'emoji';
36
34
 
37
35
  return (
38
36
  <TouchableOpacity
@@ -40,8 +38,8 @@ export const MultipleChoiceQuestion = ({
40
38
  style={[
41
39
  styles.option,
42
40
  {
43
- backgroundColor: isSelected ? tokens.colors.primary + '10' : tokens.colors.surface,
44
- borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
41
+ backgroundColor: isSelected ? colors.iconBg : colors.featureItemBg,
42
+ borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
45
43
  }
46
44
  ]}
47
45
  onPress={() => handleToggle(option.id)}
@@ -49,7 +47,7 @@ export const MultipleChoiceQuestion = ({
49
47
  >
50
48
  {option.icon && (
51
49
  <View style={styles.optionIcon}>
52
- {isEmoji(option.icon) ? (
50
+ {isEmoji ? (
53
51
  <AtomicText style={{ fontSize: 24 }}>{option.icon}</AtomicText>
54
52
  ) : (
55
53
  <AtomicIcon name={option.icon as any} customSize={24} customColor={textColor} />
@@ -62,13 +60,13 @@ export const MultipleChoiceQuestion = ({
62
60
  <View style={[
63
61
  styles.checkbox,
64
62
  {
65
- borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
66
- backgroundColor: isSelected ? tokens.colors.primary : 'transparent',
63
+ borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
64
+ backgroundColor: isSelected ? colors.iconColor : 'transparent',
67
65
  borderWidth: isSelected ? 0 : 2,
68
66
  }
69
67
  ]}>
70
68
  {isSelected && (
71
- <AtomicIcon name="checkmark" customSize={16} customColor="#FFFFFF" />
69
+ <AtomicIcon name="checkmark" customSize={16} customColor={colors.buttonTextColor} />
72
70
  )}
73
71
  </View>
74
72
  </TouchableOpacity>
@@ -79,7 +77,7 @@ export const MultipleChoiceQuestion = ({
79
77
  <View style={styles.container}>
80
78
  {question.options?.map(renderOption)}
81
79
  {question.validation?.maxSelections && (
82
- <AtomicText type="labelSmall" style={[styles.hint, { color: tokens.colors.textSecondary }]}>
80
+ <AtomicText type="labelSmall" style={[styles.hint, { color: colors.subTextColor }]}>
83
81
  Select up to {question.validation.maxSelections} options
84
82
  </AtomicText>
85
83
  )}
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { View, TouchableOpacity, StyleSheet } from "react-native";
3
- import { AtomicIcon, AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
3
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
4
+ import { useOnboardingTheme } from "../../providers/OnboardingThemeProvider";
4
5
  import type { OnboardingQuestion } from "../../../domain/entities/OnboardingQuestion";
5
6
 
6
7
  export interface RatingQuestionProps {
@@ -14,7 +15,7 @@ export const RatingQuestion = ({
14
15
  value = 0,
15
16
  onChange,
16
17
  }: RatingQuestionProps) => {
17
- const tokens = useAppDesignTokens();
18
+ const { colors } = useOnboardingTheme();
18
19
  const max = question.validation?.max ?? 5;
19
20
 
20
21
  return (
@@ -27,14 +28,14 @@ export const RatingQuestion = ({
27
28
  <AtomicIcon
28
29
  name={isFilled ? "star" : "star-outline"}
29
30
  customSize={48}
30
- customColor={isFilled ? tokens.colors.warning : tokens.colors.border}
31
+ customColor={isFilled ? "#FFD700" : colors.subTextColor}
31
32
  />
32
33
  </TouchableOpacity>
33
34
  );
34
35
  })}
35
36
  </View>
36
37
  {value > 0 && (
37
- <AtomicText type="headlineSmall" style={[styles.valueText, { color: tokens.colors.textPrimary }]}>
38
+ <AtomicText type="headlineSmall" style={[styles.valueText, { color: colors.textColor }]}>
38
39
  {value} / {max}
39
40
  </AtomicText>
40
41
  )}
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { View, TouchableOpacity, StyleSheet } from "react-native";
3
- import { AtomicIcon, AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
3
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
4
+ import { useOnboardingTheme } from "../../providers/OnboardingThemeProvider";
4
5
  import type { OnboardingQuestion, QuestionOption } from "../../../domain/entities/OnboardingQuestion";
5
6
 
6
7
  export interface SingleChoiceQuestionProps {
@@ -14,14 +15,12 @@ export const SingleChoiceQuestion = ({
14
15
  value,
15
16
  onChange,
16
17
  }: SingleChoiceQuestionProps) => {
17
- const tokens = useAppDesignTokens();
18
-
19
- const isEmoji = (icon: string) =>
20
- /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u.test(icon);
18
+ const { colors } = useOnboardingTheme();
21
19
 
22
20
  const renderOption = (option: QuestionOption) => {
23
21
  const isSelected = value === option.id;
24
- const textColor = isSelected ? tokens.colors.textPrimary : tokens.colors.textSecondary;
22
+ const textColor = isSelected ? colors.textColor : colors.subTextColor;
23
+ const isEmoji = option.iconType === 'emoji';
25
24
 
26
25
  return (
27
26
  <TouchableOpacity
@@ -29,8 +28,8 @@ export const SingleChoiceQuestion = ({
29
28
  style={[
30
29
  styles.option,
31
30
  {
32
- backgroundColor: isSelected ? tokens.colors.primary + '10' : tokens.colors.surface,
33
- borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
31
+ backgroundColor: isSelected ? colors.iconBg : colors.featureItemBg,
32
+ borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
34
33
  }
35
34
  ]}
36
35
  onPress={() => onChange(option.id)}
@@ -38,7 +37,7 @@ export const SingleChoiceQuestion = ({
38
37
  >
39
38
  {option.icon && (
40
39
  <View style={styles.optionIcon}>
41
- {isEmoji(option.icon) ? (
40
+ {isEmoji ? (
42
41
  <AtomicText style={{ fontSize: 24 }}>{option.icon}</AtomicText>
43
42
  ) : (
44
43
  <AtomicIcon
@@ -55,7 +54,7 @@ export const SingleChoiceQuestion = ({
55
54
  <View style={[
56
55
  styles.radio,
57
56
  {
58
- borderColor: isSelected ? tokens.colors.primary : tokens.colors.border,
57
+ borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
59
58
  borderWidth: isSelected ? 6 : 2,
60
59
  }
61
60
  ]} />
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { View, TextInput, StyleSheet } from "react-native";
3
- import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
3
+ import { AtomicText } from "@umituz/react-native-design-system";
4
+ import { useOnboardingTheme } from "../../providers/OnboardingThemeProvider";
4
5
  import type { OnboardingQuestion } from "../../../domain/entities/OnboardingQuestion";
5
6
 
6
7
  export interface TextInputQuestionProps {
@@ -14,7 +15,7 @@ export const TextInputQuestion = ({
14
15
  value = "",
15
16
  onChange,
16
17
  }: TextInputQuestionProps) => {
17
- const tokens = useAppDesignTokens();
18
+ const { colors } = useOnboardingTheme();
18
19
  const { validation } = question;
19
20
 
20
21
  return (
@@ -23,15 +24,15 @@ export const TextInputQuestion = ({
23
24
  style={[
24
25
  styles.input,
25
26
  {
26
- backgroundColor: tokens.colors.surface,
27
- borderColor: tokens.colors.borderLight,
28
- color: tokens.colors.textPrimary,
27
+ backgroundColor: colors.featureItemBg,
28
+ borderColor: colors.headerButtonBorder,
29
+ color: colors.textColor,
29
30
  }
30
31
  ]}
31
32
  value={value}
32
33
  onChangeText={onChange}
33
- placeholder={question.placeholder || "Type your answer..."}
34
- placeholderTextColor={tokens.colors.textSecondary}
34
+ placeholder={question.placeholder}
35
+ placeholderTextColor={colors.subTextColor}
35
36
  maxLength={validation?.maxLength}
36
37
  multiline={(validation?.maxLength ?? 0) > 100}
37
38
  numberOfLines={(validation?.maxLength ?? 0) > 100 ? 5 : 1}
@@ -39,7 +40,7 @@ export const TextInputQuestion = ({
39
40
  autoCorrect={true}
40
41
  />
41
42
  {validation?.maxLength && (
42
- <AtomicText type="labelSmall" style={[styles.charCount, { color: tokens.colors.textSecondary }]}>
43
+ <AtomicText type="labelSmall" style={[styles.charCount, { color: colors.subTextColor }]}>
43
44
  {value.length} / {validation.maxLength}
44
45
  </AtomicText>
45
46
  )}
@@ -9,8 +9,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 { OnboardingSlideService } from "../../infrastructure/services/OnboardingSlideService";
13
- import { OnboardingValidationService } from "../../infrastructure/services/OnboardingValidationService";
12
+ import { SlideManager } from "../../infrastructure/services/SlideManager";
13
+ import { ValidationManager } from "../../infrastructure/services/ValidationManager";
14
14
  import { shouldUseGradient } from "../../infrastructure/utils/gradientUtils";
15
15
 
16
16
  export interface UseOnboardingScreenStateProps {
@@ -52,7 +52,7 @@ export function useOnboardingScreenState({
52
52
  return [];
53
53
  }
54
54
  const userData = onboardingStore.userData;
55
- return OnboardingSlideService.filterSlides(slides, userData);
55
+ return SlideManager.filterSlides(slides, userData);
56
56
  }, [slides, onboardingStore.userData]);
57
57
 
58
58
  // Navigation hook
@@ -82,7 +82,7 @@ export function useOnboardingScreenState({
82
82
 
83
83
  // Get current slide
84
84
  const currentSlide = useMemo(
85
- () => OnboardingSlideService.getSlideAtIndex(filteredSlides, currentIndex),
85
+ () => SlideManager.getSlideAtIndex(filteredSlides, currentIndex),
86
86
  [filteredSlides, currentIndex],
87
87
  );
88
88
 
@@ -97,14 +97,14 @@ export function useOnboardingScreenState({
97
97
  // Handle next slide with useCallback for performance
98
98
  const handleNext = useCallback(async () => {
99
99
  if (!currentSlide) return;
100
-
100
+
101
101
  try {
102
102
  await saveCurrentAnswer(currentSlide);
103
103
  if (isLastSlide) {
104
104
  await completeOnboarding();
105
105
  } else {
106
106
  goToNext();
107
- const nextSlide = OnboardingSlideService.getSlideAtIndex(
107
+ const nextSlide = SlideManager.getSlideAtIndex(
108
108
  filteredSlides,
109
109
  currentIndex + 1,
110
110
  );
@@ -123,7 +123,7 @@ export function useOnboardingScreenState({
123
123
  const handlePrevious = useCallback(() => {
124
124
  try {
125
125
  goToPrevious();
126
- const prevSlide = OnboardingSlideService.getSlideAtIndex(
126
+ const prevSlide = SlideManager.getSlideAtIndex(
127
127
  filteredSlides,
128
128
  currentIndex - 1,
129
129
  );
@@ -156,7 +156,7 @@ export function useOnboardingScreenState({
156
156
  if (!currentSlide?.question) {
157
157
  return true;
158
158
  }
159
- return OnboardingValidationService.validateAnswer(
159
+ return ValidationManager.validateAnswer(
160
160
  currentSlide.question,
161
161
  currentAnswer,
162
162
  );
@@ -17,6 +17,7 @@ interface OnboardingColors {
17
17
  iconBg: string;
18
18
  iconBorder: string;
19
19
  errorColor: string;
20
+ featureItemBg: string;
20
21
  }
21
22
 
22
23
  interface OnboardingThemeValue {
@@ -24,7 +25,7 @@ interface OnboardingThemeValue {
24
25
  useGradient: boolean;
25
26
  }
26
27
 
27
- const OnboardingTheme = createContext<OnboardingThemeValue | null>(null);
28
+ const OnboardingThemeInternal = createContext<OnboardingThemeValue | undefined>(undefined);
28
29
 
29
30
  export interface OnboardingThemeProviderProps {
30
31
  children: React.ReactNode;
@@ -38,28 +39,29 @@ export const OnboardingThemeProvider = ({
38
39
  const tokens = useAppDesignTokens();
39
40
 
40
41
  const colors = useMemo<OnboardingColors>(() => {
42
+ const primaryContent = tokens.colors.onPrimary || "#FFFFFF";
43
+
41
44
  if (useGradient) {
42
- // For gradient backgrounds (Christmas gradients), use white text
43
45
  return {
44
- iconColor: "#FFFFFF",
45
- textColor: "#FFFFFF",
46
- subTextColor: "rgba(255, 255, 255, 0.85)",
47
- buttonBg: "#FFFFFF",
46
+ iconColor: primaryContent,
47
+ textColor: primaryContent,
48
+ subTextColor: primaryContent + "CC",
49
+ buttonBg: primaryContent,
48
50
  buttonTextColor: tokens.colors.primary,
49
- progressBarBg: "rgba(255, 255, 255, 0.25)",
50
- progressFillColor: "#FFFFFF",
51
- dotColor: "rgba(255, 255, 255, 0.4)",
52
- activeDotColor: "#FFFFFF",
53
- progressTextColor: "rgba(255, 255, 255, 0.85)",
54
- headerButtonBg: "rgba(255, 255, 255, 0.2)",
55
- headerButtonBorder: "rgba(255, 255, 255, 0.35)",
56
- iconBg: "rgba(255, 255, 255, 0.2)",
57
- iconBorder: "rgba(255, 255, 255, 0.35)",
51
+ progressBarBg: primaryContent + "40",
52
+ progressFillColor: primaryContent,
53
+ dotColor: primaryContent + "66",
54
+ activeDotColor: primaryContent,
55
+ progressTextColor: primaryContent + "CC",
56
+ headerButtonBg: primaryContent + "33",
57
+ headerButtonBorder: primaryContent + "59",
58
+ iconBg: primaryContent + "33",
59
+ iconBorder: primaryContent + "59",
58
60
  errorColor: "#FFCDD2",
61
+ featureItemBg: primaryContent + "1A",
59
62
  };
60
63
  }
61
64
 
62
- // For non-gradient backgrounds, use theme colors
63
65
  return {
64
66
  iconColor: tokens.colors.primary,
65
67
  textColor: tokens.colors.textPrimary,
@@ -76,6 +78,7 @@ export const OnboardingThemeProvider = ({
76
78
  iconBg: tokens.colors.primary + '15',
77
79
  iconBorder: tokens.colors.primary + '30',
78
80
  errorColor: tokens.colors.error,
81
+ featureItemBg: tokens.colors.surfaceSecondary || tokens.colors.surfaceVariant || "rgba(0,0,0,0.05)",
79
82
  };
80
83
  }, [tokens, useGradient]);
81
84
 
@@ -85,14 +88,14 @@ export const OnboardingThemeProvider = ({
85
88
  );
86
89
 
87
90
  return (
88
- <OnboardingTheme.Provider value={value}>
91
+ <OnboardingThemeInternal.Provider value={value}>
89
92
  {children}
90
- </OnboardingTheme.Provider>
93
+ </OnboardingThemeInternal.Provider>
91
94
  );
92
95
  };
93
96
 
94
97
  export const useOnboardingTheme = (): OnboardingThemeValue => {
95
- const theme = useContext(OnboardingTheme);
98
+ const theme = useContext(OnboardingThemeInternal);
96
99
  if (!theme) {
97
100
  throw new Error('useOnboardingTheme must be used within OnboardingThemeProvider');
98
101
  }
@@ -67,7 +67,7 @@ export const OnboardingScreen = ({
67
67
  showDots = true,
68
68
  showProgressText = true,
69
69
  storageKey,
70
- autoComplete = false,
70
+ autoComplete: _autoComplete = false,
71
71
  renderHeader,
72
72
  renderFooter,
73
73
  renderSlide,