@umituz/react-native-design-system 2.6.128 → 2.7.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.
Files changed (64) hide show
  1. package/package.json +4 -2
  2. package/src/exports/onboarding.ts +6 -0
  3. package/src/index.ts +5 -0
  4. package/src/onboarding/domain/entities/OnboardingOptions.ts +104 -0
  5. package/src/onboarding/domain/entities/OnboardingQuestion.ts +165 -0
  6. package/src/onboarding/domain/entities/OnboardingSlide.ts +152 -0
  7. package/src/onboarding/domain/entities/OnboardingUserData.ts +43 -0
  8. package/src/onboarding/hooks/useOnboardingFlow.ts +50 -0
  9. package/src/onboarding/index.ts +108 -0
  10. package/src/onboarding/infrastructure/hooks/__tests__/useOnboardingAnswers.test.ts +163 -0
  11. package/src/onboarding/infrastructure/hooks/__tests__/useOnboardingNavigation.test.ts +121 -0
  12. package/src/onboarding/infrastructure/hooks/useOnboardingAnswers.ts +69 -0
  13. package/src/onboarding/infrastructure/hooks/useOnboardingNavigation.ts +75 -0
  14. package/src/onboarding/infrastructure/services/SlideManager.ts +53 -0
  15. package/src/onboarding/infrastructure/services/ValidationManager.ts +127 -0
  16. package/src/onboarding/infrastructure/storage/OnboardingStore.ts +99 -0
  17. package/src/onboarding/infrastructure/storage/OnboardingStoreActions.ts +50 -0
  18. package/src/onboarding/infrastructure/storage/OnboardingStoreSelectors.ts +25 -0
  19. package/src/onboarding/infrastructure/storage/OnboardingStoreState.ts +22 -0
  20. package/src/onboarding/infrastructure/storage/__tests__/OnboardingStore.test.ts +85 -0
  21. package/src/onboarding/infrastructure/storage/actions/answerActions.ts +47 -0
  22. package/src/onboarding/infrastructure/storage/actions/completeAction.ts +45 -0
  23. package/src/onboarding/infrastructure/storage/actions/index.ts +22 -0
  24. package/src/onboarding/infrastructure/storage/actions/initializeAction.ts +40 -0
  25. package/src/onboarding/infrastructure/storage/actions/resetAction.ts +37 -0
  26. package/src/onboarding/infrastructure/storage/actions/skipAction.ts +46 -0
  27. package/src/onboarding/infrastructure/storage/actions/storageHelpers.ts +60 -0
  28. package/src/onboarding/infrastructure/utils/arrayUtils.ts +28 -0
  29. package/src/onboarding/infrastructure/utils/backgroundUtils.ts +38 -0
  30. package/src/onboarding/infrastructure/utils/layouts/collageLayout.ts +81 -0
  31. package/src/onboarding/infrastructure/utils/layouts/gridLayouts.ts +78 -0
  32. package/src/onboarding/infrastructure/utils/layouts/honeycombLayout.ts +36 -0
  33. package/src/onboarding/infrastructure/utils/layouts/index.ts +12 -0
  34. package/src/onboarding/infrastructure/utils/layouts/layoutTypes.ts +37 -0
  35. package/src/onboarding/infrastructure/utils/layouts/masonryLayout.ts +37 -0
  36. package/src/onboarding/infrastructure/utils/layouts/scatteredLayout.ts +34 -0
  37. package/src/onboarding/infrastructure/utils/layouts/screenDimensions.ts +11 -0
  38. package/src/onboarding/infrastructure/utils/layouts/tilesLayout.ts +34 -0
  39. package/src/onboarding/presentation/components/BackgroundImageCollage.tsx +90 -0
  40. package/src/onboarding/presentation/components/BackgroundVideo.tsx +24 -0
  41. package/src/onboarding/presentation/components/BaseSlide.tsx +47 -0
  42. package/src/onboarding/presentation/components/OnboardingBackground.tsx +91 -0
  43. package/src/onboarding/presentation/components/OnboardingFooter.tsx +151 -0
  44. package/src/onboarding/presentation/components/OnboardingHeader.tsx +92 -0
  45. package/src/onboarding/presentation/components/OnboardingResetSetting.tsx +70 -0
  46. package/src/onboarding/presentation/components/OnboardingScreenContent.tsx +146 -0
  47. package/src/onboarding/presentation/components/OnboardingSlide.tsx +124 -0
  48. package/src/onboarding/presentation/components/QuestionRenderer.tsx +60 -0
  49. package/src/onboarding/presentation/components/QuestionSlide.tsx +67 -0
  50. package/src/onboarding/presentation/components/QuestionSlideHeader.tsx +75 -0
  51. package/src/onboarding/presentation/components/questions/MultipleChoiceQuestion.tsx +74 -0
  52. package/src/onboarding/presentation/components/questions/QuestionOptionItem.tsx +115 -0
  53. package/src/onboarding/presentation/components/questions/RatingQuestion.tsx +66 -0
  54. package/src/onboarding/presentation/components/questions/SingleChoiceQuestion.tsx +117 -0
  55. package/src/onboarding/presentation/components/questions/TextInputQuestion.tsx +71 -0
  56. package/src/onboarding/presentation/hooks/__tests__/useOnboardingContainerStyle.test.ts +96 -0
  57. package/src/onboarding/presentation/hooks/useOnboardingContainerStyle.ts +37 -0
  58. package/src/onboarding/presentation/hooks/useOnboardingGestures.ts +45 -0
  59. package/src/onboarding/presentation/hooks/useOnboardingScreenHandlers.ts +114 -0
  60. package/src/onboarding/presentation/hooks/useOnboardingScreenState.ts +146 -0
  61. package/src/onboarding/presentation/providers/OnboardingProvider.tsx +51 -0
  62. package/src/onboarding/presentation/screens/OnboardingScreen.tsx +189 -0
  63. package/src/onboarding/presentation/types/OnboardingProps.ts +46 -0
  64. package/src/onboarding/presentation/types/OnboardingTheme.ts +27 -0
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Question Option Item Component
3
+ * Single Responsibility: Render a single selectable option
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
8
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
9
+ import type { QuestionOption } from "../../../domain/entities/OnboardingQuestion";
10
+ import type { OnboardingColors } from "../../types/OnboardingTheme";
11
+
12
+ export interface QuestionOptionItemProps {
13
+ option: QuestionOption;
14
+ isSelected: boolean;
15
+ onPress: () => void;
16
+ colors: OnboardingColors;
17
+ }
18
+
19
+ export const QuestionOptionItem = ({
20
+ option,
21
+ isSelected,
22
+ onPress,
23
+ colors,
24
+ }: QuestionOptionItemProps) => {
25
+ const isEmoji = option.iconType === 'emoji';
26
+
27
+ return (
28
+ <TouchableOpacity
29
+ style={[
30
+ styles.option,
31
+ {
32
+ backgroundColor: isSelected ? colors.iconBg : colors.featureItemBg,
33
+ borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
34
+ borderWidth: isSelected ? 2 : 1,
35
+ }
36
+ ]}
37
+ onPress={onPress}
38
+ activeOpacity={0.8}
39
+ >
40
+ {option.icon && (
41
+ <View style={[
42
+ styles.optionIcon,
43
+ { backgroundColor: isSelected ? colors.iconColor : colors.featureItemBg }
44
+ ]}>
45
+ {isEmoji ? (
46
+ <AtomicText style={{ fontSize: 24 }}>{option.icon}</AtomicText>
47
+ ) : (
48
+ <AtomicIcon
49
+ name={option.icon as any}
50
+ customSize={20}
51
+ customColor={isSelected ? colors.buttonTextColor : colors.subTextColor}
52
+ />
53
+ )}
54
+ </View>
55
+ )}
56
+ <AtomicText
57
+ type="bodyLarge"
58
+ style={[
59
+ styles.optionLabel,
60
+ {
61
+ color: isSelected ? colors.textColor : colors.subTextColor,
62
+ fontWeight: isSelected ? '700' : '500'
63
+ }
64
+ ]}
65
+ >
66
+ {option.label}
67
+ </AtomicText>
68
+ <View style={[
69
+ styles.checkbox,
70
+ {
71
+ borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
72
+ backgroundColor: isSelected ? colors.iconColor : 'transparent',
73
+ borderWidth: isSelected ? 0 : 2,
74
+ }
75
+ ]}>
76
+ {isSelected && (
77
+ <AtomicIcon
78
+ name="checkmark"
79
+ customSize={16}
80
+ customColor={colors.buttonTextColor}
81
+ />
82
+ )}
83
+ </View>
84
+ </TouchableOpacity>
85
+ );
86
+ };
87
+
88
+ const styles = StyleSheet.create({
89
+ option: {
90
+ flexDirection: "row",
91
+ alignItems: "center",
92
+ borderRadius: 20,
93
+ padding: 16,
94
+ marginBottom: 8,
95
+ },
96
+ optionIcon: {
97
+ width: 40,
98
+ height: 40,
99
+ borderRadius: 20,
100
+ alignItems: 'center',
101
+ justifyContent: 'center',
102
+ marginRight: 16,
103
+ },
104
+ optionLabel: {
105
+ flex: 1,
106
+ fontSize: 16,
107
+ },
108
+ checkbox: {
109
+ width: 24,
110
+ height: 24,
111
+ borderRadius: 8,
112
+ alignItems: "center",
113
+ justifyContent: "center",
114
+ },
115
+ });
@@ -0,0 +1,66 @@
1
+ import React from "react";
2
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
3
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
4
+ import { useOnboardingProvider } from "../../providers/OnboardingProvider";
5
+ import type { OnboardingQuestion } from "../../../domain/entities/OnboardingQuestion";
6
+
7
+ export interface RatingQuestionProps {
8
+ question: OnboardingQuestion;
9
+ value: number | undefined;
10
+ onChange: (value: number) => void;
11
+ }
12
+
13
+ export const RatingQuestion = ({
14
+ question,
15
+ value = 0,
16
+ onChange,
17
+ }: RatingQuestionProps) => {
18
+ const {
19
+ theme: { colors },
20
+ } = useOnboardingProvider();
21
+ const max = question.validation?.max ?? 5;
22
+
23
+ return (
24
+ <View style={styles.container}>
25
+ <View style={styles.stars}>
26
+ {Array.from({ length: max }).map((_, i) => {
27
+ const isFilled = i < value;
28
+ return (
29
+ <TouchableOpacity key={i} onPress={() => onChange(i + 1)} activeOpacity={0.8} style={styles.star}>
30
+ <AtomicIcon
31
+ name={isFilled ? "star" : "star-outline"}
32
+ customSize={48}
33
+ customColor={isFilled ? colors.iconColor : colors.headerButtonBorder}
34
+ />
35
+ </TouchableOpacity>
36
+ );
37
+ })}
38
+ </View>
39
+ {value > 0 && (
40
+ <AtomicText type="headlineSmall" style={[styles.valueText, { color: colors.textColor, marginTop: 12 }]}>
41
+ {value} <AtomicText type="bodyMedium" color="textSecondary">/ {max}</AtomicText>
42
+ </AtomicText>
43
+ )}
44
+ </View>
45
+ );
46
+ };
47
+
48
+ const styles = StyleSheet.create({
49
+ container: {
50
+ width: "100%",
51
+ alignItems: "center",
52
+ },
53
+ stars: {
54
+ flexDirection: "row",
55
+ gap: 12,
56
+ marginBottom: 20,
57
+ },
58
+ star: {
59
+ padding: 2,
60
+ },
61
+ valueText: {
62
+ fontWeight: "800",
63
+ },
64
+ });
65
+
66
+
@@ -0,0 +1,117 @@
1
+ import React from "react";
2
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
3
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
4
+ import { useOnboardingProvider } from "../../providers/OnboardingProvider";
5
+ import type { OnboardingQuestion, QuestionOption } from "../../../domain/entities/OnboardingQuestion";
6
+
7
+ export interface SingleChoiceQuestionProps {
8
+ question: OnboardingQuestion;
9
+ value: string | undefined;
10
+ onChange: (value: string) => void;
11
+ }
12
+
13
+ export const SingleChoiceQuestion = ({
14
+ question,
15
+ value,
16
+ onChange,
17
+ }: SingleChoiceQuestionProps) => {
18
+ const {
19
+ theme: { colors },
20
+ } = useOnboardingProvider();
21
+
22
+ const renderOption = (option: QuestionOption) => {
23
+ const isSelected = value === option.id;
24
+ const isEmoji = option.iconType === 'emoji';
25
+
26
+ return (
27
+ <TouchableOpacity
28
+ key={option.id}
29
+ style={[
30
+ styles.option,
31
+ {
32
+ backgroundColor: isSelected ? colors.iconBg : colors.featureItemBg,
33
+ borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
34
+ borderWidth: isSelected ? 2 : 1,
35
+ }
36
+ ]}
37
+ onPress={() => onChange(option.id)}
38
+ activeOpacity={0.8}
39
+ >
40
+ {option.icon && (
41
+ <View style={[
42
+ styles.optionIcon,
43
+ { backgroundColor: isSelected ? colors.iconColor : colors.featureItemBg }
44
+ ]}>
45
+ {isEmoji ? (
46
+ <AtomicText style={{ fontSize: 24 }}>{option.icon}</AtomicText>
47
+ ) : (
48
+ <AtomicIcon
49
+ name={option.icon as any}
50
+ customSize={20}
51
+ customColor={isSelected ? colors.buttonTextColor : colors.subTextColor}
52
+ />
53
+ )}
54
+ </View>
55
+ )}
56
+ <AtomicText type="bodyLarge" style={[styles.optionLabel, { color: isSelected ? colors.textColor : colors.subTextColor, fontWeight: isSelected ? '700' : '500' }]}>
57
+ {option.label}
58
+ </AtomicText>
59
+ <View style={[
60
+ styles.radioOuter,
61
+ { borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder }
62
+ ]}>
63
+ {isSelected && (
64
+ <View style={[styles.radioInner, { backgroundColor: colors.iconColor }]} />
65
+ )}
66
+ </View>
67
+ </TouchableOpacity>
68
+ );
69
+ };
70
+
71
+ return (
72
+ <View style={styles.container}>
73
+ {question.options?.map(renderOption)}
74
+ </View>
75
+ );
76
+ };
77
+
78
+ const styles = StyleSheet.create({
79
+ container: {
80
+ width: "100%",
81
+ gap: 12,
82
+ },
83
+ option: {
84
+ flexDirection: "row",
85
+ alignItems: "center",
86
+ borderRadius: 20,
87
+ padding: 16,
88
+ marginBottom: 8,
89
+ },
90
+ optionIcon: {
91
+ width: 40,
92
+ height: 40,
93
+ borderRadius: 20,
94
+ alignItems: 'center',
95
+ justifyContent: 'center',
96
+ marginRight: 16,
97
+ },
98
+ optionLabel: {
99
+ flex: 1,
100
+ fontSize: 16,
101
+ },
102
+ radioOuter: {
103
+ width: 24,
104
+ height: 24,
105
+ borderRadius: 12,
106
+ borderWidth: 2,
107
+ alignItems: 'center',
108
+ justifyContent: 'center',
109
+ },
110
+ radioInner: {
111
+ width: 12,
112
+ height: 12,
113
+ borderRadius: 6,
114
+ },
115
+ });
116
+
117
+
@@ -0,0 +1,71 @@
1
+ import React from "react";
2
+ import { View, TextInput, StyleSheet } from "react-native";
3
+ import { AtomicText } from "@umituz/react-native-design-system";
4
+ import { useOnboardingProvider } from "../../providers/OnboardingProvider";
5
+ import type { OnboardingQuestion } from "../../../domain/entities/OnboardingQuestion";
6
+
7
+ export interface TextInputQuestionProps {
8
+ question: OnboardingQuestion;
9
+ value: string | undefined;
10
+ onChange: (value: string) => void;
11
+ }
12
+
13
+ export const TextInputQuestion = ({
14
+ question,
15
+ value = "",
16
+ onChange,
17
+ }: TextInputQuestionProps) => {
18
+ const {
19
+ theme: { colors },
20
+ } = useOnboardingProvider();
21
+ const { validation } = question;
22
+
23
+ return (
24
+ <View style={styles.container}>
25
+ <TextInput
26
+ style={[
27
+ styles.input,
28
+ {
29
+ backgroundColor: colors.featureItemBg,
30
+ borderColor: value ? colors.iconColor : colors.headerButtonBorder,
31
+ color: colors.textColor,
32
+ }
33
+ ]}
34
+ value={value}
35
+ onChangeText={onChange}
36
+ placeholder={question.placeholder}
37
+ placeholderTextColor={colors.subTextColor}
38
+ maxLength={validation?.maxLength}
39
+ multiline={(validation?.maxLength ?? 0) > 100}
40
+ numberOfLines={(validation?.maxLength ?? 0) > 100 ? 5 : 1}
41
+ autoCapitalize="sentences"
42
+ autoCorrect={true}
43
+ textAlignVertical="top"
44
+ />
45
+ {validation?.maxLength && (
46
+ <AtomicText type="labelSmall" style={[styles.charCount, { color: colors.subTextColor }]}>
47
+ {value.length} / {validation.maxLength}
48
+ </AtomicText>
49
+ )}
50
+ </View>
51
+ );
52
+ };
53
+
54
+ const styles = StyleSheet.create({
55
+ container: {
56
+ width: "100%",
57
+ },
58
+ input: {
59
+ borderRadius: 16,
60
+ padding: 18,
61
+ fontSize: 16,
62
+ borderWidth: 2,
63
+ minHeight: 60,
64
+ },
65
+ charCount: {
66
+ textAlign: "right",
67
+ marginTop: 8,
68
+ fontWeight: "600",
69
+ },
70
+ });
71
+
@@ -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
+ }