@umituz/react-native-onboarding 3.6.4 → 3.6.8
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 +9 -7
- package/src/infrastructure/storage/OnboardingStore.ts +8 -2
- package/src/presentation/components/BaseSlide.tsx +11 -7
- package/src/presentation/components/OnboardingBackground.tsx +78 -0
- package/src/presentation/components/OnboardingScreenContent.tsx +51 -117
- package/src/presentation/components/QuestionSlide.tsx +1 -1
- package/src/presentation/hooks/useOnboardingGestures.ts +45 -0
- package/src/presentation/types/OnboardingProps.ts +46 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-onboarding",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.8",
|
|
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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
31
|
"@expo/vector-icons": ">=14.0.0",
|
|
32
|
-
"@umituz/react-native-design-system": "
|
|
32
|
+
"@umituz/react-native-design-system": "latest",
|
|
33
33
|
"@umituz/react-native-localization": "latest",
|
|
34
34
|
"@umituz/react-native-storage": "latest",
|
|
35
35
|
"expo-image": ">=2.0.0",
|
|
@@ -37,18 +37,19 @@
|
|
|
37
37
|
"expo-video": ">=1.0.0",
|
|
38
38
|
"react": ">=18.2.0",
|
|
39
39
|
"react-native": ">=0.74.0",
|
|
40
|
-
"react-native-safe-area-context": ">=4.0.0"
|
|
40
|
+
"react-native-safe-area-context": ">=4.0.0",
|
|
41
|
+
"zustand": ">=4.5.2"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@expo/vector-icons": "^14.0.0",
|
|
44
|
-
"@react-native-async-storage/async-storage": "^2.2
|
|
45
|
-
"@react-native-community/datetimepicker": "^8.
|
|
45
|
+
"@react-native-async-storage/async-storage": "^2.1.2",
|
|
46
|
+
"@react-native-community/datetimepicker": "^8.2.0",
|
|
46
47
|
"@react-native/eslint-config": "^0.83.1",
|
|
47
48
|
"@types/react": "~19.1.10",
|
|
48
49
|
"@types/react-native": "^0.72.8",
|
|
49
50
|
"@typescript-eslint/eslint-plugin": "^8.50.1",
|
|
50
51
|
"@typescript-eslint/parser": "^8.50.1",
|
|
51
|
-
"@umituz/react-native-design-system": "
|
|
52
|
+
"@umituz/react-native-design-system": "latest",
|
|
52
53
|
"@umituz/react-native-localization": "latest",
|
|
53
54
|
"@umituz/react-native-storage": "latest",
|
|
54
55
|
"eslint": "^9.39.2",
|
|
@@ -60,7 +61,8 @@
|
|
|
60
61
|
"react": "19.1.0",
|
|
61
62
|
"react-native": "0.81.5",
|
|
62
63
|
"react-native-safe-area-context": "^5.6.0",
|
|
63
|
-
"typescript": "~5.9.2"
|
|
64
|
+
"typescript": "~5.9.2",
|
|
65
|
+
"zustand": "^5.0.3"
|
|
64
66
|
},
|
|
65
67
|
"publishConfig": {
|
|
66
68
|
"access": "public"
|
|
@@ -29,11 +29,17 @@ interface OnboardingActions {
|
|
|
29
29
|
setUserData: (data: OnboardingUserData) => Promise<void>;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
export const useOnboardingStore = createStore<
|
|
32
|
+
export const useOnboardingStore = createStore<
|
|
33
|
+
OnboardingStoreState,
|
|
34
|
+
OnboardingActions
|
|
35
|
+
>({
|
|
33
36
|
name: "onboarding-store",
|
|
34
37
|
initialState: initialOnboardingState,
|
|
35
38
|
persist: false,
|
|
36
|
-
actions: (
|
|
39
|
+
actions: (
|
|
40
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
41
|
+
get: () => OnboardingStoreState
|
|
42
|
+
): OnboardingActions => {
|
|
37
43
|
const actions = createOnboardingStoreActions(set, get);
|
|
38
44
|
|
|
39
45
|
return {
|
|
@@ -12,7 +12,10 @@ export interface BaseSlideProps {
|
|
|
12
12
|
contentPosition?: ContentPosition;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export const BaseSlide = ({
|
|
15
|
+
export const BaseSlide = ({
|
|
16
|
+
children,
|
|
17
|
+
contentPosition = "center",
|
|
18
|
+
}: BaseSlideProps) => {
|
|
16
19
|
const isBottom = contentPosition === "bottom";
|
|
17
20
|
|
|
18
21
|
return (
|
|
@@ -20,14 +23,12 @@ export const BaseSlide = ({ children, contentPosition = "center" }: BaseSlidePro
|
|
|
20
23
|
style={styles.container}
|
|
21
24
|
contentContainerStyle={[
|
|
22
25
|
styles.content,
|
|
23
|
-
isBottom
|
|
26
|
+
isBottom ? styles.contentBottom : styles.contentCenter,
|
|
24
27
|
]}
|
|
25
28
|
showsVerticalScrollIndicator={false}
|
|
26
29
|
bounces={false}
|
|
27
30
|
>
|
|
28
|
-
<View style={styles.slideContainer}>
|
|
29
|
-
{children}
|
|
30
|
-
</View>
|
|
31
|
+
<View style={styles.slideContainer}>{children}</View>
|
|
31
32
|
</ScrollView>
|
|
32
33
|
);
|
|
33
34
|
};
|
|
@@ -38,14 +39,17 @@ const styles = StyleSheet.create({
|
|
|
38
39
|
},
|
|
39
40
|
content: {
|
|
40
41
|
flexGrow: 1,
|
|
41
|
-
justifyContent: "center",
|
|
42
42
|
paddingVertical: 40,
|
|
43
43
|
},
|
|
44
|
+
contentCenter: {
|
|
45
|
+
justifyContent: "center",
|
|
46
|
+
},
|
|
44
47
|
contentBottom: {
|
|
45
48
|
justifyContent: "flex-end",
|
|
46
|
-
paddingBottom: 140
|
|
49
|
+
paddingBottom: 40, // Reduced from 140 as footer handles its own padding
|
|
47
50
|
},
|
|
48
51
|
slideContainer: {
|
|
52
|
+
width: "100%",
|
|
49
53
|
paddingHorizontal: 24,
|
|
50
54
|
alignItems: "center",
|
|
51
55
|
},
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Background Component
|
|
3
|
+
* Handles rendering of video, image or gradient backgrounds
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, StyleSheet } from "react-native";
|
|
8
|
+
import { LinearGradient } from "expo-linear-gradient";
|
|
9
|
+
import { Image } from "expo-image";
|
|
10
|
+
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
11
|
+
import { BackgroundVideo } from "./BackgroundVideo";
|
|
12
|
+
|
|
13
|
+
interface OnboardingBackgroundProps {
|
|
14
|
+
currentSlide: OnboardingSlide | undefined;
|
|
15
|
+
useGradient: boolean;
|
|
16
|
+
effectivelyUseGradient: boolean;
|
|
17
|
+
overlayOpacity: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const OnboardingBackground: React.FC<OnboardingBackgroundProps> = ({
|
|
21
|
+
currentSlide,
|
|
22
|
+
useGradient,
|
|
23
|
+
effectivelyUseGradient,
|
|
24
|
+
overlayOpacity,
|
|
25
|
+
}) => {
|
|
26
|
+
if (!currentSlide) return null;
|
|
27
|
+
|
|
28
|
+
const renderContent = () => {
|
|
29
|
+
if (currentSlide.backgroundVideo) {
|
|
30
|
+
return (
|
|
31
|
+
<BackgroundVideo
|
|
32
|
+
source={currentSlide.backgroundVideo}
|
|
33
|
+
overlayOpacity={overlayOpacity}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (currentSlide.backgroundImage) {
|
|
39
|
+
return (
|
|
40
|
+
<Image
|
|
41
|
+
source={currentSlide.backgroundImage}
|
|
42
|
+
style={StyleSheet.absoluteFill}
|
|
43
|
+
contentFit="cover"
|
|
44
|
+
transition={500}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (useGradient && currentSlide.gradient) {
|
|
50
|
+
return (
|
|
51
|
+
<LinearGradient
|
|
52
|
+
colors={currentSlide.gradient as [string, string, ...string[]]}
|
|
53
|
+
start={{ x: 0, y: 0 }}
|
|
54
|
+
end={{ x: 1, y: 1 }}
|
|
55
|
+
style={StyleSheet.absoluteFill}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="none">
|
|
65
|
+
{renderContent()}
|
|
66
|
+
<View
|
|
67
|
+
style={[
|
|
68
|
+
StyleSheet.absoluteFill,
|
|
69
|
+
{
|
|
70
|
+
backgroundColor: effectivelyUseGradient
|
|
71
|
+
? `rgba(0,0,0,${overlayOpacity})`
|
|
72
|
+
: "transparent",
|
|
73
|
+
},
|
|
74
|
+
]}
|
|
75
|
+
/>
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
};
|
|
@@ -5,56 +5,14 @@
|
|
|
5
5
|
|
|
6
6
|
import React from "react";
|
|
7
7
|
import { View, StyleSheet, StatusBar } from "react-native";
|
|
8
|
-
import { LinearGradient } from "expo-linear-gradient";
|
|
9
|
-
import { Image } from "expo-image";
|
|
10
8
|
import { useTheme } from "@umituz/react-native-design-system";
|
|
11
|
-
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
12
9
|
import { OnboardingHeader } from "./OnboardingHeader";
|
|
13
10
|
import { OnboardingSlide as OnboardingSlideComponent } from "./OnboardingSlide";
|
|
14
11
|
import { QuestionSlide } from "./QuestionSlide";
|
|
15
12
|
import { OnboardingFooter } from "./OnboardingFooter";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
containerStyle?: any;
|
|
20
|
-
useGradient: boolean;
|
|
21
|
-
currentSlide: OnboardingSlide | undefined;
|
|
22
|
-
isFirstSlide: boolean;
|
|
23
|
-
isLastSlide: boolean;
|
|
24
|
-
currentIndex: number;
|
|
25
|
-
totalSlides: number;
|
|
26
|
-
currentAnswer: any;
|
|
27
|
-
isAnswerValid: boolean;
|
|
28
|
-
showBackButton: boolean;
|
|
29
|
-
showSkipButton: boolean;
|
|
30
|
-
showProgressBar: boolean;
|
|
31
|
-
showDots: boolean;
|
|
32
|
-
showProgressText: boolean;
|
|
33
|
-
skipButtonText?: string;
|
|
34
|
-
nextButtonText?: string;
|
|
35
|
-
getStartedButtonText?: string;
|
|
36
|
-
onBack: () => void;
|
|
37
|
-
onSkip: () => void;
|
|
38
|
-
onNext: () => void;
|
|
39
|
-
onAnswerChange: (value: any) => void;
|
|
40
|
-
renderHeader?: (props: {
|
|
41
|
-
isFirstSlide: boolean;
|
|
42
|
-
onBack: () => void;
|
|
43
|
-
onSkip: () => void;
|
|
44
|
-
}) => React.ReactNode;
|
|
45
|
-
renderFooter?: (props: {
|
|
46
|
-
currentIndex: number;
|
|
47
|
-
totalSlides: number;
|
|
48
|
-
isLastSlide: boolean;
|
|
49
|
-
onNext: () => void;
|
|
50
|
-
onUpgrade?: () => void;
|
|
51
|
-
showPaywallOnComplete?: boolean;
|
|
52
|
-
}) => React.ReactNode;
|
|
53
|
-
renderSlide?: (slide: OnboardingSlide) => React.ReactNode;
|
|
54
|
-
onUpgrade?: () => void;
|
|
55
|
-
showPaywallOnComplete?: boolean;
|
|
56
|
-
variant?: "default" | "card" | "minimal" | "fullscreen";
|
|
57
|
-
}
|
|
13
|
+
import { OnboardingBackground } from "./OnboardingBackground";
|
|
14
|
+
import { useOnboardingGestures } from "../hooks/useOnboardingGestures";
|
|
15
|
+
import type { OnboardingScreenContentProps } from "../types/OnboardingProps";
|
|
58
16
|
|
|
59
17
|
export const OnboardingScreenContent = ({
|
|
60
18
|
containerStyle,
|
|
@@ -86,70 +44,41 @@ export const OnboardingScreenContent = ({
|
|
|
86
44
|
variant = "default",
|
|
87
45
|
}: OnboardingScreenContentProps) => {
|
|
88
46
|
const { themeMode } = useTheme();
|
|
89
|
-
/* useGradient is now passed as a prop */
|
|
90
47
|
|
|
91
|
-
const
|
|
48
|
+
const panResponder = useOnboardingGestures({
|
|
49
|
+
isFirstSlide,
|
|
50
|
+
isAnswerValid,
|
|
51
|
+
onNext,
|
|
52
|
+
onBack,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const hasMedia =
|
|
56
|
+
!!currentSlide?.backgroundImage || !!currentSlide?.backgroundVideo;
|
|
92
57
|
const overlayOpacity = currentSlide?.overlayOpacity ?? 0.5;
|
|
93
|
-
// If media is present, valid gradient mode is enforced (white text)
|
|
94
58
|
const effectivelyUseGradient = useGradient || hasMedia;
|
|
95
59
|
|
|
96
|
-
const renderBackground = () => {
|
|
97
|
-
if (!currentSlide) return null;
|
|
98
|
-
|
|
99
|
-
if (currentSlide.backgroundVideo) {
|
|
100
|
-
return (
|
|
101
|
-
<BackgroundVideo
|
|
102
|
-
source={currentSlide.backgroundVideo}
|
|
103
|
-
overlayOpacity={overlayOpacity}
|
|
104
|
-
/>
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (currentSlide.backgroundImage) {
|
|
109
|
-
return (
|
|
110
|
-
<View style={StyleSheet.absoluteFill}>
|
|
111
|
-
<Image
|
|
112
|
-
source={currentSlide.backgroundImage}
|
|
113
|
-
style={StyleSheet.absoluteFill}
|
|
114
|
-
contentFit="cover"
|
|
115
|
-
transition={500}
|
|
116
|
-
/>
|
|
117
|
-
<View
|
|
118
|
-
style={[
|
|
119
|
-
StyleSheet.absoluteFill,
|
|
120
|
-
{ backgroundColor: `rgba(0,0,0,${overlayOpacity})` }
|
|
121
|
-
]}
|
|
122
|
-
/>
|
|
123
|
-
</View>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (useGradient && currentSlide.gradient) {
|
|
128
|
-
return (
|
|
129
|
-
<LinearGradient
|
|
130
|
-
colors={currentSlide.gradient as [string, string, ...string[]]}
|
|
131
|
-
start={{ x: 0, y: 0 }}
|
|
132
|
-
end={{ x: 1, y: 1 }}
|
|
133
|
-
style={StyleSheet.absoluteFill}
|
|
134
|
-
/>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return null;
|
|
139
|
-
};
|
|
140
|
-
|
|
141
60
|
return (
|
|
142
|
-
<View
|
|
143
|
-
|
|
61
|
+
<View
|
|
62
|
+
style={[styles.container, containerStyle]}
|
|
63
|
+
{...panResponder.panHandlers}
|
|
64
|
+
>
|
|
65
|
+
<StatusBar
|
|
66
|
+
barStyle={
|
|
67
|
+
themeMode === "dark" || effectivelyUseGradient
|
|
68
|
+
? "light-content"
|
|
69
|
+
: "dark-content"
|
|
70
|
+
}
|
|
71
|
+
/>
|
|
144
72
|
|
|
145
|
-
|
|
73
|
+
<OnboardingBackground
|
|
74
|
+
currentSlide={currentSlide}
|
|
75
|
+
useGradient={useGradient}
|
|
76
|
+
effectivelyUseGradient={effectivelyUseGradient}
|
|
77
|
+
overlayOpacity={overlayOpacity}
|
|
78
|
+
/>
|
|
146
79
|
|
|
147
80
|
{renderHeader ? (
|
|
148
|
-
renderHeader({
|
|
149
|
-
isFirstSlide,
|
|
150
|
-
onBack,
|
|
151
|
-
onSkip,
|
|
152
|
-
})
|
|
81
|
+
renderHeader({ isFirstSlide, onBack, onSkip })
|
|
153
82
|
) : (
|
|
154
83
|
<OnboardingHeader
|
|
155
84
|
isFirstSlide={isFirstSlide}
|
|
@@ -160,22 +89,24 @@ export const OnboardingScreenContent = ({
|
|
|
160
89
|
skipButtonText={skipButtonText}
|
|
161
90
|
/>
|
|
162
91
|
)}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
variant={variant}
|
|
177
|
-
|
|
178
|
-
|
|
92
|
+
|
|
93
|
+
{currentSlide && (
|
|
94
|
+
<View style={styles.content}>
|
|
95
|
+
{renderSlide ? (
|
|
96
|
+
renderSlide(currentSlide)
|
|
97
|
+
) : currentSlide.type === "question" && currentSlide.question ? (
|
|
98
|
+
<QuestionSlide
|
|
99
|
+
slide={currentSlide}
|
|
100
|
+
value={currentAnswer}
|
|
101
|
+
onChange={onAnswerChange}
|
|
102
|
+
variant={variant}
|
|
103
|
+
/>
|
|
104
|
+
) : (
|
|
105
|
+
<OnboardingSlideComponent slide={currentSlide} variant={variant} />
|
|
106
|
+
)}
|
|
107
|
+
</View>
|
|
108
|
+
)}
|
|
109
|
+
|
|
179
110
|
{renderFooter ? (
|
|
180
111
|
renderFooter({
|
|
181
112
|
currentIndex,
|
|
@@ -207,4 +138,7 @@ const styles = StyleSheet.create({
|
|
|
207
138
|
container: {
|
|
208
139
|
flex: 1,
|
|
209
140
|
},
|
|
141
|
+
content: {
|
|
142
|
+
flex: 1,
|
|
143
|
+
},
|
|
210
144
|
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOnboardingGestures Hook
|
|
3
|
+
* Handles swipe gestures for onboarding navigation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useRef } from "react";
|
|
7
|
+
import { PanResponder } from "react-native";
|
|
8
|
+
|
|
9
|
+
interface UseOnboardingGesturesProps {
|
|
10
|
+
isFirstSlide: boolean;
|
|
11
|
+
isAnswerValid: boolean;
|
|
12
|
+
onNext: () => void;
|
|
13
|
+
onBack: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useOnboardingGestures = ({
|
|
17
|
+
isFirstSlide,
|
|
18
|
+
isAnswerValid,
|
|
19
|
+
onNext,
|
|
20
|
+
onBack,
|
|
21
|
+
}: UseOnboardingGesturesProps) => {
|
|
22
|
+
const panResponder = useRef(
|
|
23
|
+
PanResponder.create({
|
|
24
|
+
onMoveShouldSetPanResponder: (_, gestureState) => {
|
|
25
|
+
// Only trigger for horizontal swipes
|
|
26
|
+
return Math.abs(gestureState.dx) > 20 && Math.abs(gestureState.dy) < 40;
|
|
27
|
+
},
|
|
28
|
+
onPanResponderRelease: (_, gestureState) => {
|
|
29
|
+
if (gestureState.dx > 50) {
|
|
30
|
+
// Swipe Right -> Previous
|
|
31
|
+
if (!isFirstSlide) {
|
|
32
|
+
onBack();
|
|
33
|
+
}
|
|
34
|
+
} else if (gestureState.dx < -50) {
|
|
35
|
+
// Swipe Left -> Next
|
|
36
|
+
if (isAnswerValid) {
|
|
37
|
+
onNext();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
).current;
|
|
43
|
+
|
|
44
|
+
return panResponder;
|
|
45
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Screen Content Props
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
6
|
+
|
|
7
|
+
export interface OnboardingScreenContentProps {
|
|
8
|
+
containerStyle?: any;
|
|
9
|
+
useGradient: boolean;
|
|
10
|
+
currentSlide: OnboardingSlide | undefined;
|
|
11
|
+
isFirstSlide: boolean;
|
|
12
|
+
isLastSlide: boolean;
|
|
13
|
+
currentIndex: number;
|
|
14
|
+
totalSlides: number;
|
|
15
|
+
currentAnswer: any;
|
|
16
|
+
isAnswerValid: boolean;
|
|
17
|
+
showBackButton: boolean;
|
|
18
|
+
showSkipButton: boolean;
|
|
19
|
+
showProgressBar: boolean;
|
|
20
|
+
showDots: boolean;
|
|
21
|
+
showProgressText: boolean;
|
|
22
|
+
skipButtonText?: string;
|
|
23
|
+
nextButtonText?: string;
|
|
24
|
+
getStartedButtonText?: string;
|
|
25
|
+
onBack: () => void;
|
|
26
|
+
onSkip: () => void;
|
|
27
|
+
onNext: () => void;
|
|
28
|
+
onAnswerChange: (value: any) => void;
|
|
29
|
+
renderHeader?: (props: {
|
|
30
|
+
isFirstSlide: boolean;
|
|
31
|
+
onBack: () => void;
|
|
32
|
+
onSkip: () => void;
|
|
33
|
+
}) => React.ReactNode;
|
|
34
|
+
renderFooter?: (props: {
|
|
35
|
+
currentIndex: number;
|
|
36
|
+
totalSlides: number;
|
|
37
|
+
isLastSlide: boolean;
|
|
38
|
+
onNext: () => void;
|
|
39
|
+
onUpgrade?: () => void;
|
|
40
|
+
showPaywallOnComplete?: boolean;
|
|
41
|
+
}) => React.ReactNode;
|
|
42
|
+
renderSlide?: (slide: OnboardingSlide) => React.ReactNode;
|
|
43
|
+
onUpgrade?: () => void;
|
|
44
|
+
showPaywallOnComplete?: boolean;
|
|
45
|
+
variant?: "default" | "card" | "minimal" | "fullscreen";
|
|
46
|
+
}
|