@croacroa/react-native-template 1.0.0 → 2.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 (69) hide show
  1. package/.github/workflows/ci.yml +187 -184
  2. package/.github/workflows/eas-build.yml +55 -55
  3. package/.github/workflows/eas-update.yml +50 -50
  4. package/CHANGELOG.md +106 -106
  5. package/CONTRIBUTING.md +377 -377
  6. package/README.md +399 -399
  7. package/__tests__/components/snapshots.test.tsx +131 -0
  8. package/__tests__/integration/auth-api.test.tsx +227 -0
  9. package/__tests__/performance/VirtualizedList.perf.test.tsx +362 -0
  10. package/app/(public)/onboarding.tsx +5 -5
  11. package/app.config.ts +45 -2
  12. package/assets/images/.gitkeep +7 -7
  13. package/components/onboarding/OnboardingScreen.tsx +370 -370
  14. package/components/onboarding/index.ts +2 -2
  15. package/components/providers/SuspenseBoundary.tsx +357 -0
  16. package/components/providers/index.ts +13 -0
  17. package/components/ui/Avatar.tsx +316 -316
  18. package/components/ui/Badge.tsx +416 -416
  19. package/components/ui/BottomSheet.tsx +307 -307
  20. package/components/ui/Checkbox.tsx +261 -261
  21. package/components/ui/OptimizedImage.tsx +369 -369
  22. package/components/ui/Select.tsx +240 -240
  23. package/components/ui/VirtualizedList.tsx +285 -0
  24. package/components/ui/index.ts +23 -18
  25. package/constants/config.ts +97 -54
  26. package/docs/adr/001-state-management.md +79 -79
  27. package/docs/adr/002-styling-approach.md +130 -130
  28. package/docs/adr/003-data-fetching.md +155 -155
  29. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  30. package/docs/adr/README.md +78 -78
  31. package/hooks/index.ts +27 -25
  32. package/hooks/useApi.ts +102 -5
  33. package/hooks/useAuth.tsx +82 -0
  34. package/hooks/useBiometrics.ts +295 -295
  35. package/hooks/useDeepLinking.ts +256 -256
  36. package/hooks/useMFA.ts +499 -0
  37. package/hooks/useNotifications.ts +39 -0
  38. package/hooks/useOffline.ts +32 -2
  39. package/hooks/usePerformance.ts +434 -434
  40. package/hooks/useTheme.tsx +76 -0
  41. package/hooks/useUpdates.ts +358 -358
  42. package/i18n/index.ts +194 -77
  43. package/i18n/locales/ar.json +101 -0
  44. package/i18n/locales/de.json +101 -0
  45. package/i18n/locales/en.json +101 -101
  46. package/i18n/locales/es.json +101 -0
  47. package/i18n/locales/fr.json +101 -101
  48. package/jest.config.js +4 -4
  49. package/maestro/README.md +113 -113
  50. package/maestro/config.yaml +35 -35
  51. package/maestro/flows/login.yaml +62 -62
  52. package/maestro/flows/mfa-login.yaml +92 -0
  53. package/maestro/flows/mfa-setup.yaml +86 -0
  54. package/maestro/flows/navigation.yaml +68 -68
  55. package/maestro/flows/offline-conflict.yaml +101 -0
  56. package/maestro/flows/offline-sync.yaml +128 -0
  57. package/maestro/flows/offline.yaml +60 -60
  58. package/maestro/flows/register.yaml +94 -94
  59. package/package.json +175 -170
  60. package/services/analytics.ts +428 -428
  61. package/services/api.ts +340 -340
  62. package/services/authAdapter.ts +333 -333
  63. package/services/backgroundSync.ts +626 -0
  64. package/services/index.ts +54 -22
  65. package/services/security.ts +229 -0
  66. package/tailwind.config.js +47 -47
  67. package/utils/accessibility.ts +446 -446
  68. package/utils/index.ts +52 -43
  69. package/utils/withAccessibility.tsx +272 -0
