@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.
- package/package.json +4 -2
- package/src/exports/onboarding.ts +6 -0
- package/src/index.ts +5 -0
- 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,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
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage Helpers
|
|
3
|
+
* Single Responsibility: Common storage operations and error handling
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { storageRepository, unwrap } from "@umituz/react-native-storage";
|
|
7
|
+
import type { OnboardingUserData } from "../../../domain/entities/OnboardingUserData";
|
|
8
|
+
|
|
9
|
+
export const DEFAULT_STORAGE_KEY = "@onboarding:completed";
|
|
10
|
+
export const USER_DATA_STORAGE_KEY = "@onboarding:user_data";
|
|
11
|
+
|
|
12
|
+
export async function loadCompletionStatus(storageKey: string): Promise<boolean> {
|
|
13
|
+
const result = await storageRepository.getString(storageKey, "false");
|
|
14
|
+
return unwrap(result, "false") === "true";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function loadUserData(
|
|
18
|
+
defaultData: OnboardingUserData
|
|
19
|
+
): Promise<OnboardingUserData> {
|
|
20
|
+
const result = await storageRepository.getItem<OnboardingUserData>(
|
|
21
|
+
USER_DATA_STORAGE_KEY,
|
|
22
|
+
defaultData
|
|
23
|
+
);
|
|
24
|
+
return unwrap(result, defaultData);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function saveCompletionStatus(storageKey: string): Promise<void> {
|
|
28
|
+
const result = await storageRepository.setString(storageKey, "true");
|
|
29
|
+
if (!result.success) {
|
|
30
|
+
throw new Error("Failed to save completion status to storage");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function saveUserData(data: OnboardingUserData): Promise<void> {
|
|
35
|
+
const result = await storageRepository.setItem(USER_DATA_STORAGE_KEY, data);
|
|
36
|
+
if (!result.success) {
|
|
37
|
+
throw new Error("Failed to save user data to storage");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function removeStorageKeys(storageKey: string): Promise<void> {
|
|
42
|
+
await storageRepository.removeItem(storageKey);
|
|
43
|
+
await storageRepository.removeItem(USER_DATA_STORAGE_KEY);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function handleError(error: unknown, context: string): string {
|
|
47
|
+
const message = error instanceof Error ? error.message : `Failed to ${context}`;
|
|
48
|
+
|
|
49
|
+
if (__DEV__) {
|
|
50
|
+
console.error(`[OnboardingStore] ${context} error:`, error);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return message;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function logSuccess(message: string): void {
|
|
57
|
+
if (__DEV__) {
|
|
58
|
+
console.log(`[OnboardingStore] ${message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Array Utilities
|
|
3
|
+
* Safe array operations for onboarding components
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Ensures the value is a valid array, returns empty array if not
|
|
8
|
+
*/
|
|
9
|
+
export const ensureArray = <T>(value: T[] | undefined | null): T[] => {
|
|
10
|
+
return Array.isArray(value) ? value : [];
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Safe includes check that handles undefined/null values
|
|
15
|
+
*/
|
|
16
|
+
export const safeIncludes = <T>(array: T[] | undefined | null, item: T): boolean => {
|
|
17
|
+
return ensureArray(array).includes(item);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Safe filter that handles undefined/null values
|
|
22
|
+
*/
|
|
23
|
+
export const safeFilter = <T>(
|
|
24
|
+
array: T[] | undefined | null,
|
|
25
|
+
predicate: (item: T) => boolean,
|
|
26
|
+
): T[] => {
|
|
27
|
+
return ensureArray(array).filter(predicate);
|
|
28
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utility functions for background-related operations
|
|
5
|
+
* Follows Single Responsibility Principle
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if slide should use custom background or overlay
|
|
12
|
+
* Indicates that we are not using the default theme background.
|
|
13
|
+
*
|
|
14
|
+
* @param slide - The slide to check
|
|
15
|
+
* @param globalUseCustomBackground - Global option from OnboardingOptions
|
|
16
|
+
* @returns true if custom background/media should be used, false otherwise
|
|
17
|
+
*/
|
|
18
|
+
export function shouldUseCustomBackground(
|
|
19
|
+
slide: OnboardingSlide | undefined,
|
|
20
|
+
globalUseCustomBackground?: boolean
|
|
21
|
+
): boolean {
|
|
22
|
+
if (!slide) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// If there is background media, we always treat it as custom (needs overlay for readability)
|
|
27
|
+
if (slide.backgroundImage || slide.backgroundVideo || (slide.backgroundImages && slide.backgroundImages.length > 0)) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// If global custom background is enabled, use it if slide has color defined
|
|
32
|
+
if (globalUseCustomBackground === true) {
|
|
33
|
+
return !!slide.backgroundColor;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Otherwise, check slide's own useCustomBackground prop
|
|
37
|
+
return slide.useCustomBackground === true && !!slide.backgroundColor;
|
|
38
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collage Layout Generator
|
|
3
|
+
* Random collage with varying sizes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { SCREEN_WIDTH, SCREEN_HEIGHT } from "./screenDimensions";
|
|
7
|
+
import type { ImageLayoutItem, LayoutConfig, ImageSourceType } from "./layoutTypes";
|
|
8
|
+
|
|
9
|
+
export const generateCollageLayout = (
|
|
10
|
+
images: ImageSourceType[],
|
|
11
|
+
config: LayoutConfig = {},
|
|
12
|
+
): ImageLayoutItem[] => {
|
|
13
|
+
const { borderRadius = 8 } = config;
|
|
14
|
+
const layouts: ImageLayoutItem[] = [];
|
|
15
|
+
const minSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.15;
|
|
16
|
+
const maxSize = Math.min(SCREEN_WIDTH, SCREEN_HEIGHT) * 0.35;
|
|
17
|
+
|
|
18
|
+
images.forEach((source) => {
|
|
19
|
+
const size = minSize + Math.random() * (maxSize - minSize);
|
|
20
|
+
const position = findNonOverlappingPosition(layouts, size);
|
|
21
|
+
|
|
22
|
+
layouts.push({
|
|
23
|
+
source,
|
|
24
|
+
style: {
|
|
25
|
+
position: "absolute" as const,
|
|
26
|
+
left: position.left,
|
|
27
|
+
top: position.top,
|
|
28
|
+
width: size,
|
|
29
|
+
height: size,
|
|
30
|
+
borderRadius,
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return layouts;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const findNonOverlappingPosition = (
|
|
39
|
+
existing: ImageLayoutItem[],
|
|
40
|
+
size: number,
|
|
41
|
+
): { left: number; top: number } => {
|
|
42
|
+
const maxX = SCREEN_WIDTH - size;
|
|
43
|
+
const maxY = SCREEN_HEIGHT - size;
|
|
44
|
+
let attempts = 0;
|
|
45
|
+
let left = 0;
|
|
46
|
+
let top = 0;
|
|
47
|
+
|
|
48
|
+
while (attempts < 50) {
|
|
49
|
+
left = Math.random() * maxX;
|
|
50
|
+
top = Math.random() * maxY;
|
|
51
|
+
|
|
52
|
+
if (!hasOverlap(existing, left, top, size)) {
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
attempts++;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { left, top };
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const hasOverlap = (
|
|
62
|
+
existing: ImageLayoutItem[],
|
|
63
|
+
left: number,
|
|
64
|
+
top: number,
|
|
65
|
+
size: number,
|
|
66
|
+
): boolean => {
|
|
67
|
+
const threshold = 20;
|
|
68
|
+
|
|
69
|
+
for (const item of existing) {
|
|
70
|
+
const { style } = item;
|
|
71
|
+
if (
|
|
72
|
+
left < style.left + style.width - threshold &&
|
|
73
|
+
left + size > style.left + threshold &&
|
|
74
|
+
top < style.top + style.height - threshold &&
|
|
75
|
+
top + size > style.top + threshold
|
|
76
|
+
) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
};
|