@umituz/react-native-onboarding 2.6.8 → 2.6.10

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": "2.6.8",
3
+ "version": "2.6.10",
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
+
@@ -0,0 +1,171 @@
1
+ /**
2
+ * useOnboardingScreenState Hook
3
+ * Single Responsibility: Manage onboarding screen state and handlers
4
+ */
5
+
6
+ import { useMemo } from "react";
7
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
9
+ import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
10
+ import { useOnboardingStore } from "../../infrastructure/storage/OnboardingStore";
11
+ import { useOnboardingNavigation } from "../../infrastructure/hooks/useOnboardingNavigation";
12
+ import { useOnboardingAnswers } from "../../infrastructure/hooks/useOnboardingAnswers";
13
+ import { OnboardingSlideService } from "../../infrastructure/services/OnboardingSlideService";
14
+ import { OnboardingValidationService } from "../../infrastructure/services/OnboardingValidationService";
15
+ import { shouldUseGradient } from "../../infrastructure/utils/gradientUtils";
16
+
17
+ export interface UseOnboardingScreenStateProps {
18
+ slides: OnboardingSlide[] | undefined;
19
+ storageKey?: string;
20
+ onComplete?: () => void | Promise<void>;
21
+ onSkip?: () => void | Promise<void>;
22
+ globalUseGradient?: boolean;
23
+ }
24
+
25
+ export interface UseOnboardingScreenStateReturn {
26
+ filteredSlides: OnboardingSlide[];
27
+ currentSlide: OnboardingSlide | undefined;
28
+ currentIndex: number;
29
+ isFirstSlide: boolean;
30
+ isLastSlide: boolean;
31
+ currentAnswer: any;
32
+ isAnswerValid: boolean;
33
+ useGradient: boolean;
34
+ containerStyle: any;
35
+ handleNext: () => Promise<void>;
36
+ handlePrevious: () => void;
37
+ handleSkip: () => Promise<void>;
38
+ setCurrentAnswer: (value: any) => void;
39
+ }
40
+
41
+ export function useOnboardingScreenState({
42
+ slides,
43
+ storageKey,
44
+ onComplete,
45
+ onSkip,
46
+ globalUseGradient = false,
47
+ }: UseOnboardingScreenStateProps): UseOnboardingScreenStateReturn {
48
+ const insets = useSafeAreaInsets();
49
+ const tokens = useAppDesignTokens();
50
+ const onboardingStore = useOnboardingStore();
51
+
52
+ // Filter slides using service
53
+ const filteredSlides = useMemo(() => {
54
+ if (!slides || !Array.isArray(slides) || slides.length === 0) {
55
+ return [];
56
+ }
57
+ const userData = onboardingStore.getUserData();
58
+ return OnboardingSlideService.filterSlides(slides, userData);
59
+ }, [slides, onboardingStore]);
60
+
61
+ // Navigation hook
62
+ const {
63
+ currentIndex,
64
+ goToNext,
65
+ goToPrevious,
66
+ complete: completeOnboarding,
67
+ skip: skipOnboarding,
68
+ isLastSlide,
69
+ isFirstSlide,
70
+ } = useOnboardingNavigation(
71
+ filteredSlides.length,
72
+ async () => {
73
+ await onboardingStore.complete(storageKey);
74
+ if (onComplete) {
75
+ await onComplete();
76
+ }
77
+ },
78
+ async () => {
79
+ await onboardingStore.skip(storageKey);
80
+ if (onSkip) {
81
+ await onSkip();
82
+ }
83
+ },
84
+ );
85
+
86
+ // Get current slide
87
+ const currentSlide = useMemo(
88
+ () => OnboardingSlideService.getSlideAtIndex(filteredSlides, currentIndex),
89
+ [filteredSlides, currentIndex],
90
+ );
91
+
92
+ // Answer management hook
93
+ const {
94
+ currentAnswer,
95
+ setCurrentAnswer,
96
+ loadAnswerForSlide,
97
+ saveCurrentAnswer,
98
+ } = useOnboardingAnswers(currentSlide);
99
+
100
+ // Handle next slide
101
+ const handleNext = async () => {
102
+ await saveCurrentAnswer(currentSlide);
103
+ if (isLastSlide) {
104
+ await completeOnboarding();
105
+ } else {
106
+ goToNext();
107
+ const nextSlide = OnboardingSlideService.getSlideAtIndex(
108
+ filteredSlides,
109
+ currentIndex + 1,
110
+ );
111
+ loadAnswerForSlide(nextSlide);
112
+ }
113
+ };
114
+
115
+ // Handle previous slide
116
+ const handlePrevious = () => {
117
+ goToPrevious();
118
+ const prevSlide = OnboardingSlideService.getSlideAtIndex(
119
+ filteredSlides,
120
+ currentIndex - 1,
121
+ );
122
+ loadAnswerForSlide(prevSlide);
123
+ };
124
+
125
+ // Handle skip
126
+ const handleSkip = async () => {
127
+ await skipOnboarding();
128
+ };
129
+
130
+ // Check if gradient should be used
131
+ const useGradient = shouldUseGradient(currentSlide, globalUseGradient);
132
+
133
+ // Validate answer using service
134
+ const isAnswerValid = useMemo(() => {
135
+ if (!currentSlide?.question) {
136
+ return true;
137
+ }
138
+ return OnboardingValidationService.validateAnswer(
139
+ currentSlide.question,
140
+ currentAnswer,
141
+ );
142
+ }, [currentSlide, currentAnswer]);
143
+
144
+ // Container style
145
+ const containerStyle = useMemo(
146
+ () => [
147
+ {
148
+ paddingTop: insets.top,
149
+ backgroundColor: useGradient ? "transparent" : tokens.colors.backgroundPrimary,
150
+ },
151
+ ],
152
+ [insets.top, useGradient, tokens.colors.backgroundPrimary],
153
+ );
154
+
155
+ return {
156
+ filteredSlides,
157
+ currentSlide,
158
+ currentIndex,
159
+ isFirstSlide,
160
+ isLastSlide,
161
+ currentAnswer,
162
+ isAnswerValid,
163
+ useGradient,
164
+ containerStyle,
165
+ handleNext,
166
+ handlePrevious,
167
+ handleSkip,
168
+ setCurrentAnswer,
169
+ };
170
+ }
171
+
@@ -3,26 +3,15 @@
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
- import React, { useMemo } from "react";
11
- import { View, StyleSheet, StatusBar } from "react-native";
12
- import { LinearGradient } from "expo-linear-gradient";
13
- import { useSafeAreaInsets } from "react-native-safe-area-context";
14
- import { useAppDesignTokens, useTheme } from "@umituz/react-native-design-system-theme";
10
+ import React from "react";
11
+ import { StyleSheet } from "react-native";
15
12
  import type { OnboardingOptions } from "../../domain/entities/OnboardingOptions";
