@umituz/react-native-onboarding 2.9.0 → 3.0.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 (34) hide show
  1. package/README.md +0 -0
  2. package/package.json +4 -4
  3. package/src/domain/entities/OnboardingOptions.ts +0 -0
  4. package/src/domain/entities/OnboardingQuestion.ts +0 -0
  5. package/src/domain/entities/OnboardingSlide.ts +0 -0
  6. package/src/domain/entities/OnboardingUserData.ts +0 -0
  7. package/src/index.ts +8 -0
  8. package/src/infrastructure/hooks/__tests__/useOnboardingAnswers.test.ts +163 -0
  9. package/src/infrastructure/hooks/__tests__/useOnboardingNavigation.test.ts +121 -0
  10. package/src/infrastructure/hooks/useOnboardingAnswers.ts +2 -2
  11. package/src/infrastructure/hooks/useOnboardingNavigation.ts +0 -0
  12. package/src/infrastructure/services/OnboardingSlideService.ts +0 -0
  13. package/src/infrastructure/services/OnboardingValidationService.ts +0 -0
  14. package/src/infrastructure/storage/OnboardingStore.ts +37 -162
  15. package/src/infrastructure/storage/OnboardingStoreActions.ts +195 -0
  16. package/src/infrastructure/storage/OnboardingStoreSelectors.ts +25 -0
  17. package/src/infrastructure/storage/OnboardingStoreState.ts +22 -0
  18. package/src/infrastructure/storage/__tests__/OnboardingStore.test.ts +85 -0
  19. package/src/infrastructure/utils/gradientUtils.ts +0 -0
  20. package/src/presentation/components/OnboardingFooter.tsx +0 -0
  21. package/src/presentation/components/OnboardingHeader.tsx +8 -7
  22. package/src/presentation/components/OnboardingScreenContent.tsx +0 -0
  23. package/src/presentation/components/OnboardingSlide.tsx +16 -8
  24. package/src/presentation/components/QuestionRenderer.tsx +0 -0
  25. package/src/presentation/components/QuestionSlide.tsx +0 -0
  26. package/src/presentation/components/QuestionSlideHeader.tsx +0 -0
  27. package/src/presentation/components/questions/MultipleChoiceQuestion.tsx +20 -17
  28. package/src/presentation/components/questions/RatingQuestion.tsx +4 -3
  29. package/src/presentation/components/questions/SingleChoiceQuestion.tsx +16 -15
  30. package/src/presentation/components/questions/TextInputQuestion.tsx +12 -7
  31. package/src/presentation/hooks/__tests__/useOnboardingContainerStyle.test.ts +96 -0
  32. package/src/presentation/hooks/useOnboardingContainerStyle.ts +37 -0
  33. package/src/presentation/hooks/useOnboardingScreenState.ts +69 -46
  34. package/src/presentation/screens/OnboardingScreen.tsx +5 -0
@@ -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={styles.input}
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="rgba(255, 255, 255, 0.5)"
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: Manage onboarding screen state and handlers
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 { useOnboardingStore } from "../../infrastructure/storage/OnboardingStore";
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 insets = useSafeAreaInsets();
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.getUserData();
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
- await saveCurrentAnswer(currentSlide);
103
- if (isLastSlide) {
104
- await completeOnboarding();
105
- } else {
106
- goToNext();
107
- const nextSlide = OnboardingSlideService.getSlideAtIndex(
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 + 1,
128
+ currentIndex - 1,
110
129
  );
111
- loadAnswerForSlide(nextSlide);
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 previous slide
116
- const handlePrevious = () => {
117
- goToPrevious();
118
- const prevSlide = OnboardingSlideService.getSlideAtIndex(
119
- filteredSlides,
120
- currentIndex - 1,
121
- );
122
- loadAnswerForSlide(prevSlide);
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 = useMemo(
146
- () => [
147
- {
148
- paddingTop: insets.top,
149
- backgroundColor: useGradient ? "transparent" : tokens.colors.backgroundPrimary,
150
- },
151
- ],
152
- [insets.top, useGradient, tokens.colors.backgroundPrimary],
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,
@@ -96,6 +96,11 @@ export const OnboardingScreen: React.FC<OnboardingScreenProps> = ({
96
96
  globalUseGradient,
97
97
  });
98
98
 
99
+ // Early return if no slides - prevents rendering empty/broken screen
100
+ if (filteredSlides.length === 0) {
101
+ return null;
102
+ }
103
+
99
104
  return (
100
105
  <OnboardingScreenContent
101
106
  containerStyle={[styles.container, containerStyle]}