@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-onboarding",
3
- "version": "3.6.4",
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": "^2.1.0",
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.0",
45
- "@react-native-community/datetimepicker": "^8.5.1",
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": "^2.1.0",
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<OnboardingStoreState, OnboardingActions>({
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: (set: (state: Partial<OnboardingStoreState>) => void, get: () => OnboardingStoreState) => {
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 = ({ children, contentPosition = "center" }: BaseSlideProps) => {
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 && styles.contentBottom
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 { BackgroundVideo } from "./BackgroundVideo";
17
-
18
- export interface OnboardingScreenContentProps {
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 hasMedia = !!currentSlide?.backgroundImage || !!currentSlide?.backgroundVideo;
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 style={[styles.container, containerStyle]}>
143
- <StatusBar barStyle={themeMode === "dark" || effectivelyUseGradient ? "light-content" : "dark-content"} />
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
- {renderBackground()}
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
- {currentSlide &&
164
- (renderSlide ? (
165
- renderSlide(currentSlide)
166
- ) : currentSlide.type === "question" && currentSlide.question ? (
167
- <QuestionSlide
168
- slide={currentSlide}
169
- value={currentAnswer}
170
- onChange={onAnswerChange}
171
- variant={variant}
172
- />
173
- ) : (
174
- <OnboardingSlideComponent
175
- slide={currentSlide}
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
  });
@@ -33,7 +33,7 @@ export const QuestionSlide = ({
33
33
  if (!question) return null;
34
34
 
35
35
  return (
36
- <BaseSlide>
36
+ <BaseSlide contentPosition={slide.contentPosition}>
37
37
  <QuestionSlideHeader slide={slide} />
38
38
 
39
39
  <View style={styles.questionContainer}>
@@ -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
+ }