@umituz/react-native-onboarding 2.6.7 → 2.6.9
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 +1 -1
- package/src/presentation/components/OnboardingScreenContent.tsx +173 -0
- package/src/presentation/components/QuestionRenderer.tsx +16 -1
- package/src/presentation/components/QuestionSlide.tsx +18 -1
- package/src/presentation/components/questions/SliderQuestion.tsx +28 -113
- package/src/presentation/screens/OnboardingScreen.tsx +65 -99
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-onboarding",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.9",
|
|
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",
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Screen Content Component
|
|
3
|
+
* Single Responsibility: Render onboarding screen content (header, slide, footer)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet, StatusBar } from "react-native";
|
|
8
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
9
|
+
import { useTheme } from "@umituz/react-native-design-system-theme";
|
|
10
|
+
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
11
|
+
import { OnboardingHeader } from "./OnboardingHeader";
|
|
12
|
+
import { OnboardingSlide as OnboardingSlideComponent } from "./OnboardingSlide";
|
|
13
|
+
import { QuestionSlide } from "./QuestionSlide";
|
|
14
|
+
import { OnboardingFooter } from "./OnboardingFooter";
|
|
15
|
+
|
|
16
|
+
export interface OnboardingScreenContentProps {
|
|
17
|
+
containerStyle?: any;
|
|
18
|
+
useGradient: boolean;
|
|
19
|
+
currentSlide: OnboardingSlide | undefined;
|
|
20
|
+
isFirstSlide: boolean;
|
|
21
|
+
isLastSlide: boolean;
|
|
22
|
+
currentIndex: number;
|
|
23
|
+
totalSlides: number;
|
|
24
|
+
currentAnswer: any;
|
|
25
|
+
isAnswerValid: boolean;
|
|
26
|
+
showBackButton: boolean;
|
|
27
|
+
showSkipButton: boolean;
|
|
28
|
+
showProgressBar: boolean;
|
|
29
|
+
showDots: boolean;
|
|
30
|
+
showProgressText: boolean;
|
|
31
|
+
skipButtonText?: string;
|
|
32
|
+
nextButtonText?: string;
|
|
33
|
+
getStartedButtonText?: string;
|
|
34
|
+
onBack: () => void;
|
|
35
|
+
onSkip: () => void;
|
|
36
|
+
onNext: () => void;
|
|
37
|
+
onAnswerChange: (value: any) => void;
|
|
38
|
+
renderHeader?: (props: {
|
|
39
|
+
isFirstSlide: boolean;
|
|
40
|
+
onBack: () => void;
|
|
41
|
+
onSkip: () => void;
|
|
42
|
+
}) => React.ReactNode;
|
|
43
|
+
renderFooter?: (props: {
|
|
44
|
+
currentIndex: number;
|
|
45
|
+
totalSlides: number;
|
|
46
|
+
isLastSlide: boolean;
|
|
47
|
+
onNext: () => void;
|
|
48
|
+
onUpgrade?: () => void;
|
|
49
|
+
showPaywallOnComplete?: boolean;
|
|
50
|
+
}) => React.ReactNode;
|
|
51
|
+
renderSlide?: (slide: OnboardingSlide) => React.ReactNode;
|
|
52
|
+
onUpgrade?: () => void;
|
|
53
|
+
showPaywallOnComplete?: boolean;
|
|
54
|
+
SliderComponent?: React.ComponentType<{
|
|
55
|
+
style?: any;
|
|
56
|
+
minimumValue: number;
|
|
57
|
+
maximumValue: number;
|
|
58
|
+
value: number;
|
|
59
|
+
onValueChange: (value: number) => void;
|
|
60
|
+
minimumTrackTintColor?: string;
|
|
61
|
+
maximumTrackTintColor?: string;
|
|
62
|
+
thumbTintColor?: string;
|
|
63
|
+
step?: number;
|
|
64
|
+
}>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const OnboardingScreenContent: React.FC<OnboardingScreenContentProps> = ({
|
|
68
|
+
containerStyle,
|
|
69
|
+
useGradient,
|
|
70
|
+
currentSlide,
|
|
71
|
+
isFirstSlide,
|
|
72
|
+
isLastSlide,
|
|
73
|
+
currentIndex,
|
|
74
|
+
totalSlides,
|
|
75
|
+
currentAnswer,
|
|
76
|
+
isAnswerValid,
|
|
77
|
+
showBackButton,
|
|
78
|
+
showSkipButton,
|
|
79
|
+
showProgressBar,
|
|
80
|
+
showDots,
|
|
81
|
+
showProgressText,
|
|
82
|
+
skipButtonText,
|
|
83
|
+
nextButtonText,
|
|
84
|
+
getStartedButtonText,
|
|
85
|
+
onBack,
|
|
86
|
+
onSkip,
|
|
87
|
+
onNext,
|
|
88
|
+
onAnswerChange,
|
|
89
|
+
renderHeader,
|
|
90
|
+
renderFooter,
|
|
91
|
+
renderSlide,
|
|
92
|
+
onUpgrade,
|
|
93
|
+
showPaywallOnComplete,
|
|
94
|
+
SliderComponent,
|
|
95
|
+
}) => {
|
|
96
|
+
const { themeMode } = useTheme();
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<View style={[styles.container, containerStyle]}>
|
|
100
|
+
<StatusBar barStyle={themeMode === "dark" ? "light-content" : "dark-content"} />
|
|
101
|
+
{useGradient && currentSlide && (
|
|
102
|
+
<LinearGradient
|
|
103
|
+
colors={currentSlide.gradient as [string, string, ...string[]]}
|
|
104
|
+
start={{ x: 0, y: 0 }}
|
|
105
|
+
end={{ x: 1, y: 1 }}
|
|
106
|
+
style={StyleSheet.absoluteFill}
|
|
107
|
+
/>
|
|
108
|
+
)}
|
|
109
|
+
{renderHeader ? (
|
|
110
|
+
renderHeader({
|
|
111
|
+
isFirstSlide,
|
|
112
|
+
onBack,
|
|
113
|
+
onSkip,
|
|
114
|
+
})
|
|
115
|
+
) : (
|
|
116
|
+
<OnboardingHeader
|
|
117
|
+
isFirstSlide={isFirstSlide}
|
|
118
|
+
onBack={onBack}
|
|
119
|
+
onSkip={onSkip}
|
|
120
|
+
showBackButton={showBackButton}
|
|
121
|
+
showSkipButton={showSkipButton}
|
|
122
|
+
skipButtonText={skipButtonText}
|
|
123
|
+
useGradient={useGradient}
|
|
124
|
+
/>
|
|
125
|
+
)}
|
|
126
|
+
{currentSlide &&
|
|
127
|
+
(renderSlide ? (
|
|
128
|
+
renderSlide(currentSlide)
|
|
129
|
+
) : currentSlide.type === "question" && currentSlide.question ? (
|
|
130
|
+
<QuestionSlide
|
|
131
|
+
slide={currentSlide}
|
|
132
|
+
value={currentAnswer}
|
|
133
|
+
onChange={onAnswerChange}
|
|
134
|
+
useGradient={useGradient}
|
|
135
|
+
SliderComponent={SliderComponent}
|
|
136
|
+
/>
|
|
137
|
+
) : (
|
|
138
|
+
<OnboardingSlideComponent slide={currentSlide} useGradient={useGradient} />
|
|
139
|
+
))}
|
|
140
|
+
{renderFooter ? (
|
|
141
|
+
renderFooter({
|
|
142
|
+
currentIndex,
|
|
143
|
+
totalSlides,
|
|
144
|
+
isLastSlide,
|
|
145
|
+
onNext,
|
|
146
|
+
onUpgrade,
|
|
147
|
+
showPaywallOnComplete,
|
|
148
|
+
})
|
|
149
|
+
) : (
|
|
150
|
+
<OnboardingFooter
|
|
151
|
+
currentIndex={currentIndex}
|
|
152
|
+
totalSlides={totalSlides}
|
|
153
|
+
isLastSlide={isLastSlide}
|
|
154
|
+
onNext={onNext}
|
|
155
|
+
showProgressBar={showProgressBar}
|
|
156
|
+
showDots={showDots}
|
|
157
|
+
showProgressText={showProgressText}
|
|
158
|
+
nextButtonText={nextButtonText}
|
|
159
|
+
getStartedButtonText={getStartedButtonText}
|
|
160
|
+
disabled={!isAnswerValid}
|
|
161
|
+
useGradient={useGradient}
|
|
162
|
+
/>
|
|
163
|
+
)}
|
|
164
|
+
</View>
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const styles = StyleSheet.create({
|
|
169
|
+
container: {
|
|
170
|
+
flex: 1,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
@@ -15,12 +15,24 @@ export interface QuestionRendererProps {
|
|
|
15
15
|
question: OnboardingQuestion;
|
|
16
16
|
value: any;
|
|
17
17
|
onChange: (value: any) => void;
|
|
18
|
+
SliderComponent?: React.ComponentType<{
|
|
19
|
+
style?: any;
|
|
20
|
+
minimumValue: number;
|
|
21
|
+
maximumValue: number;
|
|
22
|
+
value: number;
|
|
23
|
+
onValueChange: (value: number) => void;
|
|
24
|
+
minimumTrackTintColor?: string;
|
|
25
|
+
maximumTrackTintColor?: string;
|
|
26
|
+
thumbTintColor?: string;
|
|
27
|
+
step?: number;
|
|
28
|
+
}>;
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
export const QuestionRenderer: React.FC<QuestionRendererProps> = ({
|
|
21
32
|
question,
|
|
22
33
|
value,
|
|
23
34
|
onChange,
|
|
35
|
+
SliderComponent,
|
|
24
36
|
}) => {
|
|
25
37
|
switch (question.type) {
|
|
26
38
|
case "single_choice":
|
|
@@ -48,11 +60,15 @@ export const QuestionRenderer: React.FC<QuestionRendererProps> = ({
|
|
|
48
60
|
/>
|
|
49
61
|
);
|
|
50
62
|
case "slider":
|
|
63
|
+
if (!SliderComponent) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
51
66
|
return (
|
|
52
67
|
<SliderQuestion
|
|
53
68
|
question={question}
|
|
54
69
|
value={value}
|
|
55
70
|
onChange={onChange}
|
|
71
|
+
SliderComponent={SliderComponent}
|
|
56
72
|
/>
|
|
57
73
|
);
|
|
58
74
|
case "rating":
|
|
@@ -67,4 +83,3 @@ export const QuestionRenderer: React.FC<QuestionRendererProps> = ({
|
|
|
67
83
|
return null;
|
|
68
84
|
}
|
|
69
85
|
};
|
|
70
|
-
|
|
@@ -15,6 +15,17 @@ export interface QuestionSlideProps {
|
|
|
15
15
|
value: any;
|
|
16
16
|
onChange: (value: any) => void;
|
|
17
17
|
useGradient?: boolean;
|
|
18
|
+
SliderComponent?: React.ComponentType<{
|
|
19
|
+
style?: any;
|
|
20
|
+
minimumValue: number;
|
|
21
|
+
maximumValue: number;
|
|
22
|
+
value: number;
|
|
23
|
+
onValueChange: (value: number) => void;
|
|
24
|
+
minimumTrackTintColor?: string;
|
|
25
|
+
maximumTrackTintColor?: string;
|
|
26
|
+
thumbTintColor?: string;
|
|
27
|
+
step?: number;
|
|
28
|
+
}>;
|
|
18
29
|
}
|
|
19
30
|
|
|
20
31
|
export const QuestionSlide: React.FC<QuestionSlideProps> = ({
|
|
@@ -22,6 +33,7 @@ export const QuestionSlide: React.FC<QuestionSlideProps> = ({
|
|
|
22
33
|
value,
|
|
23
34
|
onChange,
|
|
24
35
|
useGradient = false,
|
|
36
|
+
SliderComponent,
|
|
25
37
|
}) => {
|
|
26
38
|
const tokens = useAppDesignTokens();
|
|
27
39
|
const styles = useMemo(() => getStyles(tokens, useGradient), [tokens, useGradient]);
|
|
@@ -36,7 +48,12 @@ export const QuestionSlide: React.FC<QuestionSlideProps> = ({
|
|
|
36
48
|
<QuestionSlideHeader slide={slide} useGradient={useGradient} />
|
|
37
49
|
|
|
38
50
|
<View style={styles.questionContainer}>
|
|
39
|
-
<QuestionRenderer
|
|
51
|
+
<QuestionRenderer
|
|
52
|
+
question={question}
|
|
53
|
+
value={value}
|
|
54
|
+
onChange={onChange}
|
|
55
|
+
SliderComponent={SliderComponent}
|
|
56
|
+
/>
|
|
40
57
|
</View>
|
|
41
58
|
|
|
42
59
|
{question.validation?.required && !value && (
|
|
@@ -4,22 +4,24 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { useMemo } from "react";
|
|
7
|
-
import { View, Text,
|
|
7
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
8
8
|
import type { OnboardingQuestion } from "../../../domain/entities/OnboardingQuestion";
|
|
9
9
|
|
|
10
|
-
// Lazy import slider to handle peer dependency gracefully
|
|
11
|
-
let SliderComponent: any = null;
|
|
12
|
-
try {
|
|
13
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
14
|
-
SliderComponent = require("@react-native-community/slider").default;
|
|
15
|
-
} catch {
|
|
16
|
-
// Slider not available - will show fallback
|
|
17
|
-
}
|
|
18
|
-
|
|
19
10
|
export interface SliderQuestionProps {
|
|
20
11
|
question: OnboardingQuestion;
|
|
21
12
|
value: number | undefined;
|
|
22
13
|
onChange: (value: number) => void;
|
|
14
|
+
SliderComponent: React.ComponentType<{
|
|
15
|
+
style?: any;
|
|
16
|
+
minimumValue: number;
|
|
17
|
+
maximumValue: number;
|
|
18
|
+
value: number;
|
|
19
|
+
onValueChange: (value: number) => void;
|
|
20
|
+
minimumTrackTintColor?: string;
|
|
21
|
+
maximumTrackTintColor?: string;
|
|
22
|
+
thumbTintColor?: string;
|
|
23
|
+
step?: number;
|
|
24
|
+
}>;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const getSliderConfig = (question: OnboardingQuestion) => {
|
|
@@ -29,87 +31,35 @@ const getSliderConfig = (question: OnboardingQuestion) => {
|
|
|
29
31
|
return { min, max };
|
|
30
32
|
};
|
|
31
33
|
|
|
32
|
-
const SliderFallback: React.FC<{
|
|
33
|
-
min: number;
|
|
34
|
-
max: number;
|
|
35
|
-
value: number;
|
|
36
|
-
onChange: (value: number) => void;
|
|
37
|
-
}> = ({ min, max, value, onChange }) => {
|
|
38
|
-
const handleIncrement = () => {
|
|
39
|
-
if (value < max) {
|
|
40
|
-
onChange(value + 1);
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const handleDecrement = () => {
|
|
45
|
-
if (value > min) {
|
|
46
|
-
onChange(value - 1);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
return (
|
|
51
|
-
<View style={styles.fallbackContainer}>
|
|
52
|
-
<View style={styles.fallbackControls}>
|
|
53
|
-
<TouchableOpacity
|
|
54
|
-
style={styles.fallbackButton}
|
|
55
|
-
onPress={handleDecrement}
|
|
56
|
-
activeOpacity={0.7}
|
|
57
|
-
>
|
|
58
|
-
<Text style={styles.fallbackButtonText}>−</Text>
|
|
59
|
-
</TouchableOpacity>
|
|
60
|
-
<Text style={styles.fallbackValue}>{value}</Text>
|
|
61
|
-
<TouchableOpacity
|
|
62
|
-
style={styles.fallbackButton}
|
|
63
|
-
onPress={handleIncrement}
|
|
64
|
-
activeOpacity={0.7}
|
|
65
|
-
>
|
|
66
|
-
<Text style={styles.fallbackButtonText}>+</Text>
|
|
67
|
-
</TouchableOpacity>
|
|
68
|
-
</View>
|
|
69
|
-
<View style={styles.fallbackLabels}>
|
|
70
|
-
<Text style={styles.label}>{min}</Text>
|
|
71
|
-
<Text style={styles.label}>{max}</Text>
|
|
72
|
-
</View>
|
|
73
|
-
</View>
|
|
74
|
-
);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
34
|
export const SliderQuestion: React.FC<SliderQuestionProps> = ({
|
|
78
35
|
question,
|
|
79
36
|
value,
|
|
80
37
|
onChange,
|
|
38
|
+
SliderComponent,
|
|
81
39
|
}) => {
|
|
82
40
|
const { min, max } = useMemo(() => getSliderConfig(question), [question]);
|
|
83
41
|
const currentValue = value ?? min;
|
|
84
42
|
|
|
85
|
-
const sliderProps = {
|
|
86
|
-
style: styles.slider,
|
|
87
|
-
minimumValue: min,
|
|
88
|
-
maximumValue: max,
|
|
89
|
-
value: currentValue,
|
|
90
|
-
onValueChange: onChange,
|
|
91
|
-
minimumTrackTintColor: "#FFFFFF",
|
|
92
|
-
maximumTrackTintColor: "rgba(255, 255, 255, 0.3)",
|
|
93
|
-
thumbTintColor: "#FFFFFF",
|
|
94
|
-
step: 1,
|
|
95
|
-
};
|
|
96
|
-
|
|
97
43
|
return (
|
|
98
44
|
<View style={styles.container}>
|
|
99
45
|
<View style={styles.valueContainer}>
|
|
100
46
|
<Text style={styles.valueText}>{currentValue}</Text>
|
|
101
47
|
</View>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
48
|
+
<SliderComponent
|
|
49
|
+
style={styles.slider}
|
|
50
|
+
minimumValue={min}
|
|
51
|
+
maximumValue={max}
|
|
52
|
+
value={currentValue}
|
|
53
|
+
onValueChange={onChange}
|
|
54
|
+
minimumTrackTintColor="#FFFFFF"
|
|
55
|
+
maximumTrackTintColor="rgba(255, 255, 255, 0.3)"
|
|
56
|
+
thumbTintColor="#FFFFFF"
|
|
57
|
+
step={1}
|
|
58
|
+
/>
|
|
59
|
+
<View style={styles.labels}>
|
|
60
|
+
<Text style={styles.label}>{min}</Text>
|
|
61
|
+
<Text style={styles.label}>{max}</Text>
|
|
62
|
+
</View>
|
|
113
63
|
</View>
|
|
114
64
|
);
|
|
115
65
|
};
|
|
@@ -142,39 +92,4 @@ const styles = StyleSheet.create({
|
|
|
142
92
|
color: "rgba(255, 255, 255, 0.7)",
|
|
143
93
|
fontWeight: "500",
|
|
144
94
|
},
|
|
145
|
-
fallbackContainer: {
|
|
146
|
-
width: "100%",
|
|
147
|
-
},
|
|
148
|
-
fallbackControls: {
|
|
149
|
-
flexDirection: "row",
|
|
150
|
-
alignItems: "center",
|
|
151
|
-
justifyContent: "center",
|
|
152
|
-
gap: 24,
|
|
153
|
-
},
|
|
154
|
-
fallbackButton: {
|
|
155
|
-
width: 60,
|
|
156
|
-
height: 60,
|
|
157
|
-
backgroundColor: "rgba(255, 255, 255, 0.2)",
|
|
158
|
-
borderRadius: 30,
|
|
159
|
-
alignItems: "center",
|
|
160
|
-
justifyContent: "center",
|
|
161
|
-
},
|
|
162
|
-
fallbackButtonText: {
|
|
163
|
-
fontSize: 48,
|
|
164
|
-
fontWeight: "bold",
|
|
165
|
-
color: "#FFFFFF",
|
|
166
|
-
lineHeight: 60,
|
|
167
|
-
},
|
|
168
|
-
fallbackValue: {
|
|
169
|
-
fontSize: 36,
|
|
170
|
-
fontWeight: "bold",
|
|
171
|
-
color: "#FFFFFF",
|
|
172
|
-
minWidth: 80,
|
|
173
|
-
textAlign: "center",
|
|
174
|
-
},
|
|
175
|
-
fallbackLabels: {
|
|
176
|
-
flexDirection: "row",
|
|
177
|
-
justifyContent: "space-between",
|
|
178
|
-
marginTop: 16,
|
|
179
|
-
},
|
|
180
95
|
});
|
|
@@ -3,15 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Main onboarding screen component with theme-aware colors
|
|
5
5
|
* Generic and reusable across hundreds of apps
|
|
6
|
-
*
|
|
6
|
+
*
|
|
7
7
|
* This component only handles UI coordination - no business logic
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import React, { useMemo } from "react";
|
|
11
|
-
import {
|
|
12
|
-
import { LinearGradient } from "expo-linear-gradient";
|
|
11
|
+
import { StyleSheet } from "react-native";
|
|
13
12
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
14
|
-
import { useAppDesignTokens
|
|
13
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
15
14
|
import type { OnboardingOptions } from "../../domain/entities/OnboardingOptions";
|
|
16
15
|
import { useOnboardingNavigation } from "../../infrastructure/hooks/useOnboardingNavigation";
|
|
17
16
|
import { useOnboardingAnswers } from "../../infrastructure/hooks/useOnboardingAnswers";
|
|
@@ -19,10 +18,7 @@ import { useOnboardingStore } from "../../infrastructure/storage/OnboardingStore
|
|
|
19
18
|
import { OnboardingSlideService } from "../../infrastructure/services/OnboardingSlideService";
|
|
20
19
|
import { OnboardingValidationService } from "../../infrastructure/services/OnboardingValidationService";
|
|
21
20
|
import { shouldUseGradient } from "../../infrastructure/utils/gradientUtils";
|
|
22
|
-
import {
|
|
23
|
-
import { OnboardingSlide as OnboardingSlideComponent } from "../components/OnboardingSlide";
|
|
24
|
-
import { QuestionSlide } from "../components/QuestionSlide";
|
|
25
|
-
import { OnboardingFooter } from "../components/OnboardingFooter";
|
|
21
|
+
import { OnboardingScreenContent } from "../components/OnboardingScreenContent";
|
|
26
22
|
|
|
27
23
|
export interface OnboardingScreenProps extends OnboardingOptions {
|
|
28
24
|
/**
|
|
@@ -62,13 +58,25 @@ export interface OnboardingScreenProps extends OnboardingOptions {
|
|
|
62
58
|
* When true, shows premium paywall before completing onboarding
|
|
63
59
|
*/
|
|
64
60
|
showPaywallOnComplete?: boolean;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Slider component for slider questions
|
|
64
|
+
* Required if using slider question type
|
|
65
|
+
* Import from @react-native-community/slider
|
|
66
|
+
*/
|
|
67
|
+
SliderComponent?: React.ComponentType<{
|
|
68
|
+
style?: any;
|
|
69
|
+
minimumValue: number;
|
|
70
|
+
maximumValue: number;
|
|
71
|
+
value: number;
|
|
72
|
+
onValueChange: (value: number) => void;
|
|
73
|
+
minimumTrackTintColor?: string;
|
|
74
|
+
maximumTrackTintColor?: string;
|
|
75
|
+
thumbTintColor?: string;
|
|
76
|
+
step?: number;
|
|
77
|
+
}>;
|
|
65
78
|
}
|
|
66
79
|
|
|
67
|
-
/**
|
|
68
|
-
* Onboarding Screen Component
|
|
69
|
-
*
|
|
70
|
-
* Displays onboarding flow with theme-aware colors, animations, and navigation
|
|
71
|
-
*/
|
|
72
80
|
export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
|
|
73
81
|
slides,
|
|
74
82
|
onComplete,
|
|
@@ -89,15 +97,14 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
|
|
|
89
97
|
onUpgrade,
|
|
90
98
|
showPaywallOnComplete = false,
|
|
91
99
|
useGradient: globalUseGradient = false,
|
|
100
|
+
SliderComponent,
|
|
92
101
|
}) => {
|
|
93
102
|
const insets = useSafeAreaInsets();
|
|
94
103
|
const tokens = useAppDesignTokens();
|
|
95
|
-
const { themeMode } = useTheme();
|
|
96
104
|
const onboardingStore = useOnboardingStore();
|
|
97
105
|
|
|
98
106
|
// Filter slides using service
|
|
99
107
|
const filteredSlides = useMemo(() => {
|
|
100
|
-
// Safety check: if slides is undefined or empty, return empty array
|
|
101
108
|
if (!slides || !Array.isArray(slides) || slides.length === 0) {
|
|
102
109
|
return [];
|
|
103
110
|
}
|
|
@@ -147,7 +154,6 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
|
|
|
147
154
|
// Handle next slide
|
|
148
155
|
const handleNext = async () => {
|
|
149
156
|
await saveCurrentAnswer(currentSlide);
|
|
150
|
-
|
|
151
157
|
if (isLastSlide) {
|
|
152
158
|
await completeOnboarding();
|
|
153
159
|
} else {
|
|
@@ -189,92 +195,52 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
|
|
|
189
195
|
);
|
|
190
196
|
}, [currentSlide, currentAnswer]);
|
|
191
197
|
|
|
192
|
-
const
|
|
193
|
-
() =>
|
|
194
|
-
|
|
198
|
+
const containerStyle = useMemo(
|
|
199
|
+
() => [
|
|
200
|
+
styles.container,
|
|
201
|
+
{
|
|
202
|
+
paddingTop: insets.top,
|
|
203
|
+
backgroundColor: useGradient ? "transparent" : tokens.colors.backgroundPrimary,
|
|
204
|
+
},
|
|
205
|
+
],
|
|
206
|
+
[insets.top, useGradient, tokens.colors.backgroundPrimary],
|
|
195
207
|
);
|
|
196
208
|
|
|
197
209
|
return (
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
{useGradient
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
{
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{
|
|
226
|
-
|
|
227
|
-
renderSlide(currentSlide)
|
|
228
|
-
) : currentSlide.type === "question" && currentSlide.question ? (
|
|
229
|
-
<QuestionSlide
|
|
230
|
-
slide={currentSlide}
|
|
231
|
-
value={currentAnswer}
|
|
232
|
-
onChange={setCurrentAnswer}
|
|
233
|
-
useGradient={useGradient}
|
|
234
|
-
/>
|
|
235
|
-
) : (
|
|
236
|
-
<OnboardingSlideComponent slide={currentSlide} useGradient={useGradient} />
|
|
237
|
-
)
|
|
238
|
-
)}
|
|
239
|
-
{renderFooter ? (
|
|
240
|
-
renderFooter({
|
|
241
|
-
currentIndex,
|
|
242
|
-
totalSlides: filteredSlides.length,
|
|
243
|
-
isLastSlide,
|
|
244
|
-
onNext: handleNext,
|
|
245
|
-
onUpgrade,
|
|
246
|
-
showPaywallOnComplete,
|
|
247
|
-
})
|
|
248
|
-
) : (
|
|
249
|
-
<OnboardingFooter
|
|
250
|
-
currentIndex={currentIndex}
|
|
251
|
-
totalSlides={filteredSlides.length}
|
|
252
|
-
isLastSlide={isLastSlide}
|
|
253
|
-
onNext={handleNext}
|
|
254
|
-
showProgressBar={showProgressBar}
|
|
255
|
-
showDots={showDots}
|
|
256
|
-
showProgressText={showProgressText}
|
|
257
|
-
nextButtonText={nextButtonText}
|
|
258
|
-
getStartedButtonText={getStartedButtonText}
|
|
259
|
-
disabled={!isAnswerValid}
|
|
260
|
-
useGradient={useGradient}
|
|
261
|
-
/>
|
|
262
|
-
)}
|
|
263
|
-
</View>
|
|
210
|
+
<OnboardingScreenContent
|
|
211
|
+
containerStyle={containerStyle}
|
|
212
|
+
useGradient={useGradient}
|
|
213
|
+
currentSlide={currentSlide}
|
|
214
|
+
isFirstSlide={isFirstSlide}
|
|
215
|
+
isLastSlide={isLastSlide}
|
|
216
|
+
currentIndex={currentIndex}
|
|
217
|
+
totalSlides={filteredSlides.length}
|
|
218
|
+
currentAnswer={currentAnswer}
|
|
219
|
+
isAnswerValid={isAnswerValid}
|
|
220
|
+
showBackButton={showBackButton}
|
|
221
|
+
showSkipButton={showSkipButton}
|
|
222
|
+
showProgressBar={showProgressBar}
|
|
223
|
+
showDots={showDots}
|
|
224
|
+
showProgressText={showProgressText}
|
|
225
|
+
skipButtonText={skipButtonText}
|
|
226
|
+
nextButtonText={nextButtonText}
|
|
227
|
+
getStartedButtonText={getStartedButtonText}
|
|
228
|
+
onBack={handlePrevious}
|
|
229
|
+
onSkip={handleSkip}
|
|
230
|
+
onNext={handleNext}
|
|
231
|
+
onAnswerChange={setCurrentAnswer}
|
|
232
|
+
renderHeader={renderHeader}
|
|
233
|
+
renderFooter={renderFooter}
|
|
234
|
+
renderSlide={renderSlide}
|
|
235
|
+
onUpgrade={onUpgrade}
|
|
236
|
+
showPaywallOnComplete={showPaywallOnComplete}
|
|
237
|
+
SliderComponent={SliderComponent}
|
|
238
|
+
/>
|
|
264
239
|
);
|
|
265
240
|
};
|
|
266
241
|
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
)
|
|
272
|
-
StyleSheet.create({
|
|
273
|
-
container: {
|
|
274
|
-
flex: 1,
|
|
275
|
-
paddingTop: insets.top,
|
|
276
|
-
// Use transparent background when gradient is used, otherwise use theme background
|
|
277
|
-
backgroundColor: useGradient ? 'transparent' : tokens.colors.backgroundPrimary,
|
|
278
|
-
},
|
|
279
|
-
});
|
|
280
|
-
|
|
242
|
+
const styles = StyleSheet.create({
|
|
243
|
+
container: {
|
|
244
|
+
flex: 1,
|
|
245
|
+
},
|
|
246
|
+
});
|