@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.
Files changed (64) hide show
  1. package/package.json +4 -2
  2. package/src/exports/onboarding.ts +6 -0
  3. package/src/index.ts +5 -0
  4. package/src/onboarding/domain/entities/OnboardingOptions.ts +104 -0
  5. package/src/onboarding/domain/entities/OnboardingQuestion.ts +165 -0
  6. package/src/onboarding/domain/entities/OnboardingSlide.ts +152 -0
  7. package/src/onboarding/domain/entities/OnboardingUserData.ts +43 -0
  8. package/src/onboarding/hooks/useOnboardingFlow.ts +50 -0
  9. package/src/onboarding/index.ts +108 -0
  10. package/src/onboarding/infrastructure/hooks/__tests__/useOnboardingAnswers.test.ts +163 -0
  11. package/src/onboarding/infrastructure/hooks/__tests__/useOnboardingNavigation.test.ts +121 -0
  12. package/src/onboarding/infrastructure/hooks/useOnboardingAnswers.ts +69 -0
  13. package/src/onboarding/infrastructure/hooks/useOnboardingNavigation.ts +75 -0
  14. package/src/onboarding/infrastructure/services/SlideManager.ts +53 -0
  15. package/src/onboarding/infrastructure/services/ValidationManager.ts +127 -0
  16. package/src/onboarding/infrastructure/storage/OnboardingStore.ts +99 -0
  17. package/src/onboarding/infrastructure/storage/OnboardingStoreActions.ts +50 -0
  18. package/src/onboarding/infrastructure/storage/OnboardingStoreSelectors.ts +25 -0
  19. package/src/onboarding/infrastructure/storage/OnboardingStoreState.ts +22 -0
  20. package/src/onboarding/infrastructure/storage/__tests__/OnboardingStore.test.ts +85 -0
  21. package/src/onboarding/infrastructure/storage/actions/answerActions.ts +47 -0
  22. package/src/onboarding/infrastructure/storage/actions/completeAction.ts +45 -0
  23. package/src/onboarding/infrastructure/storage/actions/index.ts +22 -0
  24. package/src/onboarding/infrastructure/storage/actions/initializeAction.ts +40 -0
  25. package/src/onboarding/infrastructure/storage/actions/resetAction.ts +37 -0
  26. package/src/onboarding/infrastructure/storage/actions/skipAction.ts +46 -0
  27. package/src/onboarding/infrastructure/storage/actions/storageHelpers.ts +60 -0
  28. package/src/onboarding/infrastructure/utils/arrayUtils.ts +28 -0
  29. package/src/onboarding/infrastructure/utils/backgroundUtils.ts +38 -0
  30. package/src/onboarding/infrastructure/utils/layouts/collageLayout.ts +81 -0
  31. package/src/onboarding/infrastructure/utils/layouts/gridLayouts.ts +78 -0
  32. package/src/onboarding/infrastructure/utils/layouts/honeycombLayout.ts +36 -0
  33. package/src/onboarding/infrastructure/utils/layouts/index.ts +12 -0
  34. package/src/onboarding/infrastructure/utils/layouts/layoutTypes.ts +37 -0
  35. package/src/onboarding/infrastructure/utils/layouts/masonryLayout.ts +37 -0
  36. package/src/onboarding/infrastructure/utils/layouts/scatteredLayout.ts +34 -0
  37. package/src/onboarding/infrastructure/utils/layouts/screenDimensions.ts +11 -0
  38. package/src/onboarding/infrastructure/utils/layouts/tilesLayout.ts +34 -0
  39. package/src/onboarding/presentation/components/BackgroundImageCollage.tsx +90 -0
  40. package/src/onboarding/presentation/components/BackgroundVideo.tsx +24 -0
  41. package/src/onboarding/presentation/components/BaseSlide.tsx +47 -0
  42. package/src/onboarding/presentation/components/OnboardingBackground.tsx +91 -0
  43. package/src/onboarding/presentation/components/OnboardingFooter.tsx +151 -0
  44. package/src/onboarding/presentation/components/OnboardingHeader.tsx +92 -0
  45. package/src/onboarding/presentation/components/OnboardingResetSetting.tsx +70 -0
  46. package/src/onboarding/presentation/components/OnboardingScreenContent.tsx +146 -0
  47. package/src/onboarding/presentation/components/OnboardingSlide.tsx +124 -0
  48. package/src/onboarding/presentation/components/QuestionRenderer.tsx +60 -0
  49. package/src/onboarding/presentation/components/QuestionSlide.tsx +67 -0
  50. package/src/onboarding/presentation/components/QuestionSlideHeader.tsx +75 -0
  51. package/src/onboarding/presentation/components/questions/MultipleChoiceQuestion.tsx +74 -0
  52. package/src/onboarding/presentation/components/questions/QuestionOptionItem.tsx +115 -0
  53. package/src/onboarding/presentation/components/questions/RatingQuestion.tsx +66 -0
  54. package/src/onboarding/presentation/components/questions/SingleChoiceQuestion.tsx +117 -0
  55. package/src/onboarding/presentation/components/questions/TextInputQuestion.tsx +71 -0
  56. package/src/onboarding/presentation/hooks/__tests__/useOnboardingContainerStyle.test.ts +96 -0
  57. package/src/onboarding/presentation/hooks/useOnboardingContainerStyle.ts +37 -0
  58. package/src/onboarding/presentation/hooks/useOnboardingGestures.ts +45 -0
  59. package/src/onboarding/presentation/hooks/useOnboardingScreenHandlers.ts +114 -0
  60. package/src/onboarding/presentation/hooks/useOnboardingScreenState.ts +146 -0
  61. package/src/onboarding/presentation/providers/OnboardingProvider.tsx +51 -0
  62. package/src/onboarding/presentation/screens/OnboardingScreen.tsx +189 -0
  63. package/src/onboarding/presentation/types/OnboardingProps.ts +46 -0
  64. package/src/onboarding/presentation/types/OnboardingTheme.ts +27 -0
