@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,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
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Store Actions
|
|
3
|
+
* Single Responsibility: Async store actions interface and factory
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingUserData } from "../../domain/entities/OnboardingUserData";
|
|
7
|
+
import type { OnboardingStoreState } from "./OnboardingStoreState";
|
|
8
|
+
import {
|
|
9
|
+
initializeAction,
|
|
10
|
+
completeAction,
|
|
11
|
+
skipAction,
|
|
12
|
+
resetAction,
|
|
13
|
+
saveAnswerAction,
|
|
14
|
+
setUserDataAction,
|
|
15
|
+
DEFAULT_STORAGE_KEY,
|
|
16
|
+
} from "./actions";
|
|
17
|
+
|
|
18
|
+
export interface OnboardingStoreActions {
|
|
19
|
+
initialize: (storageKey?: string) => Promise<void>;
|
|
20
|
+
complete: (storageKey?: string) => Promise<void>;
|
|
21
|
+
skip: (storageKey?: string) => Promise<void>;
|
|
22
|
+
reset: (storageKey?: string) => Promise<void>;
|
|
23
|
+
saveAnswer: (questionId: string, answer: unknown) => Promise<void>;
|
|
24
|
+
setUserData: (data: OnboardingUserData) => Promise<void>;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createOnboardingStoreActions(
|
|
28
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
29
|
+
get: () => OnboardingStoreState
|
|
30
|
+
): OnboardingStoreActions {
|
|
31
|
+
return {
|
|
32
|
+
initialize: (storageKey = DEFAULT_STORAGE_KEY) =>
|
|
33
|
+
initializeAction(set, storageKey),
|
|
34
|
+
|
|
35
|
+
complete: (storageKey = DEFAULT_STORAGE_KEY) =>
|
|
36
|
+
completeAction(set, get, storageKey),
|
|
37
|
+
|
|
38
|
+
skip: (storageKey = DEFAULT_STORAGE_KEY) =>
|
|
39
|
+
skipAction(set, get, storageKey),
|
|
40
|
+
|
|
41
|
+
reset: (storageKey = DEFAULT_STORAGE_KEY) =>
|
|
42
|
+
resetAction(set, storageKey),
|
|
43
|
+
|
|
44
|
+
saveAnswer: (questionId: string, answer: unknown) =>
|
|
45
|
+
saveAnswerAction(set, get, questionId, answer),
|
|
46
|
+
|
|
47
|
+
setUserData: (data: OnboardingUserData) =>
|
|
48
|
+
setUserDataAction(set, data),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Store Selectors
|
|
3
|
+
* Single Responsibility: Store state selectors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingStoreState } from "./OnboardingStoreState";
|
|
7
|
+
|
|
8
|
+
export interface OnboardingStoreSelectors {
|
|
9
|
+
getAnswer: (questionId: string) => any;
|
|
10
|
+
getUserData: () => OnboardingStoreState['userData'];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createOnboardingStoreSelectors(
|
|
14
|
+
get: () => OnboardingStoreState
|
|
15
|
+
): OnboardingStoreSelectors {
|
|
16
|
+
return {
|
|
17
|
+
getAnswer: (questionId: string) => {
|
|
18
|
+
return get().userData.answers[questionId];
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
getUserData: () => {
|
|
22
|
+
return get().userData;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding Store State
|
|
3
|
+
* Single Responsibility: Store state interface and initial state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingUserData } from "../../domain/entities/OnboardingUserData";
|
|
7
|
+
|
|
8
|
+
export interface OnboardingStoreState {
|
|
9
|
+
isOnboardingComplete: boolean;
|
|
10
|
+
currentStep: number;
|
|
11
|
+
loading: boolean;
|
|
12
|
+
error: string | null;
|
|
13
|
+
userData: OnboardingUserData;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const initialOnboardingState: OnboardingStoreState = {
|
|
17
|
+
isOnboardingComplete: false,
|
|
18
|
+
currentStep: 0,
|
|
19
|
+
loading: true,
|
|
20
|
+
error: null,
|
|
21
|
+
userData: { answers: {} },
|
|
22
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OnboardingStore Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook, act } from '@testing-library/react-native';
|
|
6
|
+
import { useOnboardingStore, useOnboarding } from '../OnboardingStore';
|
|
7
|
+
|
|
8
|
+
// Mock storage repository
|
|
9
|
+
jest.mock('@umituz/react-native-storage', () => ({
|
|
10
|
+
storageRepository: {
|
|
11
|
+
getString: jest.fn(),
|
|
12
|
+
setString: jest.fn(),
|
|
13
|
+
getObject: jest.fn(),
|
|
14
|
+
setObject: jest.fn(),
|
|
15
|
+
removeItem: jest.fn(),
|
|
16
|
+
},
|
|
17
|
+
StorageKey: {
|
|
18
|
+
ONBOARDING_COMPLETED: '@onboarding_completed',
|
|
19
|
+
},
|
|
20
|
+
unwrap: jest.fn((result, defaultValue) => result.success ? result.data : defaultValue),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
describe('OnboardingStore', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
jest.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('useOnboardingStore', () => {
|
|
29
|
+
it('should have initial state', () => {
|
|
30
|
+
const { result } = renderHook(() => useOnboardingStore());
|
|
31
|
+
|
|
32
|
+
expect(result.current.isOnboardingComplete).toBe(false);
|
|
33
|
+
expect(result.current.currentStep).toBe(0);
|
|
34
|
+
expect(result.current.loading).toBe(true);
|
|
35
|
+
expect(result.current.error).toBe(null);
|
|
36
|
+
expect(result.current.userData).toEqual({ answers: {} });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should set current step', () => {
|
|
40
|
+
const { result } = renderHook(() => useOnboardingStore());
|
|
41
|
+
|
|
42
|
+
act(() => {
|
|
43
|
+
result.current.setCurrentStep(5);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
expect(result.current.currentStep).toBe(5);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should set loading state', () => {
|
|
50
|
+
const { result } = renderHook(() => useOnboardingStore());
|
|
51
|
+
|
|
52
|
+
act(() => {
|
|
53
|
+
result.current.setLoading(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(result.current.loading).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should set error state', () => {
|
|
60
|
+
const { result } = renderHook(() => useOnboardingStore());
|
|
61
|
+
const errorMessage = 'Test error';
|
|
62
|
+
|
|
63
|
+
act(() => {
|
|
64
|
+
result.current.setError(errorMessage);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(result.current.error).toBe(errorMessage);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('useOnboarding', () => {
|
|
72
|
+
it('should return all store properties and methods', () => {
|
|
73
|
+
const { result } = renderHook(() => useOnboarding());
|
|
74
|
+
|
|
75
|
+
expect(typeof result.current.initialize).toBe('function');
|
|
76
|
+
expect(typeof result.current.complete).toBe('function');
|
|
77
|
+
expect(typeof result.current.skip).toBe('function');
|
|
78
|
+
expect(typeof result.current.reset).toBe('function');
|
|
79
|
+
expect(typeof result.current.saveAnswer).toBe('function');
|
|
80
|
+
expect(typeof result.current.getAnswer).toBe('function');
|
|
81
|
+
expect(typeof result.current.getUserData).toBe('function');
|
|
82
|
+
expect(typeof result.current.setUserData).toBe('function');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Answer Actions
|
|
3
|
+
* Single Responsibility: Save and update user answers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { storageRepository } from "@umituz/react-native-storage";
|
|
7
|
+
import type { OnboardingUserData } from "../../../domain/entities/OnboardingUserData";
|
|
8
|
+
import type { OnboardingStoreState } from "../OnboardingStoreState";
|
|
9
|
+
import { USER_DATA_STORAGE_KEY, handleError, logSuccess } from "./storageHelpers";
|
|
10
|
+
|
|
11
|
+
export async function saveAnswerAction(
|
|
12
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
13
|
+
get: () => OnboardingStoreState,
|
|
14
|
+
questionId: string,
|
|
15
|
+
answer: unknown
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
const userData: OnboardingUserData = {
|
|
19
|
+
...get().userData,
|
|
20
|
+
answers: {
|
|
21
|
+
...get().userData.answers,
|
|
22
|
+
[questionId]: answer,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
await storageRepository.setItem(USER_DATA_STORAGE_KEY, userData);
|
|
27
|
+
set({ userData });
|
|
28
|
+
|
|
29
|
+
logSuccess(`Answer saved for question: ${questionId}`);
|
|
30
|
+
} catch (error) {
|
|
31
|
+
handleError(error, "save answer");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function setUserDataAction(
|
|
36
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
37
|
+
data: OnboardingUserData
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
await storageRepository.setItem(USER_DATA_STORAGE_KEY, data);
|
|
41
|
+
set({ userData: data });
|
|
42
|
+
|
|
43
|
+
logSuccess("User data updated successfully");
|
|
44
|
+
} catch (error) {
|
|
45
|
+
handleError(error, "set user data");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Complete Action
|
|
3
|
+
* Single Responsibility: Mark onboarding as completed
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingUserData } from "../../../domain/entities/OnboardingUserData";
|
|
7
|
+
import type { OnboardingStoreState } from "../OnboardingStoreState";
|
|
8
|
+
import {
|
|
9
|
+
saveCompletionStatus,
|
|
10
|
+
saveUserData,
|
|
11
|
+
handleError,
|
|
12
|
+
logSuccess,
|
|
13
|
+
} from "./storageHelpers";
|
|
14
|
+
|
|
15
|
+
export async function completeAction(
|
|
16
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
17
|
+
get: () => OnboardingStoreState,
|
|
18
|
+
storageKey: string
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
try {
|
|
21
|
+
set({ loading: true, error: null });
|
|
22
|
+
|
|
23
|
+
await saveCompletionStatus(storageKey);
|
|
24
|
+
|
|
25
|
+
const userData: OnboardingUserData = {
|
|
26
|
+
...get().userData,
|
|
27
|
+
completedAt: new Date().toISOString(),
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
await saveUserData(userData);
|
|
31
|
+
|
|
32
|
+
set({
|
|
33
|
+
isOnboardingComplete: true,
|
|
34
|
+
userData,
|
|
35
|
+
loading: false,
|
|
36
|
+
error: null,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
logSuccess("Onboarding completed and persisted successfully");
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const errorMessage = handleError(error, "complete onboarding");
|
|
42
|
+
set({ loading: false, error: errorMessage });
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actions Index
|
|
3
|
+
* Single Responsibility: Export all action functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
loadCompletionStatus,
|
|
8
|
+
loadUserData,
|
|
9
|
+
saveCompletionStatus,
|
|
10
|
+
saveUserData,
|
|
11
|
+
removeStorageKeys,
|
|
12
|
+
handleError,
|
|
13
|
+
logSuccess,
|
|
14
|
+
DEFAULT_STORAGE_KEY,
|
|
15
|
+
USER_DATA_STORAGE_KEY,
|
|
16
|
+
} from "./storageHelpers";
|
|
17
|
+
|
|
18
|
+
export { initializeAction } from "./initializeAction";
|
|
19
|
+
export { completeAction } from "./completeAction";
|
|
20
|
+
export { skipAction } from "./skipAction";
|
|
21
|
+
export { resetAction } from "./resetAction";
|
|
22
|
+
export { saveAnswerAction, setUserDataAction } from "./answerActions";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initialize Action
|
|
3
|
+
* Single Responsibility: Load initial onboarding state from storage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingUserData } from "../../../domain/entities/OnboardingUserData";
|
|
7
|
+
import type { OnboardingStoreState } from "../OnboardingStoreState";
|
|
8
|
+
import {
|
|
9
|
+
loadCompletionStatus,
|
|
10
|
+
loadUserData,
|
|
11
|
+
handleError,
|
|
12
|
+
logSuccess,
|
|
13
|
+
} from "./storageHelpers";
|
|
14
|
+
|
|
15
|
+
export async function initializeAction(
|
|
16
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
17
|
+
storageKey: string
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
try {
|
|
20
|
+
set({ loading: true, error: null });
|
|
21
|
+
|
|
22
|
+
const isComplete = await loadCompletionStatus(storageKey);
|
|
23
|
+
const defaultData: OnboardingUserData = { answers: {} };
|
|
24
|
+
const userData = await loadUserData(defaultData);
|
|
25
|
+
|
|
26
|
+
set({
|
|
27
|
+
isOnboardingComplete: isComplete,
|
|
28
|
+
userData,
|
|
29
|
+
loading: false,
|
|
30
|
+
error: null,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
logSuccess(`Initialized with completion status: ${isComplete}`);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
set({
|
|
36
|
+
loading: false,
|
|
37
|
+
error: handleError(error, "initialize onboarding"),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reset Action
|
|
3
|
+
* Single Responsibility: Reset onboarding state
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingStoreState } from "../OnboardingStoreState";
|
|
7
|
+
import {
|
|
8
|
+
removeStorageKeys,
|
|
9
|
+
handleError,
|
|
10
|
+
logSuccess,
|
|
11
|
+
} from "./storageHelpers";
|
|
12
|
+
|
|
13
|
+
export async function resetAction(
|
|
14
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
15
|
+
storageKey: string
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
try {
|
|
18
|
+
set({ loading: true, error: null });
|
|
19
|
+
|
|
20
|
+
await removeStorageKeys(storageKey);
|
|
21
|
+
|
|
22
|
+
set({
|
|
23
|
+
isOnboardingComplete: false,
|
|
24
|
+
currentStep: 0,
|
|
25
|
+
userData: { answers: {} },
|
|
26
|
+
loading: false,
|
|
27
|
+
error: null,
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
logSuccess("Onboarding reset successfully");
|
|
31
|
+
} catch (error) {
|
|
32
|
+
set({
|
|
33
|
+
loading: false,
|
|
34
|
+
error: handleError(error, "reset onboarding"),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skip Action
|
|
3
|
+
* Single Responsibility: Mark onboarding as skipped
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { OnboardingUserData } from "../../../domain/entities/OnboardingUserData";
|
|
7
|
+
import type { OnboardingStoreState } from "../OnboardingStoreState";
|
|
8
|
+
import {
|
|
9
|
+
saveCompletionStatus,
|
|
10
|
+
saveUserData,
|
|
11
|
+
handleError,
|
|
12
|
+
logSuccess,
|
|
13
|
+
} from "./storageHelpers";
|
|
14
|
+
|
|
15
|
+
export async function skipAction(
|
|
16
|
+
set: (state: Partial<OnboardingStoreState>) => void,
|
|
17
|
+
get: () => OnboardingStoreState,
|
|
18
|
+
storageKey: string
|
|
19
|
+
): Promise<void> {
|
|
20
|
+
try {
|
|
21
|
+
set({ loading: true, error: null });
|
|
22
|
+
|
|
23
|
+
await saveCompletionStatus(storageKey);
|
|
24
|
+
|
|
25
|
+
const userData: OnboardingUserData = {
|
|
26
|
+
...get().userData,
|
|
27
|
+
skipped: true,
|
|
28
|
+
completedAt: new Date().toISOString(),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
await saveUserData(userData);
|
|
32
|
+
|
|
33
|
+
set({
|
|
34
|
+
isOnboardingComplete: true,
|
|
35
|
+
userData,
|
|
36
|
+
loading: false,
|
|
37
|
+
error: null,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
logSuccess("Onboarding skipped and persisted successfully");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
const errorMessage = handleError(error, "skip onboarding");
|
|
43
|
+
set({ loading: false, error: errorMessage });
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|