@umituz/react-native-onboarding 3.5.7 → 3.6.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-onboarding",
3
- "version": "3.5.7",
3
+ "version": "3.6.1",
4
4
  "description": "Advanced onboarding flow for React Native apps with personalization questions, theme-aware colors, animations, and customizable slides. SOLID, DRY, KISS principles applied.",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -37,8 +37,7 @@
37
37
  "expo-video": ">=1.0.0",
38
38
  "react": ">=18.2.0",
39
39
  "react-native": ">=0.74.0",
40
- "react-native-safe-area-context": ">=4.0.0",
41
- "zustand": "^4.5.0 || ^5.0.0"
40
+ "react-native-safe-area-context": ">=4.0.0"
42
41
  },
43
42
  "devDependencies": {
44
43
  "@expo/vector-icons": "^14.0.0",
@@ -61,8 +60,7 @@
61
60
  "react": "19.1.0",
62
61
  "react-native": "0.81.5",
63
62
  "react-native-safe-area-context": "^5.6.0",
64
- "typescript": "~5.9.2",
65
- "zustand": "^5.0.0"
63
+ "typescript": "~5.9.2"
66
64
  },
67
65
  "publishConfig": {
68
66
  "access": "public"
@@ -5,13 +5,14 @@
5
5
  * Uses @umituz/react-native-storage for persistence
6
6
  */
7
7
 
8
- import { create, StoreApi } from "zustand";
8
+ import { useMemo } from "react";
9
+ import { createStore } from "@umituz/react-native-storage";
9
10
  import type { OnboardingStoreState } from "./OnboardingStoreState";
10
11
  import { initialOnboardingState } from "./OnboardingStoreState";
11
12
  import { createOnboardingStoreActions } from "./OnboardingStoreActions";
12
13
  import { createOnboardingStoreSelectors } from "./OnboardingStoreSelectors";
13
14
 
14
- interface OnboardingStore extends OnboardingStoreState {
15
+ interface OnboardingActions {
15
16
  // Simple actions
16
17
  setCurrentStep: (step: number) => void;
17
18
  setLoading: (loading: boolean) => void;
@@ -23,43 +24,48 @@ interface OnboardingStore extends OnboardingStoreState {
23
24
  complete: (storageKey?: string) => Promise<void>;
24
25
  skip: (storageKey?: string) => Promise<void>;
25
26
  reset: (storageKey?: string) => Promise<void>;
26
- saveAnswer: (questionId: string, answer: any) => Promise<void>;
27
- setUserData: (data: any) => Promise<void>;
27
+ saveAnswer: (questionId: string, answer: unknown) => Promise<void>;
28
+ setUserData: (data: unknown) => Promise<void>;
28
29
  }
29
30
 
30
- export const useOnboardingStore = create<OnboardingStore>((set: StoreApi<OnboardingStore>['setState'], get: StoreApi<OnboardingStore>['getState']) => {
31
- const actions = createOnboardingStoreActions(set, get);
31
+ export const useOnboardingStore = createStore<OnboardingStoreState, OnboardingActions>({
32
+ name: "onboarding-store",
33
+ initialState: initialOnboardingState,
34
+ persist: false,
35
+ actions: (set, get) => {
36
+ const actions = createOnboardingStoreActions(set, get);
32
37
 
33
- return {
34
- ...initialOnboardingState,
38
+ return {
39
+ setCurrentStep: (step) => set({ currentStep: step }),
40
+ setLoading: (loading) => set({ loading }),
41
+ setError: (error) => set({ error }),
42
+ setState: set,
43
+ getState: get,
35
44
 
36
- setCurrentStep: (step) => set({ currentStep: step }),
37
- setLoading: (loading) => set({ loading }),
38
- setError: (error) => set({ error }),
39
- setState: set,
40
- getState: get,
41
-
42
- // Async actions from actions module
43
- initialize: actions.initialize,
44
- complete: actions.complete,
45
- skip: actions.skip,
46
- reset: actions.reset,
47
- saveAnswer: actions.saveAnswer,
48
- setUserData: actions.setUserData,
49
- };
45
+ // Async actions from actions module
46
+ initialize: actions.initialize,
47
+ complete: actions.complete,
48
+ skip: actions.skip,
49
+ reset: actions.reset,
50
+ saveAnswer: actions.saveAnswer,
51
+ setUserData: actions.setUserData,
52
+ };
53
+ },
50
54
  });
51
55
 
52
56
  /**
53
57
  * Hook for accessing onboarding state
58
+ * Memoized to prevent unnecessary re-renders in consumer components
54
59
  */
55
60
  export const useOnboarding = () => {
56
61
  const store = useOnboardingStore();
57
62
  const setState = store.setState;
58
- const getState = () => store;
59
- const actions = createOnboardingStoreActions(setState, getState);
60
- const selectors = createOnboardingStoreSelectors(getState);
63
+ const getState = store.getState;
64
+
65
+ const actions = useMemo(() => createOnboardingStoreActions(setState, getState), [setState, getState]);
66
+ const selectors = useMemo(() => createOnboardingStoreSelectors(getState), [getState]);
61
67
 
62
- return {
68
+ return useMemo(() => ({
63
69
  // State
64
70
  isOnboardingComplete: store.isOnboardingComplete,
65
71
  currentStep: store.currentStep,
@@ -81,6 +87,6 @@ export const useOnboarding = () => {
81
87
  // Selectors
82
88
  getAnswer: selectors.getAnswer,
83
89
  getUserData: selectors.getUserData,
84
- };
90
+ }), [store, actions, selectors]);
85
91
  };
86
92
 
@@ -11,6 +11,7 @@ import { QuestionSlideHeader } from "./QuestionSlideHeader";
11
11
  import { QuestionRenderer } from "./QuestionRenderer";
12
12
  import { BaseSlide } from "./BaseSlide";
13
13
  import { useOnboardingTheme } from "../providers/OnboardingThemeProvider";
14
+ import { useLocalization } from "@umituz/react-native-localization";
14
15
 
15
16
  export interface QuestionSlideProps {
16
17
  slide: OnboardingSlide;
@@ -26,6 +27,7 @@ export const QuestionSlide = ({
26
27
  variant: _variant = "default",
27
28
  }: QuestionSlideProps) => {
28
29
  const { colors } = useOnboardingTheme();
30
+ const { t } = useLocalization();
29
31
  const { question } = slide;
30
32
 
31
33
  if (!question) return null;
@@ -43,7 +45,7 @@ export const QuestionSlide = ({
43
45
  type="labelSmall"
44
46
  style={[styles.requiredHint, { color: colors.errorColor }]}
45
47
  >
46
- * This field is required
48
+ {t("onboarding.fieldRequired")}
47
49
  </AtomicText>
48
50
  )}
49
51
  </BaseSlide>
@@ -29,7 +29,6 @@ export const MultipleChoiceQuestion = ({
29
29
  };
30
30
  const renderOption = (option: QuestionOption) => {
31
31
  const isSelected = value.includes(option.id);
32
- const textColor = isSelected ? colors.textColor : colors.subTextColor;
33
32
  const isEmoji = option.iconType === 'emoji';
34
33
 
35
34
  return (
@@ -40,21 +39,29 @@ export const MultipleChoiceQuestion = ({
40
39
  {
41
40
  backgroundColor: isSelected ? colors.iconBg : colors.featureItemBg,
42
41
  borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
42
+ borderWidth: isSelected ? 2 : 1,
43
43
  }
44
44
  ]}
45
45
  onPress={() => handleToggle(option.id)}
46
- activeOpacity={0.7}
46
+ activeOpacity={0.8}
47
47
  >
48
48
  {option.icon && (
49
- <View style={styles.optionIcon}>
49
+ <View style={[
50
+ styles.optionIcon,
51
+ { backgroundColor: isSelected ? colors.iconColor : colors.featureItemBg }
52
+ ]}>
50
53
  {isEmoji ? (
51
54
  <AtomicText style={{ fontSize: 24 }}>{option.icon}</AtomicText>
52
55
  ) : (
53
- <AtomicIcon name={option.icon as any} customSize={24} customColor={textColor} />
56
+ <AtomicIcon
57
+ name={option.icon as any}
58
+ customSize={20}
59
+ customColor={isSelected ? colors.buttonTextColor : colors.subTextColor}
60
+ />
54
61
  )}
55
62
  </View>
56
63
  )}
57
- <AtomicText type="bodyLarge" style={[styles.optionLabel, { color: textColor, fontWeight: isSelected ? '700' : '500' }]}>
64
+ <AtomicText type="bodyLarge" style={[styles.optionLabel, { color: isSelected ? colors.textColor : colors.subTextColor, fontWeight: isSelected ? '700' : '500' }]}>
58
65
  {option.label}
59
66
  </AtomicText>
60
67
  <View style={[
@@ -93,15 +100,21 @@ const styles = StyleSheet.create({
93
100
  option: {
94
101
  flexDirection: "row",
95
102
  alignItems: "center",
96
- borderRadius: 16,
103
+ borderRadius: 20,
97
104
  padding: 16,
98
- borderWidth: 2,
105
+ marginBottom: 8,
99
106
  },
100
107
  optionIcon: {
101
- marginRight: 12,
108
+ width: 40,
109
+ height: 40,
110
+ borderRadius: 20,
111
+ alignItems: 'center',
112
+ justifyContent: 'center',
113
+ marginRight: 16,
102
114
  },
103
115
  optionLabel: {
104
116
  flex: 1,
117
+ fontSize: 16,
105
118
  },
106
119
  checkbox: {
107
120
  width: 24,
@@ -24,19 +24,19 @@ export const RatingQuestion = ({
24
24
  {Array.from({ length: max }).map((_, i) => {
25
25
  const isFilled = i < value;
26
26
  return (
27
- <TouchableOpacity key={i} onPress={() => onChange(i + 1)} activeOpacity={0.7} style={styles.star}>
27
+ <TouchableOpacity key={i} onPress={() => onChange(i + 1)} activeOpacity={0.8} style={styles.star}>
28
28
  <AtomicIcon
29
29
  name={isFilled ? "star" : "star-outline"}
30
30
  customSize={48}
31
- customColor={isFilled ? "#FFD700" : colors.subTextColor}
31
+ customColor={isFilled ? colors.iconColor : colors.headerButtonBorder}
32
32
  />
33
33
  </TouchableOpacity>
34
34
  );
35
35
  })}
36
36
  </View>
37
37
  {value > 0 && (
38
- <AtomicText type="headlineSmall" style={[styles.valueText, { color: colors.textColor }]}>
39
- {value} / {max}
38
+ <AtomicText type="headlineSmall" style={[styles.valueText, { color: colors.textColor, marginTop: 12 }]}>
39
+ {value} <AtomicText type="bodyMedium" color="textSecondary">/ {max}</AtomicText>
40
40
  </AtomicText>
41
41
  )}
42
42
  </View>
@@ -19,7 +19,6 @@ export const SingleChoiceQuestion = ({
19
19
 
20
20
  const renderOption = (option: QuestionOption) => {
21
21
  const isSelected = value === option.id;
22
- const textColor = isSelected ? colors.textColor : colors.subTextColor;
23
22
  const isEmoji = option.iconType === 'emoji';
24
23
 
25
24
  return (
@@ -30,34 +29,39 @@ export const SingleChoiceQuestion = ({
30
29
  {
31
30
  backgroundColor: isSelected ? colors.iconBg : colors.featureItemBg,
32
31
  borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
32
+ borderWidth: isSelected ? 2 : 1,
33
33
  }
34
34
  ]}
35
35
  onPress={() => onChange(option.id)}
36
- activeOpacity={0.7}
36
+ activeOpacity={0.8}
37
37
  >
38
38
  {option.icon && (
39
- <View style={styles.optionIcon}>
39
+ <View style={[
40
+ styles.optionIcon,
41
+ { backgroundColor: isSelected ? colors.iconColor : colors.featureItemBg }
42
+ ]}>
40
43
  {isEmoji ? (
41
44
  <AtomicText style={{ fontSize: 24 }}>{option.icon}</AtomicText>
42
45
  ) : (
43
46
  <AtomicIcon
44
47
  name={option.icon as any}
45
- customSize={24}
46
- customColor={textColor}
48
+ customSize={20}
49
+ customColor={isSelected ? colors.buttonTextColor : colors.subTextColor}
47
50
  />
48
51
  )}
49
52
  </View>
50
53
  )}
51
- <AtomicText type="bodyLarge" style={[styles.optionLabel, { color: textColor, fontWeight: isSelected ? '700' : '500' }]}>
54
+ <AtomicText type="bodyLarge" style={[styles.optionLabel, { color: isSelected ? colors.textColor : colors.subTextColor, fontWeight: isSelected ? '700' : '500' }]}>
52
55
  {option.label}
53
56
  </AtomicText>
54
57
  <View style={[
55
- styles.radio,
56
- {
57
- borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder,
58
- borderWidth: isSelected ? 6 : 2,
59
- }
60
- ]} />
58
+ styles.radioOuter,
59
+ { borderColor: isSelected ? colors.iconColor : colors.headerButtonBorder }
60
+ ]}>
61
+ {isSelected && (
62
+ <View style={[styles.radioInner, { backgroundColor: colors.iconColor }]} />
63
+ )}
64
+ </View>
61
65
  </TouchableOpacity>
62
66
  );
63
67
  };
@@ -77,20 +81,34 @@ const styles = StyleSheet.create({
77
81
  option: {
78
82
  flexDirection: "row",
79
83
  alignItems: "center",
80
- borderRadius: 16,
84
+ borderRadius: 20,
81
85
  padding: 16,
82
- borderWidth: 2,
86
+ marginBottom: 8,
83
87
  },
84
88
  optionIcon: {
85
- marginRight: 12,
89
+ width: 40,
90
+ height: 40,
91
+ borderRadius: 20,
92
+ alignItems: 'center',
93
+ justifyContent: 'center',
94
+ marginRight: 16,
86
95
  },
87
96
  optionLabel: {
88
97
  flex: 1,
98
+ fontSize: 16,
89
99
  },
90
- radio: {
100
+ radioOuter: {
91
101
  width: 24,
92
102
  height: 24,
93
103
  borderRadius: 12,
104
+ borderWidth: 2,
105
+ alignItems: 'center',
106
+ justifyContent: 'center',
107
+ },
108
+ radioInner: {
109
+ width: 12,
110
+ height: 12,
111
+ borderRadius: 6,
94
112
  },
95
113
  });
96
114
 
@@ -25,7 +25,7 @@ export const TextInputQuestion = ({
25
25
  styles.input,
26
26
  {
27
27
  backgroundColor: colors.featureItemBg,
28
- borderColor: colors.headerButtonBorder,
28
+ borderColor: value ? colors.iconColor : colors.headerButtonBorder,
29
29
  color: colors.textColor,
30
30
  }
31
31
  ]}
@@ -38,6 +38,7 @@ export const TextInputQuestion = ({
38
38
  numberOfLines={(validation?.maxLength ?? 0) > 100 ? 5 : 1}
39
39
  autoCapitalize="sentences"
40
40
  autoCorrect={true}
41
+ textAlignVertical="top"
41
42
  />
42
43
  {validation?.maxLength && (
43
44
  <AtomicText type="labelSmall" style={[styles.charCount, { color: colors.subTextColor }]}>