@@ -1,240 +1,240 @@
1
- import { useState, useCallback } from "react";
2
- import {
3
- View,
4
- Text,
5
- TouchableOpacity,
6
- Modal,
7
- FlatList,
8
- StyleSheet,
9
- } from "react-native";
10
- import { Ionicons } from "@expo/vector-icons";
11
- import { useTheme } from "@/hooks/useTheme";
12
- import { cn } from "@/utils/cn";
13
-
14
- export interface SelectOption<T = string> {
15
- label: string;
16
- value: T;
17
- disabled?: boolean;
18
- icon?: keyof typeof Ionicons.glyphMap;
19
- }
20
-
21
- interface SelectProps<T = string> {
22
- /**
23
- * Available options
24
- */
25
- options: SelectOption<T>[];
26
-
27
- /**
28
- * Currently selected value
29
- */
30
- value?: T;
31
-
32
- /**
33
- * Callback when value changes
34
- */
35
- onChange?: (value: T) => void;
36
-
37
- /**
38
- * Placeholder text when no value selected
39
- */
40
- placeholder?: string;
41
-
42
- /**
43
- * Label displayed above the select
44
- */
45
- label?: string;
46
-
47
- /**
48
- * Error message
49
- */
50
- error?: string;
51
-
52
- /**
53
- * Whether the select is disabled
54
- */
55
- disabled?: boolean;
56
-
57
- /**
58
- * Additional class name for container
59
- */
60
- className?: string;
61
-
62
- /**
63
- * Modal title
64
- */
65
- modalTitle?: string;
66
- }
67
-
68
- export function Select<T = string>({
69
- options,
70
- value,
71
- onChange,
72
- placeholder = "Select an option",
73
- label,
74
- error,
75
- disabled = false,
76
- className,
77
- modalTitle,
78
- }: SelectProps<T>) {
79
- const { isDark } = useTheme();
80
- const [isOpen, setIsOpen] = useState(false);
81
-
82
- const selectedOption = options.find((opt) => opt.value === value);
83
-
84
- const handleSelect = useCallback(
85
- (option: SelectOption<T>) => {
86
- if (option.disabled) return;
87
- onChange?.(option.value);
88
- setIsOpen(false);
89
- },
90
- [onChange]
91
- );
92
-
93
- const renderOption = ({ item }: { item: SelectOption<T> }) => {
94
- const isSelected = item.value === value;
95
-
96
- return (
97
- <TouchableOpacity
98
- onPress={() => handleSelect(item)}
99
- disabled={item.disabled}
100
- className={cn(
101
- "flex-row items-center px-4 py-3 border-b",
102
- isDark ? "border-surface-dark" : "border-gray-100",
103
- isSelected && (isDark ? "bg-surface-dark" : "bg-primary-50"),
104
- item.disabled && "opacity-50"
105
- )}
106
- activeOpacity={0.7}
107
- >
108
- {item.icon && (
109
- <Ionicons
110
- name={item.icon}
111
- size={20}
112
- color={isSelected ? "#10b981" : isDark ? "#94a3b8" : "#64748b"}
113
- style={styles.optionIcon}
114
- />
115
- )}
116
- <Text
117
- className={cn(
118
- "flex-1 text-base",
119
- isSelected
120
- ? "text-primary-600 font-medium"
121
- : isDark
122
- ? "text-text-dark"
123
- : "text-text-light"
124
- )}
125
- >
126
- {item.label}
127
- </Text>
128
- {isSelected && <Ionicons name="checkmark" size={20} color="#10b981" />}
129
- </TouchableOpacity>
130
- );
131
- };
132
-
133
- return (
134
- <View className={cn("mb-4", className)}>
135
- {label && (
136
- <Text
137
- className={cn(
138
- "text-sm font-medium mb-1.5",
139
- isDark ? "text-text-dark" : "text-text-light"
140
- )}
141
- >
142
- {label}
143
- </Text>
144
- )}
145
-
146
- <TouchableOpacity
147
- onPress={() => !disabled && setIsOpen(true)}
148
- disabled={disabled}
149
- className={cn(
150
- "flex-row items-center justify-between px-4 py-3 rounded-xl border",
151
- isDark
152
- ? "bg-surface-dark border-gray-700"
153
- : "bg-white border-gray-200",
154
- error && "border-red-500",
155
- disabled && "opacity-50"
156
- )}
157
- activeOpacity={0.7}
158
- >
159
- <Text
160
- className={cn(
161
- "text-base",
162
- selectedOption
163
- ? isDark
164
- ? "text-text-dark"
165
- : "text-text-light"
166
- : isDark
167
- ? "text-muted-dark"
168
- : "text-muted-light"
169
- )}
170
- >
171
- {selectedOption?.label || placeholder}
172
- </Text>
173
- <Ionicons
174
- name="chevron-down"
175
- size={20}
176
- color={isDark ? "#94a3b8" : "#64748b"}
177
- />
178
- </TouchableOpacity>
179
-
180
- {error && <Text className="text-red-500 text-sm mt-1">{error}</Text>}
181
-
182
- <Modal
183
- visible={isOpen}
184
- transparent
185
- animationType="slide"
186
- onRequestClose={() => setIsOpen(false)}
187
- >
188
- <View className="flex-1 justify-end bg-black/50">
189
- <View
190
- className={cn(
191
- "rounded-t-3xl max-h-[70%]",
192
- isDark ? "bg-background-dark" : "bg-white"
193
- )}
194
- >
195
- {/* Header */}
196
- <View
197
- className={cn(
198
- "flex-row items-center justify-between px-4 py-4 border-b",
199
- isDark ? "border-surface-dark" : "border-gray-100"
200
- )}
201
- >
202
- <Text
203
- className={cn(
204
- "text-lg font-semibold",
205
- isDark ? "text-text-dark" : "text-text-light"
206
- )}
207
- >
208
- {modalTitle || label || "Select"}
209
- </Text>
210
- <TouchableOpacity
211
- onPress={() => setIsOpen(false)}
212
- hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
213
- >
214
- <Ionicons
215
- name="close"
216
- size={24}
217
- color={isDark ? "#f8fafc" : "#0f172a"}
218
- />
219
- </TouchableOpacity>
220
- </View>
221
-
222
- {/* Options */}
223
- <FlatList
224
- data={options}
225
- renderItem={renderOption}
226
- keyExtractor={(item, index) => `${item.value}-${index}`}
227
- showsVerticalScrollIndicator={false}
228
- />
229
- </View>
230
- </View>
231
- </Modal>
232
- </View>
233
- );
234
- }
235
-
236
- const styles = StyleSheet.create({
237
- optionIcon: {
238
- marginRight: 12,
239
- },
240
- });
1
+ import { useState, useCallback } from "react";
2
+ import {
3
+ View,
4
+ Text,
5
+ TouchableOpacity,
6
+ Modal,
7
+ FlatList,
8
+ StyleSheet,
9
+ } from "react-native";
10
+ import { Ionicons } from "@expo/vector-icons";
11
+ import { useTheme } from "@/hooks/useTheme";
12
+ import { cn } from "@/utils/cn";
13
+
14
+ export interface SelectOption<T = string> {
15
+ label: string;
16
+ value: T;
17
+ disabled?: boolean;
18
+ icon?: keyof typeof Ionicons.glyphMap;
19
+ }
20
+
21
+ interface SelectProps<T = string> {
22
+ /**
23
+ * Available options
24
+ */
25
+ options: SelectOption<T>[];
26
+
27
+ /**
28
+ * Currently selected value
29
+ */
30
+ value?: T;
31
+
32
+ /**
33
+ * Callback when value changes
34
+ */
35
+ onChange?: (value: T) => void;
36
+
37
+ /**
38
+ * Placeholder text when no value selected
39
+ */
40
+ placeholder?: string;
41
+
42
+ /**
43
+ * Label displayed above the select
44
+ */
45
+ label?: string;
46
+
47
+ /**
48
+ * Error message
49
+ */
50
+ error?: string;
51
+
52
+ /**
53
+ * Whether the select is disabled
54
+ */
55
+ disabled?: boolean;
56
+
57
+ /**
58
+ * Additional class name for container
59
+ */
60
+ className?: string;
61
+
62
+ /**
63
+ * Modal title
64
+ */
65
+ modalTitle?: string;
66
+ }
67
+
68
+ export function Select<T = string>({
69
+ options,
70
+ value,
71
+ onChange,
72
+ placeholder = "Select an option",
73
+ label,
74
+ error,
75
+ disabled = false,
76
+ className,
77
+ modalTitle,
78
+ }: SelectProps<T>) {
79
+ const { isDark } = useTheme();
80
+ const [isOpen, setIsOpen] = useState(false);
81
+
82
+ const selectedOption = options.find((opt) => opt.value === value);
83
+
84
+ const handleSelect = useCallback(
85
+ (option: SelectOption<T>) => {
86
+ if (option.disabled) return;
87
+ onChange?.(option.value);
88
+ setIsOpen(false);
89
+ },
90
+ [onChange]
91
+ );
92
+
93
+ const renderOption = ({ item }: { item: SelectOption<T> }) => {
94
+ const isSelected = item.value === value;
95
+
96
+ return (
97
+ <TouchableOpacity
98
+ onPress={() => handleSelect(item)}
99
+ disabled={item.disabled}
100
+ className={cn(
101
+ "flex-row items-center px-4 py-3 border-b",
102
+ isDark ? "border-surface-dark" : "border-gray-100",
103
+ isSelected && (isDark ? "bg-surface-dark" : "bg-primary-50"),
104
+ item.disabled && "opacity-50"
105
+ )}
106
+ activeOpacity={0.7}
107
+ >
108
+ {item.icon && (
109
+ <Ionicons
110
+ name={item.icon}
111
+ size={20}
112
+ color={isSelected ? "#10b981" : isDark ? "#94a3b8" : "#64748b"}
113
+ style={styles.optionIcon}
114
+ />
115
+ )}
116
+ <Text
117
+ className={cn(
118
+ "flex-1 text-base",
119
+ isSelected
120
+ ? "text-primary-600 font-medium"
121
+ : isDark
122
+ ? "text-text-dark"
123
+ : "text-text-light"
124
+ )}
125
+ >
126
+ {item.label}
127
+ </Text>
128
+ {isSelected && <Ionicons name="checkmark" size={20} color="#10b981" />}
129
+ </TouchableOpacity>
130
+ );
131
+ };
132
+
133
+ return (
134
+ <View className={cn("mb-4", className)}>
135
+ {label && (
136
+ <Text
137
+ className={cn(
138
+ "text-sm font-medium mb-1.5",
139
+ isDark ? "text-text-dark" : "text-text-light"
140
+ )}
141
+ >
142
+ {label}
143
+ </Text>
144
+ )}
145
+
146
+ <TouchableOpacity
147
+ onPress={() => !disabled && setIsOpen(true)}
148
+ disabled={disabled}
149
+ className={cn(
150
+ "flex-row items-center justify-between px-4 py-3 rounded-xl border",
151
+ isDark
152
+ ? "bg-surface-dark border-gray-700"
153
+ : "bg-white border-gray-200",
154
+ error && "border-red-500",
155
+ disabled && "opacity-50"
156
+ )}
157
+ activeOpacity={0.7}
158
+ >
159
+ <Text
160
+ className={cn(
161
+ "text-base",
162
+ selectedOption
163
+ ? isDark
164
+ ? "text-text-dark"
165
+ : "text-text-light"
166
+ : isDark
167
+ ? "text-muted-dark"
168
+ : "text-muted-light"
169
+ )}
170
+ >
171
+ {selectedOption?.label || placeholder}
172
+ </Text>
173
+ <Ionicons
174
+ name="chevron-down"
175
+ size={20}
176
+ color={isDark ? "#94a3b8" : "#64748b"}
177
+ />
178
+ </TouchableOpacity>
179
+
180
+ {error && <Text className="text-red-500 text-sm mt-1">{error}</Text>}
181
+
182
+ <Modal
183
+ visible={isOpen}
184
+ transparent
185
+ animationType="slide"
186
+ onRequestClose={() => setIsOpen(false)}
187
+ >
188
+ <View className="flex-1 justify-end bg-black/50">
189
+ <View
190
+ className={cn(
191
+ "rounded-t-3xl max-h-[70%]",
192
+ isDark ? "bg-background-dark" : "bg-white"
193
+ )}
194
+ >
195
+ {/* Header */}
196
+ <View
197
+ className={cn(
198
+ "flex-row items-center justify-between px-4 py-4 border-b",
199
+ isDark ? "border-surface-dark" : "border-gray-100"
200
+ )}
201
+ >
202
+ <Text
203
+ className={cn(
204
+ "text-lg font-semibold",
205
+ isDark ? "text-text-dark" : "text-text-light"
206
+ )}
207
+ >
208
+ {modalTitle || label || "Select"}
209
+ </Text>
210
+ <TouchableOpacity
211
+ onPress={() => setIsOpen(false)}
212
+ hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
213
+ >
214
+ <Ionicons
215
+ name="close"
216
+ size={24}
217
+ color={isDark ? "#f8fafc" : "#0f172a"}
218
+ />
219
+ </TouchableOpacity>
220
+ </View>
221
+
222
+ {/* Options */}
223
+ <FlatList
224
+ data={options}
225
+ renderItem={renderOption}
226
+ keyExtractor={(item, index) => `${item.value}-${index}`}
227
+ showsVerticalScrollIndicator={false}
228
+ />
229
+ </View>
230
+ </View>
231
+ </Modal>
232
+ </View>
233
+ );
234
+ }
235
+
236
+ const styles = StyleSheet.create({
237
+ optionIcon: {
238
+ marginRight: 12,
239
+ },
240
+ });