@umituz/react-native-onboarding 3.3.9 → 3.3.11

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.3.9",
3
+ "version": "3.3.11",
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",
@@ -41,6 +41,11 @@ export interface OnboardingSlide {
41
41
  */
42
42
  icon: string;
43
43
 
44
+ /**
45
+ * Type of icon: 'emoji' or 'icon' (default: 'icon')
46
+ */
47
+ iconType?: 'emoji' | 'icon';
48
+
44
49
  /**
45
50
  * Gradient colors for the slide background (optional)
46
51
  * [startColor, endColor] or [color1, color2, color3] for multi-stop gradients
@@ -1,8 +1,9 @@
1
- import React, { useMemo } from "react";
1
+ import React from "react";
2
2
  import { View, StyleSheet } from "react-native";
3
3
  import { useSafeAreaInsets } from "react-native-safe-area-context";
4
4
  import { useLocalization } from "@umituz/react-native-localization";
5
- import { useAppDesignTokens, AtomicButton, AtomicText } from "@umituz/react-native-design-system";
5
+ import { AtomicButton, AtomicText } from "@umituz/react-native-design-system";
6
+ import { useOnboardingTheme } from "../providers/OnboardingThemeProvider";
6
7
 
7
8
  export interface OnboardingFooterProps {
8
9
  currentIndex: number;
@@ -15,7 +16,6 @@ export interface OnboardingFooterProps {
15
16
  nextButtonText?: string;
16
17
  getStartedButtonText?: string;
17
18
  disabled?: boolean;
18
- useGradient?: boolean;
19
19
  }
20
20
 
21
21
  export const OnboardingFooter = ({
@@ -29,11 +29,10 @@ export const OnboardingFooter = ({
29
29
  nextButtonText,
30
30
  getStartedButtonText,
31
31
  disabled = false,
32
- useGradient = false,
33
32
  }: OnboardingFooterProps) => {
34
33
  const insets = useSafeAreaInsets();
35
34
  const { t } = useLocalization();
36
- const tokens = useAppDesignTokens();
35
+ const { colors } = useOnboardingTheme();
37
36
 
38
37
  const buttonText = isLastSlide
39
38
  ? getStartedButtonText || t("onboarding.getStarted") || "Get Started"
@@ -45,8 +44,8 @@ export const OnboardingFooter = ({
45
44
  <View style={[styles.footer, { paddingBottom: insets.bottom + 24 }]}>
46
45
  {showProgressBar && (
47
46
  <View style={styles.progressContainer}>
48
- <View style={[styles.progressBar, { backgroundColor: useGradient ? "rgba(255, 255, 255, 0.2)" : tokens.colors.borderLight }]}>
49
- <View style={[styles.progressFill, { width: `${progressPercent}%`, backgroundColor: useGradient ? '#FFFFFF' : tokens.colors.primary }]} />
47
+ <View style={[styles.progressBar, { backgroundColor: colors.progressBarBg }]}>
48
+ <View style={[styles.progressFill, { width: `${progressPercent}%`, backgroundColor: colors.progressFillColor }]} />
50
49
  </View>
51
50
  </View>
52
51
  )}
@@ -58,10 +57,10 @@ export const OnboardingFooter = ({
58
57
  key={index}
59
58
  style={[
60
59
  styles.dot,
61
- { backgroundColor: useGradient ? "rgba(255, 255, 255, 0.4)" : tokens.colors.borderLight },
60
+ { backgroundColor: colors.dotColor },
62
61
  index === currentIndex && {
63
62
  width: 12,
64
- backgroundColor: useGradient ? '#FFFFFF' : tokens.colors.primary
63
+ backgroundColor: colors.activeDotColor
65
64
  }
66
65
  ]}
67
66
  />
@@ -73,9 +72,9 @@ export const OnboardingFooter = ({
73
72
  onPress={onNext}
74
73
  disabled={disabled}
75
74
  fullWidth
76
- variant={useGradient ? "secondary" : "primary"}
77
- style={useGradient && { backgroundColor: '#FFFFFF' }}
78
- textStyle={useGradient && { color: tokens.colors.primary }}
75
+ variant="primary"
76
+ style={{ backgroundColor: colors.buttonBg }}
77
+ textStyle={{ color: colors.buttonTextColor }}
79
78
  >
80
79
  {buttonText}
81
80
  </AtomicButton>
@@ -83,7 +82,7 @@ export const OnboardingFooter = ({
83
82
  {showProgressText && (
84
83
  <AtomicText
85
84
  type="labelSmall"
86
- style={[styles.progressText, { color: useGradient ? "rgba(255, 255, 255, 0.8)" : tokens.colors.textSecondary }]}
85
+ style={[styles.progressText, { color: colors.progressTextColor }]}
87
86
  >
88
87
  {currentIndex + 1} {t("general.of") || "of"} {totalSlides}
89
88
  </AtomicText>
@@ -1,7 +1,8 @@
1
1
  import React from "react";
2
2
  import { View, TouchableOpacity, StyleSheet } from "react-native";
3
- import { AtomicIcon, AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
3
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
4
4
  import { useLocalization } from "@umituz/react-native-localization";
5
+ import { useOnboardingTheme } from "../providers/OnboardingThemeProvider";
5
6
 
6
7
  export interface OnboardingHeaderProps {
7
8
  isFirstSlide: boolean;
@@ -10,7 +11,6 @@ export interface OnboardingHeaderProps {
10
11
  showBackButton?: boolean;
11
12
  showSkipButton?: boolean;
12
13
  skipButtonText?: string;
13
- useGradient?: boolean;
14
14
  }
15
15
 
16
16
  export const OnboardingHeader = ({
@@ -20,13 +20,11 @@ export const OnboardingHeader = ({
20
20
  showBackButton = true,
21
21
  showSkipButton = true,
22
22
  skipButtonText,
23
- useGradient = false,
24
23
  }: OnboardingHeaderProps) => {
25
24
  const { t } = useLocalization();
26
- const tokens = useAppDesignTokens();
25
+ const { colors } = useOnboardingTheme();
27
26
 
28
27
  const skipText = skipButtonText || t("onboarding.skip") || "Skip";
29
- const iconColor = useGradient ? "#FFFFFF" : tokens.colors.textPrimary;
30
28
 
31
29
  return (
32
30
  <View style={styles.header}>
@@ -37,15 +35,15 @@ export const OnboardingHeader = ({
37
35
  style={[
38
36
  styles.headerButton,
39
37
  {
40
- backgroundColor: useGradient ? "rgba(255, 255, 255, 0.2)" : tokens.colors.surface,
41
- borderColor: useGradient ? "rgba(255, 255, 255, 0.3)" : tokens.colors.borderLight,
38
+ backgroundColor: colors.headerButtonBg,
39
+ borderColor: colors.headerButtonBorder,
42
40
  },
43
41
  isFirstSlide && styles.headerButtonDisabled,
44
42
  ]}
45
43
  activeOpacity={0.7}
46
44
  hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
47
45
  >
48
- <AtomicIcon name="chevron-back" customSize={20} customColor={iconColor} />
46
+ <AtomicIcon name="chevron-back" customSize={20} customColor={colors.iconColor} />
49
47
  </TouchableOpacity>
50
48
  ) : (
51
49
  <View style={styles.headerButton} />
@@ -54,7 +52,7 @@ export const OnboardingHeader = ({
54
52
  <TouchableOpacity onPress={onSkip} activeOpacity={0.7}>
55
53
  <AtomicText
56
54
  type="labelLarge"
57
- style={[styles.skipText, { color: iconColor }]}
55
+ style={[styles.skipText, { color: colors.textColor }]}
58
56
  >
59
57
  {skipText}
60
58
  </AtomicText>
@@ -1,49 +1,43 @@
1
1
  import React from "react";
2
2
  import { View, ScrollView, StyleSheet } from "react-native";
3
- import { AtomicIcon, AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
3
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
4
4
  import type { OnboardingSlide as OnboardingSlideType } from "../../domain/entities/OnboardingSlide";
5
5
  import type { OnboardingThemeVariant } from "../../domain/entities/OnboardingTheme";
6
+ import { useOnboardingTheme } from "../providers/OnboardingThemeProvider";
6
7
 
7
8
  export interface OnboardingSlideProps {
8
9
  slide: OnboardingSlideType;
9
- useGradient?: boolean;
10
10
  variant?: OnboardingThemeVariant;
11
11
  }
12
12
 
13
- const EMOJI_REGEX = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u;
14
-
15
13
  export const OnboardingSlide = ({
16
14
  slide,
17
- useGradient = false,
18
15
  variant = "default"
19
16
  }: OnboardingSlideProps) => {
20
- const tokens = useAppDesignTokens();
17
+ const { colors } = useOnboardingTheme();
21
18
 
22
- const isEmoji = EMOJI_REGEX.test(slide.icon);
23
- const isValidIconName = !isEmoji && typeof slide.icon === "string" && slide.icon.length > 0;
24
- const iconColor = useGradient ? "#FFFFFF" : tokens.colors.primary;
25
- const textColor = useGradient ? "#FFFFFF" : tokens.colors.textPrimary;
26
- const subTextColor = useGradient ? "rgba(255, 255, 255, 0.8)" : tokens.colors.textSecondary;
19
+ const isEmoji = slide.iconType === 'emoji';
20
+ const hasIcon = slide.icon && slide.icon.length > 0;
27
21
  const iconSize = variant === "minimal" ? 80 : 72;
28
22
 
29
23
  return (
30
24
  <ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false} bounces={false}>
31
25
  <View style={styles.slideContainer}>
32
- {(isEmoji || isValidIconName) && (
26
+ {hasIcon && (
33
27
  <View style={styles.iconBox}>
34
28
  {isEmoji ? (
35
29
  <AtomicText style={{ fontSize: iconSize }}>{slide.icon}</AtomicText>
36
30
  ) : (
37
- <AtomicIcon name={slide.icon} customSize={iconSize} customColor={iconColor} />
31
+ <AtomicIcon name={slide.icon} customSize={iconSize} customColor={colors.iconColor} />
38
32
  )}
39
33
  </View>
40
34
  )}
41
35
 
42
- <AtomicText type="headlineMedium" style={[styles.title, { color: textColor }]}>
36
+ <AtomicText type="headlineMedium" style={[styles.title, { color: colors.textColor }]}>
43
37
  {slide.title}
44
38
  </AtomicText>
45
39
 
46
- <AtomicText type="bodyLarge" style={[styles.description, { color: subTextColor }]}>
40
+ <AtomicText type="bodyLarge" style={[styles.description, { color: colors.subTextColor }]}>
47
41
  {slide.description}
48
42
  </AtomicText>
49
43
 
@@ -51,8 +45,8 @@ export const OnboardingSlide = ({
51
45
  <View style={styles.featuresContainer}>
52
46
  {slide.features.map((feature, index) => (
53
47
  <View key={index} style={styles.featureItem}>
54
- <AtomicIcon name="checkmark-circle" size="sm" customColor={iconColor} />
55
- <AtomicText type="bodyMedium" style={[styles.featureText, { color: textColor }]}>
48
+ <AtomicIcon name="checkmark-circle" size="sm" customColor={colors.iconColor} />
49
+ <AtomicText type="bodyMedium" style={[styles.featureText, { color: colors.textColor }]}>
56
50
  {feature}
57
51
  </AtomicText>
58
52
  </View>
@@ -1,16 +1,16 @@
1
1
  import React from "react";
2
2
  import { View, ScrollView, StyleSheet } from "react-native";
3
- import { AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
3
+ import { AtomicText } from "@umituz/react-native-design-system";
4
4
  import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
5
5
  import type { OnboardingThemeVariant } from "../../domain/entities/OnboardingTheme";
6
6
  import { QuestionSlideHeader } from "./QuestionSlideHeader";
7
7
  import { QuestionRenderer } from "./QuestionRenderer";
8
+ import { useOnboardingTheme } from "../providers/OnboardingThemeProvider";
8
9
 
9
10
  export interface QuestionSlideProps {
10
11
  slide: OnboardingSlide;
11
12
  value: any;
12
13
  onChange: (value: any) => void;
13
- useGradient?: boolean;
14
14
  variant?: OnboardingThemeVariant;
15
15
  }
16
16
 
@@ -18,9 +18,8 @@ export const QuestionSlide = ({
18
18
  slide,
19
19
  value,
20
20
  onChange,
21
- useGradient = false,
22
21
  }: QuestionSlideProps) => {
23
- const tokens = useAppDesignTokens();
22
+ const { colors } = useOnboardingTheme();
24
23
  const { question } = slide;
25
24
 
26
25
  if (!question) return null;
@@ -28,7 +27,7 @@ export const QuestionSlide = ({
28
27
  return (
29
28
  <ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false} bounces={false}>
30
29
  <View style={styles.slideContainer}>
31
- <QuestionSlideHeader slide={slide} useGradient={useGradient} />
30
+ <QuestionSlideHeader slide={slide} />
32
31
 
33
32
  <View style={styles.questionContainer}>
34
33
  <QuestionRenderer question={question} value={value} onChange={onChange} />
@@ -37,7 +36,7 @@ export const QuestionSlide = ({
37
36
  {question.validation?.required && !value && (
38
37
  <AtomicText
39
38
  type="labelSmall"
40
- style={[styles.requiredHint, { color: useGradient ? '#FFFFFF' : tokens.colors.error }]}
39
+ style={[styles.requiredHint, { color: colors.errorColor }]}
41
40
  >
42
41
  * This field is required
43
42
  </AtomicText>
@@ -1,46 +1,41 @@
1
1
  import React from "react";
2
2
  import { View, StyleSheet } from "react-native";
3
- import { AtomicIcon, AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
3
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
4
4
  import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
5
+ import { useOnboardingTheme } from "../providers/OnboardingThemeProvider";
5
6
 
6
7
  export interface QuestionSlideHeaderProps {
7
8
  slide: OnboardingSlide;
8
- useGradient: boolean;
9
9
  }
10
10
 
11
- const isEmoji = (icon: string): boolean =>
12
- /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u.test(icon);
13
-
14
11
  export const QuestionSlideHeader = ({
15
12
  slide,
16
- useGradient,
17
13
  }: QuestionSlideHeaderProps) => {
18
- const tokens = useAppDesignTokens();
19
- const textColor = useGradient ? "#FFFFFF" : tokens.colors.textPrimary;
20
- const subTextColor = useGradient ? "rgba(255, 255, 255, 0.9)" : tokens.colors.textSecondary;
14
+ const { colors } = useOnboardingTheme();
15
+ const isEmoji = slide.iconType === 'emoji';
21
16
 
22
17
  return (
23
18
  <View style={styles.container}>
24
19
  <View style={[
25
20
  styles.iconContainer,
26
21
  {
27
- backgroundColor: useGradient ? "rgba(255, 255, 255, 0.2)" : tokens.colors.primary + '15',
28
- borderColor: useGradient ? "rgba(255, 255, 255, 0.4)" : tokens.colors.primary + '30',
22
+ backgroundColor: colors.iconBg,
23
+ borderColor: colors.iconBorder,
29
24
  }
30
25
  ]}>
31
- {isEmoji(slide.icon) ? (
26
+ {isEmoji ? (
32
27
  <AtomicText style={{ fontSize: 48 }}>{slide.icon}</AtomicText>
33
28
  ) : (
34
- <AtomicIcon name={slide.icon as any} customSize={48} customColor={textColor} />
29
+ <AtomicIcon name={slide.icon as any} customSize={48} customColor={colors.textColor} />
35
30
  )}
36
31
  </View>
37
32
 
38
- <AtomicText type="headlineMedium" style={[styles.title, { color: textColor }]}>
33
+ <AtomicText type="headlineMedium" style={[styles.title, { color: colors.textColor }]}>
39
34
  {slide.title}
40
35
  </AtomicText>
41
36
 
42
37
  {slide.description && (
43
- <AtomicText type="bodyMedium" style={[styles.description, { color: subTextColor }]}>
38
+ <AtomicText type="bodyMedium" style={[styles.description, { color: colors.subTextColor }]}>
44
39
  {slide.description}
45
40
  </AtomicText>
46
41
  )}
@@ -0,0 +1,98 @@
1
+ import React, { createContext, useContext, useMemo } from 'react';
2
+ import { useAppDesignTokens } from '@umituz/react-native-design-system';
3
+
4
+ interface OnboardingColors {
5
+ iconColor: string;
6
+ textColor: string;
7
+ subTextColor: string;
8
+ buttonBg: string;
9
+ buttonTextColor: string;
10
+ progressBarBg: string;
11
+ progressFillColor: string;
12
+ dotColor: string;
13
+ activeDotColor: string;
14
+ progressTextColor: string;
15
+ headerButtonBg: string;
16
+ headerButtonBorder: string;
17
+ iconBg: string;
18
+ iconBorder: string;
19
+ errorColor: string;
20
+ }
21
+
22
+ interface OnboardingThemeValue {
23
+ colors: OnboardingColors;
24
+ useGradient: boolean;
25
+ }
26
+
27
+ const OnboardingTheme = createContext<OnboardingThemeValue | null>(null);
28
+
29
+ export interface OnboardingThemeProviderProps {
30
+ children: React.ReactNode;
31
+ useGradient?: boolean;
32
+ }
33
+
34
+ export const OnboardingThemeProvider = ({
35
+ children,
36
+ useGradient = false,
37
+ }: OnboardingThemeProviderProps) => {
38
+ const tokens = useAppDesignTokens();
39
+
40
+ const colors = useMemo<OnboardingColors>(() => {
41
+ if (useGradient) {
42
+ return {
43
+ iconColor: tokens.colors.surface,
44
+ textColor: tokens.colors.surface,
45
+ subTextColor: tokens.colors.surface + 'CC',
46
+ buttonBg: tokens.colors.surface,
47
+ buttonTextColor: tokens.colors.primary,
48
+ progressBarBg: tokens.colors.surface + '30',
49
+ progressFillColor: tokens.colors.surface,
50
+ dotColor: tokens.colors.surface + '60',
51
+ activeDotColor: tokens.colors.surface,
52
+ progressTextColor: tokens.colors.surface + 'CC',
53
+ headerButtonBg: tokens.colors.surface + '30',
54
+ headerButtonBorder: tokens.colors.surface + '50',
55
+ iconBg: tokens.colors.surface + '30',
56
+ iconBorder: tokens.colors.surface + '60',
57
+ errorColor: tokens.colors.surface,
58
+ };
59
+ }
60
+
61
+ return {
62
+ iconColor: tokens.colors.primary,
63
+ textColor: tokens.colors.textPrimary,
64
+ subTextColor: tokens.colors.textSecondary,
65
+ buttonBg: tokens.colors.primary,
66
+ buttonTextColor: tokens.colors.onPrimary,
67
+ progressBarBg: tokens.colors.borderLight,
68
+ progressFillColor: tokens.colors.primary,
69
+ dotColor: tokens.colors.borderLight,
70
+ activeDotColor: tokens.colors.primary,
71
+ progressTextColor: tokens.colors.textSecondary,
72
+ headerButtonBg: tokens.colors.surface,
73
+ headerButtonBorder: tokens.colors.borderLight,
74
+ iconBg: tokens.colors.primary + '15',
75
+ iconBorder: tokens.colors.primary + '30',
76
+ errorColor: tokens.colors.error,
77
+ };
78
+ }, [tokens, useGradient]);
79
+
80
+ const value = useMemo(
81
+ () => ({ colors, useGradient }),
82
+ [colors, useGradient]
83
+ );
84
+
85
+ return (
86
+ <OnboardingTheme.Provider value={value}>
87
+ {children}
88
+ </OnboardingTheme.Provider>
89
+ );
90
+ };
91
+
92
+ export const useOnboardingTheme = (): OnboardingThemeValue => {
93
+ const theme = useContext(OnboardingTheme);
94
+ if (!theme) {
95
+ throw new Error('useOnboardingTheme must be used within OnboardingThemeProvider');
96
+ }
97
+ return theme;
98
+ };
@@ -12,6 +12,7 @@ import { StyleSheet } from "react-native";
12
12
  import type { OnboardingOptions } from "../../domain/entities/OnboardingOptions";
13
13
  import { useOnboardingScreenState } from "../hooks/useOnboardingScreenState";
14
14
  import { OnboardingScreenContent } from "../components/OnboardingScreenContent";
15
+ import { OnboardingThemeProvider } from "../providers/OnboardingThemeProvider";
15
16
 
16
17
  export interface OnboardingScreenProps extends OnboardingOptions {
17
18
  /**
@@ -114,35 +115,37 @@ export const OnboardingScreen = ({
114
115
  }
115
116
 
116
117
  return (
117
- <OnboardingScreenContent
118
- containerStyle={[styles.container, containerStyle]}
119
- useGradient={useGradient}
120
- currentSlide={currentSlide}
121
- isFirstSlide={isFirstSlide}
122
- isLastSlide={isLastSlide}
123
- currentIndex={currentIndex}
124
- totalSlides={filteredSlides.length}
125
- currentAnswer={currentAnswer}
126
- isAnswerValid={isAnswerValid}
127
- showBackButton={showBackButton}
128
- showSkipButton={showSkipButton}
129
- showProgressBar={showProgressBar}
130
- showDots={showDots}
131
- showProgressText={showProgressText}
132
- skipButtonText={skipButtonText}
133
- nextButtonText={nextButtonText}
134
- getStartedButtonText={getStartedButtonText}
135
- onBack={handlePrevious}
136
- onSkip={handleSkip}
137
- onNext={handleNext}
138
- onAnswerChange={setCurrentAnswer}
139
- renderHeader={renderHeader}
140
- renderFooter={renderFooter}
141
- renderSlide={renderSlide}
142
- onUpgrade={onUpgrade}
143
- showPaywallOnComplete={showPaywallOnComplete}
144
- variant={themeVariant}
145
- />
118
+ <OnboardingThemeProvider useGradient={useGradient}>
119
+ <OnboardingScreenContent
120
+ containerStyle={[styles.container, containerStyle]}
121
+ useGradient={useGradient}
122
+ currentSlide={currentSlide}
123
+ isFirstSlide={isFirstSlide}
124
+ isLastSlide={isLastSlide}
125
+ currentIndex={currentIndex}
126
+ totalSlides={filteredSlides.length}
127
+ currentAnswer={currentAnswer}
128
+ isAnswerValid={isAnswerValid}
129
+ showBackButton={showBackButton}
130
+ showSkipButton={showSkipButton}
131
+ showProgressBar={showProgressBar}
132
+ showDots={showDots}
133
+ showProgressText={showProgressText}
134
+ skipButtonText={skipButtonText}
135
+ nextButtonText={nextButtonText}
136
+ getStartedButtonText={getStartedButtonText}
137
+ onBack={handlePrevious}
138
+ onSkip={handleSkip}
139
+ onNext={handleNext}
140
+ onAnswerChange={setCurrentAnswer}
141
+ renderHeader={renderHeader}
142
+ renderFooter={renderFooter}
143
+ renderSlide={renderSlide}
144
+ onUpgrade={onUpgrade}
145
+ showPaywallOnComplete={showPaywallOnComplete}
146
+ variant={themeVariant}
147
+ />
148
+ </OnboardingThemeProvider>
146
149
  );
147
150
  };
148
151