@umituz/react-native-onboarding 2.10.0 → 3.0.1
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 -4
- package/src/index.ts +8 -0
- package/src/infrastructure/hooks/__tests__/useOnboardingAnswers.test.ts +163 -0
- package/src/infrastructure/hooks/__tests__/useOnboardingNavigation.test.ts +121 -0
- package/src/infrastructure/hooks/useOnboardingAnswers.ts +2 -2
- package/src/infrastructure/storage/OnboardingStore.ts +37 -162
- package/src/infrastructure/storage/OnboardingStoreActions.ts +195 -0
- package/src/infrastructure/storage/OnboardingStoreSelectors.ts +25 -0
- package/src/infrastructure/storage/OnboardingStoreState.ts +22 -0
- package/src/infrastructure/storage/__tests__/OnboardingStore.test.ts +85 -0
- package/src/presentation/components/OnboardingHeader.tsx +8 -7
- package/src/presentation/components/OnboardingSlide.tsx +16 -8
- package/src/presentation/components/questions/MultipleChoiceQuestion.tsx +20 -17
- package/src/presentation/components/questions/RatingQuestion.tsx +4 -3
- package/src/presentation/components/questions/SingleChoiceQuestion.tsx +16 -15
- package/src/presentation/components/questions/TextInputQuestion.tsx +12 -7
- package/src/presentation/hooks/__tests__/useOnboardingContainerStyle.test.ts +96 -0
- package/src/presentation/hooks/useOnboardingContainerStyle.ts +37 -0
- package/src/presentation/hooks/useOnboardingScreenState.ts +69 -46
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import React from "react";
|
|
8
8
|
import { View, TextInput, StyleSheet, Text } from "react-native";
|
|
9
|
+
import { useTheme } from "@umituz/react-native-design-system-theme";
|
|
9
10
|
import type { OnboardingQuestion } from "../../../domain/entities/OnboardingQuestion";
|
|
10
11
|
|
|
11
12
|
export interface TextInputQuestionProps {
|
|
@@ -19,16 +20,24 @@ export const TextInputQuestion: React.FC<TextInputQuestionProps> = ({
|
|
|
19
20
|
value = "",
|
|
20
21
|
onChange,
|
|
21
22
|
}) => {
|
|
23
|
+
const { tokens } = useTheme() as any;
|
|
22
24
|
const { validation } = question;
|
|
23
25
|
|
|
24
26
|
return (
|
|
25
27
|
<View style={styles.container}>
|
|
26
28
|
<TextInput
|
|
27
|
-
style={
|
|
29
|
+
style={[
|
|
30
|
+
styles.input,
|
|
31
|
+
{
|
|
32
|
+
backgroundColor: tokens.colors.backgroundSecondary,
|
|
33
|
+
borderColor: tokens.colors.borderSecondary,
|
|
34
|
+
color: tokens.colors.textPrimary,
|
|
35
|
+
}
|
|
36
|
+
]}
|
|
28
37
|
value={value}
|
|
29
38
|
onChangeText={onChange}
|
|
30
39
|
placeholder={question.placeholder || "Type your answer..."}
|
|
31
|
-
placeholderTextColor=
|
|
40
|
+
placeholderTextColor={tokens.colors.textSecondary}
|
|
32
41
|
maxLength={validation?.maxLength}
|
|
33
42
|
multiline={validation?.maxLength ? validation.maxLength > 100 : false}
|
|
34
43
|
numberOfLines={validation?.maxLength && validation.maxLength > 100 ? 4 : 1}
|
|
@@ -36,7 +45,7 @@ export const TextInputQuestion: React.FC<TextInputQuestionProps> = ({
|
|
|
36
45
|
autoCorrect={true}
|
|
37
46
|
/>
|
|
38
47
|
{validation?.maxLength && (
|
|
39
|
-
<Text style={styles.charCount}>
|
|
48
|
+
<Text style={[styles.charCount, { color: tokens.colors.textSecondary }]}>
|
|
40
49
|
{value.length} / {validation.maxLength}
|
|
41
50
|
</Text>
|
|
42
51
|
)}
|
|
@@ -49,18 +58,14 @@ const styles = StyleSheet.create({
|
|
|
49
58
|
width: "100%",
|
|
50
59
|
},
|
|
51
60
|
input: {
|
|
52
|
-
backgroundColor: "rgba(255, 255, 255, 0.15)",
|
|
53
61
|
borderRadius: 12,
|
|
54
62
|
padding: 16,
|
|
55
63
|
fontSize: 16,
|
|
56
|
-
color: "#FFFFFF",
|
|
57
64
|
borderWidth: 2,
|
|
58
|
-
borderColor: "rgba(255, 255, 255, 0.3)",
|
|
59
65
|
minHeight: 56,
|
|
60
66
|
},
|
|
61
67
|
charCount: {
|
|
62
68
|
fontSize: 13,
|
|
63
|
-
color: "rgba(255, 255, 255, 0.6)",
|
|
64
69
|
textAlign: "right",
|
|
65
70
|
marginTop: 8,
|
|
66
71
|
},
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OnboardingContainerStyle Hook Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { renderHook } from '@testing-library/react-native';
|
|
6
|
+
import { useOnboardingContainerStyle } from '../useOnboardingContainerStyle';
|
|
7
|
+
|
|
8
|
+
// Mock theme hook
|
|
9
|
+
jest.mock('@umituz/react-native-design-system-theme', () => ({
|
|
10
|
+
useAppDesignTokens: jest.fn(),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Mock safe area insets
|
|
14
|
+
jest.mock('react-native-safe-area-context', () => ({
|
|
15
|
+
useSafeAreaInsets: jest.fn(),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
19
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
20
|
+
|
|
21
|
+
const mockUseAppDesignTokens = useAppDesignTokens as jest.MockedFunction<typeof useAppDesignTokens>;
|
|
22
|
+
const mockUseSafeAreaInsets = useSafeAreaInsets as jest.MockedFunction<typeof useSafeAreaInsets>;
|
|
23
|
+
|
|
24
|
+
describe('useOnboardingContainerStyle', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
|
|
28
|
+
mockUseSafeAreaInsets.mockReturnValue({
|
|
29
|
+
top: 44,
|
|
30
|
+
bottom: 34,
|
|
31
|
+
left: 0,
|
|
32
|
+
right: 0,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
mockUseAppDesignTokens.mockReturnValue({
|
|
36
|
+
colors: {
|
|
37
|
+
backgroundPrimary: '#ffffff',
|
|
38
|
+
},
|
|
39
|
+
} as any);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should return container style with gradient disabled', () => {
|
|
43
|
+
const { result } = renderHook(() =>
|
|
44
|
+
useOnboardingContainerStyle({ useGradient: false })
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
expect(result.current.containerStyle).toEqual([
|
|
48
|
+
{
|
|
49
|
+
paddingTop: 44,
|
|
50
|
+
backgroundColor: '#ffffff',
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should return container style with gradient enabled', () => {
|
|
56
|
+
const { result } = renderHook(() =>
|
|
57
|
+
useOnboardingContainerStyle({ useGradient: true })
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(result.current.containerStyle).toEqual([
|
|
61
|
+
{
|
|
62
|
+
paddingTop: 44,
|
|
63
|
+
backgroundColor: 'transparent',
|
|
64
|
+
},
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should use correct top inset', () => {
|
|
69
|
+
mockUseSafeAreaInsets.mockReturnValue({
|
|
70
|
+
top: 50,
|
|
71
|
+
bottom: 34,
|
|
72
|
+
left: 0,
|
|
73
|
+
right: 0,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const { result } = renderHook(() =>
|
|
77
|
+
useOnboardingContainerStyle({ useGradient: false })
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(result.current.containerStyle[0].paddingTop).toBe(50);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should use theme background color', () => {
|
|
84
|
+
mockUseAppDesignTokens.mockReturnValue({
|
|
85
|
+
colors: {
|
|
86
|
+
backgroundPrimary: '#f0f0f0',
|
|
87
|
+
},
|
|
88
|
+
} as any);
|
|
89
|
+
|
|
90
|
+
const { result } = renderHook(() =>
|
|
91
|
+
useOnboardingContainerStyle({ useGradient: false })
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
expect(result.current.containerStyle[0].backgroundColor).toBe('#f0f0f0');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useOnboardingContainerStyle Hook
|
|
3
|
+
* Single Responsibility: Manage container styling for onboarding screen
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useMemo } from "react";
|
|
7
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
9
|
+
|
|
10
|
+
export interface UseOnboardingContainerStyleProps {
|
|
11
|
+
useGradient: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseOnboardingContainerStyleReturn {
|
|
15
|
+
containerStyle: any;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function useOnboardingContainerStyle({
|
|
19
|
+
useGradient,
|
|
20
|
+
}: UseOnboardingContainerStyleProps): UseOnboardingContainerStyleReturn {
|
|
21
|
+
const insets = useSafeAreaInsets();
|
|
22
|
+
const tokens = useAppDesignTokens();
|
|
23
|
+
|
|
24
|
+
const containerStyle = useMemo(
|
|
25
|
+
() => [
|
|
26
|
+
{
|
|
27
|
+
paddingTop: insets.top,
|
|
28
|
+
backgroundColor: useGradient ? "transparent" : tokens.colors.backgroundPrimary,
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
[insets.top, useGradient, tokens.colors.backgroundPrimary],
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
containerStyle,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* useOnboardingScreenState Hook
|
|
3
|
-
* Single Responsibility:
|
|
3
|
+
* Single Responsibility: Coordinate onboarding screen state and handlers
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { useMemo } from "react";
|
|
7
|
-
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
8
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
|
|
6
|
+
import { useMemo, useCallback, useEffect } from "react";
|
|
9
7
|
import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
|
|
10
|
-
import {
|
|
8
|
+
import { useOnboarding } from "../../infrastructure/storage/OnboardingStore";
|
|
11
9
|
import { useOnboardingNavigation } from "../../infrastructure/hooks/useOnboardingNavigation";
|
|
12
10
|
import { useOnboardingAnswers } from "../../infrastructure/hooks/useOnboardingAnswers";
|
|
11
|
+
import { useOnboardingContainerStyle } from "./useOnboardingContainerStyle";
|
|
13
12
|
import { OnboardingSlideService } from "../../infrastructure/services/OnboardingSlideService";
|
|
14
13
|
import { OnboardingValidationService } from "../../infrastructure/services/OnboardingValidationService";
|
|
15
14
|
import { shouldUseGradient } from "../../infrastructure/utils/gradientUtils";
|
|
@@ -45,18 +44,16 @@ export function useOnboardingScreenState({
|
|
|
45
44
|
onSkip,
|
|
46
45
|
globalUseGradient = false,
|
|
47
46
|
}: UseOnboardingScreenStateProps): UseOnboardingScreenStateReturn {
|
|
48
|
-
const
|
|
49
|
-
const tokens = useAppDesignTokens();
|
|
50
|
-
const onboardingStore = useOnboardingStore();
|
|
47
|
+
const onboardingStore = useOnboarding();
|
|
51
48
|
|
|
52
49
|
// Filter slides using service
|
|
53
50
|
const filteredSlides = useMemo(() => {
|
|
54
51
|
if (!slides || !Array.isArray(slides) || slides.length === 0) {
|
|
55
52
|
return [];
|
|
56
53
|
}
|
|
57
|
-
const userData = onboardingStore.
|
|
54
|
+
const userData = onboardingStore.userData;
|
|
58
55
|
return OnboardingSlideService.filterSlides(slides, userData);
|
|
59
|
-
}, [slides, onboardingStore]);
|
|
56
|
+
}, [slides, onboardingStore.userData]);
|
|
60
57
|
|
|
61
58
|
// Navigation hook
|
|
62
59
|
const {
|
|
@@ -97,35 +94,59 @@ export function useOnboardingScreenState({
|
|
|
97
94
|
saveCurrentAnswer,
|
|
98
95
|
} = useOnboardingAnswers(currentSlide);
|
|
99
96
|
|
|
100
|
-
// Handle next slide
|
|
101
|
-
const handleNext = async () => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
97
|
+
// Handle next slide with useCallback for performance
|
|
98
|
+
const handleNext = useCallback(async () => {
|
|
99
|
+
if (!currentSlide) return;
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
await saveCurrentAnswer(currentSlide);
|
|
103
|
+
if (isLastSlide) {
|
|
104
|
+
await completeOnboarding();
|
|
105
|
+
} else {
|
|
106
|
+
goToNext();
|
|
107
|
+
const nextSlide = OnboardingSlideService.getSlideAtIndex(
|
|
108
|
+
filteredSlides,
|
|
109
|
+
currentIndex + 1,
|
|
110
|
+
);
|
|
111
|
+
if (nextSlide) {
|
|
112
|
+
loadAnswerForSlide(nextSlide);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (__DEV__) {
|
|
117
|
+
console.error('[useOnboardingScreenState] Error in handleNext:', error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}, [currentSlide, isLastSlide, saveCurrentAnswer, completeOnboarding, goToNext, filteredSlides, currentIndex, loadAnswerForSlide]);
|
|
121
|
+
|
|
122
|
+
// Handle previous slide with useCallback for performance
|
|
123
|
+
const handlePrevious = useCallback(() => {
|
|
124
|
+
try {
|
|
125
|
+
goToPrevious();
|
|
126
|
+
const prevSlide = OnboardingSlideService.getSlideAtIndex(
|
|
108
127
|
filteredSlides,
|
|
109
|
-
currentIndex
|
|
128
|
+
currentIndex - 1,
|
|
110
129
|
);
|
|
111
|
-
|
|
130
|
+
if (prevSlide) {
|
|
131
|
+
loadAnswerForSlide(prevSlide);
|
|
132
|
+
}
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (__DEV__) {
|
|
135
|
+
console.error('[useOnboardingScreenState] Error in handlePrevious:', error);
|
|
136
|
+
}
|
|
112
137
|
}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Handle
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Handle skip
|
|
126
|
-
const handleSkip = async () => {
|
|
127
|
-
await skipOnboarding();
|
|
128
|
-
};
|
|
138
|
+
}, [goToPrevious, filteredSlides, currentIndex, loadAnswerForSlide]);
|
|
139
|
+
|
|
140
|
+
// Handle skip with useCallback for performance
|
|
141
|
+
const handleSkip = useCallback(async () => {
|
|
142
|
+
try {
|
|
143
|
+
await skipOnboarding();
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (__DEV__) {
|
|
146
|
+
console.error('[useOnboardingScreenState] Error in handleSkip:', error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}, [skipOnboarding]);
|
|
129
150
|
|
|
130
151
|
// Check if gradient should be used
|
|
131
152
|
const useGradient = shouldUseGradient(currentSlide, globalUseGradient);
|
|
@@ -141,16 +162,18 @@ export function useOnboardingScreenState({
|
|
|
141
162
|
);
|
|
142
163
|
}, [currentSlide, currentAnswer]);
|
|
143
164
|
|
|
144
|
-
// Container style
|
|
145
|
-
const containerStyle =
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
165
|
+
// Container style using dedicated hook
|
|
166
|
+
const { containerStyle } = useOnboardingContainerStyle({ useGradient });
|
|
167
|
+
|
|
168
|
+
// Cleanup effect to prevent memory leaks
|
|
169
|
+
useEffect(() => {
|
|
170
|
+
return () => {
|
|
171
|
+
// Cleanup any pending operations or subscriptions
|
|
172
|
+
if (__DEV__) {
|
|
173
|
+
console.log('[useOnboardingScreenState] Cleanup completed');
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
}, []);
|
|
154
177
|
|
|
155
178
|
return {
|
|
156
179
|
filteredSlides,
|