@croacroa/react-native-template 2.0.1 → 3.2.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 (172) hide show
  1. package/.env.example +5 -0
  2. package/.eslintrc.js +8 -0
  3. package/.github/workflows/ci.yml +187 -187
  4. package/.github/workflows/eas-build.yml +55 -55
  5. package/.github/workflows/eas-update.yml +50 -50
  6. package/.github/workflows/npm-publish.yml +57 -0
  7. package/CHANGELOG.md +195 -106
  8. package/CONTRIBUTING.md +377 -377
  9. package/LICENSE +21 -0
  10. package/README.md +446 -399
  11. package/__tests__/accessibility/components.test.tsx +285 -0
  12. package/__tests__/components/Button.test.tsx +2 -4
  13. package/__tests__/components/__snapshots__/snapshots.test.tsx.snap +512 -0
  14. package/__tests__/components/snapshots.test.tsx +131 -131
  15. package/__tests__/helpers/a11y.ts +54 -0
  16. package/__tests__/hooks/useAnalytics.test.ts +100 -0
  17. package/__tests__/hooks/useAnimations.test.ts +70 -0
  18. package/__tests__/hooks/useAuth.test.tsx +71 -28
  19. package/__tests__/hooks/useMedia.test.ts +318 -0
  20. package/__tests__/hooks/usePayments.test.tsx +307 -0
  21. package/__tests__/hooks/usePermission.test.ts +230 -0
  22. package/__tests__/hooks/useWebSocket.test.ts +329 -0
  23. package/__tests__/integration/auth-api.test.tsx +224 -227
  24. package/__tests__/performance/VirtualizedList.perf.test.tsx +385 -362
  25. package/__tests__/services/api.test.ts +24 -6
  26. package/app/(auth)/home.tsx +11 -9
  27. package/app/(auth)/profile.tsx +8 -6
  28. package/app/(auth)/settings.tsx +11 -9
  29. package/app/(public)/forgot-password.tsx +25 -15
  30. package/app/(public)/login.tsx +48 -12
  31. package/app/(public)/onboarding.tsx +5 -5
  32. package/app/(public)/register.tsx +24 -15
  33. package/app/_layout.tsx +6 -3
  34. package/app.config.ts +27 -2
  35. package/assets/images/.gitkeep +7 -7
  36. package/assets/images/adaptive-icon.png +0 -0
  37. package/assets/images/favicon.png +0 -0
  38. package/assets/images/icon.png +0 -0
  39. package/assets/images/notification-icon.png +0 -0
  40. package/assets/images/splash.png +0 -0
  41. package/components/ErrorBoundary.tsx +73 -28
  42. package/components/auth/SocialLoginButtons.tsx +168 -0
  43. package/components/forms/FormInput.tsx +5 -3
  44. package/components/onboarding/OnboardingScreen.tsx +370 -370
  45. package/components/onboarding/index.ts +2 -2
  46. package/components/providers/AnalyticsProvider.tsx +67 -0
  47. package/components/providers/SuspenseBoundary.tsx +359 -357
  48. package/components/providers/index.ts +24 -21
  49. package/components/ui/AnimatedButton.tsx +1 -9
  50. package/components/ui/AnimatedList.tsx +98 -0
  51. package/components/ui/AnimatedScreen.tsx +89 -0
  52. package/components/ui/Avatar.tsx +319 -316
  53. package/components/ui/Badge.tsx +416 -416
  54. package/components/ui/BottomSheet.tsx +307 -307
  55. package/components/ui/Button.tsx +11 -3
  56. package/components/ui/Checkbox.tsx +261 -261
  57. package/components/ui/FeatureGate.tsx +57 -0
  58. package/components/ui/ForceUpdateScreen.tsx +108 -0
  59. package/components/ui/ImagePickerButton.tsx +180 -0
  60. package/components/ui/Input.stories.tsx +2 -10
  61. package/components/ui/Input.tsx +2 -10
  62. package/components/ui/OptimizedImage.tsx +369 -369
  63. package/components/ui/Paywall.tsx +253 -0
  64. package/components/ui/PermissionGate.tsx +155 -0
  65. package/components/ui/PurchaseButton.tsx +84 -0
  66. package/components/ui/Select.tsx +240 -240
  67. package/components/ui/Skeleton.tsx +3 -1
  68. package/components/ui/Toast.tsx +427 -0
  69. package/components/ui/UploadProgress.tsx +189 -0
  70. package/components/ui/VirtualizedList.tsx +288 -285
  71. package/components/ui/index.ts +28 -23
  72. package/constants/config.ts +135 -97
  73. package/docs/adr/001-state-management.md +79 -79
  74. package/docs/adr/002-styling-approach.md +130 -130
  75. package/docs/adr/003-data-fetching.md +155 -155
  76. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  77. package/docs/adr/README.md +78 -78
  78. package/docs/guides/analytics-posthog.md +121 -0
  79. package/docs/guides/auth-supabase.md +162 -0
  80. package/docs/guides/feature-flags-launchdarkly.md +150 -0
  81. package/docs/guides/payments-revenuecat.md +169 -0
  82. package/docs/plans/2026-02-22-phase6-implementation.md +3222 -0
  83. package/docs/plans/2026-02-22-phase6-template-completion-design.md +196 -0
  84. package/docs/plans/2026-02-23-npm-publish-design.md +31 -0
  85. package/docs/plans/2026-02-23-phase7-polish-documentation-design.md +79 -0
  86. package/docs/plans/2026-02-23-phase8-additional-features-design.md +136 -0
  87. package/eas.json +2 -1
  88. package/hooks/index.ts +70 -27
  89. package/hooks/useAnimatedEntry.ts +204 -0
  90. package/hooks/useApi.ts +64 -4
  91. package/hooks/useAuth.tsx +7 -3
  92. package/hooks/useBiometrics.ts +295 -295
  93. package/hooks/useChannel.ts +111 -0
  94. package/hooks/useDeepLinking.ts +256 -256
  95. package/hooks/useExperiment.ts +36 -0
  96. package/hooks/useFeatureFlag.ts +59 -0
  97. package/hooks/useForceUpdate.ts +91 -0
  98. package/hooks/useImagePicker.ts +281 -0
  99. package/hooks/useInAppReview.ts +64 -0
  100. package/hooks/useMFA.ts +509 -499
  101. package/hooks/useParallax.ts +142 -0
  102. package/hooks/usePerformance.ts +434 -434
  103. package/hooks/usePermission.ts +190 -0
  104. package/hooks/usePresence.ts +129 -0
  105. package/hooks/useProducts.ts +36 -0
  106. package/hooks/usePurchase.ts +103 -0
  107. package/hooks/useRateLimit.ts +70 -0
  108. package/hooks/useSubscription.ts +49 -0
  109. package/hooks/useTrackEvent.ts +52 -0
  110. package/hooks/useTrackScreen.ts +40 -0
  111. package/hooks/useUpdates.ts +358 -358
  112. package/hooks/useUpload.ts +165 -0
  113. package/hooks/useWebSocket.ts +111 -0
  114. package/i18n/index.ts +197 -194
  115. package/i18n/locales/ar.json +170 -101
  116. package/i18n/locales/de.json +170 -101
  117. package/i18n/locales/en.json +170 -101
  118. package/i18n/locales/es.json +170 -101
  119. package/i18n/locales/fr.json +170 -101
  120. package/jest.config.js +1 -1
  121. package/maestro/README.md +113 -113
  122. package/maestro/config.yaml +35 -35
  123. package/maestro/flows/login.yaml +62 -62
  124. package/maestro/flows/mfa-login.yaml +92 -92
  125. package/maestro/flows/mfa-setup.yaml +86 -86
  126. package/maestro/flows/navigation.yaml +68 -68
  127. package/maestro/flows/offline-conflict.yaml +101 -101
  128. package/maestro/flows/offline-sync.yaml +128 -128
  129. package/maestro/flows/offline.yaml +60 -60
  130. package/maestro/flows/register.yaml +94 -94
  131. package/package.json +188 -175
  132. package/scripts/generate-placeholders.js +38 -0
  133. package/services/analytics/adapters/console.ts +50 -0
  134. package/services/analytics/analytics-adapter.ts +94 -0
  135. package/services/analytics/types.ts +73 -0
  136. package/services/analytics.ts +428 -428
  137. package/services/api.ts +419 -340
  138. package/services/auth/social/apple.ts +110 -0
  139. package/services/auth/social/google.ts +159 -0
  140. package/services/auth/social/social-auth.ts +100 -0
  141. package/services/auth/social/types.ts +80 -0
  142. package/services/authAdapter.ts +333 -333
  143. package/services/backgroundSync.ts +652 -626
  144. package/services/feature-flags/adapters/mock.ts +108 -0
  145. package/services/feature-flags/feature-flag-adapter.ts +174 -0
  146. package/services/feature-flags/types.ts +79 -0
  147. package/services/force-update.ts +140 -0
  148. package/services/index.ts +116 -54
  149. package/services/media/compression.ts +91 -0
  150. package/services/media/media-picker.ts +151 -0
  151. package/services/media/media-upload.ts +160 -0
  152. package/services/payments/adapters/mock.ts +159 -0
  153. package/services/payments/payment-adapter.ts +118 -0
  154. package/services/payments/types.ts +131 -0
  155. package/services/permissions/permission-manager.ts +284 -0
  156. package/services/permissions/types.ts +104 -0
  157. package/services/realtime/types.ts +100 -0
  158. package/services/realtime/websocket-manager.ts +441 -0
  159. package/services/security.ts +289 -286
  160. package/services/sentry.ts +4 -4
  161. package/stores/appStore.ts +9 -0
  162. package/stores/notificationStore.ts +3 -1
  163. package/tailwind.config.js +47 -47
  164. package/tsconfig.json +37 -13
  165. package/types/user.ts +1 -1
  166. package/utils/accessibility.ts +446 -446
  167. package/utils/animations/presets.ts +182 -0
  168. package/utils/animations/transitions.ts +62 -0
  169. package/utils/index.ts +63 -52
  170. package/utils/toast.ts +9 -2
  171. package/utils/validation.ts +4 -1
  172. package/utils/withAccessibility.tsx +272 -272
@@ -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
+ });
@@ -90,7 +90,9 @@ export function SkeletonCircle({
90
90
  size = 48,
91
91
  className,
92
92
  ...props
93
- }: Omit<SkeletonProps, "width" | "height" | "borderRadius"> & { size?: number }) {
93
+ }: Omit<SkeletonProps, "width" | "height" | "borderRadius"> & {
94
+ size?: number;
95
+ }) {
94
96
  return (
95
97
  <Skeleton
96
98
  width={size}