@umituz/react-native-onboarding 2.1.1 → 2.3.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/README.md CHANGED
@@ -185,6 +185,8 @@ const slides: OnboardingSlide[] = [
185
185
  | `showProgressText` | `boolean` | `true` | Show progress text (1 of 5) |
186
186
  | `storageKey` | `string` | - | Custom storage key for completion state |
187
187
  | `autoComplete` | `boolean` | `false` | Auto-complete on last slide |
188
+ | `enableDeviceTracking` | `boolean` | `false` | Collect device info during onboarding |
189
+ | `userId` | `string` | - | User ID for device tracking (optional) |
188
190
 
189
191
  ### OnboardingSlide Interface
190
192
 
@@ -328,6 +330,69 @@ const isComplete = userData.completedAt !== undefined;
328
330
 
329
331
  // Check if onboarding was skipped
330
332
  const wasSkipped = userData.skipped === true;
333
+
334
+ // Get device info (if device tracking was enabled)
335
+ const deviceInfo = userData.deviceInfo;
336
+ ```
337
+
338
+ ## 📱 Device Tracking (Optional)
339
+
340
+ Collect device information during onboarding for analytics or support purposes.
341
+
342
+ ### Installation
343
+
344
+ First, install the device tracking package:
345
+
346
+ ```bash
347
+ npm install @umituz/react-native-device
348
+ ```
349
+
350
+ ### Usage
351
+
352
+ ```tsx
353
+ import { OnboardingScreen } from '@umituz/react-native-onboarding';
354
+
355
+ <OnboardingScreen
356
+ slides={slides}
357
+ enableDeviceTracking={true}
358
+ userId="user123" // Optional
359
+ onComplete={async () => {
360
+ const userData = onboardingStore.getUserData();
361
+
362
+ // Device info is now available
363
+ console.log('Device:', userData.deviceInfo?.platform);
364
+ console.log('OS:', userData.deviceInfo?.osVersion);
365
+ console.log('App:', userData.deviceInfo?.appVersion);
366
+ }}
367
+ />
368
+ ```
369
+
370
+ ### Device Info Structure
371
+
372
+ ```typescript
373
+ {
374
+ deviceId: string;
375
+ platform: 'ios' | 'android' | 'web';
376
+ osVersion: string;
377
+ appVersion: string;
378
+ deviceName: string;
379
+ manufacturer: string;
380
+ brand: string;
381
+ timestamp: number;
382
+ userId?: string; // If provided
383
+ }
384
+ ```
385
+
386
+ ### Manual Device Info Collection
387
+
388
+ ```tsx
389
+ import { OnboardingDeviceTrackingService } from '@umituz/react-native-onboarding';
390
+
391
+ // Collect device info manually
392
+ const deviceInfo = await OnboardingDeviceTrackingService.collectDeviceInfo('user123');
393
+
394
+ // Check if device tracking is available
395
+ const isAvailable = await OnboardingDeviceTrackingService.isDeviceTrackingAvailable();
331
396
  ```
332
397
 
333
398
  ## 🔄 Resetting Onboarding
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@umituz/react-native-onboarding",
3
- "version": "2.1.1",
4
- "description": "Advanced onboarding flow for React Native apps with personalization questions, gradient backgrounds, animations, and customizable slides. SOLID, DRY, KISS principles applied.",
3
+ "version": "2.3.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
- "gradient",
19
+ "theme",
20
20
  "animation",
21
21
  "ddd",
22
22
  "domain-driven-design",
@@ -44,6 +44,11 @@
44
44
  "react-native-safe-area-context": "^5.0.0",
45
45
  "zustand": "^5.0.0"
46
46
  },
47
+ "peerDependenciesMeta": {
48
+ "@umituz/react-native-device": {
49
+ "optional": true
50
+ }
51
+ },
47
52
  "devDependencies": {
48
53
  "@types/react": "^18.2.45",
49
54
  "@types/react-native": "^0.73.0",
@@ -85,5 +85,17 @@ export interface OnboardingOptions {
85
85
  * Show paywall modal on onboarding completion (default: false)
86
86
  */
87
87
  showPaywallOnComplete?: boolean;
88
+
89
+ /**
90
+ * Enable device tracking (default: false)
91
+ * When enabled, collects device information during onboarding
92
+ */
93
+ enableDeviceTracking?: boolean;
94
+
95
+ /**
96
+ * User ID for device tracking (optional)
97
+ * Only used when enableDeviceTracking is true
98
+ */
99
+ userId?: string;
88
100
  }
89
101
 
@@ -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: string[];
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)
@@ -39,5 +39,17 @@ export interface OnboardingUserData {
39
39
  gender?: string;
40
40
  [key: string]: any;
41
41
  };
42
+
43
+ /**
44
+ * Device information (optional)
45
+ * Only collected when enableDeviceTracking is true
46
+ */
47
+ deviceInfo?: {
48
+ deviceId?: string;
49
+ platform?: string;
50
+ osVersion?: string;
51
+ appVersion?: string;
52
+ [key: string]: any;
53
+ };
42
54
  }
43
55
 
package/src/index.ts CHANGED
@@ -52,6 +52,7 @@ export {
52
52
  useOnboardingNavigation,
53
53
  type UseOnboardingNavigationReturn,
54
54
  } from "./infrastructure/hooks/useOnboardingNavigation";
55
+ export { OnboardingDeviceTrackingService } from "./infrastructure/services/OnboardingDeviceTrackingService";
55
56
 
56
57
  // =============================================================================
57
58
  // PRESENTATION LAYER - Components and Screens
@@ -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,60 @@
1
+ /**
2
+ * Onboarding Device Tracking Service
3
+ *
4
+ * Handles device information collection during onboarding
5
+ * Single Responsibility: Device tracking only
6
+ */
7
+
8
+ import type { OnboardingUserData } from "../../domain/entities/OnboardingUserData";
9
+
10
+ /**
11
+ * Service for device tracking during onboarding
12
+ * Optional feature - only used when enableDeviceTracking is true
13
+ */
14
+ export class OnboardingDeviceTrackingService {
15
+ /**
16
+ * Collect device information
17
+ * @param userId - Optional user ID
18
+ * @returns Device information object
19
+ */
20
+ static async collectDeviceInfo(userId?: string): Promise<OnboardingUserData['deviceInfo']> {
21
+ try {
22
+ // Dynamic import to avoid loading @umituz/react-native-device if not needed
23
+ // @ts-expect-error - Optional peer dependency, may not be installed
24
+ const { DeviceService } = await import('@umituz/react-native-device');
25
+
26
+ const systemInfo = await DeviceService.getSystemInfo({ userId });
27
+
28
+ return {
29
+ deviceId: systemInfo.device.modelId || 'unknown',
30
+ platform: systemInfo.device.platform,
31
+ osVersion: systemInfo.device.osVersion || 'unknown',
32
+ appVersion: systemInfo.application.nativeApplicationVersion || 'unknown',
33
+ deviceName: systemInfo.device.deviceName || 'unknown',
34
+ manufacturer: systemInfo.device.manufacturer || 'unknown',
35
+ brand: systemInfo.device.brand || 'unknown',
36
+ timestamp: systemInfo.timestamp,
37
+ ...(userId && { userId }),
38
+ };
39
+ } catch (error) {
40
+ /* eslint-disable-next-line no-console */
41
+ if (__DEV__) console.warn('Failed to collect device info:', error);
42
+ return undefined;
43
+ }
44
+ }
45
+
46
+ /**
47
+ * Check if device tracking is available
48
+ * @returns True if @umituz/react-native-device is available
49
+ */
50
+ static async isDeviceTrackingAvailable(): Promise<boolean> {
51
+ try {
52
+ // @ts-expect-error - Optional peer dependency, may not be installed
53
+ await import('@umituz/react-native-device');
54
+ return true;
55
+ } catch {
56
+ return false;
57
+ }
58
+ }
59
+ }
60
+
@@ -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
+
@@ -12,6 +12,7 @@ import {
12
12
  unwrap,
13
13
  } from "@umituz/react-native-storage";
14
14
  import type { OnboardingUserData } from "../../domain/entities/OnboardingUserData";
15
+ import { OnboardingDeviceTrackingService } from "../services/OnboardingDeviceTrackingService";
15
16
 
16
17
  interface OnboardingStore {
17
18
  // State
@@ -23,7 +24,7 @@ interface OnboardingStore {
23
24
 
24
25
  // Actions
25
26
  initialize: (storageKey?: string) => Promise<void>;
26
- complete: (storageKey?: string) => Promise<void>;
27
+ complete: (storageKey?: string, options?: { enableDeviceTracking?: boolean; userId?: string }) => Promise<void>;
27
28
  skip: (storageKey?: string) => Promise<void>;
28
29
  setCurrentStep: (step: number) => void;
29
30
  reset: (storageKey?: string) => Promise<void>;
@@ -33,6 +34,7 @@ interface OnboardingStore {
33
34
  getAnswer: (questionId: string) => any;
34
35
  getUserData: () => OnboardingUserData;
35
36
  setUserData: (data: OnboardingUserData) => Promise<void>;
37
+ collectDeviceInfo: (userId?: string) => Promise<void>;
36
38
  }
37
39
 
38
40
  const DEFAULT_STORAGE_KEY = StorageKey.ONBOARDING_COMPLETED;
@@ -67,7 +69,7 @@ export const useOnboardingStore = create<OnboardingStore>((set, get) => ({
67
69
  });
68
70
  },
69
71
 
70
- complete: async (storageKey = DEFAULT_STORAGE_KEY) => {
72
+ complete: async (storageKey = DEFAULT_STORAGE_KEY, options?: { enableDeviceTracking?: boolean; userId?: string }) => {
71
73
  set({ loading: true, error: null });
72
74
 
73
75
  const result = await storageRepository.setString(storageKey, "true");
@@ -75,6 +77,12 @@ export const useOnboardingStore = create<OnboardingStore>((set, get) => ({
75
77
  // Update user data with completion timestamp
76
78
  const userData = get().userData;
77
79
  userData.completedAt = new Date().toISOString();
80
+
81
+ // Collect device info if enabled
82
+ if (options?.enableDeviceTracking) {
83
+ userData.deviceInfo = await OnboardingDeviceTrackingService.collectDeviceInfo(options.userId);
84
+ }
85
+
78
86
  await storageRepository.setObject(USER_DATA_STORAGE_KEY, userData);
79
87
 
80
88
  set({
@@ -144,6 +152,13 @@ export const useOnboardingStore = create<OnboardingStore>((set, get) => ({
144
152
  await storageRepository.setObject(USER_DATA_STORAGE_KEY, data);
145
153
  set({ userData: data });
146
154
  },
155
+
156
+ collectDeviceInfo: async (userId?: string) => {
157
+ const userData = get().userData;
158
+ userData.deviceInfo = await OnboardingDeviceTrackingService.collectDeviceInfo(userId);
159
+ await storageRepository.setObject(USER_DATA_STORAGE_KEY, userData);
160
+ set({ userData: { ...userData } });
161
+ },
147
162
  }));
148
163
 
149
164
  /**
@@ -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: "rgba(255, 255, 255, 0.2)",
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: "rgba(255, 255, 255, 0.4)",
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: "rgba(255, 255, 255, 0.4)",
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: "rgba(255, 255, 255, 0.6)",
159
+ color: useGradient
160
+ ? "rgba(255, 255, 255, 0.6)"
161
+ : tokens.colors.textSecondary,
150
162
  },
151
163
  progressText: {
152
- color: "rgba(255, 255, 255, 0.75)",
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 styles = useMemo(() => getStyles(), []);
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: "rgba(255, 255, 255, 0.2)",
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: "rgba(255, 255, 255, 0.3)",
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 styles = useMemo(() => getStyles(), []);
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="#FFFFFF" />
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: "rgba(255, 255, 255, 0.2)",
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: "rgba(255, 255, 255, 0.3)",
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: "#FFFFFF",
106
+ color: tokens.colors.textPrimary,
91
107
  textAlign: "center",
92
108
  marginBottom: 16,
93
109
  },
94
110
  description: {
95
111
  fontSize: 16,
96
- color: "rgba(255, 255, 255, 0.95)",
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: "#FFFFFF",
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: "rgba(255, 255, 255, 0.9)",
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 styles = useMemo(() => getStyles(), []);
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="#FFFFFF"
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: "rgba(255, 255, 255, 0.2)",
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: "rgba(255, 255, 255, 0.3)",
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: "#FFFFFF",
171
+ color: tokens.colors.textPrimary,
156
172
  textAlign: "center",
157
173
  marginBottom: 12,
158
174
  },
159
175
  description: {
160
176
  fontSize: 15,
161
- color: "rgba(255, 255, 255, 0.9)",
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: "rgba(255, 255, 255, 0.7)",
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 gradient backgrounds
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, useState, useEffect } from "react";
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";
@@ -55,12 +62,24 @@ export interface OnboardingScreenProps extends OnboardingOptions {
55
62
  * When true, shows premium paywall before completing onboarding
56
63
  */
57
64
  showPaywallOnComplete?: boolean;
65
+
66
+ /**
67
+ * Enable device tracking (default: false)
68
+ * When enabled, collects device information during onboarding
69
+ */
70
+ enableDeviceTracking?: boolean;
71
+
72
+ /**
73
+ * User ID for device tracking (optional)
74
+ * Only used when enableDeviceTracking is true
75
+ */
76
+ userId?: string;
58
77
  }
59
78
 
60
79
  /**
61
80
  * Onboarding Screen Component
62
81
  *
63
- * Displays onboarding flow with gradient backgrounds, animations, and navigation
82
+ * Displays onboarding flow with theme-aware colors, animations, and navigation
64
83
  */
65
84
  export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
66
85
  slides,
@@ -81,41 +100,21 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
81
100
  renderSlide,
82
101
  onUpgrade,
83
102
  showPaywallOnComplete = false,
103
+ enableDeviceTracking = false,
104
+ userId,
84
105
  }) => {
85
106
  const insets = useSafeAreaInsets();
107
+ const tokens = useAppDesignTokens();
108
+ const { themeMode } = useTheme();
86
109
  const onboardingStore = useOnboardingStore();
87
- const [currentAnswer, setCurrentAnswer] = useState<any>(undefined);
88
110
 
89
- // Filter slides based on skipIf conditions
111
+ // Filter slides using service
90
112
  const filteredSlides = useMemo(() => {
91
113
  const userData = onboardingStore.getUserData();
92
- return slides.filter((slide) => {
93
- if (slide.skipIf) {
94
- return !slide.skipIf(userData.answers);
95
- }
96
- return true;
97
- });
114
+ return OnboardingSlideService.filterSlides(slides, userData);
98
115
  }, [slides, onboardingStore]);
99
116
 
100
- const handleComplete = async () => {
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
-
117
+ // Navigation hook
119
118
  const {
120
119
  currentIndex,
121
120
  goToNext,
@@ -124,108 +123,97 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
124
123
  skip: skipOnboarding,
125
124
  isLastSlide,
126
125
  isFirstSlide,
127
- } = useOnboardingNavigation(filteredSlides.length, handleComplete, handleSkip);
126
+ } = useOnboardingNavigation(
127
+ filteredSlides.length,
128
+ async () => {
129
+ await onboardingStore.complete(storageKey, { enableDeviceTracking, userId });
130
+ if (onComplete) {
131
+ await onComplete();
132
+ }
133
+ },
134
+ async () => {
135
+ await onboardingStore.skip(storageKey);
136
+ if (onSkip) {
137
+ await onSkip();
138
+ }
139
+ },
140
+ );
141
+
142
+ // Get current slide
143
+ const currentSlide = useMemo(
144
+ () => OnboardingSlideService.getSlideAtIndex(filteredSlides, currentIndex),
145
+ [filteredSlides, currentIndex],
146
+ );
128
147
 
148
+ // Answer management hook
149
+ const {
150
+ currentAnswer,
151
+ setCurrentAnswer,
152
+ loadAnswerForSlide,
153
+ saveCurrentAnswer,
154
+ } = useOnboardingAnswers(currentSlide);
155
+
156
+ // Handle next slide
129
157
  const handleNext = async () => {
130
- // Save current answer if exists
131
- if (currentSlide.question && currentAnswer !== undefined) {
132
- await onboardingStore.saveAnswer(currentSlide.question.id, currentAnswer);
133
- }
158
+ await saveCurrentAnswer(currentSlide);
134
159
 
135
160
  if (isLastSlide) {
136
- // Use useOnboardingNavigation's complete function
137
- // This will call handleComplete callback and emit event
138
161
  await completeOnboarding();
139
162
  } else {
140
163
  goToNext();
141
- // Load next slide's answer
142
- const nextSlide = filteredSlides[currentIndex + 1];
143
- if (nextSlide?.question) {
144
- const savedAnswer = onboardingStore.getAnswer(nextSlide.question.id);
145
- setCurrentAnswer(savedAnswer);
146
- } else {
147
- setCurrentAnswer(undefined);
148
- }
164
+ const nextSlide = OnboardingSlideService.getSlideAtIndex(
165
+ filteredSlides,
166
+ currentIndex + 1,
167
+ );
168
+ loadAnswerForSlide(nextSlide);
149
169
  }
150
170
  };
151
171
 
172
+ // Handle previous slide
152
173
  const handlePrevious = () => {
153
174
  goToPrevious();
154
- // Load previous slide's answer
155
- if (currentIndex > 0) {
156
- const prevSlide = filteredSlides[currentIndex - 1];
157
- if (prevSlide?.question) {
158
- const savedAnswer = onboardingStore.getAnswer(prevSlide.question.id);
159
- setCurrentAnswer(savedAnswer);
160
- } else {
161
- setCurrentAnswer(undefined);
162
- }
163
- }
175
+ const prevSlide = OnboardingSlideService.getSlideAtIndex(
176
+ filteredSlides,
177
+ currentIndex - 1,
178
+ );
179
+ loadAnswerForSlide(prevSlide);
164
180
  };
165
181
 
166
- const currentSlide = filteredSlides[currentIndex];
167
- const styles = useMemo(() => getStyles(insets), [insets]);
182
+ // Handle skip
183
+ const handleSkip = async () => {
184
+ await skipOnboarding();
185
+ };
168
186
 
169
- // Load current slide's answer on mount and when slide changes
170
- useEffect(() => {
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]);
187
+ // Check if gradient should be used
188
+ const useGradient = shouldUseGradient(currentSlide);
178
189
 
179
- // Validate current answer
190
+ // Validate answer using service
180
191
  const isAnswerValid = useMemo(() => {
181
- if (!currentSlide?.question) return true;
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;
192
+ if (!currentSlide?.question) {
193
+ return true;
215
194
  }
216
-
217
- return true;
195
+ return OnboardingValidationService.validateAnswer(
196
+ currentSlide.question,
197
+ currentAnswer,
198
+ );
218
199
  }, [currentSlide, currentAnswer]);
219
200
 
201
+ const styles = useMemo(
202
+ () => getStyles(insets, tokens, useGradient),
203
+ [insets, tokens, useGradient],
204
+ );
205
+
220
206
  return (
221
207
  <View style={styles.container}>
222
- <StatusBar barStyle="light-content" />
223
- <LinearGradient
224
- colors={currentSlide.gradient as [string, string, ...string[]]}
225
- start={{ x: 0, y: 0 }}
226
- end={{ x: 1, y: 1 }}
227
- style={StyleSheet.absoluteFill}
228
- />
208
+ <StatusBar barStyle={themeMode === "dark" ? "light-content" : "dark-content"} />
209
+ {useGradient && currentSlide && (
210
+ <LinearGradient
211
+ colors={currentSlide.gradient as [string, string, ...string[]]}
212
+ start={{ x: 0, y: 0 }}
213
+ end={{ x: 1, y: 1 }}
214
+ style={StyleSheet.absoluteFill}
215
+ />
216
+ )}
229
217
  {renderHeader ? (
230
218
  renderHeader({
231
219
  isFirstSlide,
@@ -240,18 +228,21 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
240
228
  showBackButton={showBackButton}
241
229
  showSkipButton={showSkipButton}
242
230
  skipButtonText={skipButtonText}
231
+ useGradient={useGradient}
243
232
  />
244
233
  )}
245
- {renderSlide ? (
246
- renderSlide(currentSlide)
247
- ) : currentSlide.type === "question" && currentSlide.question ? (
248
- <QuestionSlide
249
- slide={currentSlide}
250
- value={currentAnswer}
251
- onChange={setCurrentAnswer}
252
- />
253
- ) : (
254
- <OnboardingSlideComponent slide={currentSlide} />
234
+ {currentSlide && (
235
+ renderSlide ? (
236
+ renderSlide(currentSlide)
237
+ ) : currentSlide.type === "question" && currentSlide.question ? (
238
+ <QuestionSlide
239
+ slide={currentSlide}
240
+ value={currentAnswer}
241
+ onChange={setCurrentAnswer}
242
+ />
243
+ ) : (
244
+ <OnboardingSlideComponent slide={currentSlide} />
245
+ )
255
246
  )}
256
247
  {renderFooter ? (
257
248
  renderFooter({
@@ -274,17 +265,24 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
274
265
  nextButtonText={nextButtonText}
275
266
  getStartedButtonText={getStartedButtonText}
276
267
  disabled={!isAnswerValid}
268
+ useGradient={useGradient}
277
269
  />
278
270
  )}
279
271
  </View>
280
272
  );
281
273
  };
282
274
 
283
- const getStyles = (insets: { top: number }) =>
275
+ const getStyles = (
276
+ insets: { top: number },
277
+ tokens: ReturnType<typeof useAppDesignTokens>,
278
+ useGradient: boolean,
279
+ ) =>
284
280
  StyleSheet.create({
285
281
  container: {
286
282
  flex: 1,
287
283
  paddingTop: insets.top,
284
+ // Use transparent background when gradient is used, otherwise use theme background
285
+ backgroundColor: useGradient ? 'transparent' : tokens.colors.backgroundPrimary,
288
286
  },
289
287
  });
290
288