@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.
Files changed (66) 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/molecules/navigation/utils/AppNavigation.ts +53 -28
  5. package/src/molecules/splash/hooks/useSplashFlow.ts +33 -7
  6. package/src/onboarding/domain/entities/OnboardingOptions.ts +104 -0
  7. package/src/onboarding/domain/entities/OnboardingQuestion.ts +165 -0
  8. package/src/onboarding/domain/entities/OnboardingSlide.ts +152 -0
  9. package/src/onboarding/domain/entities/OnboardingUserData.ts +43 -0
  10. package/src/onboarding/hooks/useOnboardingFlow.ts +50 -0
  11. package/src/onboarding/index.ts +108 -0
  12. package/src/onboarding/infrastructure/hooks/__tests__/useOnboardingAnswers.test.ts +163 -0
  13. package/src/onboarding/infrastructure/hooks/__tests__/useOnboardingNavigation.test.ts +121 -0
  14. package/src/onboarding/infrastructure/hooks/useOnboardingAnswers.ts +69 -0
  15. package/src/onboarding/infrastructure/hooks/useOnboardingNavigation.ts +75 -0
  16. package/src/onboarding/infrastructure/services/SlideManager.ts +53 -0
  17. package/src/onboarding/infrastructure/services/ValidationManager.ts +127 -0
  18. package/src/onboarding/infrastructure/storage/OnboardingStore.ts +99 -0
  19. package/src/onboarding/infrastructure/storage/OnboardingStoreActions.ts +50 -0
  20. package/src/onboarding/infrastructure/storage/OnboardingStoreSelectors.ts +25 -0
  21. package/src/onboarding/infrastructure/storage/OnboardingStoreState.ts +22 -0
  22. package/src/onboarding/infrastructure/storage/__tests__/OnboardingStore.test.ts +85 -0
  23. package/src/onboarding/infrastructure/storage/actions/answerActions.ts +47 -0
  24. package/src/onboarding/infrastructure/storage/actions/completeAction.ts +45 -0
  25. package/src/onboarding/infrastructure/storage/actions/index.ts +22 -0
  26. package/src/onboarding/infrastructure/storage/actions/initializeAction.ts +40 -0
  27. package/src/onboarding/infrastructure/storage/actions/resetAction.ts +37 -0
  28. package/src/onboarding/infrastructure/storage/actions/skipAction.ts +46 -0
  29. package/src/onboarding/infrastructure/storage/actions/storageHelpers.ts +60 -0
  30. package/src/onboarding/infrastructure/utils/arrayUtils.ts +28 -0
  31. package/src/onboarding/infrastructure/utils/backgroundUtils.ts +38 -0
  32. package/src/onboarding/infrastructure/utils/layouts/collageLayout.ts +81 -0
  33. package/src/onboarding/infrastructure/utils/layouts/gridLayouts.ts +78 -0
  34. package/src/onboarding/infrastructure/utils/layouts/honeycombLayout.ts +36 -0
  35. package/src/onboarding/infrastructure/utils/layouts/index.ts +12 -0
  36. package/src/onboarding/infrastructure/utils/layouts/layoutTypes.ts +37 -0
  37. package/src/onboarding/infrastructure/utils/layouts/masonryLayout.ts +37 -0
  38. package/src/onboarding/infrastructure/utils/layouts/scatteredLayout.ts +34 -0
  39. package/src/onboarding/infrastructure/utils/layouts/screenDimensions.ts +11 -0
  40. package/src/onboarding/infrastructure/utils/layouts/tilesLayout.ts +34 -0
  41. package/src/onboarding/presentation/components/BackgroundImageCollage.tsx +90 -0
  42. package/src/onboarding/presentation/components/BackgroundVideo.tsx +24 -0
  43. package/src/onboarding/presentation/components/BaseSlide.tsx +47 -0
  44. package/src/onboarding/presentation/components/OnboardingBackground.tsx +91 -0
  45. package/src/onboarding/presentation/components/OnboardingFooter.tsx +151 -0
  46. package/src/onboarding/presentation/components/OnboardingHeader.tsx +92 -0
  47. package/src/onboarding/presentation/components/OnboardingResetSetting.tsx +70 -0
  48. package/src/onboarding/presentation/components/OnboardingScreenContent.tsx +146 -0
  49. package/src/onboarding/presentation/components/OnboardingSlide.tsx +124 -0
  50. package/src/onboarding/presentation/components/QuestionRenderer.tsx +60 -0
  51. package/src/onboarding/presentation/components/QuestionSlide.tsx +67 -0
  52. package/src/onboarding/presentation/components/QuestionSlideHeader.tsx +75 -0
  53. package/src/onboarding/presentation/components/questions/MultipleChoiceQuestion.tsx +74 -0
  54. package/src/onboarding/presentation/components/questions/QuestionOptionItem.tsx +115 -0
  55. package/src/onboarding/presentation/components/questions/RatingQuestion.tsx +66 -0
  56. package/src/onboarding/presentation/components/questions/SingleChoiceQuestion.tsx +117 -0
  57. package/src/onboarding/presentation/components/questions/TextInputQuestion.tsx +71 -0
  58. package/src/onboarding/presentation/hooks/__tests__/useOnboardingContainerStyle.test.ts +96 -0
  59. package/src/onboarding/presentation/hooks/useOnboardingContainerStyle.ts +37 -0
  60. package/src/onboarding/presentation/hooks/useOnboardingGestures.ts +45 -0
  61. package/src/onboarding/presentation/hooks/useOnboardingScreenHandlers.ts +114 -0
  62. package/src/onboarding/presentation/hooks/useOnboardingScreenState.ts +146 -0
  63. package/src/onboarding/presentation/providers/OnboardingProvider.tsx +51 -0
  64. package/src/onboarding/presentation/screens/OnboardingScreen.tsx +189 -0
  65. package/src/onboarding/presentation/types/OnboardingProps.ts +46 -0
  66. package/src/onboarding/presentation/types/OnboardingTheme.ts +27 -0
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Onboarding User Data Entity
3
+ *
4
+ * Domain entity representing collected user data from onboarding
5
+ */
6
+
7
+ /**
8
+ * User data collected during onboarding
9
+ */
10
+ export interface OnboardingUserData {
11
+ /**
12
+ * User's answers to questions
13
+ * Key: question ID, Value: answer
14
+ */
15
+ answers: Record<string, any>;
16
+
17
+ /**
18
+ * Timestamp when onboarding was completed
19
+ */
20
+ completedAt?: string;
21
+
22
+ /**
23
+ * Was onboarding skipped?
24
+ */
25
+ skipped?: boolean;
26
+
27
+ /**
28
+ * User preferences derived from answers
29
+ */
30
+ preferences?: Record<string, any>;
31
+
32
+ /**
33
+ * User profile data
34
+ */
35
+ profile?: {
36
+ name?: string;
37
+ email?: string;
38
+ age?: number;
39
+ gender?: string;
40
+ [key: string]: any;
41
+ };
42
+ }
43
+
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Onboarding Flow Hook
3
+ * Manages onboarding completion state with persistence
4
+ */
5
+
6
+ import { useState, useEffect, useCallback } from 'react';
7
+ import { DeviceEventEmitter } from 'react-native';
8
+ import AsyncStorage from '@react-native-async-storage/async-storage';
9
+
10
+ const ONBOARDING_KEY = 'onboarding_complete';
11
+
12
+ export interface UseOnboardingFlowResult {
13
+ isOnboardingComplete: boolean;
14
+ completeOnboarding: () => Promise<void>;
15
+ }
16
+
17
+ export const useOnboardingFlow = (): UseOnboardingFlowResult => {
18
+ const [isOnboardingComplete, setIsOnboardingComplete] = useState(false);
19
+
20
+ // Load persisted state
21
+ useEffect(() => {
22
+ const loadPersistedState = async () => {
23
+ const value = await AsyncStorage.getItem(ONBOARDING_KEY);
24
+ setIsOnboardingComplete(value === 'true');
25
+ };
26
+
27
+ loadPersistedState();
28
+
29
+ const subscription = DeviceEventEmitter.addListener(
30
+ 'onboarding-complete',
31
+ () => {
32
+ setIsOnboardingComplete(true);
33
+ AsyncStorage.setItem(ONBOARDING_KEY, 'true');
34
+ },
35
+ );
36
+
37
+ return () => subscription.remove();
38
+ }, []);
39
+
40
+ const completeOnboarding = useCallback(async () => {
41
+ await AsyncStorage.setItem(ONBOARDING_KEY, 'true');
42
+ setIsOnboardingComplete(true);
43
+ DeviceEventEmitter.emit('onboarding-complete');
44
+ }, []);
45
+
46
+ return {
47
+ isOnboardingComplete,
48
+ completeOnboarding,
49
+ };
50
+ };
@@ -0,0 +1,108 @@
1
+ /**
2
+ * React Native Onboarding - Public API
3
+ *
4
+ * Generic onboarding flow for React Native apps with custom backgrounds,
5
+ * animations, and customizable slides. Follows SOLID, DRY, KISS principles.
6
+ *
7
+ * Architecture:
8
+ * - Domain: Entities and interfaces (business logic)
9
+ * - Infrastructure: Storage and hooks (state management)
10
+ * - Presentation: Components and screens (UI)
11
+ *
12
+ * Usage:
13
+ * import { OnboardingScreen, OnboardingSlide } from '@umituz/react-native-onboarding';
14
+ *
15
+ * <OnboardingScreen
16
+ * slides={[
17
+ * {
18
+ * id: '1',
19
+ * title: 'Welcome',
20
+ * description: 'Welcome to the app',
21
+ * icon: '🎉',
22
+ * backgroundColor: '#3B82F6',
23
+ * },
24
+ * ]}
25
+ * onComplete={() => console.log('Completed')}
26
+ * />
27
+ */
28
+
29
+ // =============================================================================
30
+ // DOMAIN LAYER - Entities and Interfaces
31
+ // =============================================================================
32
+
33
+ export type { OnboardingSlide, SlideType } from "./domain/entities/OnboardingSlide";
34
+ export type { OnboardingOptions } from "./domain/entities/OnboardingOptions";
35
+ export type {
36
+ OnboardingQuestion,
37
+ QuestionType,
38
+ QuestionOption,
39
+ QuestionValidation,
40
+ } from "./domain/entities/OnboardingQuestion";
41
+ export type { OnboardingUserData } from "./domain/entities/OnboardingUserData";
42
+
43
+ // =============================================================================
44
+ // INFRASTRUCTURE LAYER - Storage and Hooks
45
+ // =============================================================================
46
+
47
+ export {
48
+ useOnboardingStore,
49
+ useOnboarding,
50
+ } from "./infrastructure/storage/OnboardingStore";
51
+ export type { OnboardingStoreState } from "./infrastructure/storage/OnboardingStoreState";
52
+ export type { OnboardingStoreActions } from "./infrastructure/storage/OnboardingStoreActions";
53
+ export type { OnboardingStoreSelectors } from "./infrastructure/storage/OnboardingStoreSelectors";
54
+ export {
55
+ useOnboardingNavigation,
56
+ type UseOnboardingNavigationReturn,
57
+ } from "./infrastructure/hooks/useOnboardingNavigation";
58
+ export {
59
+ useOnboardingContainerStyle,
60
+ type UseOnboardingContainerStyleProps,
61
+ type UseOnboardingContainerStyleReturn,
62
+ } from "./presentation/hooks/useOnboardingContainerStyle";
63
+
64
+ // =============================================================================
65
+ // PRESENTATION LAYER - Components and Screens
66
+ // =============================================================================
67
+
68
+ export { OnboardingScreen, type OnboardingScreenProps } from "./presentation/screens/OnboardingScreen";
69
+ export { OnboardingHeader, type OnboardingHeaderProps } from "./presentation/components/OnboardingHeader";
70
+ export { OnboardingFooter, type OnboardingFooterProps } from "./presentation/components/OnboardingFooter";
71
+ export { OnboardingProvider, type OnboardingProviderProps, useOnboardingProvider } from "./presentation/providers/OnboardingProvider";
72
+ export { BackgroundImageCollage, type CollageLayout, type BackgroundImageCollageProps } from "./presentation/components/BackgroundImageCollage";
73
+ export type { OnboardingTheme, OnboardingColors } from "./presentation/types/OnboardingTheme";
74
+
75
+ // =============================================================================
76
+ // UTILITIES - Helper functions
77
+ // =============================================================================
78
+
79
+ export { ensureArray, safeIncludes, safeFilter } from "./infrastructure/utils/arrayUtils";
80
+ export type { ImageLayoutItem, LayoutConfig } from "./infrastructure/utils/layouts";
81
+
82
+ // Export OnboardingSlide component
83
+ // Note: TypeScript doesn't allow exporting both a type and a value with the same name
84
+ // The type is exported above as OnboardingSlide
85
+ // The component is exported here with a different name to avoid conflict
86
+ import { OnboardingSlide as OnboardingSlideComponent } from "./presentation/components/OnboardingSlide";
87
+ export { OnboardingSlideComponent };
88
+ export type { OnboardingSlideProps } from "./presentation/components/OnboardingSlide";
89
+
90
+ // Export QuestionSlide component
91
+ export { QuestionSlide } from "./presentation/components/QuestionSlide";
92
+ export type { QuestionSlideProps } from "./presentation/components/QuestionSlide";
93
+
94
+ // Export question components
95
+ export { SingleChoiceQuestion } from "./presentation/components/questions/SingleChoiceQuestion";
96
+ export type { SingleChoiceQuestionProps } from "./presentation/components/questions/SingleChoiceQuestion";
97
+ export { MultipleChoiceQuestion } from "./presentation/components/questions/MultipleChoiceQuestion";
98
+ export type { MultipleChoiceQuestionProps } from "./presentation/components/questions/MultipleChoiceQuestion";
99
+ export { TextInputQuestion } from "./presentation/components/questions/TextInputQuestion";
100
+ export type { TextInputQuestionProps } from "./presentation/components/questions/TextInputQuestion";
101
+ export { RatingQuestion } from "./presentation/components/questions/RatingQuestion";
102
+ export type { RatingQuestionProps } from "./presentation/components/questions/RatingQuestion";
103
+
104
+ export { OnboardingResetSetting } from "./presentation/components/OnboardingResetSetting";
105
+ export type { OnboardingResetSettingProps } from "./presentation/components/OnboardingResetSetting";
106
+
107
+
108
+ export { useOnboardingFlow, type UseOnboardingFlowResult } from './hooks/useOnboardingFlow';
@@ -0,0 +1,163 @@
1
+ /**
2
+ * OnboardingAnswers Hook Tests
3
+ */
4
+
5
+ import { renderHook, act } from '@testing-library/react-native';
6
+ import { useOnboardingAnswers } from '../useOnboardingAnswers';
7
+ import { useOnboardingStore } from '../../storage/OnboardingStore';
8
+
9
+ // Mock the store
10
+ jest.mock('../../storage/OnboardingStore', () => ({
11
+ useOnboardingStore: jest.fn(),
12
+ }));
13
+
14
+ const mockUseOnboardingStore = useOnboardingStore as jest.MockedFunction<typeof useOnboardingStore>;
15
+
16
+ describe('useOnboardingAnswers', () => {
17
+ const mockGetAnswer = jest.fn();
18
+ const mockSaveAnswer = jest.fn();
19
+
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ mockUseOnboardingStore.mockReturnValue({
23
+ getAnswer: mockGetAnswer,
24
+ saveAnswer: mockSaveAnswer,
25
+ } as any);
26
+ });
27
+
28
+ it('should initialize with undefined answer', () => {
29
+ const mockSlide = {
30
+ id: '1',
31
+ type: 'question',
32
+ question: {
33
+ id: 'q1',
34
+ type: 'single',
35
+ text: 'Test question',
36
+ },
37
+ };
38
+
39
+ const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
40
+
41
+ expect(result.current.currentAnswer).toBeUndefined();
42
+ expect(mockGetAnswer).toHaveBeenCalledWith('q1');
43
+ });
44
+
45
+ it('should load saved answer for slide', () => {
46
+ const mockSlide = {
47
+ id: '1',
48
+ type: 'question',
49
+ question: {
50
+ id: 'q1',
51
+ type: 'single',
52
+ text: 'Test question',
53
+ },
54
+ };
55
+
56
+ mockGetAnswer.mockReturnValue('option1');
57
+
58
+ const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
59
+
60
+ expect(result.current.currentAnswer).toBe('option1');
61
+ });
62
+
63
+ it('should use default value when no saved answer', () => {
64
+ const mockSlide = {
65
+ id: '1',
66
+ type: 'question',
67
+ question: {
68
+ id: 'q1',
69
+ type: 'single',
70
+ text: 'Test question',
71
+ defaultValue: 'default',
72
+ },
73
+ };
74
+
75
+ mockGetAnswer.mockReturnValue(undefined);
76
+
77
+ const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
78
+
79
+ expect(result.current.currentAnswer).toBe('default');
80
+ });
81
+
82
+ it('should set current answer', () => {
83
+ const mockSlide = {
84
+ id: '1',
85
+ type: 'question',
86
+ question: {
87
+ id: 'q1',
88
+ type: 'single',
89
+ text: 'Test question',
90
+ },
91
+ };
92
+
93
+ const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
94
+
95
+ act(() => {
96
+ result.current.setCurrentAnswer('newAnswer');
97
+ });
98
+
99
+ expect(result.current.currentAnswer).toBe('newAnswer');
100
+ });
101
+
102
+ it('should save answer for slide', async () => {
103
+ const mockSlide = {
104
+ id: '1',
105
+ type: 'question',
106
+ question: {
107
+ id: 'q1',
108
+ type: 'single',
109
+ text: 'Test question',
110
+ },
111
+ };
112
+
113
+ mockSaveAnswer.mockResolvedValue(undefined);
114
+
115
+ const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
116
+
117
+ act(() => {
118
+ result.current.setCurrentAnswer('answer1');
119
+ });
120
+
121
+ await act(async () => {
122
+ await result.current.saveCurrentAnswer(mockSlide);
123
+ });
124
+
125
+ expect(mockSaveAnswer).toHaveBeenCalledWith('q1', 'answer1');
126
+ });
127
+
128
+ it('should not save answer for non-question slide', async () => {
129
+ const mockSlide = {
130
+ id: '1',
131
+ type: 'welcome',
132
+ title: 'Welcome',
133
+ };
134
+
135
+ const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
136
+
137
+ await act(async () => {
138
+ await result.current.saveCurrentAnswer(mockSlide);
139
+ });
140
+
141
+ expect(mockSaveAnswer).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it('should not save undefined answer', async () => {
145
+ const mockSlide = {
146
+ id: '1',
147
+ type: 'question',
148
+ question: {
149
+ id: 'q1',
150
+ type: 'single',
151
+ text: 'Test question',
152
+ },
153
+ };
154
+
155
+ const { result } = renderHook(() => useOnboardingAnswers(mockSlide));
156
+
157
+ await act(async () => {
158
+ await result.current.saveCurrentAnswer(mockSlide);
159
+ });
160
+
161
+ expect(mockSaveAnswer).not.toHaveBeenCalled();
162
+ });
163
+ });
@@ -0,0 +1,121 @@
1
+ /**
2
+ * OnboardingNavigation Hook Tests
3
+ */
4
+
5
+ import { renderHook, act } from '@testing-library/react-native';
6
+ import { useOnboardingNavigation } from '../useOnboardingNavigation';
7
+
8
+ describe('useOnboardingNavigation', () => {
9
+ const mockOnComplete = jest.fn();
10
+ const mockOnSkip = jest.fn();
11
+
12
+ beforeEach(() => {
13
+ jest.clearAllMocks();
14
+ });
15
+
16
+ it('should initialize with first slide', () => {
17
+ const { result } = renderHook(() =>
18
+ useOnboardingNavigation(3, mockOnComplete, mockOnSkip)
19
+ );
20
+
21
+ expect(result.current.currentIndex).toBe(0);
22
+ expect(result.current.isFirstSlide).toBe(true);
23
+ expect(result.current.isLastSlide).toBe(false);
24
+ });
25
+
26
+ it('should navigate to next slide', () => {
27
+ const { result } = renderHook(() =>
28
+ useOnboardingNavigation(3, mockOnComplete, mockOnSkip)
29
+ );
30
+
31
+ act(() => {
32
+ result.current.goToNext();
33
+ });
34
+
35
+ expect(result.current.currentIndex).toBe(1);
36
+ expect(result.current.isFirstSlide).toBe(false);
37
+ expect(result.current.isLastSlide).toBe(false);
38
+ });
39
+
40
+ it('should navigate to previous slide', () => {
41
+ const { result } = renderHook(() =>
42
+ useOnboardingNavigation(3, mockOnComplete, mockOnSkip)
43
+ );
44
+
45
+ act(() => {
46
+ result.current.goToNext();
47
+ });
48
+ act(() => {
49
+ result.current.goToPrevious();
50
+ });
51
+
52
+ expect(result.current.currentIndex).toBe(0);
53
+ expect(result.current.isFirstSlide).toBe(true);
54
+ });
55
+
56
+ it('should handle last slide correctly', () => {
57
+ const { result } = renderHook(() =>
58
+ useOnboardingNavigation(2, mockOnComplete, mockOnSkip)
59
+ );
60
+
61
+ act(() => {
62
+ result.current.goToNext();
63
+ });
64
+
65
+ expect(result.current.currentIndex).toBe(1);
66
+ expect(result.current.isLastSlide).toBe(true);
67
+ });
68
+
69
+ it('should not go beyond last slide', () => {
70
+ const { result } = renderHook(() =>
71
+ useOnboardingNavigation(2, mockOnComplete, mockOnSkip)
72
+ );
73
+
74
+ act(() => {
75
+ result.current.goToNext();
76
+ });
77
+ act(() => {
78
+ result.current.goToNext();
79
+ });
80
+
81
+ expect(result.current.currentIndex).toBe(1);
82
+ expect(result.current.isLastSlide).toBe(true);
83
+ });
84
+
85
+ it('should not go before first slide', () => {
86
+ const { result } = renderHook(() =>
87
+ useOnboardingNavigation(2, mockOnComplete, mockOnSkip)
88
+ );
89
+
90
+ act(() => {
91
+ result.current.goToPrevious();
92
+ });
93
+
94
+ expect(result.current.currentIndex).toBe(0);
95
+ expect(result.current.isFirstSlide).toBe(true);
96
+ });
97
+
98
+ it('should call onComplete when completing', async () => {
99
+ const { result } = renderHook(() =>
100
+ useOnboardingNavigation(1, mockOnComplete, mockOnSkip)
101
+ );
102
+
103
+ await act(async () => {
104
+ await result.current.complete();
105
+ });
106
+
107
+ expect(mockOnComplete).toHaveBeenCalled();
108
+ });
109
+
110
+ it('should call onSkip when skipping', async () => {
111
+ const { result } = renderHook(() =>
112
+ useOnboardingNavigation(1, mockOnComplete, mockOnSkip)
113
+ );
114
+
115
+ await act(async () => {
116
+ await result.current.skip();
117
+ });
118
+
119
+ expect(mockOnSkip).toHaveBeenCalled();
120
+ });
121
+ });
@@ -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 { useOnboarding } 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 = useOnboarding();
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,75 @@
1
+ /**
2
+ * useOnboardingNavigation Hook
3
+ *
4
+ * Manages navigation state and callbacks for onboarding flow
5
+ */
6
+
7
+ import { useState, useCallback } from "react";
8
+ import { DeviceEventEmitter } from "react-native";
9
+
10
+ export interface UseOnboardingNavigationReturn {
11
+ currentIndex: number;
12
+ goToNext: () => void;
13
+ goToPrevious: () => void;
14
+ complete: () => Promise<void>;
15
+ skip: () => Promise<void>;
16
+ isLastSlide: boolean;
17
+ isFirstSlide: boolean;
18
+ }
19
+
20
+ /**
21
+ * Hook for managing onboarding navigation
22
+ *
23
+ * @param totalSlides - Total number of slides
24
+ * @param onComplete - Callback when onboarding completes
25
+ * @param onSkip - Callback when onboarding is skipped
26
+ * @returns Navigation state and handlers
27
+ */
28
+ export const useOnboardingNavigation = (
29
+ totalSlides: number,
30
+ onComplete?: () => void | Promise<void>,
31
+ onSkip?: () => void | Promise<void>,
32
+ ): UseOnboardingNavigationReturn => {
33
+ const [currentIndex, setCurrentIndex] = useState(0);
34
+
35
+ const goToNext = useCallback(() => {
36
+ if (currentIndex < totalSlides - 1) {
37
+ setCurrentIndex(currentIndex + 1);
38
+ }
39
+ }, [currentIndex, totalSlides]);
40
+
41
+ const goToPrevious = useCallback(() => {
42
+ if (currentIndex > 0) {
43
+ setCurrentIndex(currentIndex - 1);
44
+ }
45
+ }, [currentIndex]);
46
+
47
+ const complete = useCallback(async () => {
48
+ if (onComplete) {
49
+ await onComplete();
50
+ }
51
+ // Emit event for app-level handling
52
+
53
+ if (__DEV__) console.log("[useOnboardingNavigation] Emitting onboarding-complete event");
54
+ DeviceEventEmitter.emit("onboarding-complete");
55
+ }, [onComplete]);
56
+
57
+ const skip = useCallback(async () => {
58
+ if (onSkip) {
59
+ await onSkip();
60
+ }
61
+ // Emit event for app-level handling
62
+ DeviceEventEmitter.emit("onboarding-complete");
63
+ }, [onSkip]);
64
+
65
+ return {
66
+ currentIndex,
67
+ goToNext,
68
+ goToPrevious,
69
+ complete,
70
+ skip,
71
+ isLastSlide: currentIndex === totalSlides - 1,
72
+ isFirstSlide: currentIndex === 0,
73
+ };
74
+ };
75
+
@@ -0,0 +1,53 @@
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
+ * SlideManager
13
+ */
14
+ export class SlideManager {
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[] | undefined,
23
+ userData: OnboardingUserData,
24
+ ): OnboardingSlide[] {
25
+ // Safety check: return empty array if slides is undefined or not an array
26
+ if (!slides || !Array.isArray(slides)) {
27
+ return [];
28
+ }
29
+ return slides.filter((slide) => {
30
+ if (slide.skipIf) {
31
+ return !slide.skipIf(userData.answers);
32
+ }
33
+ return true;
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Get slide at specific index
39
+ * @param slides - Filtered slides array
40
+ * @param index - Slide index
41
+ * @returns Slide at index or undefined
42
+ */
43
+ static getSlideAtIndex(
44
+ slides: OnboardingSlide[],
45
+ index: number,
46
+ ): OnboardingSlide | undefined {
47
+ if (index < 0 || index >= slides.length) {
48
+ return undefined;
49
+ }
50
+ return slides[index];
51
+ }
52
+ }
53
+