@umituz/react-native-design-system 2.6.128 → 2.8.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 +4 -2
- package/src/exports/onboarding.ts +6 -0
- package/src/index.ts +5 -0
- package/src/molecules/navigation/utils/AppNavigation.ts +53 -28
- package/src/molecules/splash/hooks/useSplashFlow.ts +33 -7
- package/src/onboarding/domain/entities/OnboardingOptions.ts +104 -0
- package/src/onboarding/domain/entities/OnboardingQuestion.ts +165 -0
- package/src/onboarding/domain/entities/OnboardingSlide.ts +152 -0
- package/src/onboarding/domain/entities/OnboardingUserData.ts +43 -0
- package/src/onboarding/hooks/useOnboardingFlow.ts +50 -0
- package/src/onboarding/index.ts +108 -0
- package/src/onboarding/infrastructure/hooks/__tests__/useOnboardingAnswers.test.ts +163 -0
- package/src/onboarding/infrastructure/hooks/__tests__/useOnboardingNavigation.test.ts +121 -0
- package/src/onboarding/infrastructure/hooks/useOnboardingAnswers.ts +69 -0
- package/src/onboarding/infrastructure/hooks/useOnboardingNavigation.ts +75 -0
- package/src/onboarding/infrastructure/services/SlideManager.ts +53 -0
- package/src/onboarding/infrastructure/services/ValidationManager.ts +127 -0
- package/src/onboarding/infrastructure/storage/OnboardingStore.ts +99 -0
- package/src/onboarding/infrastructure/storage/OnboardingStoreActions.ts +50 -0
- package/src/onboarding/infrastructure/storage/OnboardingStoreSelectors.ts +25 -0
- package/src/onboarding/infrastructure/storage/OnboardingStoreState.ts +22 -0
- package/src/onboarding/infrastructure/storage/__tests__/OnboardingStore.test.ts +85 -0
- package/src/onboarding/infrastructure/storage/actions/answerActions.ts +47 -0
- package/src/onboarding/infrastructure/storage/actions/completeAction.ts +45 -0
- package/src/onboarding/infrastructure/storage/actions/index.ts +22 -0
- package/src/onboarding/infrastructure/storage/actions/initializeAction.ts +40 -0
- package/src/onboarding/infrastructure/storage/actions/resetAction.ts +37 -0
- package/src/onboarding/infrastructure/storage/actions/skipAction.ts +46 -0
- package/src/onboarding/infrastructure/storage/actions/storageHelpers.ts +60 -0
- package/src/onboarding/infrastructure/utils/arrayUtils.ts +28 -0
- package/src/onboarding/infrastructure/utils/backgroundUtils.ts +38 -0
- package/src/onboarding/infrastructure/utils/layouts/collageLayout.ts +81 -0
- package/src/onboarding/infrastructure/utils/layouts/gridLayouts.ts +78 -0
- package/src/onboarding/infrastructure/utils/layouts/honeycombLayout.ts +36 -0
- package/src/onboarding/infrastructure/utils/layouts/index.ts +12 -0
- package/src/onboarding/infrastructure/utils/layouts/layoutTypes.ts +37 -0
- package/src/onboarding/infrastructure/utils/layouts/masonryLayout.ts +37 -0
- package/src/onboarding/infrastructure/utils/layouts/scatteredLayout.ts +34 -0
- package/src/onboarding/infrastructure/utils/layouts/screenDimensions.ts +11 -0
- package/src/onboarding/infrastructure/utils/layouts/tilesLayout.ts +34 -0
- package/src/onboarding/presentation/components/BackgroundImageCollage.tsx +90 -0
- package/src/onboarding/presentation/components/BackgroundVideo.tsx +24 -0
- package/src/onboarding/presentation/components/BaseSlide.tsx +47 -0
- package/src/onboarding/presentation/components/OnboardingBackground.tsx +91 -0
- package/src/onboarding/presentation/components/OnboardingFooter.tsx +151 -0
- package/src/onboarding/presentation/components/OnboardingHeader.tsx +92 -0
- package/src/onboarding/presentation/components/OnboardingResetSetting.tsx +70 -0
- package/src/onboarding/presentation/components/OnboardingScreenContent.tsx +146 -0
- package/src/onboarding/presentation/components/OnboardingSlide.tsx +124 -0
- package/src/onboarding/presentation/components/QuestionRenderer.tsx +60 -0
- package/src/onboarding/presentation/components/QuestionSlide.tsx +67 -0
- package/src/onboarding/presentation/components/QuestionSlideHeader.tsx +75 -0
- package/src/onboarding/presentation/components/questions/MultipleChoiceQuestion.tsx +74 -0
- package/src/onboarding/presentation/components/questions/QuestionOptionItem.tsx +115 -0
- package/src/onboarding/presentation/components/questions/RatingQuestion.tsx +66 -0
- package/src/onboarding/presentation/components/questions/SingleChoiceQuestion.tsx +117 -0
- package/src/onboarding/presentation/components/questions/TextInputQuestion.tsx +71 -0
- package/src/onboarding/presentation/hooks/__tests__/useOnboardingContainerStyle.test.ts +96 -0
- package/src/onboarding/presentation/hooks/useOnboardingContainerStyle.ts +37 -0
- package/src/onboarding/presentation/hooks/useOnboardingGestures.ts +45 -0
- package/src/onboarding/presentation/hooks/useOnboardingScreenHandlers.ts +114 -0
- package/src/onboarding/presentation/hooks/useOnboardingScreenState.ts +146 -0
- package/src/onboarding/presentation/providers/OnboardingProvider.tsx +51 -0
- package/src/onboarding/presentation/screens/OnboardingScreen.tsx +189 -0
- package/src/onboarding/presentation/types/OnboardingProps.ts +46 -0
- package/src/onboarding/presentation/types/OnboardingTheme.ts +27 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OnboardingContainerStyle Hook Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook } from '@testing-library/react-native';
|
|
6
|
+
import { useOnboardingContainerStyle } from '../useOnboardingContainerStyle';
|
|
7
|
+
|
|
8
|
+
// Mock theme hook
|
|
9
|
+
jest.mock('@umituz/react-native-design-system', () => ({
|
|
10
|
+
useAppDesignTokens: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Mock safe area insets
|
|
14
|
+
jest.mock('react-native-safe-area-context', () => ({
|
|
15
|
+
useSafeAreaInsets: jest.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system';
|
|
19
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
20
|
+
|
|
21
|
+
const mockUseAppDesignTokens = useAppDesignTokens as jest.MockedFunction<typeof useAppDesignTokens>;
|
|
22
|
+
const mockUseSafeAreaInsets = useSafeAreaInsets as jest.MockedFunction<typeof useSafeAreaInsets>;
|
|
23
|
+
|
|
24
|
+
describe('useOnboardingContainerStyle', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
|
|
28
|
+
mockUseSafeAreaInsets.mockReturnValue({
|
|
29
|
+
top: 44,
|
|
30
|
+
bottom: 34,
|
|
31
|
+
left: 0,
|
|
32
|
+
right: 0,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
mockUseAppDesignTokens.mockReturnValue({
|
|
36
|
+
colors: {
|
|
37
|
+
backgroundPrimary: '#ffffff',
|
|
38
|
+
},
|
|
39
|
+
} as any);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return container style with custom background disabled', () => {
|
|
43
|
+
const { result } = renderHook(() =>
|
|
44
|
+
useOnboardingContainerStyle({ useCustomBackground: false })
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(result.current.containerStyle).toEqual([
|
|
48
|
+
{
|
|
49
|
+
paddingTop: 44,
|
|
50
|
+
backgroundColor: '#ffffff',
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return container style with custom background enabled', () => {
|
|
56
|
+
const { result } = renderHook(() =>
|
|
57
|
+
useOnboardingContainerStyle({ useCustomBackground: true })
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(result.current.containerStyle).toEqual([
|
|
61
|
+
{
|
|
62
|
+
paddingTop: 44,
|
|
63
|
+
backgroundColor: 'transparent',
|
|
64
|
+
},
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should use correct top inset', () => {
|
|
69
|
+
mockUseSafeAreaInsets.mockReturnValue({
|
|
70
|
+
top: 50,
|
|
71
|
+
bottom: 34,
|
|
72
|
+
left: 0,
|
|
73
|
+
right: 0,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const { result } = renderHook(() =>
|
|
77
|
+
useOnboardingContainerStyle({ useCustomBackground: false })
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(result.current.containerStyle[0].paddingTop).toBe(50);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should use theme background color', () => {
|
|
84
|
+
mockUseAppDesignTokens.mockReturnValue({
|
|
85
|
+
colors: {
|
|
86
|
+
backgroundPrimary: '#f0f0f0',
|
|
87
|
+
},
|
|
88
|
+
} as any);
|
|
89
|
+
|
|
90
|
+
const { result } = renderHook(() =>
|
|
91
|
+
useOnboardingContainerStyle({ useCustomBackground: false })
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(result.current.containerStyle[0].backgroundColor).toBe('#f0f0f0');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOnboardingContainerStyle Hook
|
|
3
|
+
* Single Responsibility: Manage container styling for onboarding screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
9
|
+
|
|
10
|
+
export interface UseOnboardingContainerStyleProps {
|
|
11
|
+
useCustomBackground: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseOnboardingContainerStyleReturn {
|
|
15
|
+
containerStyle: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useOnboardingContainerStyle({
|
|
19
|
+
useCustomBackground,
|
|
20
|
+
}: UseOnboardingContainerStyleProps): UseOnboardingContainerStyleReturn {
|
|
21
|
+
const insets = useSafeAreaInsets();
|
|
22
|
+
const tokens = useAppDesignTokens();
|
|
23
|
+
|
|
24
|
+
const containerStyle = useMemo(
|
|
25
|
+
() => [
|
|
26
|
+
{
|
|
27
|
+
paddingTop: insets.top,
|
|
28
|
+
backgroundColor: useCustomBackground ? "transparent" : tokens.colors.backgroundPrimary,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
[insets.top, useCustomBackground, tokens.colors.backgroundPrimary],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
containerStyle,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOnboardingGestures Hook
|
|
3
|
+
* Handles swipe gestures for onboarding navigation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useRef } from "react";
|
|
7
|
+
import { PanResponder } from "react-native";
|
|
8
|
+
|
|
9
|
+
interface UseOnboardingGesturesProps {
|
|
10
|
+
isFirstSlide: boolean;
|
|
11
|
+
isAnswerValid: boolean;
|
|
12
|
+
onNext: () => void;
|
|
13
|
+
onBack: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const useOnboardingGestures = ({
|
|
17
|
+
isFirstSlide,
|
|
18
|
+
isAnswerValid,
|
|
19
|
+
onNext,
|
|
20
|
+
onBack,
|
|
21
|
+
}: UseOnboardingGesturesProps) => {
|
|
22
|
+
const panResponder = useRef(
|
|
23
|
+
PanResponder.create({
|
|
24
|
+
onMoveShouldSetPanResponder: (_, gestureState) => {
|
|
25
|
+
// Only trigger for horizontal swipes
|
|
26
|
+
return Math.abs(gestureState.dx) > 20 && Math.abs(gestureState.dy) < 40;
|
|
27
|
+
},
|
|
28
|
+
onPanResponderRelease: (_, gestureState) => {
|
|
29
|
+
if (gestureState.dx > 50) {
|
|
30
|
+
// Swipe Right -> Previous
|
|
31
|
+
if (!isFirstSlide) {
|
|
32
|
+
onBack();
|
|
33
|
+
}
|
|
34
|
+
} else if (gestureState.dx < -50) {
|
|
35
|
+
// Swipe Left -> Next
|
|
36
|
+
if (isAnswerValid) {
|
|
37
|
+
onNext();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
).current;
|
|
43
|
+
|
|
44
|
+
return panResponder;
|
|
45
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOnboardingScreenHandlers Hook
|
|
3
|
+
* Single Responsibility: Handle onboarding screen user interactions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useCallback } from "react";
|
|
7
|
+
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
8
|
+
import { SlideManager } from "../../infrastructure/services/SlideManager";
|
|
9
|
+
|
|
10
|
+
export interface UseOnboardingScreenHandlersProps {
|
|
11
|
+
filteredSlides: OnboardingSlide[];
|
|
12
|
+
currentSlide: OnboardingSlide | undefined;
|
|
13
|
+
currentIndex: number;
|
|
14
|
+
isLastSlide: boolean;
|
|
15
|
+
saveCurrentAnswer: (slide: OnboardingSlide) => Promise<void>;
|
|
16
|
+
completeOnboarding: () => Promise<void>;
|
|
17
|
+
goToNext: () => void;
|
|
18
|
+
goToPrevious: () => void;
|
|
19
|
+
skipOnboarding: () => Promise<void>;
|
|
20
|
+
loadAnswerForSlide: (slide: OnboardingSlide) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UseOnboardingScreenHandlersReturn {
|
|
24
|
+
handleNext: () => Promise<void>;
|
|
25
|
+
handlePrevious: () => void;
|
|
26
|
+
handleSkip: () => Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function useOnboardingScreenHandlers({
|
|
30
|
+
filteredSlides,
|
|
31
|
+
currentSlide,
|
|
32
|
+
currentIndex,
|
|
33
|
+
isLastSlide,
|
|
34
|
+
saveCurrentAnswer,
|
|
35
|
+
completeOnboarding,
|
|
36
|
+
goToNext,
|
|
37
|
+
goToPrevious,
|
|
38
|
+
skipOnboarding,
|
|
39
|
+
loadAnswerForSlide,
|
|
40
|
+
}: UseOnboardingScreenHandlersProps): UseOnboardingScreenHandlersReturn {
|
|
41
|
+
const handleNext = useCallback(async () => {
|
|
42
|
+
if (!currentSlide) return;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await saveCurrentAnswer(currentSlide);
|
|
46
|
+
|
|
47
|
+
if (isLastSlide) {
|
|
48
|
+
await completeOnboarding();
|
|
49
|
+
} else {
|
|
50
|
+
goToNext();
|
|
51
|
+
|
|
52
|
+
const nextSlide = SlideManager.getSlideAtIndex(
|
|
53
|
+
filteredSlides,
|
|
54
|
+
currentIndex + 1
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (nextSlide) {
|
|
58
|
+
loadAnswerForSlide(nextSlide);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (__DEV__) {
|
|
63
|
+
console.error("[useOnboardingScreenHandlers] Error in handleNext:", error);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}, [
|
|
67
|
+
currentSlide,
|
|
68
|
+
isLastSlide,
|
|
69
|
+
saveCurrentAnswer,
|
|
70
|
+
completeOnboarding,
|
|
71
|
+
goToNext,
|
|
72
|
+
filteredSlides,
|
|
73
|
+
currentIndex,
|
|
74
|
+
loadAnswerForSlide,
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
const handlePrevious = useCallback(() => {
|
|
78
|
+
try {
|
|
79
|
+
goToPrevious();
|
|
80
|
+
|
|
81
|
+
const prevSlide = SlideManager.getSlideAtIndex(
|
|
82
|
+
filteredSlides,
|
|
83
|
+
currentIndex - 1
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (prevSlide) {
|
|
87
|
+
loadAnswerForSlide(prevSlide);
|
|
88
|
+
}
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (__DEV__) {
|
|
91
|
+
console.error(
|
|
92
|
+
"[useOnboardingScreenHandlers] Error in handlePrevious:",
|
|
93
|
+
error
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}, [goToPrevious, filteredSlides, currentIndex, loadAnswerForSlide]);
|
|
98
|
+
|
|
99
|
+
const handleSkip = useCallback(async () => {
|
|
100
|
+
try {
|
|
101
|
+
await skipOnboarding();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
if (__DEV__) {
|
|
104
|
+
console.error("[useOnboardingScreenHandlers] Error in handleSkip:", error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}, [skipOnboarding]);
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
handleNext,
|
|
111
|
+
handlePrevious,
|
|
112
|
+
handleSkip,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOnboardingScreenState Hook
|
|
3
|
+
* Single Responsibility: Coordinate onboarding screen state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo, useEffect } from "react";
|
|
7
|
+
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
8
|
+
import { useOnboarding } from "../../infrastructure/storage/OnboardingStore";
|
|
9
|
+
import { useOnboardingNavigation } from "../../infrastructure/hooks/useOnboardingNavigation";
|
|
10
|
+
import { useOnboardingAnswers } from "../../infrastructure/hooks/useOnboardingAnswers";
|
|
11
|
+
import { useOnboardingContainerStyle } from "./useOnboardingContainerStyle";
|
|
12
|
+
import { useOnboardingScreenHandlers } from "./useOnboardingScreenHandlers";
|
|
13
|
+
import { SlideManager } from "../../infrastructure/services/SlideManager";
|
|
14
|
+
import { ValidationManager } from "../../infrastructure/services/ValidationManager";
|
|
15
|
+
import { shouldUseCustomBackground } from "../../infrastructure/utils/backgroundUtils";
|
|
16
|
+
|
|
17
|
+
export interface UseOnboardingScreenStateProps {
|
|
18
|
+
slides: OnboardingSlide[] | undefined;
|
|
19
|
+
storageKey?: string;
|
|
20
|
+
onComplete?: () => void | Promise<void>;
|
|
21
|
+
onSkip?: () => void | Promise<void>;
|
|
22
|
+
globalUseCustomBackground?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UseOnboardingScreenStateReturn {
|
|
26
|
+
filteredSlides: OnboardingSlide[];
|
|
27
|
+
currentSlide: OnboardingSlide | undefined;
|
|
28
|
+
currentIndex: number;
|
|
29
|
+
isFirstSlide: boolean;
|
|
30
|
+
isLastSlide: boolean;
|
|
31
|
+
currentAnswer: unknown;
|
|
32
|
+
isAnswerValid: boolean;
|
|
33
|
+
useCustomBackground: boolean;
|
|
34
|
+
containerStyle: unknown;
|
|
35
|
+
handleNext: () => Promise<void>;
|
|
36
|
+
handlePrevious: () => void;
|
|
37
|
+
handleSkip: () => Promise<void>;
|
|
38
|
+
setCurrentAnswer: (value: unknown) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useOnboardingScreenState({
|
|
42
|
+
slides,
|
|
43
|
+
storageKey,
|
|
44
|
+
onComplete,
|
|
45
|
+
onSkip,
|
|
46
|
+
globalUseCustomBackground = false,
|
|
47
|
+
}: UseOnboardingScreenStateProps): UseOnboardingScreenStateReturn {
|
|
48
|
+
const onboardingStore = useOnboarding();
|
|
49
|
+
|
|
50
|
+
const filteredSlides = useMemo(() => {
|
|
51
|
+
if (!slides || !Array.isArray(slides) || slides.length === 0) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const userData = onboardingStore.userData;
|
|
55
|
+
return SlideManager.filterSlides(slides, userData);
|
|
56
|
+
}, [slides, onboardingStore.userData]);
|
|
57
|
+
|
|
58
|
+
const {
|
|
59
|
+
currentIndex,
|
|
60
|
+
goToNext,
|
|
61
|
+
goToPrevious,
|
|
62
|
+
complete: completeOnboarding,
|
|
63
|
+
skip: skipOnboarding,
|
|
64
|
+
isLastSlide,
|
|
65
|
+
isFirstSlide,
|
|
66
|
+
} = useOnboardingNavigation(
|
|
67
|
+
filteredSlides.length,
|
|
68
|
+
async () => {
|
|
69
|
+
await onboardingStore.complete(storageKey);
|
|
70
|
+
if (onComplete) {
|
|
71
|
+
await onComplete();
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
async () => {
|
|
75
|
+
await onboardingStore.skip(storageKey);
|
|
76
|
+
if (onSkip) {
|
|
77
|
+
await onSkip();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const currentSlide = useMemo(
|
|
83
|
+
() => SlideManager.getSlideAtIndex(filteredSlides, currentIndex),
|
|
84
|
+
[filteredSlides, currentIndex]
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const {
|
|
88
|
+
currentAnswer,
|
|
89
|
+
setCurrentAnswer,
|
|
90
|
+
loadAnswerForSlide,
|
|
91
|
+
saveCurrentAnswer,
|
|
92
|
+
} = useOnboardingAnswers(currentSlide);
|
|
93
|
+
|
|
94
|
+
const { handleNext, handlePrevious, handleSkip } = useOnboardingScreenHandlers(
|
|
95
|
+
{
|
|
96
|
+
filteredSlides,
|
|
97
|
+
currentSlide,
|
|
98
|
+
currentIndex,
|
|
99
|
+
isLastSlide,
|
|
100
|
+
saveCurrentAnswer,
|
|
101
|
+
completeOnboarding,
|
|
102
|
+
goToNext,
|
|
103
|
+
goToPrevious,
|
|
104
|
+
skipOnboarding,
|
|
105
|
+
loadAnswerForSlide,
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const useCustomBackground = shouldUseCustomBackground(currentSlide, globalUseCustomBackground);
|
|
110
|
+
|
|
111
|
+
const isAnswerValid = useMemo(() => {
|
|
112
|
+
if (!currentSlide?.question) {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
return ValidationManager.validateAnswer(
|
|
116
|
+
currentSlide.question,
|
|
117
|
+
currentAnswer
|
|
118
|
+
);
|
|
119
|
+
}, [currentSlide, currentAnswer]);
|
|
120
|
+
|
|
121
|
+
const { containerStyle } = useOnboardingContainerStyle({ useCustomBackground });
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
return () => {
|
|
125
|
+
if (__DEV__) {
|
|
126
|
+
console.log("[useOnboardingScreenState] Cleanup completed");
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}, []);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
filteredSlides,
|
|
133
|
+
currentSlide,
|
|
134
|
+
currentIndex,
|
|
135
|
+
isFirstSlide,
|
|
136
|
+
isLastSlide,
|
|
137
|
+
currentAnswer,
|
|
138
|
+
isAnswerValid,
|
|
139
|
+
useCustomBackground,
|
|
140
|
+
containerStyle,
|
|
141
|
+
handleNext,
|
|
142
|
+
handlePrevious,
|
|
143
|
+
handleSkip,
|
|
144
|
+
setCurrentAnswer,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
@@ -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
|
+
useCustomBackground: boolean;
|
|
20
|
+
colors: OnboardingColors;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const OnboardingProvider = ({
|
|
24
|
+
children,
|
|
25
|
+
useCustomBackground,
|
|
26
|
+
colors,
|
|
27
|
+
}: OnboardingProviderProps) => {
|
|
28
|
+
const value = useMemo(
|
|
29
|
+
() => ({
|
|
30
|
+
theme: {
|
|
31
|
+
colors,
|
|
32
|
+
useCustomBackground,
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
[colors, useCustomBackground]
|
|
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
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Screen
|
|
3
|
+
*
|
|
4
|
+
* Main onboarding screen component with theme-aware colors
|
|
5
|
+
* Generic and reusable across hundreds of apps
|
|
6
|
+
*
|
|
7
|
+
* This component only handles UI coordination - no business logic
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useMemo } from "react";
|
|
11
|
+
import { StyleSheet } from "react-native";
|
|
12
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system";
|
|
13
|
+
import type { OnboardingOptions } from "../../domain/entities/OnboardingOptions";
|
|
14
|
+
import { useOnboardingScreenState } from "../hooks/useOnboardingScreenState";
|
|
15
|
+
import { OnboardingScreenContent } from "../components/OnboardingScreenContent";
|
|
16
|
+
import { OnboardingProvider } from "../providers/OnboardingProvider";
|
|
17
|
+
import type { OnboardingColors } from "../types/OnboardingTheme";
|
|
18
|
+
|
|
19
|
+
export interface OnboardingScreenProps extends OnboardingOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Optional custom header component
|
|
22
|
+
*/
|
|
23
|
+
renderHeader?: (props: {
|
|
24
|
+
isFirstSlide: boolean;
|
|
25
|
+
onBack: () => void;
|
|
26
|
+
onSkip: () => void;
|
|
27
|
+
}) => React.ReactNode;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Optional custom footer component
|
|
31
|
+
*/
|
|
32
|
+
renderFooter?: (props: {
|
|
33
|
+
currentIndex: number;
|
|
34
|
+
totalSlides: number;
|
|
35
|
+
isLastSlide: boolean;
|
|
36
|
+
onNext: () => void;
|
|
37
|
+
onUpgrade?: () => void;
|
|
38
|
+
showPaywallOnComplete?: boolean;
|
|
39
|
+
}) => React.ReactNode;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Optional custom slide component
|
|
43
|
+
*/
|
|
44
|
+
renderSlide?: (slide: OnboardingOptions["slides"][0]) => React.ReactNode;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Optional upgrade callback for premium features
|
|
48
|
+
* Called when user wants to upgrade from onboarding
|
|
49
|
+
*/
|
|
50
|
+
onUpgrade?: () => void;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Show paywall modal on onboarding completion (default: false)
|
|
54
|
+
* When true, shows premium paywall before completing onboarding
|
|
55
|
+
*/
|
|
56
|
+
showPaywallOnComplete?: boolean;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Theme colors for the onboarding (Optional - will use design tokens if not provided)
|
|
60
|
+
*/
|
|
61
|
+
themeColors?: OnboardingColors;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const OnboardingScreen = ({
|
|
65
|
+
slides,
|
|
66
|
+
onComplete,
|
|
67
|
+
onSkip,
|
|
68
|
+
skipButtonText,
|
|
69
|
+
nextButtonText,
|
|
70
|
+
getStartedButtonText,
|
|
71
|
+
showSkipButton = true,
|
|
72
|
+
showBackButton = true,
|
|
73
|
+
showProgressBar = true,
|
|
74
|
+
showDots = true,
|
|
75
|
+
showProgressText = true,
|
|
76
|
+
storageKey,
|
|
77
|
+
autoComplete: _autoComplete = false,
|
|
78
|
+
renderHeader,
|
|
79
|
+
renderFooter,
|
|
80
|
+
renderSlide,
|
|
81
|
+
onUpgrade,
|
|
82
|
+
showPaywallOnComplete = false,
|
|
83
|
+
useCustomBackground: globalUseCustomBackground = false,
|
|
84
|
+
themeVariant = "default",
|
|
85
|
+
themeColors: providedThemeColors,
|
|
86
|
+
}: OnboardingScreenProps) => {
|
|
87
|
+
if (__DEV__) {
|
|
88
|
+
console.log("[OnboardingScreen] Rendering with slides:", slides?.length);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const tokens = useAppDesignTokens();
|
|
92
|
+
|
|
93
|
+
const themeColors = useMemo(
|
|
94
|
+
() =>
|
|
95
|
+
providedThemeColors ?? {
|
|
96
|
+
iconColor: tokens.colors.primary,
|
|
97
|
+
textColor: tokens.colors.textPrimary,
|
|
98
|
+
subTextColor: tokens.colors.textSecondary,
|
|
99
|
+
buttonBg: tokens.colors.primary,
|
|
100
|
+
buttonTextColor: tokens.colors.onPrimary,
|
|
101
|
+
progressBarBg: tokens.colors.surfaceSecondary,
|
|
102
|
+
progressFillColor: tokens.colors.primary,
|
|
103
|
+
dotColor: tokens.colors.surfaceSecondary,
|
|
104
|
+
activeDotColor: tokens.colors.primary,
|
|
105
|
+
progressTextColor: tokens.colors.textSecondary,
|
|
106
|
+
headerButtonBg: tokens.colors.surface,
|
|
107
|
+
headerButtonBorder: tokens.colors.borderLight,
|
|
108
|
+
iconBg: tokens.colors.surfaceSecondary,
|
|
109
|
+
iconBorder: tokens.colors.borderLight,
|
|
110
|
+
errorColor: tokens.colors.error,
|
|
111
|
+
featureItemBg: tokens.colors.surfaceSecondary,
|
|
112
|
+
},
|
|
113
|
+
[providedThemeColors, tokens]
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const {
|
|
117
|
+
filteredSlides,
|
|
118
|
+
currentSlide,
|
|
119
|
+
currentIndex,
|
|
120
|
+
isFirstSlide,
|
|
121
|
+
isLastSlide,
|
|
122
|
+
currentAnswer,
|
|
123
|
+
isAnswerValid,
|
|
124
|
+
useCustomBackground,
|
|
125
|
+
containerStyle,
|
|
126
|
+
handleNext,
|
|
127
|
+
handlePrevious,
|
|
128
|
+
handleSkip,
|
|
129
|
+
setCurrentAnswer,
|
|
130
|
+
} = useOnboardingScreenState({
|
|
131
|
+
slides,
|
|
132
|
+
storageKey,
|
|
133
|
+
onComplete,
|
|
134
|
+
onSkip,
|
|
135
|
+
globalUseCustomBackground,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (__DEV__) {
|
|
139
|
+
console.log("[OnboardingScreen] filteredSlides:", filteredSlides?.length);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Early return if no slides - prevents rendering empty/broken screen
|
|
143
|
+
if (filteredSlides.length === 0) {
|
|
144
|
+
if (__DEV__) {
|
|
145
|
+
console.log("[OnboardingScreen] No slides, returning null");
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<OnboardingProvider useCustomBackground={useCustomBackground} colors={themeColors}>
|
|
152
|
+
<OnboardingScreenContent
|
|
153
|
+
containerStyle={[styles.container, containerStyle]}
|
|
154
|
+
useCustomBackground={useCustomBackground}
|
|
155
|
+
currentSlide={currentSlide}
|
|
156
|
+
isFirstSlide={isFirstSlide}
|
|
157
|
+
isLastSlide={isLastSlide}
|
|
158
|
+
currentIndex={currentIndex}
|
|
159
|
+
totalSlides={filteredSlides.length}
|
|
160
|
+
currentAnswer={currentAnswer}
|
|
161
|
+
isAnswerValid={isAnswerValid}
|
|
162
|
+
showBackButton={showBackButton}
|
|
163
|
+
showSkipButton={showSkipButton}
|
|
164
|
+
showProgressBar={showProgressBar}
|
|
165
|
+
showDots={showDots}
|
|
166
|
+
showProgressText={showProgressText}
|
|
167
|
+
skipButtonText={skipButtonText}
|
|
168
|
+
nextButtonText={nextButtonText}
|
|
169
|
+
getStartedButtonText={getStartedButtonText}
|
|
170
|
+
onBack={handlePrevious}
|
|
171
|
+
onSkip={handleSkip}
|
|
172
|
+
onNext={handleNext}
|
|
173
|
+
onAnswerChange={setCurrentAnswer}
|
|
174
|
+
renderHeader={renderHeader}
|
|
175
|
+
renderFooter={renderFooter}
|
|
176
|
+
renderSlide={renderSlide}
|
|
177
|
+
onUpgrade={onUpgrade}
|
|
178
|
+
showPaywallOnComplete={showPaywallOnComplete}
|
|
179
|
+
variant={themeVariant}
|
|
180
|
+
/>
|
|
181
|
+
</OnboardingProvider>
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const styles = StyleSheet.create({
|
|
186
|
+
container: {
|
|
187
|
+
flex: 1,
|
|
188
|
+
},
|
|
189
|
+
});
|