@umituz/react-native-onboarding 3.6.9 → 3.6.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 +1 -1
- package/src/domain/entities/OnboardingSlide.ts +16 -0
- package/src/index.ts +3 -1
- package/src/presentation/components/BackgroundImageCollage.tsx +159 -0
- package/src/presentation/components/OnboardingBackground.tsx +10 -0
- package/src/presentation/components/OnboardingFooter.tsx +4 -2
- package/src/presentation/components/OnboardingHeader.tsx +4 -2
- package/src/presentation/components/OnboardingScreenContent.tsx +3 -1
- package/src/presentation/components/OnboardingSlide.tsx +4 -2
- package/src/presentation/components/QuestionSlide.tsx +4 -2
- package/src/presentation/components/QuestionSlideHeader.tsx +5 -5
- package/src/presentation/components/questions/MultipleChoiceQuestion.tsx +4 -2
- package/src/presentation/components/questions/RatingQuestion.tsx +4 -2
- package/src/presentation/components/questions/SingleChoiceQuestion.tsx +4 -2
- package/src/presentation/components/questions/TextInputQuestion.tsx +4 -2
- package/src/presentation/providers/OnboardingProvider.tsx +51 -0
- package/src/presentation/screens/OnboardingScreen.tsx +10 -3
- package/src/presentation/types/OnboardingTheme.ts +27 -0
- package/src/domain/entities/OnboardingTheme.ts +0 -6
- package/src/presentation/providers/OnboardingThemeProvider.tsx +0 -103
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.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",
|
|
@@ -86,6 +86,22 @@ export interface OnboardingSlide {
|
|
|
86
86
|
*/
|
|
87
87
|
backgroundImage?: any;
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Optional multiple background images (URLs or require paths)
|
|
91
|
+
* Displayed in a collage/grid pattern behind content
|
|
92
|
+
* If provided, takes precedence over single backgroundImage
|
|
93
|
+
*/
|
|
94
|
+
backgroundImages?: any[];
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Layout pattern for multiple background images
|
|
98
|
+
* 'grid' - Equal sized grid
|
|
99
|
+
* 'masonry' - Pinterest-style masonry layout
|
|
100
|
+
* 'collage' - Random sizes and positions
|
|
101
|
+
* Default: 'grid'
|
|
102
|
+
*/
|
|
103
|
+
backgroundImagesLayout?: 'grid' | 'masonry' | 'collage';
|
|
104
|
+
|
|
89
105
|
/**
|
|
90
106
|
* Optional background video (URL or require path)
|
|
91
107
|
* Plays in loop behind content
|
package/src/index.ts
CHANGED
|
@@ -68,7 +68,9 @@ export {
|
|
|
68
68
|
export { OnboardingScreen, type OnboardingScreenProps } from "./presentation/screens/OnboardingScreen";
|
|
69
69
|
export { OnboardingHeader, type OnboardingHeaderProps } from "./presentation/components/OnboardingHeader";
|
|
70
70
|
export { OnboardingFooter, type OnboardingFooterProps } from "./presentation/components/OnboardingFooter";
|
|
71
|
-
export {
|
|
71
|
+
export { OnboardingProvider, type OnboardingProviderProps, useOnboardingProvider } from "./presentation/providers/OnboardingProvider";
|
|
72
|
+
export { BackgroundImageCollage } from "./presentation/components/BackgroundImageCollage";
|
|
73
|
+
export type { OnboardingTheme, OnboardingColors } from "./presentation/types/OnboardingTheme";
|
|
72
74
|
|
|
73
75
|
// Export OnboardingSlide component
|
|
74
76
|
// Note: TypeScript doesn't allow exporting both a type and a value with the same name
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Image Collage Component
|
|
3
|
+
* Displays multiple images in various layout patterns
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, { useMemo } from "react";
|
|
7
|
+
import { View, StyleSheet, Dimensions } from "react-native";
|
|
8
|
+
import { Image } from "expo-image";
|
|
9
|
+
|
|
10
|
+
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get("window");
|
|
11
|
+
|
|
12
|
+
interface BackgroundImageCollageProps {
|
|
13
|
+
images: any[];
|
|
14
|
+
layout?: 'grid' | 'masonry' | 'collage';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ImageLayout {
|
|
18
|
+
source: any;
|
|
19
|
+
style: {
|
|
20
|
+
position: 'absolute';
|
|
21
|
+
top: number;
|
|
22
|
+
left: number;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const BackgroundImageCollage: React.FC<BackgroundImageCollageProps> = ({
|
|
29
|
+
images,
|
|
30
|
+
layout = 'grid',
|
|
31
|
+
}) => {
|
|
32
|
+
const imageLayouts = useMemo(() => {
|
|
33
|
+
if (images.length === 0) return [];
|
|
34
|
+
|
|
35
|
+
switch (layout) {
|
|
36
|
+
case 'grid':
|
|
37
|
+
return generateGridLayout(images);
|
|
38
|
+
case 'masonry':
|
|
39
|
+
return generateMasonryLayout(images);
|
|
40
|
+
case 'collage':
|
|
41
|
+
return generateCollageLayout(images);
|
|
42
|
+
default:
|
|
43
|
+
return generateGridLayout(images);
|
|
44
|
+
}
|
|
45
|
+
}, [images, layout]);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="none">
|
|
49
|
+
{imageLayouts.map((item, index) => (
|
|
50
|
+
<Image
|
|
51
|
+
key={index}
|
|
52
|
+
source={item.source}
|
|
53
|
+
style={item.style}
|
|
54
|
+
contentFit="cover"
|
|
55
|
+
transition={300}
|
|
56
|
+
/>
|
|
57
|
+
))}
|
|
58
|
+
</View>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const generateGridLayout = (images: any[]): ImageLayout[] => {
|
|
63
|
+
const count = images.length;
|
|
64
|
+
const cols = Math.ceil(Math.sqrt(count));
|
|
65
|
+
const rows = Math.ceil(count / cols);
|
|
66
|
+
const cellWidth = SCREEN_WIDTH / cols;
|
|
67
|
+
const cellHeight = SCREEN_HEIGHT / rows;
|
|
68
|
+
|
|
69
|
+
return images.map((source, index) => {
|
|
70
|
+
const col = index % cols;
|
|
71
|
+
const row = Math.floor(index / cols);
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
source,
|
|
75
|
+
style: {
|
|
76
|
+
position: 'absolute' as const,
|
|
77
|
+
left: col * cellWidth,
|
|
78
|
+
top: row * cellHeight,
|
|
79
|
+
width: cellWidth,
|
|
80
|
+
height: cellHeight,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const generateMasonryLayout = (images: any[]): ImageLayout[] => {
|
|
87
|
+
const cols = 3;
|
|
88
|
+
const colWidth = SCREEN_WIDTH / cols;
|
|
89
|
+
const columnHeights = new Array(cols).fill(0);
|
|
90
|
+
|
|
91
|
+
return images.map((source, _index) => {
|
|
92
|
+
const shortestCol = columnHeights.indexOf(Math.min(...columnHeights));
|
|
93
|
+
const height = colWidth * (0.7 + Math.random() * 0.6);
|
|
94
|
+
|
|
95
|
+
const layout: ImageLayout = {
|
|
96
|
+
source,
|
|
97
|
+
style: {
|
|
98
|
+
position: 'absolute' as const,
|
|
99
|
+
left: shortestCol * colWidth,
|
|
100
|
+
top: columnHeights[shortestCol],
|
|
101
|
+
width: colWidth,
|
|
102
|
+
height,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
columnHeights[shortestCol] += height;
|
|
107
|
+
return layout;
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const generateCollageLayout = (images: any[]): ImageLayout[] => {
|
|
112
|
+
const layouts: ImageLayout[] = [];
|
|
113
|
+
const minSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.25;
|
|
114
|
+
const maxSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.45;
|
|
115
|
+
|
|
116
|
+
images.forEach((source, _index) => {
|
|
117
|
+
const size = minSize + Math.random() * (maxSize - minSize);
|
|
118
|
+
const maxX = SCREEN_WIDTH - size;
|
|
119
|
+
const maxY = SCREEN_HEIGHT - size;
|
|
120
|
+
|
|
121
|
+
let attempts = 0;
|
|
122
|
+
let overlaps = true;
|
|
123
|
+
let left = 0;
|
|
124
|
+
let top = 0;
|
|
125
|
+
|
|
126
|
+
while (overlaps && attempts < 50) {
|
|
127
|
+
left = Math.random() * maxX;
|
|
128
|
+
top = Math.random() * maxY;
|
|
129
|
+
overlaps = false;
|
|
130
|
+
|
|
131
|
+
for (const existing of layouts) {
|
|
132
|
+
if (
|
|
133
|
+
left < existing.style.left + existing.style.width &&
|
|
134
|
+
left + size > existing.style.left &&
|
|
135
|
+
top < existing.style.top + existing.style.height &&
|
|
136
|
+
top + size > existing.style.top
|
|
137
|
+
) {
|
|
138
|
+
overlaps = true;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
attempts++;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
layouts.push({
|
|
147
|
+
source,
|
|
148
|
+
style: {
|
|
149
|
+
position: 'absolute' as const,
|
|
150
|
+
left,
|
|
151
|
+
top,
|
|
152
|
+
width: size,
|
|
153
|
+
height: size,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return layouts;
|
|
159
|
+
};
|
|
@@ -9,6 +9,7 @@ import { LinearGradient } from "expo-linear-gradient";
|
|
|
9
9
|
import { Image } from "expo-image";
|
|
10
10
|
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
11
11
|
import { BackgroundVideo } from "./BackgroundVideo";
|
|
12
|
+
import { BackgroundImageCollage } from "./BackgroundImageCollage";
|
|
12
13
|
|
|
13
14
|
interface OnboardingBackgroundProps {
|
|
14
15
|
currentSlide: OnboardingSlide | undefined;
|
|
@@ -35,6 +36,15 @@ export const OnboardingBackground: React.FC<OnboardingBackgroundProps> = ({
|
|
|
35
36
|
);
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
if (currentSlide.backgroundImages && currentSlide.backgroundImages.length > 0) {
|
|
40
|
+
return (
|
|
41
|
+
<BackgroundImageCollage
|
|
42
|
+
images={currentSlide.backgroundImages}
|
|
43
|
+
layout={currentSlide.backgroundImagesLayout || 'grid'}
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
if (currentSlide.backgroundImage) {
|
|
39
49
|
return (
|
|
40
50
|
<Image
|
|
@@ -3,7 +3,7 @@ import { View, StyleSheet, TouchableOpacity } from "react-native";
|
|
|
3
3
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
4
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
5
5
|
import { AtomicText } from "@umituz/react-native-design-system";
|
|
6
|
-
import {
|
|
6
|
+
import { useOnboardingProvider } from "../providers/OnboardingProvider";
|
|
7
7
|
|
|
8
8
|
export interface OnboardingFooterProps {
|
|
9
9
|
currentIndex: number;
|
|
@@ -32,7 +32,9 @@ export const OnboardingFooter = ({
|
|
|
32
32
|
}: OnboardingFooterProps) => {
|
|
33
33
|
const insets = useSafeAreaInsets();
|
|
34
34
|
const { t } = useLocalization();
|
|
35
|
-
const {
|
|
35
|
+
const {
|
|
36
|
+
theme: { colors },
|
|
37
|
+
} = useOnboardingProvider();
|
|
36
38
|
|
|
37
39
|
const buttonText = isLastSlide
|
|
38
40
|
? getStartedButtonText || t("onboarding.getStarted")
|
|
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
2
2
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
|
|
4
4
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
5
|
-
import {
|
|
5
|
+
import { useOnboardingProvider } from "../providers/OnboardingProvider";
|
|
6
6
|
|
|
7
7
|
export interface OnboardingHeaderProps {
|
|
8
8
|
isFirstSlide: boolean;
|
|
@@ -22,7 +22,9 @@ export const OnboardingHeader = ({
|
|
|
22
22
|
skipButtonText,
|
|
23
23
|
}: OnboardingHeaderProps) => {
|
|
24
24
|
const { t } = useLocalization();
|
|
25
|
-
const {
|
|
25
|
+
const {
|
|
26
|
+
theme: { colors },
|
|
27
|
+
} = useOnboardingProvider();
|
|
26
28
|
|
|
27
29
|
const skipText = skipButtonText || t("onboarding.skip");
|
|
28
30
|
|
|
@@ -53,7 +53,9 @@ export const OnboardingScreenContent = ({
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
const hasMedia =
|
|
56
|
-
!!currentSlide?.backgroundImage ||
|
|
56
|
+
!!currentSlide?.backgroundImage ||
|
|
57
|
+
!!currentSlide?.backgroundVideo ||
|
|
58
|
+
(!!currentSlide?.backgroundImages && currentSlide.backgroundImages.length > 0);
|
|
57
59
|
const overlayOpacity = currentSlide?.overlayOpacity ?? 0.5;
|
|
58
60
|
const effectivelyUseGradient = useGradient || hasMedia;
|
|
59
61
|
|
|
@@ -8,7 +8,7 @@ import { View, StyleSheet } from "react-native";
|
|
|
8
8
|
import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
|
|
9
9
|
import type { OnboardingSlide as OnboardingSlideType } from "../../domain/entities/OnboardingSlide";
|
|
10
10
|
import { BaseSlide } from "./BaseSlide";
|
|
11
|
-
import {
|
|
11
|
+
import { useOnboardingProvider } from "../providers/OnboardingProvider";
|
|
12
12
|
|
|
13
13
|
export interface OnboardingSlideProps {
|
|
14
14
|
slide: OnboardingSlideType;
|
|
@@ -19,7 +19,9 @@ export const OnboardingSlide = ({
|
|
|
19
19
|
slide,
|
|
20
20
|
variant = "default",
|
|
21
21
|
}: OnboardingSlideProps) => {
|
|
22
|
-
const {
|
|
22
|
+
const {
|
|
23
|
+
theme: { colors },
|
|
24
|
+
} = useOnboardingProvider();
|
|
23
25
|
|
|
24
26
|
const shouldShowIcon = slide.icon && !slide.hideIcon;
|
|
25
27
|
const isEmoji = slide.iconType === 'emoji';
|
|
@@ -10,7 +10,7 @@ import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
|
10
10
|
import { QuestionSlideHeader } from "./QuestionSlideHeader";
|
|
11
11
|
import { QuestionRenderer } from "./QuestionRenderer";
|
|
12
12
|
import { BaseSlide } from "./BaseSlide";
|
|
13
|
-
import {
|
|
13
|
+
import { useOnboardingProvider } from "../providers/OnboardingProvider";
|
|
14
14
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
15
15
|
|
|
16
16
|
export interface QuestionSlideProps {
|
|
@@ -26,7 +26,9 @@ export const QuestionSlide = ({
|
|
|
26
26
|
onChange,
|
|
27
27
|
variant: _variant = "default",
|
|
28
28
|
}: QuestionSlideProps) => {
|
|
29
|
-
const {
|
|
29
|
+
const {
|
|
30
|
+
theme: { colors },
|
|
31
|
+
} = useOnboardingProvider();
|
|
30
32
|
const { t } = useLocalization();
|
|
31
33
|
const { question } = slide;
|
|
32
34
|
|
|
@@ -2,16 +2,16 @@ import React from "react";
|
|
|
2
2
|
import { View, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
|
|
4
4
|
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
5
|
-
import {
|
|
5
|
+
import { useOnboardingProvider } from "../providers/OnboardingProvider";
|
|
6
6
|
|
|
7
7
|
export interface QuestionSlideHeaderProps {
|
|
8
8
|
slide: OnboardingSlide;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
export const QuestionSlideHeader = ({
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
export const QuestionSlideHeader = ({ slide }: QuestionSlideHeaderProps) => {
|
|
12
|
+
const {
|
|
13
|
+
theme: { colors },
|
|
14
|
+
} = useOnboardingProvider();
|
|
15
15
|
const isEmoji = slide.iconType === 'emoji';
|
|
16
16
|
|
|
17
17
|
return (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
|
|
4
|
-
import {
|
|
4
|
+
import { useOnboardingProvider } from "../../providers/OnboardingProvider";
|
|
5
5
|
import type { OnboardingQuestion, QuestionOption } from "../../../domain/entities/OnboardingQuestion";
|
|
6
6
|
|
|
7
7
|
export interface MultipleChoiceQuestionProps {
|
|
@@ -15,7 +15,9 @@ export const MultipleChoiceQuestion = ({
|
|
|
15
15
|
value = [],
|
|
16
16
|
onChange,
|
|
17
17
|
}: MultipleChoiceQuestionProps) => {
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
theme: { colors },
|
|
20
|
+
} = useOnboardingProvider();
|
|
19
21
|
|
|
20
22
|
const handleToggle = (optionId: string) => {
|
|
21
23
|
const newValue = value.includes(optionId)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
|
|
4
|
-
import {
|
|
4
|
+
import { useOnboardingProvider } from "../../providers/OnboardingProvider";
|
|
5
5
|
import type { OnboardingQuestion } from "../../../domain/entities/OnboardingQuestion";
|
|
6
6
|
|
|
7
7
|
export interface RatingQuestionProps {
|
|
@@ -15,7 +15,9 @@ export const RatingQuestion = ({
|
|
|
15
15
|
value = 0,
|
|
16
16
|
onChange,
|
|
17
17
|
}: RatingQuestionProps) => {
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
theme: { colors },
|
|
20
|
+
} = useOnboardingProvider();
|
|
19
21
|
const max = question.validation?.max ?? 5;
|
|
20
22
|
|
|
21
23
|
return (
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
|
|
4
|
-
import {
|
|
4
|
+
import { useOnboardingProvider } from "../../providers/OnboardingProvider";
|
|
5
5
|
import type { OnboardingQuestion, QuestionOption } from "../../../domain/entities/OnboardingQuestion";
|
|
6
6
|
|
|
7
7
|
export interface SingleChoiceQuestionProps {
|
|
@@ -15,7 +15,9 @@ export const SingleChoiceQuestion = ({
|
|
|
15
15
|
value,
|
|
16
16
|
onChange,
|
|
17
17
|
}: SingleChoiceQuestionProps) => {
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
theme: { colors },
|
|
20
|
+
} = useOnboardingProvider();
|
|
19
21
|
|
|
20
22
|
const renderOption = (option: QuestionOption) => {
|
|
21
23
|
const isSelected = value === option.id;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { View, TextInput, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicText } from "@umituz/react-native-design-system";
|
|
4
|
-
import {
|
|
4
|
+
import { useOnboardingProvider } from "../../providers/OnboardingProvider";
|
|
5
5
|
import type { OnboardingQuestion } from "../../../domain/entities/OnboardingQuestion";
|
|
6
6
|
|
|
7
7
|
export interface TextInputQuestionProps {
|
|
@@ -15,7 +15,9 @@ export const TextInputQuestion = ({
|
|
|
15
15
|
value = "",
|
|
16
16
|
onChange,
|
|
17
17
|
}: TextInputQuestionProps) => {
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
theme: { colors },
|
|
20
|
+
} = useOnboardingProvider();
|
|
19
21
|
const { validation } = question;
|
|
20
22
|
|
|
21
23
|
return (
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Provider
|
|
3
|
+
*
|
|
4
|
+
* Central manager for onboarding theme and configuration.
|
|
5
|
+
* All values are passed from the main application.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React, { createContext, useContext, useMemo } from "react";
|
|
9
|
+
import type { OnboardingTheme, OnboardingColors } from "../types/OnboardingTheme";
|
|
10
|
+
|
|
11
|
+
interface OnboardingProviderValue {
|
|
12
|
+
theme: OnboardingTheme;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const OnboardingScope = createContext<OnboardingProviderValue | undefined>(undefined);
|
|
16
|
+
|
|
17
|
+
export interface OnboardingProviderProps {
|
|
18
|
+
children: React.ReactNode;
|
|
19
|
+
useGradient: boolean;
|
|
20
|
+
colors: OnboardingColors;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const OnboardingProvider = ({
|
|
24
|
+
children,
|
|
25
|
+
useGradient,
|
|
26
|
+
colors,
|
|
27
|
+
}: OnboardingProviderProps) => {
|
|
28
|
+
const value = useMemo(
|
|
29
|
+
() => ({
|
|
30
|
+
theme: {
|
|
31
|
+
colors,
|
|
32
|
+
useGradient,
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
[colors, useGradient]
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<OnboardingScope.Provider value={value}>
|
|
40
|
+
{children}
|
|
41
|
+
</OnboardingScope.Provider>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const useOnboardingProvider = (): OnboardingProviderValue => {
|
|
46
|
+
const scope = useContext(OnboardingScope);
|
|
47
|
+
if (!scope) {
|
|
48
|
+
throw new Error("useOnboardingProvider must be used within OnboardingProvider");
|
|
49
|
+
}
|
|
50
|
+
return scope;
|
|
51
|
+
};
|
|
@@ -12,7 +12,8 @@ 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 {
|
|
15
|
+
import { OnboardingProvider } from "../providers/OnboardingProvider";
|
|
16
|
+
import type { OnboardingColors } from "../types/OnboardingTheme";
|
|
16
17
|
|
|
17
18
|
export interface OnboardingScreenProps extends OnboardingOptions {
|
|
18
19
|
/**
|
|
@@ -52,6 +53,11 @@ export interface OnboardingScreenProps extends OnboardingOptions {
|
|
|
52
53
|
* When true, shows premium paywall before completing onboarding
|
|
53
54
|
*/
|
|
54
55
|
showPaywallOnComplete?: boolean;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Theme colors for the onboarding (Required to avoid hardcoded values)
|
|
59
|
+
*/
|
|
60
|
+
themeColors: OnboardingColors;
|
|
55
61
|
}
|
|
56
62
|
|
|
57
63
|
export const OnboardingScreen = ({
|
|
@@ -75,6 +81,7 @@ export const OnboardingScreen = ({
|
|
|
75
81
|
showPaywallOnComplete = false,
|
|
76
82
|
useGradient: globalUseGradient = false,
|
|
77
83
|
themeVariant = "default",
|
|
84
|
+
themeColors,
|
|
78
85
|
}: OnboardingScreenProps) => {
|
|
79
86
|
if (__DEV__) {
|
|
80
87
|
console.log("[OnboardingScreen] Rendering with slides:", slides?.length);
|
|
@@ -115,7 +122,7 @@ export const OnboardingScreen = ({
|
|
|
115
122
|
}
|
|
116
123
|
|
|
117
124
|
return (
|
|
118
|
-
<
|
|
125
|
+
<OnboardingProvider useGradient={useGradient} colors={themeColors}>
|
|
119
126
|
<OnboardingScreenContent
|
|
120
127
|
containerStyle={[styles.container, containerStyle]}
|
|
121
128
|
useGradient={useGradient}
|
|
@@ -145,7 +152,7 @@ export const OnboardingScreen = ({
|
|
|
145
152
|
showPaywallOnComplete={showPaywallOnComplete}
|
|
146
153
|
variant={themeVariant}
|
|
147
154
|
/>
|
|
148
|
-
</
|
|
155
|
+
</OnboardingProvider>
|
|
149
156
|
);
|
|
150
157
|
};
|
|
151
158
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Theme Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface OnboardingColors {
|
|
6
|
+
iconColor: string;
|
|
7
|
+
textColor: string;
|
|
8
|
+
subTextColor: string;
|
|
9
|
+
buttonBg: string;
|
|
10
|
+
buttonTextColor: string;
|
|
11
|
+
progressBarBg: string;
|
|
12
|
+
progressFillColor: string;
|
|
13
|
+
dotColor: string;
|
|
14
|
+
activeDotColor: string;
|
|
15
|
+
progressTextColor: string;
|
|
16
|
+
headerButtonBg: string;
|
|
17
|
+
headerButtonBorder: string;
|
|
18
|
+
iconBg: string;
|
|
19
|
+
iconBorder: string;
|
|
20
|
+
errorColor: string;
|
|
21
|
+
featureItemBg: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface OnboardingTheme {
|
|
25
|
+
colors: OnboardingColors;
|
|
26
|
+
useGradient: boolean;
|
|
27
|
+
}
|
|
@@ -1,103 +0,0 @@
|
|
|
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
|
-
featureItemBg: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface OnboardingThemeValue {
|
|
24
|
-
colors: OnboardingColors;
|
|
25
|
-
useGradient: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const OnboardingThemeInternal = createContext<OnboardingThemeValue | undefined>(undefined);
|
|
29
|
-
|
|
30
|
-
export interface OnboardingThemeProviderProps {
|
|
31
|
-
children: React.ReactNode;
|
|
32
|
-
useGradient?: boolean;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export const OnboardingThemeProvider = ({
|
|
36
|
-
children,
|
|
37
|
-
useGradient = false,
|
|
38
|
-
}: OnboardingThemeProviderProps) => {
|
|
39
|
-
const tokens = useAppDesignTokens();
|
|
40
|
-
|
|
41
|
-
const colors = useMemo<OnboardingColors>(() => {
|
|
42
|
-
const primaryContent = tokens.colors.onPrimary || "#FFFFFF";
|
|
43
|
-
|
|
44
|
-
if (useGradient) {
|
|
45
|
-
return {
|
|
46
|
-
iconColor: primaryContent,
|
|
47
|
-
textColor: primaryContent,
|
|
48
|
-
subTextColor: primaryContent + "CC",
|
|
49
|
-
buttonBg: primaryContent,
|
|
50
|
-
buttonTextColor: tokens.colors.primary,
|
|
51
|
-
progressBarBg: primaryContent + "40",
|
|
52
|
-
progressFillColor: primaryContent,
|
|
53
|
-
dotColor: primaryContent + "66",
|
|
54
|
-
activeDotColor: primaryContent,
|
|
55
|
-
progressTextColor: primaryContent + "CC",
|
|
56
|
-
headerButtonBg: primaryContent + "33",
|
|
57
|
-
headerButtonBorder: primaryContent + "59",
|
|
58
|
-
iconBg: primaryContent + "33",
|
|
59
|
-
iconBorder: primaryContent + "59",
|
|
60
|
-
errorColor: "#FFCDD2",
|
|
61
|
-
featureItemBg: primaryContent + "1A",
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
iconColor: tokens.colors.primary,
|
|
67
|
-
textColor: tokens.colors.textPrimary,
|
|
68
|
-
subTextColor: tokens.colors.textSecondary,
|
|
69
|
-
buttonBg: tokens.colors.primary,
|
|
70
|
-
buttonTextColor: tokens.colors.onPrimary,
|
|
71
|
-
progressBarBg: tokens.colors.borderLight,
|
|
72
|
-
progressFillColor: tokens.colors.primary,
|
|
73
|
-
dotColor: tokens.colors.borderLight,
|
|
74
|
-
activeDotColor: tokens.colors.primary,
|
|
75
|
-
progressTextColor: tokens.colors.textSecondary,
|
|
76
|
-
headerButtonBg: tokens.colors.surface,
|
|
77
|
-
headerButtonBorder: tokens.colors.borderLight,
|
|
78
|
-
iconBg: tokens.colors.primary + '15',
|
|
79
|
-
iconBorder: tokens.colors.primary + '30',
|
|
80
|
-
errorColor: tokens.colors.error,
|
|
81
|
-
featureItemBg: tokens.colors.surfaceSecondary || tokens.colors.surfaceVariant || "rgba(0,0,0,0.05)",
|
|
82
|
-
};
|
|
83
|
-
}, [tokens, useGradient]);
|
|
84
|
-
|
|
85
|
-
const value = useMemo(
|
|
86
|
-
() => ({ colors, useGradient }),
|
|
87
|
-
[colors, useGradient]
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
return (
|
|
91
|
-
<OnboardingThemeInternal.Provider value={value}>
|
|
92
|
-
{children}
|
|
93
|
-
</OnboardingThemeInternal.Provider>
|
|
94
|
-
);
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
export const useOnboardingTheme = (): OnboardingThemeValue => {
|
|
98
|
-
const theme = useContext(OnboardingThemeInternal);
|
|
99
|
-
if (!theme) {
|
|
100
|
-
throw new Error('useOnboardingTheme must be used within OnboardingThemeProvider');
|
|
101
|
-
}
|
|
102
|
-
return theme;
|
|
103
|
-
};
|