@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 +21 -15
- package/src/domain/entities/OnboardingQuestion.ts +10 -0
- package/src/infrastructure/hooks/useOnboardingNavigation.ts +1 -1
- package/src/infrastructure/services/{OnboardingSlideService.ts → SlideManager.ts} +2 -2
- package/src/infrastructure/services/{OnboardingValidationService.ts → ValidationManager.ts} +2 -2
- package/src/presentation/components/BaseSlide.tsx +41 -0
- package/src/presentation/components/OnboardingFooter.tsx +3 -3
- package/src/presentation/components/OnboardingHeader.tsx +1 -1
- package/src/presentation/components/OnboardingResetSetting.tsx +3 -3
- package/src/presentation/components/OnboardingScreenContent.tsx +0 -1
- package/src/presentation/components/OnboardingSlide.tsx +69 -54
- package/src/presentation/components/QuestionSlide.tsx +24 -28
- package/src/presentation/components/questions/MultipleChoiceQuestion.tsx +12 -14
- package/src/presentation/components/questions/RatingQuestion.tsx +5 -4
- package/src/presentation/components/questions/SingleChoiceQuestion.tsx +9 -10
- package/src/presentation/components/questions/TextInputQuestion.tsx +9 -8
- package/src/presentation/hooks/useOnboardingScreenState.ts +8 -8
- package/src/presentation/providers/OnboardingThemeProvider.tsx +22 -19
- package/src/presentation/screens/OnboardingScreen.tsx +1 -1
package/package.json
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-onboarding",
|
|
3
|
-
"version": "3.5.
|
|
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": "
|
|
9
|
-
"lint": "
|
|
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
|
-
|
|
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
|
-
*
|
|
12
|
+
* SlideManager
|
|
13
13
|
*/
|
|
14
|
-
export class
|
|
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
|
-
*
|
|
11
|
+
* ValidationManager
|
|
12
12
|
*/
|
|
13
|
-
export class
|
|
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")
|
|
39
|
-
: nextButtonText || t("general.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")
|
|
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")
|
|
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
|
|
19
|
-
description
|
|
20
|
-
iconName = "
|
|
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,
|
|
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
|
|
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?:
|
|
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
|
-
<
|
|
25
|
-
|
|
26
|
-
{
|
|
27
|
-
|
|
28
|
-
{
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
44
|
+
<AtomicText
|
|
45
|
+
type="displaySmall"
|
|
46
|
+
style={[styles.title, { color: colors.textColor }]}
|
|
47
|
+
>
|
|
48
|
+
{slide.title}
|
|
49
|
+
</AtomicText>
|
|
39
50
|
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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:
|
|
72
|
-
height:
|
|
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
|
-
|
|
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,
|
|
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?:
|
|
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
|
-
<
|
|
29
|
-
<
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ?
|
|
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 ?
|
|
44
|
-
borderColor: isSelected ?
|
|
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
|
|
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 ?
|
|
66
|
-
backgroundColor: isSelected ?
|
|
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=
|
|
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:
|
|
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
|
|
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
|
|
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 ?
|
|
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:
|
|
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
|
|
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
|
|
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 ?
|
|
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 ?
|
|
33
|
-
borderColor: isSelected ?
|
|
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
|
|
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 ?
|
|
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
|
|
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
|
|
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:
|
|
27
|
-
borderColor:
|
|
28
|
-
color:
|
|
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
|
|
34
|
-
placeholderTextColor={
|
|
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:
|
|
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 {
|
|
13
|
-
import {
|
|
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
|
|
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
|
-
() =>
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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:
|
|
45
|
-
textColor:
|
|
46
|
-
subTextColor:
|
|
47
|
-
buttonBg:
|
|
46
|
+
iconColor: primaryContent,
|
|
47
|
+
textColor: primaryContent,
|
|
48
|
+
subTextColor: primaryContent + "CC",
|
|
49
|
+
buttonBg: primaryContent,
|
|
48
50
|
buttonTextColor: tokens.colors.primary,
|
|
49
|
-
progressBarBg:
|
|
50
|
-
progressFillColor:
|
|
51
|
-
dotColor:
|
|
52
|
-
activeDotColor:
|
|
53
|
-
progressTextColor:
|
|
54
|
-
headerButtonBg:
|
|
55
|
-
headerButtonBorder:
|
|
56
|
-
iconBg:
|
|
57
|
-
iconBorder:
|
|
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
|
-
<
|
|
91
|
+
<OnboardingThemeInternal.Provider value={value}>
|
|
89
92
|
{children}
|
|
90
|
-
</
|
|
93
|
+
</OnboardingThemeInternal.Provider>
|
|
91
94
|
);
|
|
92
95
|
};
|
|
93
96
|
|
|
94
97
|
export const useOnboardingTheme = (): OnboardingThemeValue => {
|
|
95
|
-
const theme = useContext(
|
|
98
|
+
const theme = useContext(OnboardingThemeInternal);
|
|
96
99
|
if (!theme) {
|
|
97
100
|
throw new Error('useOnboardingTheme must be used within OnboardingThemeProvider');
|
|
98
101
|
}
|