@umituz/react-native-onboarding 2.6.5 → 2.6.7

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": "2.6.5",
3
+ "version": "2.6.7",
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",
@@ -30,10 +30,8 @@
30
30
  "type": "git",
31
31
  "url": "https://github.com/umituz/react-native-onboarding"
32
32
  },
33
- "dependencies": {
34
- "@react-native-community/slider": "^4.5.0"
35
- },
36
33
  "peerDependencies": {
34
+ "@react-native-community/slider": "^4.5.0",
37
35
  "@umituz/react-native-storage": "latest",
38
36
  "@umituz/react-native-localization": "latest",
39
37
  "@umituz/react-native-design-system-theme": "latest",
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Question Renderer Component
3
+ * Single Responsibility: Render appropriate question component based on type
4
+ */
5
+
6
+ import React from "react";
7
+ import type { OnboardingQuestion } from "../../domain/entities/OnboardingQuestion";
8
+ import { SingleChoiceQuestion } from "./questions/SingleChoiceQuestion";
9
+ import { MultipleChoiceQuestion } from "./questions/MultipleChoiceQuestion";
10
+ import { TextInputQuestion } from "./questions/TextInputQuestion";
11
+ import { SliderQuestion } from "./questions/SliderQuestion";
12
+ import { RatingQuestion } from "./questions/RatingQuestion";
13
+
14
+ export interface QuestionRendererProps {
15
+ question: OnboardingQuestion;
16
+ value: any;
17
+ onChange: (value: any) => void;
18
+ }
19
+
20
+ export const QuestionRenderer: React.FC<QuestionRendererProps> = ({
21
+ question,
22
+ value,
23
+ onChange,
24
+ }) => {
25
+ switch (question.type) {
26
+ case "single_choice":
27
+ return (
28
+ <SingleChoiceQuestion
29
+ question={question}
30
+ value={value}
31
+ onChange={onChange}
32
+ />
33
+ );
34
+ case "multiple_choice":
35
+ return (
36
+ <MultipleChoiceQuestion
37
+ question={question}
38
+ value={value}
39
+ onChange={onChange}
40
+ />
41
+ );
42
+ case "text_input":
43
+ return (
44
+ <TextInputQuestion
45
+ question={question}
46
+ value={value}
47
+ onChange={onChange}
48
+ />
49
+ );
50
+ case "slider":
51
+ return (
52
+ <SliderQuestion
53
+ question={question}
54
+ value={value}
55
+ onChange={onChange}
56
+ />
57
+ );
58
+ case "rating":
59
+ return (
60
+ <RatingQuestion
61
+ question={question}
62
+ value={value}
63
+ onChange={onChange}
64
+ />
65
+ );
66
+ default:
67
+ return null;
68
+ }
69
+ };
70
+
@@ -1,19 +1,14 @@
1
1
  /**
2
2
  * Question Slide Component
3
- *
4
- * Displays a personalization question slide
3
+ * Single Responsibility: Display question slide with header and question
5
4
  */
6
5
 
7
6
  import React, { useMemo } from "react";
8
7
  import { View, Text, StyleSheet, ScrollView } from "react-native";
9
- import { AtomicIcon } from "@umituz/react-native-design-system-atoms";
10
- import { useAppDesignTokens, withAlpha } from "@umituz/react-native-design-system-theme";
8
+ import { useAppDesignTokens } from "@umituz/react-native-design-system-theme";
11
9
  import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
12
- import { SingleChoiceQuestion } from "./questions/SingleChoiceQuestion";
13
- import { MultipleChoiceQuestion } from "./questions/MultipleChoiceQuestion";
14
- import { TextInputQuestion } from "./questions/TextInputQuestion";
15
- import { SliderQuestion } from "./questions/SliderQuestion";
16
- import { RatingQuestion } from "./questions/RatingQuestion";
10
+ import { QuestionSlideHeader } from "./QuestionSlideHeader";
11
+ import { QuestionRenderer } from "./QuestionRenderer";
17
12
 
18
13
  export interface QuestionSlideProps {
19
14
  slide: OnboardingSlide;
@@ -36,55 +31,19 @@ export const QuestionSlide: React.FC<QuestionSlideProps> = ({
36
31
  return null;
37
32
  }
38
33
 
39
- // Check if icon is an emoji or Lucide icon name
40
- const isEmoji = /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u.test(slide.icon);
34
+ const content = (
35
+ <>
36
+ <QuestionSlideHeader slide={slide} useGradient={useGradient} />
41
37
 
42
- const renderQuestion = () => {
43
- switch (question.type) {
44
- case "single_choice":
45
- return (
46
- <SingleChoiceQuestion
47
- question={question}
48
- value={value}
49
- onChange={onChange}
50
- />
51
- );
52
- case "multiple_choice":
53
- return (
54
- <MultipleChoiceQuestion
55
- question={question}
56
- value={value}
57
- onChange={onChange}
58
- />
59
- );
60
- case "text_input":
61
- return (
62
- <TextInputQuestion
63
- question={question}
64
- value={value}
65
- onChange={onChange}
66
- />
67
- );
68
- case "slider":
69
- return (
70
- <SliderQuestion
71
- question={question}
72
- value={value}
73
- onChange={onChange}
74
- />
75
- );
76
- case "rating":
77
- return (
78
- <RatingQuestion
79
- question={question}
80
- value={value}
81
- onChange={onChange}
82
- />
83
- );
84
- default:
85
- return null;
86
- }
87
- };
38
+ <View style={styles.questionContainer}>
39
+ <QuestionRenderer question={question} value={value} onChange={onChange} />
40
+ </View>
41
+
42
+ {question.validation?.required && !value && (
43
+ <Text style={styles.requiredHint}>* This field is required</Text>
44
+ )}
45
+ </>
46
+ );
88
47
 
89
48
  return (
90
49
  <ScrollView
@@ -92,75 +51,18 @@ export const QuestionSlide: React.FC<QuestionSlideProps> = ({
92
51
  showsVerticalScrollIndicator={false}
93
52
  >
94
53
  {useGradient ? (
95
- // Gradient kullanıldığında card olmadan direkt içerik
96
- <>
97
- {/* Icon */}
98
- <View style={styles.iconContainer}>
99
- {isEmoji ? (
100
- <Text style={styles.icon}>{slide.icon}</Text>
101
- ) : (
102
- <AtomicIcon
103
- name={slide.icon as any}
104
- customSize={48}
105
- customColor="#FFFFFF"
106
- />
107
- )}
108
- </View>
109
-
110
- {/* Title */}
111
- <Text style={styles.title}>{slide.title}</Text>
112
-
113
- {/* Description */}
114
- {slide.description && (
115
- <Text style={styles.description}>{slide.description}</Text>
116
- )}
117
-
118
- {/* Question */}
119
- <View style={styles.questionContainer}>{renderQuestion()}</View>
120
-
121
- {/* Validation hint */}
122
- {question.validation?.required && !value && (
123
- <Text style={styles.requiredHint}>* This field is required</Text>
124
- )}
125
- </>
54
+ content
126
55
  ) : (
127
- // Normal modda card ile
128
- <View style={styles.slideContent}>
129
- {/* Icon */}
130
- <View style={styles.iconContainer}>
131
- {isEmoji ? (
132
- <Text style={styles.icon}>{slide.icon}</Text>
133
- ) : (
134
- <AtomicIcon
135
- name={slide.icon as any}
136
- customSize={48}
137
- customColor={tokens.colors.textPrimary}
138
- />
139
- )}
140
- </View>
141
-
142
- {/* Title */}
143
- <Text style={styles.title}>{slide.title}</Text>
144
-
145
- {/* Description */}
146
- {slide.description && (
147
- <Text style={styles.description}>{slide.description}</Text>
148
- )}
149
-
150
- {/* Question */}
151
- <View style={styles.questionContainer}>{renderQuestion()}</View>
152
-
153
- {/* Validation hint */}
154
- {question.validation?.required && !value && (
155
- <Text style={styles.requiredHint}>* This field is required</Text>
156
- )}
157
- </View>
56
+ <View style={styles.slideContent}>{content}</View>
158
57
  )}
159
58
  </ScrollView>
160
59
  );
161
60
  };
162
61
 
163
- const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>, useGradient: boolean) =>
62
+ const getStyles = (
63
+ tokens: ReturnType<typeof useAppDesignTokens>,
64
+ useGradient: boolean,
65
+ ) =>
164
66
  StyleSheet.create({
165
67
  content: {
166
68
  flexGrow: 1,
@@ -187,38 +89,6 @@ const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>, useGradient: b
187
89
  shadowRadius: 8,
188
90
  elevation: 4,
189
91
  },
190
- iconContainer: {
191
- width: 96,
192
- height: 96,
193
- borderRadius: 48,
194
- backgroundColor: useGradient
195
- ? "rgba(255, 255, 255, 0.25)"
196
- : withAlpha(tokens.colors.primary, 0.2),
197
- alignItems: "center",
198
- justifyContent: "center",
199
- marginBottom: 24,
200
- borderWidth: 2,
201
- borderColor: useGradient
202
- ? "rgba(255, 255, 255, 0.4)"
203
- : withAlpha(tokens.colors.primary, 0.4),
204
- },
205
- icon: {
206
- fontSize: 48,
207
- },
208
- title: {
209
- fontSize: 24,
210
- fontWeight: "bold",
211
- color: useGradient ? "#FFFFFF" : tokens.colors.textPrimary,
212
- textAlign: "center",
213
- marginBottom: 12,
214
- },
215
- description: {
216
- fontSize: 15,
217
- color: useGradient ? "rgba(255, 255, 255, 0.9)" : tokens.colors.textSecondary,
218
- textAlign: "center",
219
- lineHeight: 22,
220
- marginBottom: 24,
221
- },
222
92
  questionContainer: {
223
93
  width: "100%",
224
94
  marginTop: 8,
@@ -230,4 +100,3 @@ const getStyles = (tokens: ReturnType<typeof useAppDesignTokens>, useGradient: b
230
100
  marginTop: 12,
231
101
  },
232
102
  });
233
-
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Question Slide Header Component
3
+ * Single Responsibility: Display slide header (icon, title, description)
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, Text, StyleSheet } from "react-native";
8
+ import { AtomicIcon } from "@umituz/react-native-design-system-atoms";
9
+ import { useAppDesignTokens, withAlpha } from "@umituz/react-native-design-system-theme";
10
+ import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
11
+
12
+ export interface QuestionSlideHeaderProps {
13
+ slide: OnboardingSlide;
14
+ useGradient: boolean;
15
+ }
16
+
17
+ const isEmoji = (icon: string): boolean =>
18
+ /[\u{1F300}-\u{1F9FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/u.test(icon);
19
+
20
+ export const QuestionSlideHeader: React.FC<QuestionSlideHeaderProps> = ({
21
+ slide,
22
+ useGradient,
23
+ }) => {
24
+ const tokens = useAppDesignTokens();
25
+ const styles = getStyles(tokens, useGradient);
26
+
27
+ return (
28
+ <>
29
+ <View style={styles.iconContainer}>
30
+ {isEmoji(slide.icon) ? (
31
+ <Text style={styles.icon}>{slide.icon}</Text>
32
+ ) : (
33
+ <AtomicIcon
34
+ name={slide.icon as any}
35
+ customSize={48}
36
+ customColor={useGradient ? "#FFFFFF" : tokens.colors.textPrimary}
37
+ />
38
+ )}
39
+ </View>
40
+
41
+ <Text style={styles.title}>{slide.title}</Text>
42
+
43
+ {slide.description && <Text style={styles.description}>{slide.description}</Text>}
44
+ </>
45
+ );
46
+ };
47
+
48
+ const getStyles = (
49
+ tokens: ReturnType<typeof useAppDesignTokens>,
50
+ useGradient: boolean,
51
+ ) =>
52
+ StyleSheet.create({
53
+ iconContainer: {
54
+ width: 96,
55
+ height: 96,
56
+ borderRadius: 48,
57
+ backgroundColor: useGradient
58
+ ? "rgba(255, 255, 255, 0.25)"
59
+ : withAlpha(tokens.colors.primary, 0.2),
60
+ alignItems: "center",
61
+ justifyContent: "center",
62
+ marginBottom: 24,
63
+ borderWidth: 2,
64
+ borderColor: useGradient
65
+ ? "rgba(255, 255, 255, 0.4)"
66
+ : withAlpha(tokens.colors.primary, 0.4),
67
+ },
68
+ icon: {
69
+ fontSize: 48,
70
+ },
71
+ title: {
72
+ fontSize: 24,
73
+ fontWeight: "bold",
74
+ color: useGradient ? "#FFFFFF" : tokens.colors.textPrimary,
75
+ textAlign: "center",
76
+ marginBottom: 12,
77
+ },
78
+ description: {
79
+ fontSize: 15,
80
+ color: useGradient ? "rgba(255, 255, 255, 0.9)" : tokens.colors.textSecondary,
81
+ textAlign: "center",
82
+ lineHeight: 22,
83
+ marginBottom: 24,
84
+ },
85
+ });
86
+
@@ -1,50 +1,115 @@
1
1
  /**
2
2
  * Slider Question Component
3
- *
4
- * Slider for numeric value selection
3
+ * Single Responsibility: Display slider for numeric value selection
5
4
  */
6
5
 
7
- import React from "react";
8
- import { View, Text, StyleSheet } from "react-native";
9
- import Slider from "@react-native-community/slider";
6
+ import React, { useMemo } from "react";
7
+ import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
10
8
  import type { OnboardingQuestion } from "../../../domain/entities/OnboardingQuestion";
11
9
 
10
+ // Lazy import slider to handle peer dependency gracefully
11
+ let SliderComponent: any = null;
12
+ try {
13
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
14
+ SliderComponent = require("@react-native-community/slider").default;
15
+ } catch {
16
+ // Slider not available - will show fallback
17
+ }
18
+
12
19
  export interface SliderQuestionProps {
13
20
  question: OnboardingQuestion;
14
21
  value: number | undefined;
15
22
  onChange: (value: number) => void;
16
23
  }
17
24
 
25
+ const getSliderConfig = (question: OnboardingQuestion) => {
26
+ const { validation } = question;
27
+ const min = validation?.min ?? 0;
28
+ const max = validation?.max ?? 100;
29
+ return { min, max };
30
+ };
31
+
32
+ const SliderFallback: React.FC<{
33
+ min: number;
34
+ max: number;
35
+ value: number;
36
+ onChange: (value: number) => void;
37
+ }> = ({ min, max, value, onChange }) => {
38
+ const handleIncrement = () => {
39
+ if (value < max) {
40
+ onChange(value + 1);
41
+ }
42
+ };
43
+
44
+ const handleDecrement = () => {
45
+ if (value > min) {
46
+ onChange(value - 1);
47
+ }
48
+ };
49
+
50
+ return (
51
+ <View style={styles.fallbackContainer}>
52
+ <View style={styles.fallbackControls}>
53
+ <TouchableOpacity
54
+ style={styles.fallbackButton}
55
+ onPress={handleDecrement}
56
+ activeOpacity={0.7}
57
+ >
58
+ <Text style={styles.fallbackButtonText}>−</Text>
59
+ </TouchableOpacity>
60
+ <Text style={styles.fallbackValue}>{value}</Text>
61
+ <TouchableOpacity
62
+ style={styles.fallbackButton}
63
+ onPress={handleIncrement}
64
+ activeOpacity={0.7}
65
+ >
66
+ <Text style={styles.fallbackButtonText}>+</Text>
67
+ </TouchableOpacity>
68
+ </View>
69
+ <View style={styles.fallbackLabels}>
70
+ <Text style={styles.label}>{min}</Text>
71
+ <Text style={styles.label}>{max}</Text>
72
+ </View>
73
+ </View>
74
+ );
75
+ };
76
+
18
77
  export const SliderQuestion: React.FC<SliderQuestionProps> = ({
19
78
  question,
20
79
  value,
21
80
  onChange,
22
81
  }) => {
23
- const { validation } = question;
24
- const min = validation?.min ?? 0;
25
- const max = validation?.max ?? 100;
82
+ const { min, max } = useMemo(() => getSliderConfig(question), [question]);
26
83
  const currentValue = value ?? min;
27
84
 
85
+ const sliderProps = {
86
+ style: styles.slider,
87
+ minimumValue: min,
88
+ maximumValue: max,
89
+ value: currentValue,
90
+ onValueChange: onChange,
91
+ minimumTrackTintColor: "#FFFFFF",
92
+ maximumTrackTintColor: "rgba(255, 255, 255, 0.3)",
93
+ thumbTintColor: "#FFFFFF",
94
+ step: 1,
95
+ };
96
+
28
97
  return (
29
98
  <View style={styles.container}>
30
99
  <View style={styles.valueContainer}>
31
100
  <Text style={styles.valueText}>{currentValue}</Text>
32
101
  </View>
33
- <Slider
34
- style={styles.slider}
35
- minimumValue={min}
36
- maximumValue={max}
37
- value={currentValue}
38
- onValueChange={onChange}
39
- minimumTrackTintColor="#FFFFFF"
40
- maximumTrackTintColor="rgba(255, 255, 255, 0.3)"
41
- thumbTintColor="#FFFFFF"
42
- step={1}
43
- />
44
- <View style={styles.labels}>
45
- <Text style={styles.label}>{min}</Text>
46
- <Text style={styles.label}>{max}</Text>
47
- </View>
102
+ {SliderComponent ? (
103
+ <>
104
+ <SliderComponent {...sliderProps} />
105
+ <View style={styles.labels}>
106
+ <Text style={styles.label}>{min}</Text>
107
+ <Text style={styles.label}>{max}</Text>
108
+ </View>
109
+ </>
110
+ ) : (
111
+ <SliderFallback min={min} max={max} value={currentValue} onChange={onChange} />
112
+ )}
48
113
  </View>
49
114
  );
50
115
  };
@@ -77,5 +142,39 @@ const styles = StyleSheet.create({
77
142
  color: "rgba(255, 255, 255, 0.7)",
78
143
  fontWeight: "500",
79
144
  },
145
+ fallbackContainer: {
146
+ width: "100%",
147
+ },
148
+ fallbackControls: {
149
+ flexDirection: "row",
150
+ alignItems: "center",
151
+ justifyContent: "center",
152
+ gap: 24,
153
+ },
154
+ fallbackButton: {
155
+ width: 60,
156
+ height: 60,
157
+ backgroundColor: "rgba(255, 255, 255, 0.2)",
158
+ borderRadius: 30,
159
+ alignItems: "center",
160
+ justifyContent: "center",
161
+ },
162
+ fallbackButtonText: {
163
+ fontSize: 48,
164
+ fontWeight: "bold",
165
+ color: "#FFFFFF",
166
+ lineHeight: 60,
167
+ },
168
+ fallbackValue: {
169
+ fontSize: 36,
170
+ fontWeight: "bold",
171
+ color: "#FFFFFF",
172
+ minWidth: 80,
173
+ textAlign: "center",
174
+ },
175
+ fallbackLabels: {
176
+ flexDirection: "row",
177
+ justifyContent: "space-between",
178
+ marginTop: 16,
179
+ },
80
180
  });
81
-