16
- import { useOnboardingNavigation } from "../../infrastructure/hooks/useOnboardingNavigation";
17
- import { useOnboardingAnswers } from "../../infrastructure/hooks/useOnboardingAnswers";
18
- import { useOnboardingStore } from "../../infrastructure/storage/OnboardingStore";
19
- import { OnboardingSlideService } from "../../infrastructure/services/OnboardingSlideService";
20
- import { OnboardingValidationService } from "../../infrastructure/services/OnboardingValidationService";
21
- import { shouldUseGradient } from "../../infrastructure/utils/gradientUtils";
22
- import { OnboardingHeader } from "../components/OnboardingHeader";
23
- import { OnboardingSlide as OnboardingSlideComponent } from "../components/OnboardingSlide";
24
- import { QuestionSlide } from "../components/QuestionSlide";
25
- import { OnboardingFooter } from "../components/OnboardingFooter";
13
+ import { useOnboardingScreenState } from "../hooks/useOnboardingScreenState";
14
+ import { OnboardingScreenContent } from "../components/OnboardingScreenContent";
26
15
 
27
16
  export interface OnboardingScreenProps extends OnboardingOptions {
28
17
  /**
@@ -81,11 +70,6 @@ export interface OnboardingScreenProps extends OnboardingOptions {
81
70
  }>;
82
71
  }
83
72
 
84
- /**
85
- * Onboarding Screen Component
86
- *
87
- * Displays onboarding flow with theme-aware colors, animations, and navigation
88
- */
89
73
  export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
90
74
  slides,
91
75
  onComplete,
@@ -108,192 +92,63 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
108
92
  useGradient: globalUseGradient = false,
109
93
  SliderComponent,
110
94
  }) => {
111
- const insets = useSafeAreaInsets();
112
- const tokens = useAppDesignTokens();
113
- const { themeMode } = useTheme();
114
- const onboardingStore = useOnboardingStore();
115
-
116
- // Filter slides using service
117
- const filteredSlides = useMemo(() => {
118
- // Safety check: if slides is undefined or empty, return empty array
119
- if (!slides || !Array.isArray(slides) || slides.length === 0) {
120
- return [];
121
- }
122
- const userData = onboardingStore.getUserData();
123
- return OnboardingSlideService.filterSlides(slides, userData);
124
- }, [slides, onboardingStore]);
125
-
126
- // Navigation hook
127
95
  const {
96
+ filteredSlides,
97
+ currentSlide,
128
98
  currentIndex,
129
- goToNext,
130
- goToPrevious,
131
- complete: completeOnboarding,
132
- skip: skipOnboarding,
133
- isLastSlide,
134
99
  isFirstSlide,
135
- } = useOnboardingNavigation(
136
- filteredSlides.length,
137
- async () => {
138
- await onboardingStore.complete(storageKey);
139
- if (onComplete) {
140
- await onComplete();
141
- }
142
- },
143
- async () => {
144
- await onboardingStore.skip(storageKey);
145
- if (onSkip) {
146
- await onSkip();
147
- }
148
- },
149
- );
150
-
151
- // Get current slide
152
- const currentSlide = useMemo(
153
- () => OnboardingSlideService.getSlideAtIndex(filteredSlides, currentIndex),
154
- [filteredSlides, currentIndex],
155
- );
156
-
157
- // Answer management hook
158
- const {
100
+ isLastSlide,
159
101
  currentAnswer,
102
+ isAnswerValid,
103
+ useGradient,
104
+ containerStyle,
105
+ handleNext,
106
+ handlePrevious,
107
+ handleSkip,
160
108
  setCurrentAnswer,
161
- loadAnswerForSlide,
162
- saveCurrentAnswer,
163
- } = useOnboardingAnswers(currentSlide);
164
-
165
- // Handle next slide
166
- const handleNext = async () => {
167
- await saveCurrentAnswer(currentSlide);
168
-
169
- if (isLastSlide) {
170
- await completeOnboarding();
171
- } else {
172
- goToNext();
173
- const nextSlide = OnboardingSlideService.getSlideAtIndex(
174
- filteredSlides,
175
- currentIndex + 1,
176
- );
177
- loadAnswerForSlide(nextSlide);
178
- }
179
- };
180
-
181
- // Handle previous slide
182
- const handlePrevious = () => {
183
- goToPrevious();
184
- const prevSlide = OnboardingSlideService.getSlideAtIndex(
185
- filteredSlides,
186
- currentIndex - 1,
187
- );
188
- loadAnswerForSlide(prevSlide);
189
- };
190
-
191
- // Handle skip
192
- const handleSkip = async () => {
193
- await skipOnboarding();
194
- };
195
-
196
- // Check if gradient should be used
197
- const useGradient = shouldUseGradient(currentSlide, globalUseGradient);
198
-
199
- // Validate answer using service
200
- const isAnswerValid = useMemo(() => {
201
- if (!currentSlide?.question) {
202
- return true;
203
- }
204
- return OnboardingValidationService.validateAnswer(
205
- currentSlide.question,
206
- currentAnswer,
207
- );
208
- }, [currentSlide, currentAnswer]);
209
-
210
- const styles = useMemo(
211
- () => getStyles(insets, tokens, useGradient),
212
- [insets, tokens, useGradient],
213
- );
109
+ } = useOnboardingScreenState({
110
+ slides,
111
+ storageKey,
112
+ onComplete,
113
+ onSkip,
114
+ globalUseGradient,
115
+ });
214
116
 
