@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,261 +1,261 @@
1
- import { TouchableOpacity, View, Text } from "react-native";
2
- import Animated, {
3
- useAnimatedStyle,
4
- withSpring,
5
- withTiming,
6
- interpolateColor,
7
- } from "react-native-reanimated";
8
- import { Ionicons } from "@expo/vector-icons";
9
- import { useTheme } from "@/hooks/useTheme";
10
- import { cn } from "@/utils/cn";
11
-
12
- interface CheckboxProps {
13
- /**
14
- * Whether the checkbox is checked
15
- */
16
- checked: boolean;
17
-
18
- /**
19
- * Callback when the checkbox is toggled
20
- */
21
- onChange: (checked: boolean) => void;
22
-
23
- /**
24
- * Label text
25
- */
26
- label?: string;
27
-
28
- /**
29
- * Description text (below label)
30
- */
31
- description?: string;
32
-
33
- /**
34
- * Whether the checkbox is disabled
35
- */
36
- disabled?: boolean;
37
-
38
- /**
39
- * Size variant
40
- */
41
- size?: "sm" | "md" | "lg";
42
-
43
- /**
44
- * Additional class name
45
- */
46
- className?: string;
47
-
48
- /**
49
- * Error message
50
- */
51
- error?: string;
52
- }
53
-
54
- const AnimatedView = Animated.createAnimatedComponent(View);
55
-
56
- const sizes = {
57
- sm: { box: 18, icon: 12, label: "text-sm" },
58
- md: { box: 22, icon: 16, label: "text-base" },
59
- lg: { box: 26, icon: 20, label: "text-lg" },
60
- };
61
-
62
- export function Checkbox({
63
- checked,
64
- onChange,
65
- label,
66
- description,
67
- disabled = false,
68
- size = "md",
69
- className,
70
- error,
71
- }: CheckboxProps) {
72
- const { isDark } = useTheme();
73
- const sizeConfig = sizes[size];
74
-
75
- const animatedBoxStyle = useAnimatedStyle(() => {
76
- const backgroundColor = interpolateColor(
77
- checked ? 1 : 0,
78
- [0, 1],
79
- ["transparent", "#10b981"]
80
- );
81
-
82
- const borderColor = interpolateColor(
83
- checked ? 1 : 0,
84
- [0, 1],
85
- [error ? "#ef4444" : isDark ? "#475569" : "#cbd5e1", "#10b981"]
86
- );
87
-
88
- return {
89
- backgroundColor: withTiming(backgroundColor, { duration: 150 }),
90
- borderColor: withTiming(borderColor, { duration: 150 }),
91
- transform: [
92
- {
93
- scale: withSpring(checked ? 1 : 0.95, {
94
- damping: 15,
95
- stiffness: 400,
96
- }),
97
- },
98
- ],
99
- };
100
- }, [checked, isDark, error]);
101
-
102
- const animatedCheckStyle = useAnimatedStyle(() => {
103
- return {
104
- opacity: withTiming(checked ? 1 : 0, { duration: 150 }),
105
- transform: [
106
- {
107
- scale: withSpring(checked ? 1 : 0.5, {
108
- damping: 15,
109
- stiffness: 400,
110
- }),
111
- },
112
- ],
113
- };
114
- }, [checked]);
115
-
116
- return (
117
- <TouchableOpacity
118
- onPress={() => !disabled && onChange(!checked)}
119
- disabled={disabled}
120
- activeOpacity={0.7}
121
- className={cn(
122
- "flex-row items-start",
123
- disabled && "opacity-50",
124
- className
125
- )}
126
- >
127
- <AnimatedView
128
- style={[
129
- animatedBoxStyle,
130
- {
131
- width: sizeConfig.box,
132
- height: sizeConfig.box,
133
- borderWidth: 2,
134
- borderRadius: 6,
135
- justifyContent: "center",
136
- alignItems: "center",
137
- marginTop: 2,
138
- },
139
- ]}
140
- >
141
- <Animated.View style={animatedCheckStyle}>
142
- <Ionicons name="checkmark" size={sizeConfig.icon} color="white" />
143
- </Animated.View>
144
- </AnimatedView>
145
-
146
- {(label || description) && (
147
- <View className="flex-1 ml-3">
148
- {label && (
149
- <Text
150
- className={cn(
151
- sizeConfig.label,
152
- isDark ? "text-text-dark" : "text-text-light",
153
- disabled && "opacity-70"
154
- )}
155
- >
156
- {label}
157
- </Text>
158
- )}
159
- {description && (
160
- <Text
161
- className={cn(
162
- "text-sm mt-0.5",
163
- isDark ? "text-muted-dark" : "text-muted-light"
164
- )}
165
- >
166
- {description}
167
- </Text>
168
- )}
169
- {error && <Text className="text-red-500 text-sm mt-1">{error}</Text>}
170
- </View>
171
- )}
172
- </TouchableOpacity>
173
- );
174
- }
175
-
176
- /**
177
- * Checkbox Group component for multiple checkboxes
178
- */
179
- interface CheckboxGroupProps<T extends string> {
180
- /**
181
- * Available options
182
- */
183
- options: {
184
- value: T;
185
- label: string;
186
- description?: string;
187
- disabled?: boolean;
188
- }[];
189
-
190
- /**
191
- * Currently selected values
192
- */
193
- value: T[];
194
-
195
- /**
196
- * Callback when selection changes
197
- */
198
- onChange: (value: T[]) => void;
199
-
200
- /**
201
- * Group label
202
- */
203
- label?: string;
204
-
205
- /**
206
- * Size variant
207
- */
208
- size?: "sm" | "md" | "lg";
209
-
210
- /**
211
- * Additional class name
212
- */
213
- className?: string;
214
- }
215
-
216
- export function CheckboxGroup<T extends string>({
217
- options,
218
- value,
219
- onChange,
220
- label,
221
- size = "md",
222
- className,
223
- }: CheckboxGroupProps<T>) {
224
- const { isDark } = useTheme();
225
-
226
- const handleToggle = (optionValue: T) => {
227
- if (value.includes(optionValue)) {
228
- onChange(value.filter((v) => v !== optionValue));
229
- } else {
230
- onChange([...value, optionValue]);
231
- }
232
- };
233
-
234
- return (
235
- <View className={className}>
236
- {label && (
237
- <Text
238
- className={cn(
239
- "text-sm font-medium mb-3",
240
- isDark ? "text-text-dark" : "text-text-light"
241
- )}
242
- >
243
- {label}
244
- </Text>
245
- )}
246
- <View className="gap-3">
247
- {options.map((option) => (
248
- <Checkbox
249
- key={option.value}
250
- checked={value.includes(option.value)}
251
- onChange={() => handleToggle(option.value)}
252
- label={option.label}
253
- description={option.description}
254
- disabled={option.disabled}
255
- size={size}
256
- />
257
- ))}
258
- </View>
259
- </View>
260
- );
261
- }
1
+ import { TouchableOpacity, View, Text } from "react-native";
2
+ import Animated, {
3
+ useAnimatedStyle,
4
+ withSpring,
5
+ withTiming,
6
+ interpolateColor,
7
+ } from "react-native-reanimated";
8
+ import { Ionicons } from "@expo/vector-icons";
9
+ import { useTheme } from "@/hooks/useTheme";
10
+ import { cn } from "@/utils/cn";
11
+
12
+ interface CheckboxProps {
13
+ /**
14
+ * Whether the checkbox is checked
15
+ */
16
+ checked: boolean;
17
+
18
+ /**
19
+ * Callback when the checkbox is toggled
20
+ */
21
+ onChange: (checked: boolean) => void;
22
+
23
+ /**
24
+ * Label text
25
+ */
26
+ label?: string;
27
+
28
+ /**
29
+ * Description text (below label)
30
+ */
31
+ description?: string;
32
+
33
+ /**
34
+ * Whether the checkbox is disabled
35
+ */
36
+ disabled?: boolean;
37
+
38
+ /**
39
+ * Size variant
40
+ */
41
+ size?: "sm" | "md" | "lg";
42
+
43
+ /**
44
+ * Additional class name
45
+ */
46
+ className?: string;
47
+
48
+ /**
49
+ * Error message
50
+ */
51
+ error?: string;
52
+ }
53
+
54
+ const AnimatedView = Animated.createAnimatedComponent(View);
55
+
56
+ const sizes = {
57
+ sm: { box: 18, icon: 12, label: "text-sm" },
58
+ md: { box: 22, icon: 16, label: "text-base" },
59
+ lg: { box: 26, icon: 20, label: "text-lg" },
60
+ };
61
+
62
+ export function Checkbox({
63
+ checked,
64
+ onChange,
65
+ label,
66
+ description,
67
+ disabled = false,
68
+ size = "md",
69
+ className,
70
+ error,
71
+ }: CheckboxProps) {
72
+ const { isDark } = useTheme();
73
+ const sizeConfig = sizes[size];
74
+
75
+ const animatedBoxStyle = useAnimatedStyle(() => {
76
+ const backgroundColor = interpolateColor(
77
+ checked ? 1 : 0,
78
+ [0, 1],
79
+ ["transparent", "#10b981"]
80
+ );
81
+
82
+ const borderColor = interpolateColor(
83
+ checked ? 1 : 0,
84
+ [0, 1],
85
+ [error ? "#ef4444" : isDark ? "#475569" : "#cbd5e1", "#10b981"]
86
+ );
87
+
88
+ return {
89
+ backgroundColor: withTiming(backgroundColor, { duration: 150 }),
90
+ borderColor: withTiming(borderColor, { duration: 150 }),
91
+ transform: [
92
+ {
93
+ scale: withSpring(checked ? 1 : 0.95, {
94
+ damping: 15,
95
+ stiffness: 400,
96
+ }),
97
+ },
98
+ ],
99
+ };
100
+ }, [checked, isDark, error]);
101
+
102
+ const animatedCheckStyle = useAnimatedStyle(() => {
103
+ return {
104
+ opacity: withTiming(checked ? 1 : 0, { duration: 150 }),
105
+ transform: [
106
+ {
107
+ scale: withSpring(checked ? 1 : 0.5, {
108
+ damping: 15,
109
+ stiffness: 400,
110
+ }),
111
+ },
112
+ ],
113
+ };
114
+ }, [checked]);
115
+
116
+ return (
117
+ <TouchableOpacity
118
+ onPress={() => !disabled && onChange(!checked)}
119
+ disabled={disabled}
120
+ activeOpacity={0.7}
121
+ className={cn(
122
+ "flex-row items-start",
123
+ disabled && "opacity-50",
124
+ className
125
+ )}
126
+ >
127
+ <AnimatedView
128
+ style={[
129
+ animatedBoxStyle,
130
+ {
131
+ width: sizeConfig.box,
132
+ height: sizeConfig.box,
133
+ borderWidth: 2,
134
+ borderRadius: 6,
135
+ justifyContent: "center",
136
+ alignItems: "center",
137
+ marginTop: 2,
138
+ },
139
+ ]}
140
+ >
141
+ <Animated.View style={animatedCheckStyle}>
142
+ <Ionicons name="checkmark" size={sizeConfig.icon} color="white" />
143
+ </Animated.View>
144
+ </AnimatedView>
145
+
146
+ {(label || description) && (
147
+ <View className="flex-1 ml-3">
148
+ {label && (
149
+ <Text
150
+ className={cn(
151
+ sizeConfig.label,
152
+ isDark ? "text-text-dark" : "text-text-light",
153
+ disabled && "opacity-70"
154
+ )}
155
+ >
156
+ {label}
157
+ </Text>
158
+ )}
159
+ {description && (
160
+ <Text
161
+ className={cn(
162
+ "text-sm mt-0.5",
163
+ isDark ? "text-muted-dark" : "text-muted-light"
164
+ )}
165
+ >
166
+ {description}
167
+ </Text>
168
+ )}
169
+ {error && <Text className="text-red-500 text-sm mt-1">{error}</Text>}
170
+ </View>
171
+ )}
172
+ </TouchableOpacity>
173
+ );
174
+ }
175
+
176
+ /**
177
+ * Checkbox Group component for multiple checkboxes
178
+ */
179
+ interface CheckboxGroupProps<T extends string> {
180
+ /**
181
+ * Available options
182
+ */
183
+ options: {
184
+ value: T;
185
+ label: string;
186
+ description?: string;
187
+ disabled?: boolean;
188
+ }[];
189
+
190
+ /**
191
+ * Currently selected values
192
+ */
193
+ value: T[];
194
+
195
+ /**
196
+ * Callback when selection changes
197
+ */
198
+ onChange: (value: T[]) => void;
199
+
200
+ /**
201
+ * Group label
202
+ */
203
+ label?: string;
204
+
205
+ /**
206
+ * Size variant
207
+ */
208
+ size?: "sm" | "md" | "lg";
209
+
210
+ /**
211
+ * Additional class name
212
+ */
213
+ className?: string;
214
+ }
215
+
216
+ export function CheckboxGroup<T extends string>({
217
+ options,
218
+ value,
219
+ onChange,
220
+ label,
221
+ size = "md",
222
+ className,
223
+ }: CheckboxGroupProps<T>) {
224
+ const { isDark } = useTheme();
225
+
226
+ const handleToggle = (optionValue: T) => {
227
+ if (value.includes(optionValue)) {
228
+ onChange(value.filter((v) => v !== optionValue));
229
+ } else {
230
+ onChange([...value, optionValue]);
231
+ }
232
+ };
233
+
234
+ return (
235
+ <View className={className}>
236
+ {label && (
237
+ <Text
238
+ className={cn(
239
+ "text-sm font-medium mb-3",
240
+ isDark ? "text-text-dark" : "text-text-light"
241
+ )}
242
+ >
243
+ {label}
244
+ </Text>
245
+ )}
246
+ <View className="gap-3">
247
+ {options.map((option) => (
248
+ <Checkbox
249
+ key={option.value}
250
+ checked={value.includes(option.value)}
251
+ onChange={() => handleToggle(option.value)}
252
+ label={option.label}
253
+ description={option.description}
254
+ disabled={option.disabled}
255
+ size={size}
256
+ />
257
+ ))}
258
+ </View>
259
+ </View>
260
+ );
261
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @fileoverview Feature-gated content component
3
+ * Renders children only when a feature flag is enabled,
4
+ * otherwise shows an optional fallback.
5
+ * @module components/ui/FeatureGate
6
+ */
7
+
8
+ import { ReactNode } from "react";
9
+
10
+ import { useFeatureFlag } from "@/hooks/useFeatureFlag";
11
+
12
+ // ============================================================================
13
+ // Props
14
+ // ============================================================================
15
+
16
+ interface FeatureGateProps {
17
+ /** The feature flag key to evaluate */
18
+ flag: string;
19
+ /** Content to render when the flag is enabled */
20
+ children: ReactNode;
21
+ /** Optional content to render when the flag is disabled */
22
+ fallback?: ReactNode;
23
+ }
24
+
25
+ // ============================================================================
26
+ // Component
27
+ // ============================================================================
28
+
29
+ /**
30
+ * Declarative component that shows or hides content based on a feature flag.
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * // Basic usage
35
+ * <FeatureGate flag="new_dashboard">
36
+ * <NewDashboard />
37
+ * </FeatureGate>
38
+ * ```
39
+ *
40
+ * @example
41
+ * ```tsx
42
+ * // With fallback
43
+ * <FeatureGate flag="redesigned_profile" fallback={<OldProfile />}>
44
+ * <NewProfile />
45
+ * </FeatureGate>
46
+ * ```
47
+ */
48
+ export function FeatureGate({
49
+ flag,
50
+ children,
51
+ fallback = null,
52
+ }: FeatureGateProps) {
53
+ const { isEnabled, isLoading } = useFeatureFlag(flag);
54
+
55
+ if (isLoading) return null;
56
+ return isEnabled ? <>{children}</> : <>{fallback}</>;
57
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * @fileoverview Full-screen non-dismissible force update screen
3
+ * Displayed when the running app version is below the server's minimum.
4
+ * @module components/ui/ForceUpdateScreen
5
+ */
6
+
7
+ import { View, Text, Pressable } from "react-native";
8
+ import { Ionicons } from "@expo/vector-icons";
9
+ import * as Linking from "expo-linking";
10
+ import { useTranslation } from "react-i18next";
11
+
12
+ // ============================================================================
13
+ // Types
14
+ // ============================================================================
15
+
16
+ interface ForceUpdateScreenProps {
17
+ /** Store URL to open when the user taps "Update Now" */
18
+ storeUrl: string;
19
+ /** The version currently running */
20
+ currentVersion: string;
21
+ /** The minimum version required by the server */
22
+ minimumVersion: string;
23
+ }
24
+
25
+ // ============================================================================
26
+ // Component
27
+ // ============================================================================
28
+
29
+ /**
30
+ * Full-screen, non-dismissible view that blocks the app until the user
31
+ * updates to the minimum required version.
32
+ *
33
+ * Renders as a top-level screen (not a Modal) so the user cannot navigate
34
+ * around it. The "Update Now" button opens the appropriate store URL.
35
+ *
36
+ * @example
37
+ * ```tsx
38
+ * if (isUpdateRequired) {
39
+ * return (
40
+ * <ForceUpdateScreen
41
+ * storeUrl={storeUrl}
42
+ * currentVersion={currentVersion}
43
+ * minimumVersion={minimumVersion}
44
+ * />
45
+ * );
46
+ * }
47
+ * ```
48
+ */
49
+ export function ForceUpdateScreen({
50
+ storeUrl,
51
+ currentVersion,
52
+ minimumVersion,
53
+ }: ForceUpdateScreenProps) {
54
+ const { t } = useTranslation();
55
+
56
+ const handleUpdate = () => {
57
+ Linking.openURL(storeUrl);
58
+ };
59
+
60
+ return (
61
+ <View className="flex-1 items-center justify-center bg-white px-8 dark:bg-gray-900">
62
+ {/* Icon */}
63
+ <View className="mb-8 h-24 w-24 items-center justify-center rounded-full bg-primary-100 dark:bg-primary-900">
64
+ <Ionicons name="cloud-download-outline" size={48} color="#3b82f6" />
65
+ </View>
66
+
67
+ {/* Title */}
68
+ <Text className="mb-4 text-center text-2xl font-bold text-gray-900 dark:text-white">
69
+ {t("forceUpdate.title")}
70
+ </Text>
71
+
72
+ {/* Message */}
73
+ <Text className="mb-8 text-center text-base leading-6 text-gray-600 dark:text-gray-400">
74
+ {t("forceUpdate.message")}
75
+ </Text>
76
+
77
+ {/* Version info */}
78
+ <View className="mb-8 w-full rounded-xl bg-gray-100 p-4 dark:bg-gray-800">
79
+ <View className="mb-2 flex-row items-center justify-between">
80
+ <Text className="text-sm text-gray-500 dark:text-gray-400">
81
+ {t("forceUpdate.currentVersion")}
82
+ </Text>
83
+ <Text className="text-sm font-semibold text-gray-700 dark:text-gray-300">
84
+ {currentVersion}
85
+ </Text>
86
+ </View>
87
+ <View className="flex-row items-center justify-between">
88
+ <Text className="text-sm text-gray-500 dark:text-gray-400">
89
+ {t("forceUpdate.minimumVersion")}
90
+ </Text>
91
+ <Text className="text-sm font-semibold text-gray-700 dark:text-gray-300">
92
+ {minimumVersion}
93
+ </Text>
94
+ </View>
95
+ </View>
96
+
97
+ {/* Update button */}
98
+ <Pressable
99
+ onPress={handleUpdate}
100
+ className="w-full items-center rounded-xl bg-primary-600 px-6 py-4 active:bg-primary-700"
101
+ >
102
+ <Text className="text-lg font-semibold text-white">
103
+ {t("forceUpdate.button")}
104
+ </Text>
105
+ </Pressable>
106
+ </View>
107
+ );
108
+ }