@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,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
+
@@ -0,0 +1,127 @@
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
+ * ValidationManager
12
+ */
13
+ export class ValidationManager {
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 "rating":
41
+ return this.validateNumeric(answer, validation);
42
+ default:
43
+ break;
44
+ }
45
+
46
+ // Custom validator
47
+ if (validation.customValidator) {
48
+ const customResult = validation.customValidator(answer);
49
+ return customResult === true;
50
+ }
51
+
52
+ return true;
53
+ }
54
+
55
+ /**
56
+ * Validate multiple choice answer
57
+ */
58
+ private static validateMultipleChoice(
59
+ answer: any,
60
+ validation: OnboardingQuestion["validation"],
61
+ ): boolean {
62
+ if (!validation) return true;
63
+
64
+ if (validation.minSelections) {
65
+ if (!answer || !Array.isArray(answer) || answer.length < validation.minSelections) {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ if (validation.maxSelections) {
71
+ if (Array.isArray(answer) && answer.length > validation.maxSelections) {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ return true;
77
+ }
78
+
79
+ /**
80
+ * Validate text input answer
81
+ */
82
+ private static validateTextInput(
83
+ answer: any,
84
+ validation: OnboardingQuestion["validation"],
85
+ ): boolean {
86
+ if (!validation) return true;
87
+
88
+ if (typeof answer !== "string") {
89
+ return false;
90
+ }
91
+
92
+ if (validation.minLength && answer.length < validation.minLength) {
93
+ return false;
94
+ }
95
+
96
+ if (validation.maxLength && answer.length > validation.maxLength) {
97
+ return false;
98
+ }
99
+
100
+ return true;
101
+ }
102
+
103
+ /**
104
+ * Validate numeric answer (rating)
105
+ */
106
+ private static validateNumeric(
107
+ answer: any,
108
+ validation: OnboardingQuestion["validation"],
109
+ ): boolean {
110
+ if (!validation) return true;
111
+
112
+ if (typeof answer !== "number") {
113
+ return false;
114
+ }
115
+
116
+ if (validation.min !== undefined && answer < validation.min) {
117
+ return false;
118
+ }
119
+
120
+ if (validation.max !== undefined && answer > validation.max) {
121
+ return false;
122
+ }
123
+
124
+ return true;
125
+ }
126
+ }
127
+
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Onboarding Store
3
+ *
4
+ * Zustand store for managing onboarding completion state
5
+ * Uses @umituz/react-native-storage for persistence
6
+ */
7
+
8
+ import { useMemo } from "react";
9
+ import { createStore } from "@umituz/react-native-storage";
10
+ import type { OnboardingStoreState } from "./OnboardingStoreState";
11
+ import { initialOnboardingState } from "./OnboardingStoreState";
12
+ import { createOnboardingStoreActions } from "./OnboardingStoreActions";
13
+ import type { OnboardingUserData } from "../../domain/entities/OnboardingUserData";
14
+ import { createOnboardingStoreSelectors } from "./OnboardingStoreSelectors";
15
+
16
+ interface OnboardingActions {
17
+ // Simple actions
18
+ setCurrentStep: (step: number) => void;
19
+ setLoading: (loading: boolean) => void;
20
+ setError: (error: string | null) => void;
21
+ setState: (state: Partial<OnboardingStoreState>) => void;
22
+ getState: () => OnboardingStoreState;
23
+ // Async actions for initialization (match OnboardingStoreActions signatures)
24
+ initialize: (storageKey?: string) => Promise<void>;
25
+ complete: (storageKey?: string) => Promise<void>;
26
+ skip: (storageKey?: string) => Promise<void>;
27
+ reset: (storageKey?: string) => Promise<void>;
28
+ saveAnswer: (questionId: string, answer: unknown) => Promise<void>;
29
+ setUserData: (data: OnboardingUserData) => Promise<void>;
30
+ }
31
+
32
+ export const useOnboardingStore = createStore<
33
+ OnboardingStoreState,
34
+ OnboardingActions
35
+ >({
36
+ name: "onboarding-store",
37
+ initialState: initialOnboardingState,
38
+ persist: false,
39
+ actions: (
40
+ set: (state: Partial<OnboardingStoreState>) => void,
41
+ get: () => OnboardingStoreState
42
+ ): OnboardingActions => {
43
+ const actions = createOnboardingStoreActions(set, get);
44
+
45
+ return {
46
+ setCurrentStep: (step: number) => set({ currentStep: step }),
47
+ setLoading: (loading: boolean) => set({ loading }),
48
+ setError: (error: string | null) => set({ error }),
49
+ setState: set,
50
+ getState: get,
51
+
52
+ // Async actions from actions module
53
+ initialize: actions.initialize,
54
+ complete: actions.complete,
55
+ skip: actions.skip,
56
+ reset: actions.reset,
57
+ saveAnswer: actions.saveAnswer,
58
+ setUserData: actions.setUserData,
59
+ };
60
+ },
61
+ });
62
+
63
+ /**
64
+ * Hook for accessing onboarding state
65
+ * Memoized to prevent unnecessary re-renders in consumer components
66
+ */
67
+ export const useOnboarding = () => {
68
+ const store = useOnboardingStore();
69
+ const setState = store.setState;
70
+ const getState = store.getState;
71
+
72
+ const actions = useMemo(() => createOnboardingStoreActions(setState, getState), [setState, getState]);
73
+ const selectors = useMemo(() => createOnboardingStoreSelectors(getState), [getState]);
74
+
75
+ return useMemo(() => ({
76
+ // State
77
+ isOnboardingComplete: store.isOnboardingComplete,
78
+ currentStep: store.currentStep,
79
+ loading: store.loading,
80
+ error: store.error,
81
+ userData: store.userData,
82
+
83
+ // Actions
84
+ initialize: actions.initialize,
85
+ complete: actions.complete,
86
+ skip: actions.skip,
87
+ setCurrentStep: store.setCurrentStep,
88
+ reset: actions.reset,
89
+ setLoading: store.setLoading,
90
+ setError: store.setError,
91
+ saveAnswer: actions.saveAnswer,
92
+ setUserData: actions.setUserData,
93
+
94
+ // Selectors
95
+ getAnswer: selectors.getAnswer,
96
+ getUserData: selectors.getUserData,
97
+ }), [store, actions, selectors]);
98
+ };
99
+