@umituz/react-native-onboarding 2.1.1 → 2.2.0
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 +3 -3
- package/src/domain/entities/OnboardingSlide.ts +10 -2
- package/src/infrastructure/hooks/useOnboardingAnswers.ts +69 -0
- package/src/infrastructure/services/OnboardingSlideService.ts +49 -0
- package/src/infrastructure/services/OnboardingValidationService.ts +128 -0
- package/src/infrastructure/utils/gradientUtils.ts +26 -0
- package/src/presentation/components/OnboardingFooter.tsx +24 -10
- package/src/presentation/components/OnboardingHeader.tsx +17 -6
- package/src/presentation/components/OnboardingSlide.tsx +25 -9
- package/src/presentation/components/QuestionSlide.tsx +24 -8
- package/src/presentation/screens/OnboardingScreen.tsx +93 -111
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-onboarding",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "Advanced onboarding flow for React Native apps with personalization questions,
|
|
3
|
+
"version": "2.2.0",
|
|
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",
|
|
7
7
|
"scripts": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"onboarding",
|
|
17
17
|
"welcome",
|
|
18
18
|
"tutorial",
|
|
19
|
-
"
|
|
19
|
+
"theme",
|
|
20
20
|
"animation",
|
|
21
21
|
"ddd",
|
|
22
22
|
"domain-driven-design",
|
|
@@ -42,10 +42,18 @@ export interface OnboardingSlide {
|
|
|
42
42
|
icon: string;
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
* Gradient colors for the slide background
|
|
45
|
+
* Gradient colors for the slide background (optional)
|
|
46
46
|
* [startColor, endColor] or [color1, color2, color3] for multi-stop gradients
|
|
47
|
+
* Only used if useGradient is true
|
|
47
48
|
*/
|
|
48
|
-
gradient
|
|
49
|
+
gradient?: string[];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Use gradient background instead of theme colors (default: false)
|
|
53
|
+
* If true and gradient is provided, gradient will be used
|
|
54
|
+
* If false or gradient not provided, theme background colors will be used
|
|
55
|
+
*/
|
|
56
|
+
useGradient?: boolean;
|
|
49
57
|
|
|
50
58
|
/**
|
|
51
59
|
* Optional image URL (alternative to icon)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOnboardingAnswers Hook
|
|
3
|
+
*
|
|
4
|
+
* Manages answer state and operations for onboarding questions
|
|
5
|
+
* Follows Single Responsibility Principle
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useState, useEffect, useCallback } from "react";
|
|
9
|
+
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
10
|
+
import { useOnboardingStore } from "../storage/OnboardingStore";
|
|
11
|
+
|
|
12
|
+
export interface UseOnboardingAnswersReturn {
|
|
13
|
+
currentAnswer: any;
|
|
14
|
+
setCurrentAnswer: (answer: any) => void;
|
|
15
|
+
loadAnswerForSlide: (slide: OnboardingSlide | undefined) => void;
|
|
16
|
+
saveCurrentAnswer: (slide: OnboardingSlide | undefined) => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hook for managing onboarding question answers
|
|
21
|
+
* @param currentSlide - The current slide being displayed
|
|
22
|
+
* @returns Answer state and operations
|
|
23
|
+
*/
|
|
24
|
+
export function useOnboardingAnswers(
|
|
25
|
+
currentSlide: OnboardingSlide | undefined,
|
|
26
|
+
): UseOnboardingAnswersReturn {
|
|
27
|
+
const onboardingStore = useOnboardingStore();
|
|
28
|
+
const [currentAnswer, setCurrentAnswer] = useState<any>(undefined);
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Load answer for a specific slide
|
|
32
|
+
*/
|
|
33
|
+
const loadAnswerForSlide = useCallback(
|
|
34
|
+
(slide: OnboardingSlide | undefined) => {
|
|
35
|
+
if (slide?.question) {
|
|
36
|
+
const savedAnswer = onboardingStore.getAnswer(slide.question.id);
|
|
37
|
+
setCurrentAnswer(savedAnswer ?? slide.question.defaultValue);
|
|
38
|
+
} else {
|
|
39
|
+
setCurrentAnswer(undefined);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
[onboardingStore],
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Save current answer for a slide
|
|
47
|
+
*/
|
|
48
|
+
const saveCurrentAnswer = useCallback(
|
|
49
|
+
async (slide: OnboardingSlide | undefined) => {
|
|
50
|
+
if (slide?.question && currentAnswer !== undefined) {
|
|
51
|
+
await onboardingStore.saveAnswer(slide.question.id, currentAnswer);
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
[currentAnswer, onboardingStore],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Load answer when slide changes
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
loadAnswerForSlide(currentSlide);
|
|
60
|
+
}, [currentSlide, loadAnswerForSlide]);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
currentAnswer,
|
|
64
|
+
setCurrentAnswer,
|
|
65
|
+
loadAnswerForSlide,
|
|
66
|
+
saveCurrentAnswer,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Slide Service
|
|
3
|
+
*
|
|
4
|
+
* Business logic for filtering and processing onboarding slides
|
|
5
|
+
* Follows Single Responsibility Principle
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
9
|
+
import type { OnboardingUserData } from "../../domain/entities/OnboardingUserData";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Service for managing onboarding slide operations
|
|
13
|
+
*/
|
|
14
|
+
export class OnboardingSlideService {
|
|
15
|
+
/**
|
|
16
|
+
* Filter slides based on skipIf conditions
|
|
17
|
+
* @param slides - All available slides
|
|
18
|
+
* @param userData - User's onboarding data including answers
|
|
19
|
+
* @returns Filtered slides that should be shown
|
|
20
|
+
*/
|
|
21
|
+
static filterSlides(
|
|
22
|
+
slides: OnboardingSlide[],
|
|
23
|
+
userData: OnboardingUserData,
|
|
24
|
+
): OnboardingSlide[] {
|
|
25
|
+
return slides.filter((slide) => {
|
|
26
|
+
if (slide.skipIf) {
|
|
27
|
+
return !slide.skipIf(userData.answers);
|
|
28
|
+
}
|
|
29
|
+
return true;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get slide at specific index
|
|
35
|
+
* @param slides - Filtered slides array
|
|
36
|
+
* @param index - Slide index
|
|
37
|
+
* @returns Slide at index or undefined
|
|
38
|
+
*/
|
|
39
|
+
static getSlideAtIndex(
|
|
40
|
+
slides: OnboardingSlide[],
|
|
41
|
+
index: number,
|
|
42
|
+
): OnboardingSlide | undefined {
|
|
43
|
+
if (index < 0 || index >= slides.length) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
return slides[index];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Validation Service
|
|
3
|
+
*
|
|
4
|
+
* Business logic for validating onboarding question answers
|
|
5
|
+
* Follows Single Responsibility Principle
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OnboardingQuestion } from "../../domain/entities/OnboardingQuestion";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Service for validating onboarding question answers
|
|
12
|
+
*/
|
|
13
|
+
export class OnboardingValidationService {
|
|
14
|
+
/**
|
|
15
|
+
* Validate answer against question validation rules
|
|
16
|
+
* @param question - The question to validate against
|
|
17
|
+
* @param answer - The answer to validate
|
|
18
|
+
* @returns true if valid, false otherwise
|
|
19
|
+
*/
|
|
20
|
+
static validateAnswer(
|
|
21
|
+
question: OnboardingQuestion,
|
|
22
|
+
answer: any,
|
|
23
|
+
): boolean {
|
|
24
|
+
const { validation } = question;
|
|
25
|
+
if (!validation) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Required validation
|
|
30
|
+
if (validation.required && !answer) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Type-specific validations
|
|
35
|
+
switch (question.type) {
|
|
36
|
+
case "multiple_choice":
|
|
37
|
+
return this.validateMultipleChoice(answer, validation);
|
|
38
|
+
case "text_input":
|
|
39
|
+
return this.validateTextInput(answer, validation);
|
|
40
|
+
case "slider":
|
|
41
|
+
case "rating":
|
|
42
|
+
return this.validateNumeric(answer, validation);
|
|
43
|
+
default:
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Custom validator
|
|
48
|
+
if (validation.customValidator) {
|
|
49
|
+
const customResult = validation.customValidator(answer);
|
|
50
|
+
return customResult === true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Validate multiple choice answer
|
|
58
|
+
*/
|
|
59
|
+
private static validateMultipleChoice(
|
|
60
|
+
answer: any,
|
|
61
|
+
validation: OnboardingQuestion["validation"],
|
|
62
|
+
): boolean {
|
|
63
|
+
if (!validation) return true;
|
|
64
|
+
|
|
65
|
+
if (validation.minSelections) {
|
|
66
|
+
if (!answer || !Array.isArray(answer) || answer.length < validation.minSelections) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (validation.maxSelections) {
|
|
72
|
+
if (Array.isArray(answer) && answer.length > validation.maxSelections) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validate text input answer
|
|
82
|
+
*/
|
|
83
|
+
private static validateTextInput(
|
|
84
|
+
answer: any,
|
|
85
|
+
validation: OnboardingQuestion["validation"],
|
|
86
|
+
): boolean {
|
|
87
|
+
if (!validation) return true;
|
|
88
|
+
|
|
89
|
+
if (typeof answer !== "string") {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (validation.minLength && answer.length < validation.minLength) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (validation.maxLength && answer.length > validation.maxLength) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate numeric answer (slider/rating)
|
|
106
|
+
*/
|
|
107
|
+
private static validateNumeric(
|
|
108
|
+
answer: any,
|
|
109
|
+
validation: OnboardingQuestion["validation"],
|
|
110
|
+
): boolean {
|
|
111
|
+
if (!validation) return true;
|
|
112
|
+
|
|
113
|
+
if (typeof answer !== "number") {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (validation.min !== undefined && answer < validation.min) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (validation.max !== undefined && answer > validation.max) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gradient Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utility functions for gradient-related operations
|
|
5
|
+
* Follows Single Responsibility Principle
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if slide should use gradient background
|
|
12
|
+
* @param slide - The slide to check
|
|
13
|
+
* @returns true if gradient should be used, false otherwise
|
|
14
|
+
*/
|
|
15
|
+
export function shouldUseGradient(slide: OnboardingSlide | undefined): boolean {
|
|
16
|
+
if (!slide) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
slide.useGradient === true &&
|
|
22
|
+
slide.gradient !== undefined &&
|
|
23
|
+
slide.gradient.length > 0
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
@@ -21,6 +21,7 @@ export interface OnboardingFooterProps {
|
|
|
21
21
|
nextButtonText?: string;
|
|
22
22
|
getStartedButtonText?: string;
|
|
23
23
|
disabled?: boolean;
|
|
24
|
+
useGradient?: boolean;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export const OnboardingFooter: React.FC<OnboardingFooterProps> = ({
|
|
@@ -34,11 +35,12 @@ export const OnboardingFooter: React.FC<OnboardingFooterProps> = ({
|
|
|
34
35
|
nextButtonText,
|
|
35
36
|
getStartedButtonText,
|
|
36
37
|
disabled = false,
|
|
38
|
+
useGradient = false,
|
|
37
39
|
}) => {
|
|
38
40
|
const insets = useSafeAreaInsets();
|
|
39
41
|
const { t } = useLocalization();
|
|
40
42
|
const tokens = useAppDesignTokens();
|
|
41
|
-
const styles = useMemo(() => getStyles(insets, tokens), [insets, tokens]);
|
|
43
|
+
const styles = useMemo(() => getStyles(insets, tokens, useGradient), [insets, tokens, useGradient]);
|
|
42
44
|
|
|
43
45
|
const buttonText = isLastSlide
|
|
44
46
|
? getStartedButtonText || t("onboarding.getStarted", "Get Started")
|
|
@@ -93,6 +95,7 @@ export const OnboardingFooter: React.FC<OnboardingFooterProps> = ({
|
|
|
93
95
|
const getStyles = (
|
|
94
96
|
insets: { bottom: number },
|
|
95
97
|
tokens: ReturnType<typeof useAppDesignTokens>,
|
|
98
|
+
useGradient: boolean,
|
|
96
99
|
) =>
|
|
97
100
|
StyleSheet.create({
|
|
98
101
|
footer: {
|
|
@@ -105,13 +108,15 @@ const getStyles = (
|
|
|
105
108
|
},
|
|
106
109
|
progressBar: {
|
|
107
110
|
height: 4,
|
|
108
|
-
backgroundColor:
|
|
111
|
+
backgroundColor: useGradient
|
|
112
|
+
? "rgba(255, 255, 255, 0.2)"
|
|
113
|
+
: tokens.colors.borderLight,
|
|
109
114
|
borderRadius: 2,
|
|
110
115
|
overflow: "hidden",
|
|
111
116
|
},
|
|
112
117
|
progressFill: {
|
|
113
118
|
height: "100%",
|
|
114
|
-
backgroundColor: "#FFFFFF",
|
|
119
|
+
backgroundColor: useGradient ? "#FFFFFF" : tokens.colors.primary,
|
|
115
120
|
borderRadius: 2,
|
|
116
121
|
},
|
|
117
122
|
dots: {
|
|
@@ -124,32 +129,41 @@ const getStyles = (
|
|
|
124
129
|
width: 6,
|
|
125
130
|
height: 6,
|
|
126
131
|
borderRadius: 3,
|
|
127
|
-
backgroundColor:
|
|
132
|
+
backgroundColor: useGradient
|
|
133
|
+
? "rgba(255, 255, 255, 0.4)"
|
|
134
|
+
: tokens.colors.borderLight,
|
|
128
135
|
},
|
|
129
136
|
dotActive: {
|
|
130
137
|
width: 8,
|
|
131
|
-
backgroundColor: "#FFFFFF",
|
|
138
|
+
backgroundColor: useGradient ? "#FFFFFF" : tokens.colors.primary,
|
|
132
139
|
},
|
|
133
140
|
button: {
|
|
134
|
-
backgroundColor: "#FFFFFF",
|
|
141
|
+
backgroundColor: useGradient ? "#FFFFFF" : tokens.colors.primary,
|
|
135
142
|
paddingVertical: 16,
|
|
136
143
|
borderRadius: 28,
|
|
137
144
|
alignItems: "center",
|
|
138
145
|
marginBottom: 12,
|
|
139
146
|
},
|
|
140
147
|
buttonDisabled: {
|
|
141
|
-
backgroundColor:
|
|
148
|
+
backgroundColor: useGradient
|
|
149
|
+
? "rgba(255, 255, 255, 0.4)"
|
|
150
|
+
: tokens.colors.borderLight,
|
|
151
|
+
opacity: 0.5,
|
|
142
152
|
},
|
|
143
153
|
buttonText: {
|
|
144
|
-
color: tokens.colors.primary,
|
|
154
|
+
color: useGradient ? tokens.colors.primary : tokens.colors.surface,
|
|
145
155
|
fontSize: 16,
|
|
146
156
|
fontWeight: "bold",
|
|
147
157
|
},
|
|
148
158
|
buttonTextDisabled: {
|
|
149
|
-
color:
|
|
159
|
+
color: useGradient
|
|
160
|
+
? "rgba(255, 255, 255, 0.6)"
|
|
161
|
+
: tokens.colors.textSecondary,
|
|
150
162
|
},
|
|
151
163
|
progressText: {
|
|
152
|
-
color:
|
|
164
|
+
color: useGradient
|
|
165
|
+
? "rgba(255, 255, 255, 0.75)"
|
|
166
|
+
: tokens.colors.textSecondary,
|
|
153
167
|
fontSize: 12,
|
|
154
168
|
textAlign: "center",
|
|
155
169
|
},
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import React, { useMemo } from "react";
|
|
8
8
|
import { View, TouchableOpacity, Text, StyleSheet } from "react-native";
|
|
9
9
|
import { useLocalization } from "@umituz/react-native-localization";
|
|
10
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
10
11
|
|
|
11
12
|
export interface OnboardingHeaderProps {
|
|
12
13
|
isFirstSlide: boolean;
|
|
@@ -15,6 +16,7 @@ export interface OnboardingHeaderProps {
|
|
|
15
16
|
showBackButton?: boolean;
|
|
16
17
|
showSkipButton?: boolean;
|
|
17
18
|
skipButtonText?: string;
|
|
19
|
+
useGradient?: boolean;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export const OnboardingHeader: React.FC<OnboardingHeaderProps> = ({
|
|
@@ -24,9 +26,11 @@ export const OnboardingHeader: React.FC<OnboardingHeaderProps> = ({
|
|
|
24
26
|
showBackButton = true,
|
|
25
27
|
showSkipButton = true,
|
|
26
28
|
skipButtonText,
|
|
29
|
+
useGradient = false,
|
|
27
30
|
}) => {
|
|
28
31
|
const { t } = useLocalization();
|
|
29
|
-
const
|
|
32
|
+
const tokens = useAppDesignTokens();
|
|
33
|
+
const styles = useMemo(() => getStyles(tokens, useGradient), [tokens, useGradient]);
|
|
30
34
|
|
|
31
35
|
const skipText = skipButtonText || t("onboarding.skip", "Skip");
|
|
32
36
|
|
|
@@ -55,7 +59,10 @@ export const OnboardingHeader: React.FC<OnboardingHeaderProps> = ({
|
|
|
55
59
|
);
|
|
56
60
|
};
|
|
57
61
|
|
|
58
|
-
const getStyles = (
|
|
62
|
+
const getStyles = (
|
|
63
|
+
tokens: ReturnType<typeof useAppDesignTokens>,
|
|
64
|
+
useGradient: boolean,
|
|
65
|
+
) =>
|
|
59
66
|
StyleSheet.create({
|
|
60
67
|
header: {
|
|
61
68
|
flexDirection: "row",
|
|
@@ -69,22 +76,26 @@ const getStyles = () =>
|
|
|
69
76
|
width: 40,
|
|
70
77
|
height: 40,
|
|
71
78
|
borderRadius: 20,
|
|
72
|
-
backgroundColor:
|
|
79
|
+
backgroundColor: useGradient
|
|
80
|
+
? "rgba(255, 255, 255, 0.2)"
|
|
81
|
+
: tokens.colors.surface,
|
|
73
82
|
alignItems: "center",
|
|
74
83
|
justifyContent: "center",
|
|
75
84
|
borderWidth: 1,
|
|
76
|
-
borderColor:
|
|
85
|
+
borderColor: useGradient
|
|
86
|
+
? "rgba(255, 255, 255, 0.3)"
|
|
87
|
+
: tokens.colors.borderLight,
|
|
77
88
|
},
|
|
78
89
|
headerButtonDisabled: {
|
|
79
90
|
opacity: 0.3,
|
|
80
91
|
},
|
|
81
92
|
headerButtonText: {
|
|
82
|
-
color: "#FFFFFF",
|
|
93
|
+
color: useGradient ? "#FFFFFF" : tokens.colors.textPrimary,
|
|
83
94
|
fontSize: 20,
|
|
84
95
|
fontWeight: "bold",
|
|
85
96
|
},
|
|
86
97
|
skipText: {
|
|
87
|
-
color: "#FFFFFF",
|
|
98
|
+
color: useGradient ? "#FFFFFF" : tokens.colors.textPrimary,
|
|
88
99
|
fontSize: 16,
|
|
89
100
|
fontWeight: "600",
|
|
90
101
|
},
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import React, { useMemo } from "react";
|
|
8
8
|
import { View, Text, StyleSheet, ScrollView } from "react-native";
|
|
9
9
|
import * as LucideIcons from "lucide-react-native";
|
|
10
|
+
import { useAppDesignTokens, withAlpha } from "@umituz/react-native-design-system-theme";
|
|
10
11
|
import type { OnboardingSlide as OnboardingSlideType } from "../../domain/entities/OnboardingSlide";
|
|
11
12
|
|
|
12
13
|
export interface OnboardingSlideProps {
|
|
@@ -14,7 +15,8 @@ export interface OnboardingSlideProps {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
export const OnboardingSlide: React.FC<OnboardingSlideProps> = ({ slide }) => {
|
|
17
|
-
const
|
|
18
|
+
const tokens = useAppDesignTokens();
|
|
19
|
+
const styles = useMemo(() => getStyles(tokens), [tokens]);
|
|
18
20
|
|
|
19
21
|
// Check if icon is an emoji (contains emoji characters) or Lucide icon name
|
|
20
22
|
const isEmoji = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u.test(slide.icon);
|
|
@@ -30,7 +32,7 @@ export const OnboardingSlide: React.FC<OnboardingSlideProps> = ({ slide }) => {
|
|
|
30
32
|
{isEmoji ? (
|
|
31
33
|
<Text style={styles.icon}>{slide.icon}</Text>
|
|
32
34
|
) : IconComponent ? (
|
|
33
|
-
<IconComponent size={60} color=
|
|
35
|
+
<IconComponent size={60} color={tokens.colors.textPrimary} />
|
|
34
36
|
) : (
|
|
35
37
|
<Text style={styles.icon}>📱</Text>
|
|
36
38
|
)}
|
|
@@ -53,7 +55,7 @@ export const OnboardingSlide: React.FC<OnboardingSlideProps> = ({ slide }) => {
|
|
|
53
55
|
);
|
|
54
56
|
};
|
|
55
57
|
|
|
56
|
-
const getStyles = () =>
|
|
58
|
+
const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
57
59
|
StyleSheet.create({
|
|
58
60
|
container: {
|
|
59
61
|
flex: 1,
|
|
@@ -69,17 +71,31 @@ const getStyles = () =>
|
|
|
69
71
|
alignItems: "center",
|
|
70
72
|
maxWidth: 400,
|
|
71
73
|
width: "100%",
|
|
74
|
+
// Add background for readability with theme colors
|
|
75
|
+
backgroundColor: tokens.colors.surface,
|
|
76
|
+
padding: 30,
|
|
77
|
+
borderRadius: 24,
|
|
78
|
+
borderWidth: 1,
|
|
79
|
+
borderColor: tokens.colors.borderLight,
|
|
80
|
+
shadowColor: tokens.colors.textPrimary,
|
|
81
|
+
shadowOffset: {
|
|
82
|
+
width: 0,
|
|
83
|
+
height: 4,
|
|
84
|
+
},
|
|
85
|
+
shadowOpacity: 0.1,
|
|
86
|
+
shadowRadius: 8,
|
|
87
|
+
elevation: 4,
|
|
72
88
|
},
|
|
73
89
|
iconContainer: {
|
|
74
90
|
width: 120,
|
|
75
91
|
height: 120,
|
|
76
92
|
borderRadius: 60,
|
|
77
|
-
backgroundColor:
|
|
93
|
+
backgroundColor: withAlpha(tokens.colors.primary, 0.2),
|
|
78
94
|
alignItems: "center",
|
|
79
95
|
justifyContent: "center",
|
|
80
96
|
marginBottom: 40,
|
|
81
97
|
borderWidth: 2,
|
|
82
|
-
borderColor:
|
|
98
|
+
borderColor: withAlpha(tokens.colors.primary, 0.4),
|
|
83
99
|
},
|
|
84
100
|
icon: {
|
|
85
101
|
fontSize: 60,
|
|
@@ -87,13 +103,13 @@ const getStyles = () =>
|
|
|
87
103
|
title: {
|
|
88
104
|
fontSize: 28,
|
|
89
105
|
fontWeight: "bold",
|
|
90
|
-
color:
|
|
106
|
+
color: tokens.colors.textPrimary,
|
|
91
107
|
textAlign: "center",
|
|
92
108
|
marginBottom: 16,
|
|
93
109
|
},
|
|
94
110
|
description: {
|
|
95
111
|
fontSize: 16,
|
|
96
|
-
color:
|
|
112
|
+
color: tokens.colors.textSecondary,
|
|
97
113
|
textAlign: "center",
|
|
98
114
|
lineHeight: 24,
|
|
99
115
|
marginBottom: 20,
|
|
@@ -108,7 +124,7 @@ const getStyles = () =>
|
|
|
108
124
|
marginBottom: 12,
|
|
109
125
|
},
|
|
110
126
|
featureBullet: {
|
|
111
|
-
color:
|
|
127
|
+
color: tokens.colors.primary,
|
|
112
128
|
fontSize: 20,
|
|
113
129
|
marginRight: 12,
|
|
114
130
|
marginTop: 2,
|
|
@@ -116,7 +132,7 @@ const getStyles = () =>
|
|
|
116
132
|
featureText: {
|
|
117
133
|
flex: 1,
|
|
118
134
|
fontSize: 15,
|
|
119
|
-
color:
|
|
135
|
+
color: tokens.colors.textSecondary,
|
|
120
136
|
lineHeight: 22,
|
|
121
137
|
},
|
|
122
138
|
});
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import React, { useMemo } from "react";
|
|
8
8
|
import { View, Text, StyleSheet, ScrollView } from "react-native";
|
|
9
9
|
import { AtomicIcon } from "@umituz/react-native-design-system-atoms";
|
|
10
|
+
import { useAppDesignTokens, withAlpha } from "@umituz/react-native-design-system-theme";
|
|
10
11
|
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
11
12
|
import { SingleChoiceQuestion } from "./questions/SingleChoiceQuestion";
|
|
12
13
|
import { MultipleChoiceQuestion } from "./questions/MultipleChoiceQuestion";
|
|
@@ -25,7 +26,8 @@ export const QuestionSlide: React.FC<QuestionSlideProps> = ({
|
|
|
25
26
|
value,
|
|
26
27
|
onChange,
|
|
27
28
|
}) => {
|
|
28
|
-
const
|
|
29
|
+
const tokens = useAppDesignTokens();
|
|
30
|
+
const styles = useMemo(() => getStyles(tokens), [tokens]);
|
|
29
31
|
const { question } = slide;
|
|
30
32
|
|
|
31
33
|
if (!question) {
|
|
@@ -96,7 +98,7 @@ export const QuestionSlide: React.FC<QuestionSlideProps> = ({
|
|
|
96
98
|
<AtomicIcon
|
|
97
99
|
name={slide.icon as any}
|
|
98
100
|
customSize={48}
|
|
99
|
-
customColor=
|
|
101
|
+
customColor={tokens.colors.textPrimary}
|
|
100
102
|
/>
|
|
101
103
|
)}
|
|
102
104
|
</View>
|
|
@@ -121,7 +123,7 @@ export const QuestionSlide: React.FC<QuestionSlideProps> = ({
|
|
|
121
123
|
);
|
|
122
124
|
};
|
|
123
125
|
|
|
124
|
-
const getStyles = () =>
|
|
126
|
+
const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>) =>
|
|
125
127
|
StyleSheet.create({
|
|
126
128
|
content: {
|
|
127
129
|
flexGrow: 1,
|
|
@@ -134,17 +136,31 @@ const getStyles = () =>
|
|
|
134
136
|
alignItems: "center",
|
|
135
137
|
maxWidth: 500,
|
|
136
138
|
width: "100%",
|
|
139
|
+
// Add background for readability with theme colors
|
|
140
|
+
backgroundColor: tokens.colors.surface,
|
|
141
|
+
padding: 30,
|
|
142
|
+
borderRadius: 24,
|
|
143
|
+
borderWidth: 1,
|
|
144
|
+
borderColor: tokens.colors.borderLight,
|
|
145
|
+
shadowColor: tokens.colors.textPrimary,
|
|
146
|
+
shadowOffset: {
|
|
147
|
+
width: 0,
|
|
148
|
+
height: 4,
|
|
149
|
+
},
|
|
150
|
+
shadowOpacity: 0.1,
|
|
151
|
+
shadowRadius: 8,
|
|
152
|
+
elevation: 4,
|
|
137
153
|
},
|
|
138
154
|
iconContainer: {
|
|
139
155
|
width: 96,
|
|
140
156
|
height: 96,
|
|
141
157
|
borderRadius: 48,
|
|
142
|
-
backgroundColor:
|
|
158
|
+
backgroundColor: withAlpha(tokens.colors.primary, 0.2),
|
|
143
159
|
alignItems: "center",
|
|
144
160
|
justifyContent: "center",
|
|
145
161
|
marginBottom: 24,
|
|
146
162
|
borderWidth: 2,
|
|
147
|
-
borderColor:
|
|
163
|
+
borderColor: withAlpha(tokens.colors.primary, 0.4),
|
|
148
164
|
},
|
|
149
165
|
icon: {
|
|
150
166
|
fontSize: 48,
|
|
@@ -152,13 +168,13 @@ const getStyles = () =>
|
|
|
152
168
|
title: {
|
|
153
169
|
fontSize: 24,
|
|
154
170
|
fontWeight: "bold",
|
|
155
|
-
color:
|
|
171
|
+
color: tokens.colors.textPrimary,
|
|
156
172
|
textAlign: "center",
|
|
157
173
|
marginBottom: 12,
|
|
158
174
|
},
|
|
159
175
|
description: {
|
|
160
176
|
fontSize: 15,
|
|
161
|
-
color:
|
|
177
|
+
color: tokens.colors.textSecondary,
|
|
162
178
|
textAlign: "center",
|
|
163
179
|
lineHeight: 22,
|
|
164
180
|
marginBottom: 24,
|
|
@@ -169,7 +185,7 @@ const getStyles = () =>
|
|
|
169
185
|
},
|
|
170
186
|
requiredHint: {
|
|
171
187
|
fontSize: 13,
|
|
172
|
-
color:
|
|
188
|
+
color: tokens.colors.textSecondary,
|
|
173
189
|
fontStyle: "italic",
|
|
174
190
|
marginTop: 12,
|
|
175
191
|
},
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Onboarding Screen
|
|
3
3
|
*
|
|
4
|
-
* Main onboarding screen component with
|
|
4
|
+
* Main onboarding screen component with theme-aware colors
|
|
5
5
|
* Generic and reusable across hundreds of apps
|
|
6
|
+
*
|
|
7
|
+
* This component only handles UI coordination - no business logic
|
|
6
8
|
*/
|
|
7
9
|
|
|
8
|
-
import React, { useMemo
|
|
10
|
+
import React, { useMemo } from "react";
|
|
9
11
|
import { View, StyleSheet, StatusBar } from "react-native";
|
|
10
12
|
import { LinearGradient } from "expo-linear-gradient";
|
|
11
13
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
14
|
+
import { useAppDesignTokens, useTheme } from "@umituz/react-native-design-system-theme";
|
|
12
15
|
import type { OnboardingOptions } from "../../domain/entities/OnboardingOptions";
|
|
13
16
|
import { useOnboardingNavigation } from "../../infrastructure/hooks/useOnboardingNavigation";
|
|
17
|
+
import { useOnboardingAnswers } from "../../infrastructure/hooks/useOnboardingAnswers";
|
|
14
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";
|
|
15
22
|
import { OnboardingHeader } from "../components/OnboardingHeader";
|
|
16
23
|
import { OnboardingSlide as OnboardingSlideComponent } from "../components/OnboardingSlide";
|
|
17
24
|
import { QuestionSlide } from "../components/QuestionSlide";
|
|
@@ -60,7 +67,7 @@ export interface OnboardingScreenProps extends OnboardingOptions {
|
|
|
60
67
|
/**
|
|
61
68
|
* Onboarding Screen Component
|
|
62
69
|
*
|
|
63
|
-
* Displays onboarding flow with
|
|
70
|
+
* Displays onboarding flow with theme-aware colors, animations, and navigation
|
|
64
71
|
*/
|
|
65
72
|
export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
|
|
66
73
|
slides,
|
|
@@ -83,39 +90,17 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
|
|
|
83
90
|
showPaywallOnComplete = false,
|
|
84
91
|
}) => {
|
|
85
92
|
const insets = useSafeAreaInsets();
|
|
93
|
+
const tokens = useAppDesignTokens();
|
|
94
|
+
const { themeMode } = useTheme();
|
|
86
95
|
const onboardingStore = useOnboardingStore();
|
|
87
|
-
const [currentAnswer, setCurrentAnswer] = useState<any>(undefined);
|
|
88
96
|
|
|
89
|
-
// Filter slides
|
|
97
|
+
// Filter slides using service
|
|
90
98
|
const filteredSlides = useMemo(() => {
|
|
91
99
|
const userData = onboardingStore.getUserData();
|
|
92
|
-
return
|
|
93
|
-
if (slide.skipIf) {
|
|
94
|
-
return !slide.skipIf(userData.answers);
|
|
95
|
-
}
|
|
96
|
-
return true;
|
|
97
|
-
});
|
|
100
|
+
return OnboardingSlideService.filterSlides(slides, userData);
|
|
98
101
|
}, [slides, onboardingStore]);
|
|
99
102
|
|
|
100
|
-
|
|
101
|
-
// Save current answer if exists
|
|
102
|
-
if (currentSlide.question && currentAnswer !== undefined) {
|
|
103
|
-
await onboardingStore.saveAnswer(currentSlide.question.id, currentAnswer);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
await onboardingStore.complete(storageKey);
|
|
107
|
-
if (onComplete) {
|
|
108
|
-
await onComplete();
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const handleSkip = async () => {
|
|
113
|
-
await onboardingStore.skip(storageKey);
|
|
114
|
-
if (onSkip) {
|
|
115
|
-
await onSkip();
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
103
|
+
// Navigation hook
|
|
119
104
|
const {
|
|
120
105
|
currentIndex,
|
|
121
106
|
goToNext,
|
|
@@ -124,108 +109,97 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
|
|
|
124
109
|
skip: skipOnboarding,
|
|
125
110
|
isLastSlide,
|
|
126
111
|
isFirstSlide,
|
|
127
|
-
} = useOnboardingNavigation(
|
|
112
|
+
} = useOnboardingNavigation(
|
|
113
|
+
filteredSlides.length,
|
|
114
|
+
async () => {
|
|
115
|
+
await onboardingStore.complete(storageKey);
|
|
116
|
+
if (onComplete) {
|
|
117
|
+
await onComplete();
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
async () => {
|
|
121
|
+
await onboardingStore.skip(storageKey);
|
|
122
|
+
if (onSkip) {
|
|
123
|
+
await onSkip();
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Get current slide
|
|
129
|
+
const currentSlide = useMemo(
|
|
130
|
+
() => OnboardingSlideService.getSlideAtIndex(filteredSlides, currentIndex),
|
|
131
|
+
[filteredSlides, currentIndex],
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Answer management hook
|
|
135
|
+
const {
|
|
136
|
+
currentAnswer,
|
|
137
|
+
setCurrentAnswer,
|
|
138
|
+
loadAnswerForSlide,
|
|
139
|
+
saveCurrentAnswer,
|
|
140
|
+
} = useOnboardingAnswers(currentSlide);
|
|
128
141
|
|
|
142
|
+
// Handle next slide
|
|
129
143
|
const handleNext = async () => {
|
|
130
|
-
|
|
131
|
-
if (currentSlide.question && currentAnswer !== undefined) {
|
|
132
|
-
await onboardingStore.saveAnswer(currentSlide.question.id, currentAnswer);
|
|
133
|
-
}
|
|
144
|
+
await saveCurrentAnswer(currentSlide);
|
|
134
145
|
|
|
135
146
|
if (isLastSlide) {
|
|
136
|
-
// Use useOnboardingNavigation's complete function
|
|
137
|
-
// This will call handleComplete callback and emit event
|
|
138
147
|
await completeOnboarding();
|
|
139
148
|
} else {
|
|
140
149
|
goToNext();
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
} else {
|
|
147
|
-
setCurrentAnswer(undefined);
|
|
148
|
-
}
|
|
150
|
+
const nextSlide = OnboardingSlideService.getSlideAtIndex(
|
|
151
|
+
filteredSlides,
|
|
152
|
+
currentIndex + 1,
|
|
153
|
+
);
|
|
154
|
+
loadAnswerForSlide(nextSlide);
|
|
149
155
|
}
|
|
150
156
|
};
|
|
151
157
|
|
|
158
|
+
// Handle previous slide
|
|
152
159
|
const handlePrevious = () => {
|
|
153
160
|
goToPrevious();
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
setCurrentAnswer(savedAnswer);
|
|
160
|
-
} else {
|
|
161
|
-
setCurrentAnswer(undefined);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
161
|
+
const prevSlide = OnboardingSlideService.getSlideAtIndex(
|
|
162
|
+
filteredSlides,
|
|
163
|
+
currentIndex - 1,
|
|
164
|
+
);
|
|
165
|
+
loadAnswerForSlide(prevSlide);
|
|
164
166
|
};
|
|
165
167
|
|
|
166
|
-
|
|
167
|
-
const
|
|
168
|
+
// Handle skip
|
|
169
|
+
const handleSkip = async () => {
|
|
170
|
+
await skipOnboarding();
|
|
171
|
+
};
|
|
168
172
|
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
if (currentSlide?.question) {
|
|
172
|
-
const savedAnswer = onboardingStore.getAnswer(currentSlide.question.id);
|
|
173
|
-
setCurrentAnswer(savedAnswer ?? currentSlide.question.defaultValue);
|
|
174
|
-
} else {
|
|
175
|
-
setCurrentAnswer(undefined);
|
|
176
|
-
}
|
|
177
|
-
}, [currentIndex, currentSlide, onboardingStore]);
|
|
173
|
+
// Check if gradient should be used
|
|
174
|
+
const useGradient = shouldUseGradient(currentSlide);
|
|
178
175
|
|
|
179
|
-
// Validate
|
|
176
|
+
// Validate answer using service
|
|
180
177
|
const isAnswerValid = useMemo(() => {
|
|
181
|
-
if (!currentSlide?.question)
|
|
182
|
-
|
|
183
|
-
const { validation } = currentSlide.question;
|
|
184
|
-
if (!validation) return true;
|
|
185
|
-
|
|
186
|
-
// Required validation
|
|
187
|
-
if (validation.required && !currentAnswer) return false;
|
|
188
|
-
|
|
189
|
-
// Type-specific validations
|
|
190
|
-
switch (currentSlide.question.type) {
|
|
191
|
-
case "multiple_choice":
|
|
192
|
-
if (validation.minSelections && (!currentAnswer || currentAnswer.length < validation.minSelections)) {
|
|
193
|
-
return false;
|
|
194
|
-
}
|
|
195
|
-
break;
|
|
196
|
-
case "text_input":
|
|
197
|
-
if (validation.minLength && (!currentAnswer || currentAnswer.length < validation.minLength)) {
|
|
198
|
-
return false;
|
|
199
|
-
}
|
|
200
|
-
break;
|
|
201
|
-
case "slider":
|
|
202
|
-
case "rating":
|
|
203
|
-
if (validation.min !== undefined && currentAnswer < validation.min) {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
if (validation.max !== undefined && currentAnswer > validation.max) {
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
break;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Custom validator
|
|
213
|
-
if (validation.customValidator) {
|
|
214
|
-
return validation.customValidator(currentAnswer) === true;
|
|
178
|
+
if (!currentSlide?.question) {
|
|
179
|
+
return true;
|
|
215
180
|
}
|
|
216
|
-
|
|
217
|
-
|
|
181
|
+
return OnboardingValidationService.validateAnswer(
|
|
182
|
+
currentSlide.question,
|
|
183
|
+
currentAnswer,
|
|
184
|
+
);
|
|
218
185
|
}, [currentSlide, currentAnswer]);
|
|
219
186
|
|
|
187
|
+
const styles = useMemo(
|
|
188
|
+
() => getStyles(insets, tokens, useGradient),
|
|
189
|
+
[insets, tokens, useGradient],
|
|
190
|
+
);
|
|
191
|
+
|
|
220
192
|
return (
|
|
221
193
|
<View style={styles.container}>
|
|
222
|
-
<StatusBar barStyle="light-content" />
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
194
|
+
<StatusBar barStyle={themeMode === "dark" ? "light-content" : "dark-content"} />
|
|
195
|
+
{useGradient && currentSlide && (
|
|
196
|
+
<LinearGradient
|
|
197
|
+
colors={currentSlide.gradient as [string, string, ...string[]]}
|
|
198
|
+
start={{ x: 0, y: 0 }}
|
|
199
|
+
end={{ x: 1, y: 1 }}
|
|
200
|
+
style={StyleSheet.absoluteFill}
|
|
201
|
+
/>
|
|
202
|
+
)}
|
|
229
203
|
{renderHeader ? (
|
|
230
204
|
renderHeader({
|
|
231
205
|
isFirstSlide,
|
|
@@ -240,6 +214,7 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
|
|
|
240
214
|
showBackButton={showBackButton}
|
|
241
215
|
showSkipButton={showSkipButton}
|
|
242
216
|
skipButtonText={skipButtonText}
|
|
217
|
+
useGradient={useGradient}
|
|
243
218
|
/>
|
|
244
219
|
)}
|
|
245
220
|
{renderSlide ? (
|
|
@@ -274,17 +249,24 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
|
|
|
274
249
|
nextButtonText={nextButtonText}
|
|
275
250
|
getStartedButtonText={getStartedButtonText}
|
|
276
251
|
disabled={!isAnswerValid}
|
|
252
|
+
useGradient={useGradient}
|
|
277
253
|
/>
|
|
278
254
|
)}
|
|
279
255
|
</View>
|
|
280
256
|
);
|
|
281
257
|
};
|
|
282
258
|
|
|
283
|
-
const getStyles = (
|
|
259
|
+
const getStyles = (
|
|
260
|
+
insets: { top: number },
|
|
261
|
+
tokens: ReturnType<typeof useAppDesignTokens>,
|
|
262
|
+
useGradient: boolean,
|
|
263
|
+
) =>
|
|
284
264
|
StyleSheet.create({
|
|
285
265
|
container: {
|
|
286
266
|
flex: 1,
|
|
287
267
|
paddingTop: insets.top,
|
|
268
|
+
// Use transparent background when gradient is used, otherwise use theme background
|
|
269
|
+
backgroundColor: useGradient ? 'transparent' : tokens.colors.backgroundPrimary,
|
|
288
270
|
},
|
|
289
271
|
});
|
|
290
272
|
|