@umituz/react-native-onboarding 3.5.6 → 3.6.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 +2 -2
- package/src/infrastructure/storage/OnboardingStore.ts +8 -5
- package/src/presentation/components/QuestionSlide.tsx +3 -1
- package/src/presentation/components/questions/MultipleChoiceQuestion.tsx +21 -8
- package/src/presentation/components/questions/RatingQuestion.tsx +4 -4
- package/src/presentation/components/questions/SingleChoiceQuestion.tsx +34 -16
- package/src/presentation/components/questions/TextInputQuestion.tsx +2 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-onboarding",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0",
|
|
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",
|
|
@@ -72,4 +72,4 @@
|
|
|
72
72
|
"README.md",
|
|
73
73
|
"LICENSE"
|
|
74
74
|
]
|
|
75
|
-
}
|
|
75
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* Uses @umituz/react-native-storage for persistence
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import { useMemo } from "react";
|
|
8
9
|
import { create, StoreApi } from "zustand";
|
|
9
10
|
import type { OnboardingStoreState } from "./OnboardingStoreState";
|
|
10
11
|
import { initialOnboardingState } from "./OnboardingStoreState";
|
|
@@ -51,15 +52,17 @@ export const useOnboardingStore = create<OnboardingStore>((set: StoreApi<Onboard
|
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Hook for accessing onboarding state
|
|
55
|
+
* Memoized to prevent unnecessary re-renders in consumer components
|
|
54
56
|
*/
|
|
55
57
|
export const useOnboarding = () => {
|
|
56
58
|
const store = useOnboardingStore();
|
|
57
59
|
const setState = store.setState;
|
|
58
|
-
const getState =
|
|
59
|
-
const actions = createOnboardingStoreActions(setState, getState);
|
|
60
|
-
const selectors = createOnboardingStoreSelectors(getState);
|
|
60
|
+
const getState = store.getState;
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
const actions = useMemo(() => createOnboardingStoreActions(setState, getState), [setState, getState]);
|
|
63
|
+
const selectors = useMemo(() => createOnboardingStoreSelectors(getState), [getState]);
|
|
64
|
+
|
|
65
|
+
return useMemo(() => ({
|
|
63
66
|
// State
|
|
64
67
|
isOnboardingComplete: store.isOnboardingComplete,
|
|
65
68
|
currentStep: store.currentStep,
|
|
@@ -81,6 +84,6 @@ export const useOnboarding = () => {
|
|
|
81
84
|
// Selectors
|
|
82
85
|
getAnswer: selectors.getAnswer,
|
|
83
86
|
getUserData: selectors.getUserData,
|
|
84
|
-
};
|
|
87
|
+
}), [store, actions, selectors]);
|
|
85
88
|
};
|
|
86
89
|
|
|
@@ -11,6 +11,7 @@ import { QuestionSlideHeader } from "./QuestionSlideHeader";
|
|
|
11
11
|
import { QuestionRenderer } from "./QuestionRenderer";
|
|
12
12
|
import { BaseSlide } from "./BaseSlide";
|
|
13
13
|
import { useOnboardingTheme } from "../providers/OnboardingThemeProvider";
|
|
14
|
+
import { useLocalization } from "@umituz/react-native-localization";
|
|
14
15
|
|
|
15
16
|
export interface QuestionSlideProps {
|
|
16
17
|
slide: OnboardingSlide;
|
|
@@ -26,6 +27,7 @@ export const QuestionSlide = ({
|
|
|
26
27
|
variant: _variant = "default",
|
|
27
28
|
}: QuestionSlideProps) => {
|
|
28
29
|
const { colors } = useOnboardingTheme();
|
|
30
|
+
const { t } = useLocalization();
|
|
29
31
|
const { question } = slide;
|
|
30
32
|
|
|
31
33
|
if (!question) return null;
|
|
@@ -43,7 +45,7 @@ export const QuestionSlide = ({
|
|
|
43
45
|
type="labelSmall"
|
|
44
46
|
style={[styles.requiredHint, { color: colors.errorColor }]}
|
|
45
47
|
>
|
|
46
|
-
|
|
48
|
+
{t("onboarding.fieldRequired")}
|
|
47
49
|
</AtomicText>
|
|
48
50
|
)}
|
|
49
51
|
</BaseSlide>
|
|
@@ -29,7 +29,6 @@ export const MultipleChoiceQuestion = ({
|
|
|
29
29
|
};
|
|
30
30
|
const renderOption = (option: QuestionOption) => {
|
|
31
31
|
const isSelected = value.includes(option.id);
|
|
32
|
-
const textColor = isSelected ? colors.textColor : colors.subTextColor;
|
|
33
32
|
const isEmoji = option.iconType === 'emoji';
|
|
34
33
|
|
|
35
34
|
return (
|
|
@@ -40,21 +39,29 @@ export const MultipleChoiceQuestion = ({
|
|
|
40
39
|
{
|
|
41
40
|
backgroundColor: isSelected ? colors.iconBg : colors.featureItemBg,
|
|
42
41
|
borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
|
|
42
|
+
borderWidth: isSelected ? 2 : 1,
|
|
43
43
|
}
|
|
44
44
|
]}
|
|
45
45
|
onPress={() => handleToggle(option.id)}
|
|
46
|
-
activeOpacity={0.
|
|
46
|
+
activeOpacity={0.8}
|
|
47
47
|
>
|
|
48
48
|
{option.icon && (
|
|
49
|
-
<View style={
|
|
49
|
+
<View style={[
|
|
50
|
+
styles.optionIcon,
|
|
51
|
+
{ backgroundColor: isSelected ? colors.iconColor : colors.featureItemBg }
|
|
52
|
+
]}>
|
|
50
53
|
{isEmoji ? (
|
|
51
54
|
<AtomicText style={{ fontSize: 24 }}>{option.icon}</AtomicText>
|
|
52
55
|
) : (
|
|
53
|
-
<AtomicIcon
|
|
56
|
+
<AtomicIcon
|
|
57
|
+
name={option.icon as any}
|
|
58
|
+
customSize={20}
|
|
59
|
+
customColor={isSelected ? colors.buttonTextColor : colors.subTextColor}
|
|
60
|
+
/>
|
|
54
61
|
)}
|
|
55
62
|
</View>
|
|
56
63
|
)}
|
|
57
|
-
<AtomicText type="bodyLarge" style={[styles.optionLabel, { color: textColor, fontWeight: isSelected ? '700' : '500' }]}>
|
|
64
|
+
<AtomicText type="bodyLarge" style={[styles.optionLabel, { color: isSelected ? colors.textColor : colors.subTextColor, fontWeight: isSelected ? '700' : '500' }]}>
|
|
58
65
|
{option.label}
|
|
59
66
|
</AtomicText>
|
|
60
67
|
<View style={[
|
|
@@ -93,15 +100,21 @@ const styles = StyleSheet.create({
|
|
|
93
100
|
option: {
|
|
94
101
|
flexDirection: "row",
|
|
95
102
|
alignItems: "center",
|
|
96
|
-
borderRadius:
|
|
103
|
+
borderRadius: 20,
|
|
97
104
|
padding: 16,
|
|
98
|
-
|
|
105
|
+
marginBottom: 8,
|
|
99
106
|
},
|
|
100
107
|
optionIcon: {
|
|
101
|
-
|
|
108
|
+
width: 40,
|
|
109
|
+
height: 40,
|
|
110
|
+
borderRadius: 20,
|
|
111
|
+
alignItems: 'center',
|
|
112
|
+
justifyContent: 'center',
|
|
113
|
+
marginRight: 16,
|
|
102
114
|
},
|
|
103
115
|
optionLabel: {
|
|
104
116
|
flex: 1,
|
|
117
|
+
fontSize: 16,
|
|
105
118
|
},
|
|
106
119
|
checkbox: {
|
|
107
120
|
width: 24,
|
|
@@ -24,19 +24,19 @@ export const RatingQuestion = ({
|
|
|
24
24
|
{Array.from({ length: max }).map((_, i) => {
|
|
25
25
|
const isFilled = i < value;
|
|
26
26
|
return (
|
|
27
|
-
<TouchableOpacity key={i} onPress={() => onChange(i + 1)} activeOpacity={0.
|
|
27
|
+
<TouchableOpacity key={i} onPress={() => onChange(i + 1)} activeOpacity={0.8} style={styles.star}>
|
|
28
28
|
<AtomicIcon
|
|
29
29
|
name={isFilled ? "star" : "star-outline"}
|
|
30
30
|
customSize={48}
|
|
31
|
-
customColor={isFilled ?
|
|
31
|
+
customColor={isFilled ? colors.iconColor : colors.headerButtonBorder}
|
|
32
32
|
/>
|
|
33
33
|
</TouchableOpacity>
|
|
34
34
|
);
|
|
35
35
|
})}
|
|
36
36
|
</View>
|
|
37
37
|
{value > 0 && (
|
|
38
|
-
<AtomicText type="headlineSmall" style={[styles.valueText, { color: colors.textColor }]}>
|
|
39
|
-
{value}
|
|
38
|
+
<AtomicText type="headlineSmall" style={[styles.valueText, { color: colors.textColor, marginTop: 12 }]}>
|
|
39
|
+
{value} <AtomicText type="bodyMedium" color="textSecondary">/ {max}</AtomicText>
|
|
40
40
|
</AtomicText>
|
|
41
41
|
)}
|
|
42
42
|
</View>
|
|
@@ -19,7 +19,6 @@ export const SingleChoiceQuestion = ({
|
|
|
19
19
|
|
|
20
20
|
const renderOption = (option: QuestionOption) => {
|
|
21
21
|
const isSelected = value === option.id;
|
|
22
|
-
const textColor = isSelected ? colors.textColor : colors.subTextColor;
|
|
23
22
|
const isEmoji = option.iconType === 'emoji';
|
|
24
23
|
|
|
25
24
|
return (
|
|
@@ -30,34 +29,39 @@ export const SingleChoiceQuestion = ({
|
|
|
30
29
|
{
|
|
31
30
|
backgroundColor: isSelected ? colors.iconBg : colors.featureItemBg,
|
|
32
31
|
borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
|
|
32
|
+
borderWidth: isSelected ? 2 : 1,
|
|
33
33
|
}
|
|
34
34
|
]}
|
|
35
35
|
onPress={() => onChange(option.id)}
|
|
36
|
-
activeOpacity={0.
|
|
36
|
+
activeOpacity={0.8}
|
|
37
37
|
>
|
|
38
38
|
{option.icon && (
|
|
39
|
-
<View style={
|
|
39
|
+
<View style={[
|
|
40
|
+
styles.optionIcon,
|
|
41
|
+
{ backgroundColor: isSelected ? colors.iconColor : colors.featureItemBg }
|
|
42
|
+
]}>
|
|
40
43
|
{isEmoji ? (
|
|
41
44
|
<AtomicText style={{ fontSize: 24 }}>{option.icon}</AtomicText>
|
|
42
45
|
) : (
|
|
43
46
|
<AtomicIcon
|
|
44
47
|
name={option.icon as any}
|
|
45
|
-
customSize={
|
|
46
|
-
customColor={
|
|
48
|
+
customSize={20}
|
|
49
|
+
customColor={isSelected ? colors.buttonTextColor : colors.subTextColor}
|
|
47
50
|
/>
|
|
48
51
|
)}
|
|
49
52
|
</View>
|
|
50
53
|
)}
|
|
51
|
-
<AtomicText type="bodyLarge" style={[styles.optionLabel, { color: textColor, fontWeight: isSelected ? '700' : '500' }]}>
|
|
54
|
+
<AtomicText type="bodyLarge" style={[styles.optionLabel, { color: isSelected ? colors.textColor : colors.subTextColor, fontWeight: isSelected ? '700' : '500' }]}>
|
|
52
55
|
{option.label}
|
|
53
56
|
</AtomicText>
|
|
54
57
|
<View style={[
|
|
55
|
-
styles.
|
|
56
|
-
{
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
styles.radioOuter,
|
|
59
|
+
{ borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder }
|
|
60
|
+
]}>
|
|
61
|
+
{isSelected && (
|
|
62
|
+
<View style={[styles.radioInner, { backgroundColor: colors.iconColor }]} />
|
|
63
|
+
)}
|
|
64
|
+
</View>
|
|
61
65
|
</TouchableOpacity>
|
|
62
66
|
);
|
|
63
67
|
};
|
|
@@ -77,20 +81,34 @@ const styles = StyleSheet.create({
|
|
|
77
81
|
option: {
|
|
78
82
|
flexDirection: "row",
|
|
79
83
|
alignItems: "center",
|
|
80
|
-
borderRadius:
|
|
84
|
+
borderRadius: 20,
|
|
81
85
|
padding: 16,
|
|
82
|
-
|
|
86
|
+
marginBottom: 8,
|
|
83
87
|
},
|
|
84
88
|
optionIcon: {
|
|
85
|
-
|
|
89
|
+
width: 40,
|
|
90
|
+
height: 40,
|
|
91
|
+
borderRadius: 20,
|
|
92
|
+
alignItems: 'center',
|
|
93
|
+
justifyContent: 'center',
|
|
94
|
+
marginRight: 16,
|
|
86
95
|
},
|
|
87
96
|
optionLabel: {
|
|
88
97
|
flex: 1,
|
|
98
|
+
fontSize: 16,
|
|
89
99
|
},
|
|
90
|
-
|
|
100
|
+
radioOuter: {
|
|
91
101
|
width: 24,
|
|
92
102
|
height: 24,
|
|
93
103
|
borderRadius: 12,
|
|
104
|
+
borderWidth: 2,
|
|
105
|
+
alignItems: 'center',
|
|
106
|
+
justifyContent: 'center',
|
|
107
|
+
},
|
|
108
|
+
radioInner: {
|
|
109
|
+
width: 12,
|
|
110
|
+
height: 12,
|
|
111
|
+
borderRadius: 6,
|
|
94
112
|
},
|
|
95
113
|
});
|
|
96
114
|
|
|
@@ -25,7 +25,7 @@ export const TextInputQuestion = ({
|
|
|
25
25
|
styles.input,
|
|
26
26
|
{
|
|
27
27
|
backgroundColor: colors.featureItemBg,
|
|
28
|
-
borderColor: colors.headerButtonBorder,
|
|
28
|
+
borderColor: value ? colors.iconColor : colors.headerButtonBorder,
|
|
29
29
|
color: colors.textColor,
|
|
30
30
|
}
|
|
31
31
|
]}
|
|
@@ -38,6 +38,7 @@ export const TextInputQuestion = ({
|
|
|
38
38
|
numberOfLines={(validation?.maxLength ?? 0) > 100 ? 5 : 1}
|
|
39
39
|
autoCapitalize="sentences"
|
|
40
40
|
autoCorrect={true}
|
|
41
|
+
textAlignVertical="top"
|
|
41
42
|
/>
|
|
42
43
|
{validation?.maxLength && (
|
|
43
44
|
<AtomicText type="labelSmall" style={[styles.charCount, { color: colors.subTextColor }]}>
|