@@ -0,0 +1,92 @@
1
+ import React from "react";
2
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
3
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
4
+ import { useLocalization } from "@umituz/react-native-localization";
5
+ import { useOnboardingProvider } from "../providers/OnboardingProvider";
6
+
7
+ export interface OnboardingHeaderProps {
8
+ isFirstSlide: boolean;
9
+ onBack: () => void;
10
+ onSkip: () => void;
11
+ showBackButton?: boolean;
12
+ showSkipButton?: boolean;
13
+ skipButtonText?: string;
14
+ }
15
+
16
+ export const OnboardingHeader = ({
17
+ isFirstSlide,
18
+ onBack,
19
+ onSkip,
20
+ showBackButton = true,
21
+ showSkipButton = true,
22
+ skipButtonText,
23
+ }: OnboardingHeaderProps) => {
24
+ const { t } = useLocalization();
25
+ const {
26
+ theme: { colors },
27
+ } = useOnboardingProvider();
28
+
29
+ const skipText = skipButtonText || t("onboarding.skip");
30
+
31
+ return (
32
+ <View style={styles.header}>
33
+ {showBackButton ? (
34
+ <TouchableOpacity
35
+ onPress={() => !isFirstSlide && onBack?.()}
36
+ disabled={isFirstSlide}
37
+ style={[
38
+ styles.headerButton,
39
+ {
40
+ backgroundColor: colors.headerButtonBg,
41
+ borderColor: colors.headerButtonBorder,
42
+ },
43
+ isFirstSlide && styles.headerButtonDisabled,
44
+ ]}
45
+ activeOpacity={0.7}
46
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
47
+ >
48
+ <AtomicIcon name="chevron-back" customSize={20} customColor={colors.iconColor} />
49
+ </TouchableOpacity>
50
+ ) : (
51
+ <View style={styles.headerButton} />
52
+ )}
53
+ {showSkipButton ? (
54
+ <TouchableOpacity onPress={onSkip} activeOpacity={0.7}>
55
+ <AtomicText
56
+ type="labelLarge"
57
+ style={[styles.skipText, { color: colors.textColor }]}
58
+ >
59
+ {skipText}
60
+ </AtomicText>
61
+ </TouchableOpacity>
62
+ ) : <View />}
63
+ </View>
64
+ );
65
+ };
66
+
67
+ const styles = StyleSheet.create({
68
+ header: {
69
+ flexDirection: "row",
70
+ justifyContent: "space-between",
71
+ alignItems: "center",
72
+ paddingHorizontal: 20,
73
+ paddingTop: 10,
74
+ paddingBottom: 20,
75
+ },
76
+ headerButton: {
77
+ width: 40,
78
+ height: 40,
79
+ borderRadius: 20,
80
+ alignItems: "center",
81
+ justifyContent: "center",
82
+ borderWidth: 1,
83
+ },
84
+ headerButtonDisabled: {
85
+ opacity: 0,
86
+ },
87
+ skipText: {
88
+ fontWeight: "700",
89
+ },
90
+ });
91
+
92
+
@@ -0,0 +1,70 @@
1
+ import React, { useCallback } from "react";
2
+ import { View, TouchableOpacity, StyleSheet } from "react-native";
3
+ import { AtomicIcon, AtomicText, useAppDesignTokens } from "@umituz/react-native-design-system";
4
+
5
+ export interface OnboardingResetSettingProps {
6
+ onReset: () => void | Promise<void>;
7
+ title?: string;
8
+ description?: string;
9
+ iconName?: string;
10
+ iconColor?: string;
11
+ titleColor?: string;
12
+ visible?: boolean;
13
+ isLast?: boolean;
14
+ }
15
+
16
+ export const OnboardingResetSetting = ({
17
+ onReset,
18
+ title,
19
+ description,
20
+ iconName = "extension-puzzle",
21
+ iconColor,
22
+ titleColor,
23
+ visible = __DEV__,
24
+ isLast = false,
25
+ }: OnboardingResetSettingProps) => {
26
+ const tokens = useAppDesignTokens();
27
+
28
+ const handlePress = useCallback(async () => {
29
+ await onReset();
30
+ }, [onReset]);
31
+
32
+ if (!visible) return null;
33
+
34
+ const defaultIconColor = iconColor || tokens.colors.error;
35
+ const defaultTitleColor = titleColor || tokens.colors.error;
36
+
37
+ return (
38
+ <TouchableOpacity onPress={handlePress} activeOpacity={0.7}>
39
+ <View style={[styles.container, !isLast && { borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: tokens.colors.border }]}>
40
+ <View style={styles.iconContainer}>
41
+ <AtomicIcon name={iconName} size="md" customColor={defaultIconColor} />
42
+ </View>
43
+ <View style={styles.content}>
44
+ <AtomicText type="bodyLarge" style={{ color: defaultTitleColor, fontWeight: '600' }} numberOfLines={1}>
45
+ {title}
46
+ </AtomicText>
47
+ <AtomicText type="bodyMedium" style={{ color: tokens.colors.textSecondary }} numberOfLines={1}>
48
+ {description}
49
+ </AtomicText>
50
+ </View>
51
+ </View>
52
+ </TouchableOpacity>
53
+ );
54
+ };
55
+
56
+ const styles = StyleSheet.create({
57
+ container: {
58
+ flexDirection: "row",
59
+ alignItems: "center",
60
+ paddingVertical: 16,
61
+ paddingHorizontal: 20,
62
+ },
63
+ iconContainer: {
64
+ marginRight: 16,
65
+ },
66
+ content: {
67
+ flex: 1,
68
+ }
69
+ });
70
+
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Onboarding Screen Content Component
3
+ * Single Responsibility: Render onboarding screen content (header, slide, footer)
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet, StatusBar } from "react-native";
8
+ import { useTheme } from "@umituz/react-native-design-system";
9
+ import { OnboardingHeader } from "./OnboardingHeader";
10
+ import { OnboardingSlide as OnboardingSlideComponent } from "./OnboardingSlide";
11
+ import { QuestionSlide } from "./QuestionSlide";
12
+ import { OnboardingFooter } from "./OnboardingFooter";
13
+ import { OnboardingBackground } from "./OnboardingBackground";
14
+ import { useOnboardingGestures } from "../hooks/useOnboardingGestures";
15
+ import type { OnboardingScreenContentProps } from "../types/OnboardingProps";
16
+
17
+ export const OnboardingScreenContent = ({
18
+ containerStyle,
19
+ useCustomBackground,
20
+ currentSlide,
21
+ isFirstSlide,
22
+ isLastSlide,
23
+ currentIndex,
24
+ totalSlides,
25
+ currentAnswer,
26
+ isAnswerValid,
27
+ showBackButton,
28
+ showSkipButton,
29
+ showProgressBar,
30
+ showDots,
31
+ showProgressText,
32
+ skipButtonText,
33
+ nextButtonText,
34
+ getStartedButtonText,
35
+ onBack,
36
+ onSkip,
37
+ onNext,
38
+ onAnswerChange,
39
+ renderHeader,
40
+ renderFooter,
41
+ renderSlide,
42
+ onUpgrade,
43
+ showPaywallOnComplete,
44
+ variant = "default",
45
+ }: OnboardingScreenContentProps) => {
46
+ const { themeMode } = useTheme();
47
+
48
+ const panResponder = useOnboardingGestures({
49
+ isFirstSlide,
50
+ isAnswerValid,
51
+ onNext,
52
+ onBack,
53
+ });
54
+
55
+ const hasMedia =
56
+ !!currentSlide?.backgroundImage ||
57
+ !!currentSlide?.backgroundVideo ||
58
+ (!!currentSlide?.backgroundImages && currentSlide.backgroundImages.length > 0);
59
+ const overlayOpacity = currentSlide?.overlayOpacity ?? 0.5;
60
+ const showOverlay = useCustomBackground || hasMedia;
61
+
62
+ return (
63
+ <View
64
+ style={[styles.container, containerStyle]}
65
+ {...panResponder.panHandlers}
66
+ >
67
+ <StatusBar
68
+ barStyle={
69
+ themeMode === "dark" || showOverlay
70
+ ? "light-content"
71
+ : "dark-content"
72
+ }
73
+ />
74
+
75
+ <OnboardingBackground
76
+ currentSlide={currentSlide}
77
+ useCustomBackground={useCustomBackground}
78
+ showOverlay={showOverlay}
79
+ overlayOpacity={overlayOpacity}
80
+ />
81
+
82
+ {renderHeader ? (
83
+ renderHeader({ isFirstSlide, onBack, onSkip })
84
+ ) : (
85
+ <OnboardingHeader
86
+ isFirstSlide={isFirstSlide}
87
+ onBack={onBack}
88
+ onSkip={onSkip}
89
+ showBackButton={showBackButton}
90
+ showSkipButton={showSkipButton}
91
+ skipButtonText={skipButtonText}
92
+ />
93
+ )}
94
+
95
+ {currentSlide && (
96
+ <View style={styles.content}>
97
+ {renderSlide ? (
98
+ renderSlide(currentSlide)
99
+ ) : currentSlide.type === "question" && currentSlide.question ? (
100
+ <QuestionSlide
101
+ slide={currentSlide}
102
+ value={currentAnswer}
103
+ onChange={onAnswerChange}
104
+ variant={variant}
105
+ />
106
+ ) : (
107
+ <OnboardingSlideComponent slide={currentSlide} variant={variant} />
108
+ )}
109
+ </View>
110
+ )}
111
+
112
+ {renderFooter ? (
113
+ renderFooter({
114
+ currentIndex,
115
+ totalSlides,
116
+ isLastSlide,
117
+ onNext,
118
+ onUpgrade,
119
+ showPaywallOnComplete,
120
+ })
121
+ ) : (
122
+ <OnboardingFooter
123
+ currentIndex={currentIndex}
124
+ totalSlides={totalSlides}
125
+ isLastSlide={isLastSlide}
126
+ onNext={onNext}
127
+ showProgressBar={showProgressBar}
128
+ showDots={showDots}
129
+ showProgressText={showProgressText}
130
+ nextButtonText={nextButtonText}
131
+ getStartedButtonText={getStartedButtonText}
132
+ disabled={!isAnswerValid}
133
+ />
134
+ )}
135
+ </View>
136
+ );
137
+ };
138
+
139
+ const styles = StyleSheet.create({
140
+ container: {
141
+ flex: 1,
142
+ },
143
+ content: {
144
+ flex: 1,
145
+ },
146
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * OnboardingSlide Component
3
+ * Single Responsibility: Render a single onboarding slide
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
9
+ import type { OnboardingSlide as OnboardingSlideType } from "../../domain/entities/OnboardingSlide";
10
+ import { BaseSlide } from "./BaseSlide";
11
+ import { useOnboardingProvider } from "../providers/OnboardingProvider";
12
+
13
+ export interface OnboardingSlideProps {
14
+ slide: OnboardingSlideType;
15
+ variant?: "default" | "card" | "minimal" | "fullscreen";
16
+ }
17
+
18
+ export const OnboardingSlide = ({
19
+ slide,
20
+ variant = "default",
21
+ }: OnboardingSlideProps) => {
22
+ const {
23
+ theme: { colors },
24
+ } = useOnboardingProvider();
25
+
26
+ const shouldShowIcon = slide.icon && !slide.hideIcon;
27
+ const isEmoji = slide.iconType === 'emoji';
28
+ const iconSize = variant === "minimal" ? 80 : 72;
29
+ const contentPosition = slide.contentPosition || "center";
30
+
31
+ return (
32
+ <BaseSlide contentPosition={contentPosition}>
33
+ {shouldShowIcon && slide.icon && (
34
+ <View style={styles.iconBox}>
35
+ {isEmoji ? (
36
+ <AtomicText style={{ fontSize: iconSize }}>{slide.icon}</AtomicText>
37
+ ) : (
38
+ <AtomicIcon
39
+ name={slide.icon as any}
40
+ customSize={iconSize}
41
+ customColor={colors.iconColor}
42
+ />
43
+ )}
44
+ </View>
45
+ )}
46
+
47
+ <AtomicText
48
+ type="displaySmall"
49
+ style={[styles.title, { color: colors.textColor }]}
50
+ >
51
+ {slide.title}
52
+ </AtomicText>
53
+
54
+ {slide.description && (
55
+ <AtomicText
56
+ type="bodyLarge"
57
+ style={[styles.description, { color: colors.subTextColor }]}
58
+ >
59
+ {slide.description}
60
+ </AtomicText>
61
+ )}
62
+
63
+ {slide.features && slide.features.length > 0 && (
64
+ <View style={styles.features}>
65
+ {slide.features.map((feature, index) => (
66
+ <View
67
+ key={index}
68
+ style={[
69
+ styles.featureItem,
70
+ { backgroundColor: colors.featureItemBg },
71
+ ]}
72
+ >
73
+ <AtomicIcon
74
+ name="checkmark-circle"
75
+ size="sm"
76
+ customColor={colors.iconColor}
77
+ />
78
+ <AtomicText
79
+ type="bodyMedium"
80
+ style={[styles.featureText, { color: colors.textColor }]}
81
+ >
82
+ {feature}
83
+ </AtomicText>
84
+ </View>
85
+ ))}
86
+ </View>
87
+ )}
88
+ </BaseSlide>
89
+ );
90
+ };
91
+
92
+ const styles = StyleSheet.create({
93
+ iconBox: {
94
+ marginBottom: 32,
95
+ height: 100,
96
+ justifyContent: "center",
97
+ alignItems: "center",
98
+ },
99
+ title: {
100
+ fontWeight: "800",
101
+ textAlign: "center",
102
+ marginBottom: 16,
103
+ },
104
+ description: {
105
+ textAlign: "center",
106
+ lineHeight: 24,
107
+ marginBottom: 32,
108
+ },
109
+ features: {
110
+ width: "100%",
111
+ gap: 12,
112
+ },
113
+ featureItem: {
114
+ flexDirection: "row",
115
+ alignItems: "center",
116
+ padding: 16,
117
+ borderRadius: 16,
118
+ gap: 12,
119
+ },
120
+ featureText: {
121
+ fontWeight: "600",
122
+ flex: 1,
123
+ },
124
+ });
@@ -0,0 +1,60 @@
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 { RatingQuestion } from "./questions/RatingQuestion";
12
+
13
+ export interface QuestionRendererProps {
14
+ question: OnboardingQuestion;
15
+ value: any;
16
+ onChange: (value: any) => void;
17
+ }
18
+
19
+ export const QuestionRenderer = ({
20
+ question,
21
+ value,
22
+ onChange,
23
+ }: QuestionRendererProps) => {
24
+ switch (question.type) {
25
+ case "single_choice":
26
+ return (
27
+ <SingleChoiceQuestion
28
+ question={question}
29
+ value={value}
30
+ onChange={onChange}
31
+ />
32
+ );
33
+ case "multiple_choice":
34
+ return (
35
+ <MultipleChoiceQuestion
36
+ question={question}
37
+ value={value}
38
+ onChange={onChange}
39
+ />
40
+ );
41
+ case "text_input":
42
+ return (
43
+ <TextInputQuestion
44
+ question={question}
45
+ value={value}
46
+ onChange={onChange}
47
+ />
48
+ );
49
+ case "rating":
50
+ return (
51
+ <RatingQuestion
52
+ question={question}
53
+ value={value}
54
+ onChange={onChange}
55
+ />
56
+ );
57
+ default:
58
+ return null;
59
+ }
60
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * QuestionSlide Component
3
+ * Single Responsibility: Render a question-type slide
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { AtomicText } from "@umituz/react-native-design-system";
9
+ import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
10
+ import { QuestionSlideHeader } from "./QuestionSlideHeader";
11
+ import { QuestionRenderer } from "./QuestionRenderer";
12
+ import { BaseSlide } from "./BaseSlide";
13
+ import { useOnboardingProvider } from "../providers/OnboardingProvider";
14
+ import { useLocalization } from "@umituz/react-native-localization";
15
+
16
+ export interface QuestionSlideProps {
17
+ slide: OnboardingSlide;
18
+ value: any;
19
+ onChange: (value: any) => void;
20
+ variant?: "default" | "card" | "minimal" | "fullscreen";
21
+ }
22
+
23
+ export const QuestionSlide = ({
24
+ slide,
25
+ value,
26
+ onChange,
27
+ variant: _variant = "default",
28
+ }: QuestionSlideProps) => {
29
+ const {
30
+ theme: { colors },
31
+ } = useOnboardingProvider();
32
+ const { t } = useLocalization();
33
+ const { question } = slide;
34
+
35
+ if (!question) return null;
36
+
37
+ return (
38
+ <BaseSlide contentPosition={slide.contentPosition}>
39
+ <QuestionSlideHeader slide={slide} />
40
+
41
+ <View style={styles.questionContainer}>
42
+ <QuestionRenderer question={question} value={value} onChange={onChange} />
43
+ </View>
44
+
45
+ {question.validation?.required && !value && (
46
+ <AtomicText
47
+ type="labelSmall"
48
+ style={[styles.requiredHint, { color: colors.errorColor }]}
49
+ >
50
+ {t("onboarding.fieldRequired")}
51
+ </AtomicText>
52
+ )}
53
+ </BaseSlide>
54
+ );
55
+ };
56
+
57
+ const styles = StyleSheet.create({
58
+ questionContainer: {
59
+ marginTop: 24,
60
+ width: "100%",
61
+ },
62
+ requiredHint: {
63
+ marginTop: 12,
64
+ textAlign: "center",
65
+ fontWeight: "600",
66
+ },
67
+ });
@@ -0,0 +1,75 @@
1
+ import React from "react";
2
+ import { View, StyleSheet } from "react-native";
3
+ import { AtomicIcon, AtomicText } from "@umituz/react-native-design-system";
4
+ import type { OnboardingSlide } from "../../domain/entities/OnboardingSlide";
5
+ import { useOnboardingProvider } from "../providers/OnboardingProvider";
6
+
7
+ export interface QuestionSlideHeaderProps {
8
+ slide: OnboardingSlide;
9
+ }
10
+
11
+ export const QuestionSlideHeader = ({ slide }: QuestionSlideHeaderProps) => {
12
+ const {
13
+ theme: { colors },
14
+ } = useOnboardingProvider();
15
+ const isEmoji = slide.iconType === 'emoji';
16
+
17
+ return (
18
+ <View style={styles.container}>
19
+ <View style={[
20
+ styles.iconContainer,
21
+ {
22
+ backgroundColor: colors.iconBg,
23
+ borderColor: colors.iconBorder,
24
+ }
25
+ ]}>
26
+ {isEmoji ? (
27
+ <AtomicText style={{ fontSize: 48 }}>{slide.icon}</AtomicText>
28
+ ) : (
29
+ <AtomicIcon name={slide.icon as any} customSize={48} customColor={colors.textColor} />
30
+ )}
31
+ </View>
32
+
33
+ <AtomicText type="headlineMedium" style={[styles.title, { color: colors.textColor }]}>
34
+ {slide.title}
35
+ </AtomicText>
36
+
37
+ {slide.description && (
38
+ <AtomicText type="bodyMedium" style={[styles.description, { color: colors.subTextColor }]}>
39
+ {slide.description}
40
+ </AtomicText>
41
+ )}
42
+ </View>
43
+ );
44
+ };
45
+
46
+ const styles = StyleSheet.create({
47
+ container: {
48
+ alignItems: "center",
49
+ },
50
+ iconContainer: {
51
+ width: 96,
52
+ height: 96,
53
+ borderRadius: 48,
54
+ alignItems: "center",
55
+ justifyContent: "center",
56
+ marginBottom: 24,
57
+ borderWidth: 2,
58
+ },
59
+ title: {
60
+ fontWeight: "800",
61
+ textAlign: "center",
62
+ marginBottom: 12,
63
+ },
64
+ description: {
65
+ textAlign: "center",
66
+ lineHeight: 22,
67
+ marginBottom: 24,
68
+ },
69
+ });
70
+
71
+
72
+
73
+
74
+
75
+
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Multiple Choice Question Component
3
+ * Single Responsibility: Render multiple choice question with options
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, StyleSheet } from "react-native";
8
+ import { AtomicText } from "@umituz/react-native-design-system";
9
+ import { useOnboardingProvider } from "../../providers/OnboardingProvider";
10
+ import { ensureArray } from "../../../infrastructure/utils/arrayUtils";
11
+ import type { OnboardingQuestion } from "../../../domain/entities/OnboardingQuestion";
12
+ import { QuestionOptionItem } from "./QuestionOptionItem";
13
+
14
+ export interface MultipleChoiceQuestionProps {
15
+ question: OnboardingQuestion;
16
+ value: string[] | undefined;
17
+ onChange: (value: string[]) => void;
18
+ }
19
+
20
+ export const MultipleChoiceQuestion = ({
21
+ question,
22
+ value,
23
+ onChange,
24
+ }: MultipleChoiceQuestionProps) => {
25
+ const {
26
+ theme: { colors },
27
+ } = useOnboardingProvider();
28
+
29
+ const safeValue = ensureArray(value);
30
+
31
+ const handleToggle = (optionId: string) => {
32
+ const newValue = safeValue.includes(optionId)
33
+ ? safeValue.filter((id) => id !== optionId)
34
+ : [...safeValue, optionId];
35
+
36
+ if (question.validation?.maxSelections && newValue.length > question.validation.maxSelections) {
37
+ return;
38
+ }
39
+ onChange(newValue);
40
+ };
41
+
42
+ return (
43
+ <View style={styles.container}>
44
+ {question.options?.map((option) => (
45
+ <QuestionOptionItem
46
+ key={option.id}
47
+ option={option}
48
+ isSelected={safeValue.includes(option.id)}
49
+ onPress={() => handleToggle(option.id)}
50
+ colors={colors}
51
+ />
52
+ ))}
53
+ {question.validation?.maxSelections && (
54
+ <AtomicText
55
+ type="labelSmall"
56
+ style={[styles.hint, { color: colors.subTextColor }]}
57
+ >
58
+ Select up to {question.validation.maxSelections} options
59
+ </AtomicText>
60
+ )}
61
+ </View>
62
+ );
63
+ };
64
+
65
+ const styles = StyleSheet.create({
66
+ container: {
67
+ width: "100%",
68
+ gap: 12,
69
+ },
70
+ hint: {
71
+ textAlign: "center",
72
+ marginTop: 8,
73
+ },
74
+ });