215
117
  return (
216
- <View style={styles.container}>
217
- <StatusBar barStyle={themeMode === "dark" ? "light-content" : "dark-content"} />
218
- {useGradient && currentSlide && (
219
- <LinearGradient
220
- colors={currentSlide.gradient as [string, string, ...string[]]}
221
- start={{ x: 0, y: 0 }}
222
- end={{ x: 1, y: 1 }}
223
- style={StyleSheet.absoluteFill}
224
- />
225
- )}
226
- {renderHeader ? (
227
- renderHeader({
228
- isFirstSlide,
229
- onBack: handlePrevious,
230
- onSkip: handleSkip,
231
- })
232
- ) : (
233
- <OnboardingHeader
234
- isFirstSlide={isFirstSlide}
235
- onBack={handlePrevious}
236
- onSkip={handleSkip}
237
- showBackButton={showBackButton}
238
- showSkipButton={showSkipButton}
239
- skipButtonText={skipButtonText}
240
- useGradient={useGradient}
241
- />
242
- )}
243
- {currentSlide && (
244
- renderSlide ? (
245
- renderSlide(currentSlide)
246
- ) : currentSlide.type === "question" && currentSlide.question ? (
247
- <QuestionSlide
248
- slide={currentSlide}
249
- value={currentAnswer}
250
- onChange={setCurrentAnswer}
251
- useGradient={useGradient}
252
- SliderComponent={SliderComponent}
253
- />
254
- ) : (
255
- <OnboardingSlideComponent slide={currentSlide} useGradient={useGradient} />
256
- )
257
- )}
258
- {renderFooter ? (
259
- renderFooter({
260
- currentIndex,
261
- totalSlides: filteredSlides.length,
262
- isLastSlide,
263
- onNext: handleNext,
264
- onUpgrade,
265
- showPaywallOnComplete,
266
- })
267
- ) : (
268
- <OnboardingFooter
269
- currentIndex={currentIndex}
270
- totalSlides={filteredSlides.length}
271
- isLastSlide={isLastSlide}
272
- onNext={handleNext}
273
- showProgressBar={showProgressBar}
274
- showDots={showDots}
275
- showProgressText={showProgressText}
276
- nextButtonText={nextButtonText}
277
- getStartedButtonText={getStartedButtonText}
278
- disabled={!isAnswerValid}
279
- useGradient={useGradient}
280
- />
281
- )}
282
- </View>
118
+ <OnboardingScreenContent
119
+ containerStyle={[styles.container, containerStyle]}
120
+ useGradient={useGradient}
121
+ currentSlide={currentSlide}
122
+ isFirstSlide={isFirstSlide}
123
+ isLastSlide={isLastSlide}
124
+ currentIndex={currentIndex}
125
+ totalSlides={filteredSlides.length}
126
+ currentAnswer={currentAnswer}
127
+ isAnswerValid={isAnswerValid}
128
+ showBackButton={showBackButton}
129
+ showSkipButton={showSkipButton}
130
+ showProgressBar={showProgressBar}
131
+ showDots={showDots}
132
+ showProgressText={showProgressText}
133
+ skipButtonText={skipButtonText}
134
+ nextButtonText={nextButtonText}
135
+ getStartedButtonText={getStartedButtonText}
136
+ onBack={handlePrevious}
137
+ onSkip={handleSkip}
138
+ onNext={handleNext}
139
+ onAnswerChange={setCurrentAnswer}
140
+ renderHeader={renderHeader}
141
+ renderFooter={renderFooter}
142
+ renderSlide={renderSlide}
143
+ onUpgrade={onUpgrade}
144
+ showPaywallOnComplete={showPaywallOnComplete}
145
+ SliderComponent={SliderComponent}
146
+ />
283
147
  );
284
148
  };
285
149
 
286
- const getStyles = (
287
- insets: { top: number },
288
- tokens: ReturnType<typeof useAppDesignTokens>,
289
- useGradient: boolean,
290
- ) =>
291
- StyleSheet.create({
292
- container: {
293
- flex: 1,
294
- paddingTop: insets.top,
295
- // Use transparent background when gradient is used, otherwise use theme background
296
- backgroundColor: useGradient ? 'transparent' : tokens.colors.backgroundPrimary,
297
- },
298
- });
299
-
150
+ const styles = StyleSheet.create({
151
+ container: {
152
+ flex: 1,
153
+ },
154
+ });