@umituz/react-native-settings 5.4.9 → 5.4.10

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.
@@ -0,0 +1,278 @@
1
+ /**
2
+ * GenericScreen Component
3
+ *
4
+ * Universal screen component that works with ScreenConfig.
5
+ * Handles loading, error, and empty states automatically.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * const screenData = useScreenData({ fetch: async () => await api.getData() });
10
+ *
11
+ * return (
12
+ * <GenericScreen
13
+ * data={screenData}
14
+ * config={ScreenPresets.default}
15
+ * >
16
+ * <MyContent data={screenData.data} />
17
+ * </GenericScreen>
18
+ * );
19
+ * ```
20
+ */
21
+
22
+ import React, { useMemo } from 'react';
23
+ import { View, StyleSheet, TouchableOpacity } from 'react-native';
24
+ import { ScreenLayout } from '@umituz/react-native-design-system/layouts';
25
+ import {
26
+ NavigationHeader,
27
+ useAppNavigation,
28
+ } from '@umituz/react-native-design-system/molecules';
29
+ import {
30
+ AtomicText,
31
+ AtomicButton,
32
+ AtomicSpinner,
33
+ AtomicIcon,
34
+ } from '@umituz/react-native-design-system/atoms';
35
+ import { useAppDesignTokens } from '@umituz/react-native-design-system/theme';
36
+ import type {
37
+ ScreenConfig,
38
+ ScreenData,
39
+ } from '../../core/patterns/Screen/ScreenConfig';
40
+
41
+ export interface GenericScreenProps<T> {
42
+ /**
43
+ * Screen data from useScreenData hook
44
+ */
45
+ data: ScreenData<T>;
46
+
47
+ /**
48
+ * Screen configuration
49
+ */
50
+ config?: ScreenConfig;
51
+
52
+ /**
53
+ * Content component (renders when data is loaded)
54
+ */
55
+ children: React.ReactNode | ((data: T) => React.ReactNode);
56
+
57
+ /**
58
+ * Test ID for E2E testing
59
+ */
60
+ testID?: string;
61
+ }
62
+
63
+ export function GenericScreen<T>({
64
+ data,
65
+ config = {},
66
+ children,
67
+ testID = 'generic-screen',
68
+ }: GenericScreenProps<T>) {
69
+ const tokens = useAppDesignTokens();
70
+ const navigation = useAppNavigation();
71
+ const styles = useMemo(() => getStyles(tokens), [tokens]);
72
+
73
+ const {
74
+ header,
75
+ loading = {},
76
+ error = {},
77
+ empty,
78
+ layout = {},
79
+ } = config;
80
+
81
+ // Header configuration
82
+ const showBackButton = header?.showBackButton !== false;
83
+ const onBackPress = header?.onBackPress || (() => navigation.goBack());
84
+
85
+ const navigationHeader = header?.custom ? (
86
+ header.custom
87
+ ) : header?.title || showBackButton ? (
88
+ <NavigationHeader
89
+ title={header?.title || ''}
90
+ onBackPress={showBackButton ? onBackPress : undefined}
91
+ actions={header?.actions?.map((action) => ({
92
+ icon: action.icon,
93
+ onPress: action.onPress,
94
+ testID: action.testID,
95
+ }))}
96
+ />
97
+ ) : undefined;
98
+
99
+ // Loading state
100
+ if (data.loading) {
101
+ return (
102
+ <ScreenLayout
103
+ edges={layout.edges || ['top', 'bottom', 'left', 'right']}
104
+ header={navigationHeader}
105
+ scrollable={layout.scrollable}
106
+ keyboardAvoiding={layout.keyboardAvoiding}
107
+ hideScrollIndicator={layout.hideScrollIndicator}
108
+ contentContainerStyle={layout.contentContainerStyle}
109
+ testID={testID}
110
+ >
111
+ {loading.custom ? (
112
+ loading.custom
113
+ ) : (
114
+ <View style={styles.centerContainer}>
115
+ <AtomicSpinner
116
+ size={loading.size || 'lg'}
117
+ fullContainer={loading.fullContainer !== false}
118
+ />
119
+ {loading.message && (
120
+ <AtomicText
121
+ type="bodyMedium"
122
+ color="onSurfaceVariant"
123
+ style={styles.stateMessage}
124
+ >
125
+ {loading.message}
126
+ </AtomicText>
127
+ )}
128
+ </View>
129
+ )}
130
+ </ScreenLayout>
131
+ );
132
+ }
133
+
134
+ // Error state
135
+ if (data.error) {
136
+ return (
137
+ <ScreenLayout
138
+ edges={layout.edges || ['top', 'bottom', 'left', 'right']}
139
+ header={navigationHeader}
140
+ scrollable={layout.scrollable}
141
+ keyboardAvoiding={layout.keyboardAvoiding}
142
+ hideScrollIndicator={layout.hideScrollIndicator}
143
+ contentContainerStyle={layout.contentContainerStyle}
144
+ testID={testID}
145
+ >
146
+ {error.custom ? (
147
+ error.custom
148
+ ) : (
149
+ <View style={styles.centerContainer}>
150
+ <AtomicIcon
151
+ name={error.icon || 'error'}
152
+ size="xl"
153
+ color="error"
154
+ style={styles.stateIcon}
155
+ />
156
+ <AtomicText
157
+ type="headlineMedium"
158
+ color="onSurface"
159
+ style={styles.stateTitle}
160
+ >
161
+ {error.prefix || 'Error'}
162
+ </AtomicText>
163
+ <AtomicText
164
+ type="bodyMedium"
165
+ color="onSurfaceVariant"
166
+ style={styles.stateMessage}
167
+ >
168
+ {error.message || data.error}
169
+ </AtomicText>
170
+ {error.showRetry && (
171
+ <AtomicButton
172
+ variant="outline"
173
+ onPress={error.onRetry || data.refresh}
174
+ style={styles.retryButton}
175
+ >
176
+ {error.retryText || 'Retry'}
177
+ </AtomicButton>
178
+ )}
179
+ </View>
180
+ )}
181
+ </ScreenLayout>
182
+ );
183
+ }
184
+
185
+ // Empty state
186
+ if (!data.data && empty) {
187
+ return (
188
+ <ScreenLayout
189
+ edges={layout.edges || ['top', 'bottom', 'left', 'right']}
190
+ header={navigationHeader}
191
+ scrollable={layout.scrollable}
192
+ keyboardAvoiding={layout.keyboardAvoiding}
193
+ hideScrollIndicator={layout.hideScrollIndicator}
194
+ contentContainerStyle={layout.contentContainerStyle}
195
+ testID={testID}
196
+ >
197
+ {empty.custom ? (
198
+ empty.custom
199
+ ) : (
200
+ <View style={styles.centerContainer}>
201
+ {empty.icon && (
202
+ <AtomicIcon
203
+ name={empty.icon}
204
+ size="xl"
205
+ color="onSurfaceVariant"
206
+ style={styles.stateIcon}
207
+ />
208
+ )}
209
+ <AtomicText
210
+ type="headlineMedium"
211
+ color="onSurface"
212
+ style={styles.stateTitle}
213
+ >
214
+ {empty.message || 'No Data'}
215
+ </AtomicText>
216
+ {empty.description && (
217
+ <AtomicText
218
+ type="bodyMedium"
219
+ color="onSurfaceVariant"
220
+ style={styles.stateMessage}
221
+ >
222
+ {empty.description}
223
+ </AtomicText>
224
+ )}
225
+ {empty.actionLabel && empty.onAction && (
226
+ <AtomicButton
227
+ variant="outline"
228
+ onPress={empty.onAction}
229
+ style={styles.retryButton}
230
+ >
231
+ {empty.actionLabel}
232
+ </AtomicButton>
233
+ )}
234
+ </View>
235
+ )}
236
+ </ScreenLayout>
237
+ );
238
+ }
239
+
240
+ // Content
241
+ return (
242
+ <ScreenLayout
243
+ edges={layout.edges || ['top', 'bottom', 'left', 'right']}
244
+ header={navigationHeader}
245
+ scrollable={layout.scrollable}
246
+ keyboardAvoiding={layout.keyboardAvoiding}
247
+ hideScrollIndicator={layout.hideScrollIndicator}
248
+ contentContainerStyle={layout.contentContainerStyle}
249
+ testID={testID}
250
+ >
251
+ {typeof children === 'function' ? (children as (data: T) => React.ReactNode)(data.data!) : children}
252
+ </ScreenLayout>
253
+ );
254
+ }
255
+
256
+ const getStyles = (_tokens: ReturnType<typeof useAppDesignTokens>) =>
257
+ StyleSheet.create({
258
+ centerContainer: {
259
+ flex: 1,
260
+ justifyContent: 'center',
261
+ alignItems: 'center',
262
+ paddingHorizontal: 32,
263
+ },
264
+ stateIcon: {
265
+ marginBottom: 16,
266
+ },
267
+ stateTitle: {
268
+ marginBottom: 8,
269
+ textAlign: 'center',
270
+ },
271
+ stateMessage: {
272
+ textAlign: 'center',
273
+ marginBottom: 24,
274
+ },
275
+ retryButton: {
276
+ minWidth: 120,
277
+ },
278
+ });
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Presentation Components - Public API
3
+ *
4
+ * Shared UI components for the entire application.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { GenericModal, GenericScreen } from '@/presentation/components';
9
+ *
10
+ * const modal = useModalState();
11
+ * return <GenericModal state={modal} />;
12
+ * ```
13
+ */
14
+
15
+ // =============================================================================
16
+ // MODAL COMPONENTS
17
+ // =============================================================================
18
+
19
+ export { GenericModal } from './GenericModal';
20
+ export type { GenericModalProps } from './GenericModal';
21
+
22
+ // =============================================================================
23
+ // SCREEN COMPONENTS
24
+ // =============================================================================
25
+
26
+ export { GenericScreen } from './GenericScreen';
27
+ export type { GenericScreenProps } from './GenericScreen';
@@ -1,103 +0,0 @@
1
- /**
2
- * Disclaimer Modal Component
3
- * Extracted from DisclaimerSetting to follow single responsibility and 200-line rules
4
- */
5
-
6
- import React from 'react';
7
- import {
8
- View,
9
- StyleSheet,
10
- TouchableOpacity,
11
- ScrollView,
12
- } from 'react-native';
13
-
14
- import { AtomicText, AtomicIcon } from '@umituz/react-native-design-system/atoms';
15
- import { BaseModal } from '@umituz/react-native-design-system/molecules';
16
- import { useAppDesignTokens } from '@umituz/react-native-design-system/theme';
17
-
18
- export interface DisclaimerModalProps {
19
- visible: boolean;
20
- title: string;
21
- content: string;
22
- onClose: () => void;
23
- }
24
-
25
- export const DisclaimerModal: React.FC<DisclaimerModalProps> = ({
26
- visible,
27
- title,
28
- content,
29
- onClose,
30
- }) => {
31
- const tokens = useAppDesignTokens();
32
- const styles = getStyles(tokens);
33
-
34
- return (
35
- <BaseModal visible={visible} onClose={onClose}>
36
- <View style={styles.modalContentWrapper}>
37
- {/* Modal Header */}
38
- <View
39
- style={[
40
- styles.modalHeader,
41
- { borderBottomColor: tokens.colors.border },
42
- ]}
43
- >
44
- <AtomicText type="headlineMedium" color="primary">
45
- {title}
46
- </AtomicText>
47
- <TouchableOpacity
48
- onPress={onClose}
49
- testID="close-disclaimer-modal"
50
- >
51
- <AtomicIcon name="close" color="primary" size="md" />
52
- </TouchableOpacity>
53
- </View>
54
-
55
- {/* Scrollable Content */}
56
- <ScrollView
57
- style={styles.modalContent}
58
- contentContainerStyle={styles.modalContentContainer}
59
- >
60
- <AtomicText
61
- type="bodyMedium"
62
- color="textPrimary"
63
- style={styles.modalText}
64
- >
65
- {content}
66
- </AtomicText>
67
- </ScrollView>
68
- </View>
69
- </BaseModal>
70
- );
71
- };
72
-
73
-
74
- const getStyles = (_tokens: ReturnType<typeof useAppDesignTokens>) =>
75
- StyleSheet.create({
76
- modalContainer: {
77
- flex: 1,
78
- },
79
- modalContentWrapper: {
80
- flex: 1,
81
- },
82
- modalHeader: {
83
- flexDirection: 'row',
84
- justifyContent: 'space-between',
85
- alignItems: 'center',
86
- paddingHorizontal: 20,
87
- paddingVertical: 16,
88
- borderBottomWidth: 1,
89
- },
90
-
91
- modalContent: {
92
- flex: 1,
93
- },
94
-
95
- modalContentContainer: {
96
- padding: 20,
97
- },
98
-
99
- modalText: {
100
- lineHeight: 24,
101
- fontSize: 15, // or tokens.typography.bodyMedium.responsiveFontSize
102
- },
103
- });
@@ -1,99 +0,0 @@
1
- /**
2
- * Feedback Modal Component
3
- * Modal wrapper for providing feedback
4
- */
5
-
6
- import React from "react";
7
- import { View, StyleSheet, TouchableOpacity } from "react-native";
8
- import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
- import { ScreenLayout } from "@umituz/react-native-design-system/layouts";
10
- import { BaseModal } from "@umituz/react-native-design-system/molecules";
11
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
12
- import { FeedbackForm } from "./FeedbackForm";
13
- import type { FeedbackType, FeedbackRating } from "../../domain/entities/FeedbackEntity";
14
- import type { FeedbackFormProps } from "./FeedbackFormProps";
15
-
16
- export interface FeedbackModalProps {
17
- visible: boolean;
18
- onClose: () => void;
19
- onSubmit: (data: { type: FeedbackType; rating: FeedbackRating; description: string; title: string }) => Promise<void>;
20
- initialType?: FeedbackType;
21
- isSubmitting?: boolean;
22
- title?: string;
23
- subtitle?: string;
24
- texts: FeedbackFormProps['texts'];
25
- }
26
-
27
- export const FeedbackModal: React.FC<FeedbackModalProps> = ({
28
- visible,
29
- onClose,
30
- onSubmit,
31
- initialType,
32
- isSubmitting,
33
- title,
34
- subtitle,
35
- texts,
36
- }) => {
37
- const tokens = useAppDesignTokens();
38
- const styles = getStyles(tokens);
39
-
40
- const header = (
41
- <View style={styles.header}>
42
- <View style={styles.headerText}>
43
- {title && <AtomicText style={{ fontSize: 20, fontWeight: '600' }}>{title}</AtomicText>}
44
- {subtitle && <AtomicText style={{ fontSize: 14, marginTop: 4 }}>{subtitle}</AtomicText>}
45
- </View>
46
- <TouchableOpacity
47
- onPress={onClose}
48
- style={[styles.closeButton, { backgroundColor: tokens.colors.surfaceVariant }]}
49
- >
50
- <AtomicIcon name="close" size="sm" color="onSurface" />
51
- </TouchableOpacity>
52
- </View>
53
- );
54
-
55
- return (
56
- <BaseModal visible={visible} onClose={onClose}>
57
- <ScreenLayout
58
- header={header}
59
- scrollable={true}
60
- edges={[]}
61
- keyboardAvoiding={true}
62
- contentContainerStyle={styles.content}
63
- hideScrollIndicator={false}
64
- >
65
- <FeedbackForm
66
- onSubmit={onSubmit}
67
- initialType={initialType}
68
- isSubmitting={isSubmitting}
69
- texts={texts}
70
- />
71
- </ScreenLayout>
72
- </BaseModal>
73
- );
74
- };
75
-
76
-
77
- const getStyles = (_tokens: ReturnType<typeof useAppDesignTokens>) =>
78
- StyleSheet.create({
79
- header: {
80
- flexDirection: "row",
81
- justifyContent: "space-between",
82
- alignItems: "center",
83
- padding: 16,
84
- borderBottomWidth: 1,
85
- },
86
- headerText: {
87
- flex: 1,
88
- },
89
- closeButton: {
90
- width: 36,
91
- height: 36,
92
- borderRadius: 18,
93
- justifyContent: "center",
94
- alignItems: "center",
95
- },
96
- content: {
97
- padding: 20,
98
- },
99
- });
@@ -1,152 +0,0 @@
1
- /**
2
- * Rating Prompt Modal
3
- * 2-step rating prompt: Custom modal → Native review prompt
4
- */
5
-
6
- import React from "react";
7
- import { Modal, View, StyleSheet } from "react-native";
8
- import { AtomicText, AtomicButton, AtomicIcon } from "@umituz/react-native-design-system/atoms";
9
- import { useResponsive } from "@umituz/react-native-design-system/responsive";
10
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
11
- import type { RatingTranslations } from "../../domain/entities/RatingConfig";
12
-
13
- export interface RatingPromptModalProps {
14
- visible: boolean;
15
- onPositive: () => void;
16
- onNegative: () => void;
17
- onLater: () => void;
18
- onDismiss: () => void;
19
- translations?: RatingTranslations;
20
- appName?: string;
21
- }
22
-
23
- export const RatingPromptModal: React.FC<RatingPromptModalProps> = ({
24
- visible,
25
- onPositive,
26
- onNegative,
27
- onLater,
28
- onDismiss,
29
- translations,
30
- appName = "this app",
31
- }) => {
32
- const tokens = useAppDesignTokens();
33
- const responsive = useResponsive();
34
-
35
- const defaultTranslations: RatingTranslations = {
36
- title: translations?.title ?? "Enjoying the app?",
37
- message:
38
- translations?.message ??
39
- `If you love using ${appName}, would you mind taking a moment to rate it?`,
40
- positiveButton: translations?.positiveButton ?? "Yes, I love it!",
41
- negativeButton: translations?.negativeButton ?? "Not really",
42
- laterButton: translations?.laterButton ?? "Maybe later",
43
- };
44
-
45
- return (
46
- <Modal
47
- visible={visible}
48
- transparent
49
- animationType="none"
50
- onRequestClose={onDismiss}
51
- statusBarTranslucent
52
- >
53
- <View
54
- style={[
55
- styles.overlay,
56
- { backgroundColor: "rgba(0, 0, 0, 0.5)" },
57
- ]}
58
- >
59
- <View
60
- style={[
61
- styles.container,
62
- {
63
- backgroundColor: tokens.colors.surface,
64
- borderRadius: tokens.borders.radius.xl,
65
- padding: tokens.spacing.lg,
66
- maxWidth: responsive.maxContentWidth * 0.85,
67
- width: "90%",
68
- },
69
- ]}
70
- >
71
- <View style={styles.iconContainer}>
72
- <AtomicIcon name="star" size="xl" color="primary" />
73
- </View>
74
-
75
- <AtomicText
76
- type="headlineMedium"
77
- color="onSurface"
78
- style={[
79
- styles.title,
80
- { marginBottom: tokens.spacing.sm },
81
- ]}
82
- >
83
- {defaultTranslations.title}
84
- </AtomicText>
85
-
86
- <AtomicText
87
- type="bodyMedium"
88
- color="onSurfaceVariant"
89
- style={[
90
- styles.message,
91
- { marginBottom: tokens.spacing.lg },
92
- ]}
93
- >
94
- {defaultTranslations.message}
95
- </AtomicText>
96
-
97
- <View style={[styles.buttonContainer, { gap: tokens.spacing.sm }]}>
98
- <AtomicButton
99
- variant="primary"
100
- onPress={onPositive}
101
- style={styles.button}
102
- >
103
- {defaultTranslations.positiveButton}
104
- </AtomicButton>
105
-
106
- <AtomicButton
107
- variant="outline"
108
- onPress={onNegative}
109
- style={styles.button}
110
- >
111
- {defaultTranslations.negativeButton}
112
- </AtomicButton>
113
-
114
- <AtomicButton
115
- variant="text"
116
- onPress={onLater}
117
- style={styles.button}
118
- >
119
- {defaultTranslations.laterButton}
120
- </AtomicButton>
121
- </View>
122
- </View>
123
- </View>
124
- </Modal>
125
- );
126
- };
127
-
128
- const styles = StyleSheet.create({
129
- overlay: {
130
- flex: 1,
131
- justifyContent: "center",
132
- alignItems: "center",
133
- },
134
- container: {
135
- alignItems: "center",
136
- },
137
- iconContainer: {
138
- marginBottom: 16,
139
- },
140
- title: {
141
- textAlign: "center",
142
- },
143
- message: {
144
- textAlign: "center",
145
- },
146
- buttonContainer: {
147
- width: "100%",
148
- },
149
- button: {
150
- width: "100%",
151
- },
152
- });