@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,358 +1,358 @@
1
- import { useEffect, useState, useCallback } from "react";
2
- import * as Updates from "expo-updates";
3
- import { Alert, AppState, AppStateStatus } from "react-native";
4
- import { IS_DEV } from "@/constants/config";
5
-
6
- export type UpdateStatus =
7
- | "idle"
8
- | "checking"
9
- | "available"
10
- | "downloading"
11
- | "ready"
12
- | "error"
13
- | "no-update";
14
-
15
- interface UpdateInfo {
16
- /**
17
- * Whether an update is available
18
- */
19
- isAvailable: boolean;
20
-
21
- /**
22
- * Current update status
23
- */
24
- status: UpdateStatus;
25
-
26
- /**
27
- * Download progress (0-100)
28
- */
29
- progress: number;
30
-
31
- /**
32
- * Error message if status is 'error'
33
- */
34
- error: string | null;
35
-
36
- /**
37
- * Update manifest info
38
- */
39
- manifest: Updates.Manifest | null;
40
- }
41
-
42
- interface UseUpdatesOptions {
43
- /**
44
- * Check for updates on mount
45
- * @default true
46
- */
47
- checkOnMount?: boolean;
48
-
49
- /**
50
- * Check for updates when app returns to foreground
51
- * @default true
52
- */
53
- checkOnForeground?: boolean;
54
-
55
- /**
56
- * Show alert when update is available
57
- * @default true
58
- */
59
- showAlert?: boolean;
60
-
61
- /**
62
- * Auto download and apply updates
63
- * @default false
64
- */
65
- autoUpdate?: boolean;
66
-
67
- /**
68
- * Callback when update is available
69
- */
70
- onUpdateAvailable?: (manifest: Updates.Manifest) => void;
71
-
72
- /**
73
- * Callback when update is downloaded and ready
74
- */
75
- onUpdateReady?: () => void;
76
-
77
- /**
78
- * Callback on error
79
- */
80
- onError?: (error: Error) => void;
81
- }
82
-
83
- interface UseUpdatesReturn extends UpdateInfo {
84
- /**
85
- * Manually check for updates
86
- */
87
- checkForUpdate: () => Promise<boolean>;
88
-
89
- /**
90
- * Download the available update
91
- */
92
- downloadUpdate: () => Promise<void>;
93
-
94
- /**
95
- * Apply the downloaded update (restarts the app)
96
- */
97
- applyUpdate: () => Promise<void>;
98
-
99
- /**
100
- * Reset update state
101
- */
102
- reset: () => void;
103
- }
104
-
105
- /**
106
- * Hook for managing OTA updates with expo-updates
107
- *
108
- * @example
109
- * ```tsx
110
- * function App() {
111
- * const { status, isAvailable, checkForUpdate, applyUpdate } = useUpdates({
112
- * checkOnMount: true,
113
- * showAlert: true,
114
- * });
115
- *
116
- * if (status === 'ready') {
117
- * return (
118
- * <Button onPress={applyUpdate}>
119
- * Restart to update
120
- * </Button>
121
- * );
122
- * }
123
- *
124
- * return <App />;
125
- * }
126
- * ```
127
- */
128
- export function useUpdates(options: UseUpdatesOptions = {}): UseUpdatesReturn {
129
- const {
130
- checkOnMount = true,
131
- checkOnForeground = true,
132
- showAlert = true,
133
- autoUpdate = false,
134
- onUpdateAvailable,
135
- onUpdateReady,
136
- onError,
137
- } = options;
138
-
139
- const [status, setStatus] = useState<UpdateStatus>("idle");
140
- const [progress, setProgress] = useState(0);
141
- const [error, setError] = useState<string | null>(null);
142
- const [manifest, setManifest] = useState<Updates.Manifest | null>(null);
143
-
144
- const isAvailable =
145
- status === "available" || status === "downloading" || status === "ready";
146
-
147
- /**
148
- * Download the available update
149
- */
150
- const downloadUpdate = useCallback(async (): Promise<void> => {
151
- if (IS_DEV || !Updates.isEnabled) {
152
- return;
153
- }
154
-
155
- try {
156
- setStatus("downloading");
157
- setProgress(0);
158
-
159
- const progressInterval = setInterval(() => {
160
- setProgress((p) => Math.min(p + 10, 90));
161
- }, 200);
162
-
163
- await Updates.fetchUpdateAsync();
164
-
165
- clearInterval(progressInterval);
166
- setProgress(100);
167
- setStatus("ready");
168
- onUpdateReady?.();
169
- } catch (e) {
170
- const err = e as Error;
171
- setStatus("error");
172
- setError(err.message);
173
- onError?.(err);
174
- }
175
- }, [onUpdateReady, onError]);
176
-
177
- /**
178
- * Apply the downloaded update (restarts the app)
179
- */
180
- const applyUpdate = useCallback(async (): Promise<void> => {
181
- if (IS_DEV || !Updates.isEnabled) {
182
- return;
183
- }
184
-
185
- try {
186
- await Updates.reloadAsync();
187
- } catch (e) {
188
- const err = e as Error;
189
- setStatus("error");
190
- setError(err.message);
191
- onError?.(err);
192
- }
193
- }, [onError]);
194
-
195
- /**
196
- * Check for available updates
197
- */
198
- const checkForUpdate = useCallback(async (): Promise<boolean> => {
199
- // Skip in development
200
- if (IS_DEV || !Updates.isEnabled) {
201
- return false;
202
- }
203
-
204
- try {
205
- setStatus("checking");
206
- setError(null);
207
-
208
- const update = await Updates.checkForUpdateAsync();
209
-
210
- if (update.isAvailable) {
211
- setStatus("available");
212
- setManifest(update.manifest);
213
- onUpdateAvailable?.(update.manifest);
214
-
215
- if (showAlert && !autoUpdate) {
216
- Alert.alert(
217
- "Update Available",
218
- "A new version of the app is available. Would you like to update now?",
219
- [
220
- { text: "Later", style: "cancel" },
221
- {
222
- text: "Update",
223
- onPress: () => downloadUpdate(),
224
- },
225
- ]
226
- );
227
- }
228
-
229
- if (autoUpdate) {
230
- await downloadUpdate();
231
- }
232
-
233
- return true;
234
- } else {
235
- setStatus("no-update");
236
- return false;
237
- }
238
- } catch (e) {
239
- const err = e as Error;
240
- setStatus("error");
241
- setError(err.message);
242
- onError?.(err);
243
- return false;
244
- }
245
- }, [
246
- showAlert,
247
- autoUpdate,
248
- onUpdateAvailable,
249
- onError,
250
- downloadUpdate,
251
- applyUpdate,
252
- ]);
253
-
254
- /**
255
- * Reset update state
256
- */
257
- const reset = useCallback(() => {
258
- setStatus("idle");
259
- setProgress(0);
260
- setError(null);
261
- setManifest(null);
262
- }, []);
263
-
264
- // Check on mount
265
- useEffect(() => {
266
- if (checkOnMount) {
267
- checkForUpdate();
268
- }
269
- // eslint-disable-next-line react-hooks/exhaustive-deps
270
- }, [checkOnMount]);
271
-
272
- // Check when app returns to foreground
273
- useEffect(() => {
274
- if (!checkOnForeground) return;
275
-
276
- const handleAppStateChange = (nextState: AppStateStatus) => {
277
- if (nextState === "active") {
278
- checkForUpdate();
279
- }
280
- };
281
-
282
- const subscription = AppState.addEventListener(
283
- "change",
284
- handleAppStateChange
285
- );
286
- return () => subscription.remove();
287
- }, [checkOnForeground, checkForUpdate]);
288
-
289
- return {
290
- isAvailable,
291
- status,
292
- progress,
293
- error,
294
- manifest,
295
- checkForUpdate,
296
- downloadUpdate,
297
- applyUpdate,
298
- reset,
299
- };
300
- }
301
-
302
- /**
303
- * Get current update info
304
- */
305
- export function getUpdateInfo(): {
306
- isEnabled: boolean;
307
- channel: string | null;
308
- runtimeVersion: string | null;
309
- updateId: string | null;
310
- createdAt: Date | null;
311
- } {
312
- if (IS_DEV || !Updates.isEnabled) {
313
- return {
314
- isEnabled: false,
315
- channel: null,
316
- runtimeVersion: null,
317
- updateId: null,
318
- createdAt: null,
319
- };
320
- }
321
-
322
- return {
323
- isEnabled: Updates.isEnabled,
324
- channel: Updates.channel,
325
- runtimeVersion: Updates.runtimeVersion,
326
- updateId: Updates.updateId,
327
- createdAt: Updates.createdAt,
328
- };
329
- }
330
-
331
- /**
332
- * Force check and apply update (useful for settings screen)
333
- */
334
- export async function forceUpdate(): Promise<void> {
335
- if (IS_DEV || !Updates.isEnabled) {
336
- Alert.alert(
337
- "Development Mode",
338
- "Updates are not available in development."
339
- );
340
- return;
341
- }
342
-
343
- try {
344
- const update = await Updates.checkForUpdateAsync();
345
- if (update.isAvailable) {
346
- Alert.alert("Updating...", "Downloading the latest version.");
347
- await Updates.fetchUpdateAsync();
348
- await Updates.reloadAsync();
349
- } else {
350
- Alert.alert("Up to Date", "You're running the latest version.");
351
- }
352
- } catch (error) {
353
- Alert.alert(
354
- "Update Failed",
355
- "Could not check for updates. Please try again."
356
- );
357
- }
358
- }
1
+ import { useEffect, useState, useCallback } from "react";
2
+ import * as Updates from "expo-updates";
3
+ import { Alert, AppState, AppStateStatus } from "react-native";
4
+ import { IS_DEV } from "@/constants/config";
5
+
6
+ export type UpdateStatus =
7
+ | "idle"
8
+ | "checking"
9
+ | "available"
10
+ | "downloading"
11
+ | "ready"
12
+ | "error"
13
+ | "no-update";
14
+
15
+ interface UpdateInfo {
16
+ /**
17
+ * Whether an update is available
18
+ */
19
+ isAvailable: boolean;
20
+
21
+ /**
22
+ * Current update status
23
+ */
24
+ status: UpdateStatus;
25
+
26
+ /**
27
+ * Download progress (0-100)
28
+ */
29
+ progress: number;
30
+
31
+ /**
32
+ * Error message if status is 'error'
33
+ */
34
+ error: string | null;
35
+
36
+ /**
37
+ * Update manifest info
38
+ */
39
+ manifest: Updates.Manifest | null;
40
+ }
41
+
42
+ interface UseUpdatesOptions {
43
+ /**
44
+ * Check for updates on mount
45
+ * @default true
46
+ */
47
+ checkOnMount?: boolean;
48
+
49
+ /**
50
+ * Check for updates when app returns to foreground
51
+ * @default true
52
+ */
53
+ checkOnForeground?: boolean;
54
+
55
+ /**
56
+ * Show alert when update is available
57
+ * @default true
58
+ */
59
+ showAlert?: boolean;
60
+
61
+ /**
62
+ * Auto download and apply updates
63
+ * @default false
64
+ */
65
+ autoUpdate?: boolean;
66
+
67
+ /**
68
+ * Callback when update is available
69
+ */
70
+ onUpdateAvailable?: (manifest: Updates.Manifest) => void;
71
+
72
+ /**
73
+ * Callback when update is downloaded and ready
74
+ */
75
+ onUpdateReady?: () => void;
76
+
77
+ /**
78
+ * Callback on error
79
+ */
80
+ onError?: (error: Error) => void;
81
+ }
82
+
83
+ interface UseUpdatesReturn extends UpdateInfo {
84
+ /**
85
+ * Manually check for updates
86
+ */
87
+ checkForUpdate: () => Promise<boolean>;
88
+
89
+ /**
90
+ * Download the available update
91
+ */
92
+ downloadUpdate: () => Promise<void>;
93
+
94
+ /**
95
+ * Apply the downloaded update (restarts the app)
96
+ */
97
+ applyUpdate: () => Promise<void>;
98
+
99
+ /**
100
+ * Reset update state
101
+ */
102
+ reset: () => void;
103
+ }
104
+
105
+ /**
106
+ * Hook for managing OTA updates with expo-updates
107
+ *
108
+ * @example
109
+ * ```tsx
110
+ * function App() {
111
+ * const { status, isAvailable, checkForUpdate, applyUpdate } = useUpdates({
112
+ * checkOnMount: true,
113
+ * showAlert: true,
114
+ * });
115
+ *
116
+ * if (status === 'ready') {
117
+ * return (
118
+ * <Button onPress={applyUpdate}>
119
+ * Restart to update
120
+ * </Button>
121
+ * );
122
+ * }
123
+ *
124
+ * return <App />;
125
+ * }
126
+ * ```
127
+ */
128
+ export function useUpdates(options: UseUpdatesOptions = {}): UseUpdatesReturn {
129
+ const {
130
+ checkOnMount = true,
131
+ checkOnForeground = true,
132
+ showAlert = true,
133
+ autoUpdate = false,
134
+ onUpdateAvailable,
135
+ onUpdateReady,
136
+ onError,
137
+ } = options;
138
+
139
+ const [status, setStatus] = useState<UpdateStatus>("idle");
140
+ const [progress, setProgress] = useState(0);
141
+ const [error, setError] = useState<string | null>(null);
142
+ const [manifest, setManifest] = useState<Updates.Manifest | null>(null);
143
+
144
+ const isAvailable =
145
+ status === "available" || status === "downloading" || status === "ready";
146
+
147
+ /**
148
+ * Download the available update
149
+ */
150
+ const downloadUpdate = useCallback(async (): Promise<void> => {
151
+ if (IS_DEV || !Updates.isEnabled) {
152
+ return;
153
+ }
154
+
155
+ try {
156
+ setStatus("downloading");
157
+ setProgress(0);
158
+
159
+ const progressInterval = setInterval(() => {
160
+ setProgress((p) => Math.min(p + 10, 90));
161
+ }, 200);
162
+
163
+ await Updates.fetchUpdateAsync();
164
+
165
+ clearInterval(progressInterval);
166
+ setProgress(100);
167
+ setStatus("ready");
168
+ onUpdateReady?.();
169
+ } catch (e) {
170
+ const err = e as Error;
171
+ setStatus("error");
172
+ setError(err.message);
173
+ onError?.(err);
174
+ }
175
+ }, [onUpdateReady, onError]);
176
+
177
+ /**
178
+ * Apply the downloaded update (restarts the app)
179
+ */
180
+ const applyUpdate = useCallback(async (): Promise<void> => {
181
+ if (IS_DEV || !Updates.isEnabled) {
182
+ return;
183
+ }
184
+
185
+ try {
186
+ await Updates.reloadAsync();
187
+ } catch (e) {
188
+ const err = e as Error;
189
+ setStatus("error");
190
+ setError(err.message);
191
+ onError?.(err);
192
+ }
193
+ }, [onError]);
194
+
195
+ /**
196
+ * Check for available updates
197
+ */
198
+ const checkForUpdate = useCallback(async (): Promise<boolean> => {
199
+ // Skip in development
200
+ if (IS_DEV || !Updates.isEnabled) {
201
+ return false;
202
+ }
203
+
204
+ try {
205
+ setStatus("checking");
206
+ setError(null);
207
+
208
+ const update = await Updates.checkForUpdateAsync();
209
+
210
+ if (update.isAvailable) {
211
+ setStatus("available");
212
+ setManifest(update.manifest);
213
+ onUpdateAvailable?.(update.manifest);
214
+
215
+ if (showAlert && !autoUpdate) {
216
+ Alert.alert(
217
+ "Update Available",
218
+ "A new version of the app is available. Would you like to update now?",
219
+ [
220
+ { text: "Later", style: "cancel" },
221
+ {
222
+ text: "Update",
223
+ onPress: () => downloadUpdate(),
224
+ },
225
+ ]
226
+ );
227
+ }
228
+
229
+ if (autoUpdate) {
230
+ await downloadUpdate();
231
+ }
232
+
233
+ return true;
234
+ } else {
235
+ setStatus("no-update");
236
+ return false;
237
+ }
238
+ } catch (e) {
239
+ const err = e as Error;
240
+ setStatus("error");
241
+ setError(err.message);
242
+ onError?.(err);
243
+ return false;
244
+ }
245
+ }, [
246
+ showAlert,
247
+ autoUpdate,
248
+ onUpdateAvailable,
249
+ onError,
250
+ downloadUpdate,
251
+ applyUpdate,
252
+ ]);
253
+
254
+ /**
255
+ * Reset update state
256
+ */
257
+ const reset = useCallback(() => {
258
+ setStatus("idle");
259
+ setProgress(0);
260
+ setError(null);
261
+ setManifest(null);
262
+ }, []);
263
+
264
+ // Check on mount
265
+ useEffect(() => {
266
+ if (checkOnMount) {
267
+ checkForUpdate();
268
+ }
269
+ // eslint-disable-next-line react-hooks/exhaustive-deps
270
+ }, [checkOnMount]);
271
+
272
+ // Check when app returns to foreground
273
+ useEffect(() => {
274
+ if (!checkOnForeground) return;
275
+
276
+ const handleAppStateChange = (nextState: AppStateStatus) => {
277
+ if (nextState === "active") {
278
+ checkForUpdate();
279
+ }
280
+ };
281
+
282
+ const subscription = AppState.addEventListener(
283
+ "change",
284
+ handleAppStateChange
285
+ );
286
+ return () => subscription.remove();
287
+ }, [checkOnForeground, checkForUpdate]);
288
+
289
+ return {
290
+ isAvailable,
291
+ status,
292
+ progress,
293
+ error,
294
+ manifest,
295
+ checkForUpdate,
296
+ downloadUpdate,
297
+ applyUpdate,
298
+ reset,
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Get current update info
304
+ */
305
+ export function getUpdateInfo(): {
306
+ isEnabled: boolean;
307
+ channel: string | null;
308
+ runtimeVersion: string | null;
309
+ updateId: string | null;
310
+ createdAt: Date | null;
311
+ } {
312
+ if (IS_DEV || !Updates.isEnabled) {
313
+ return {
314
+ isEnabled: false,
315
+ channel: null,
316
+ runtimeVersion: null,
317
+ updateId: null,
318
+ createdAt: null,
319
+ };
320
+ }
321
+
322
+ return {
323
+ isEnabled: Updates.isEnabled,
324
+ channel: Updates.channel,
325
+ runtimeVersion: Updates.runtimeVersion,
326
+ updateId: Updates.updateId,
327
+ createdAt: Updates.createdAt,
328
+ };
329
+ }
330
+
331
+ /**
332
+ * Force check and apply update (useful for settings screen)
333
+ */
334
+ export async function forceUpdate(): Promise<void> {
335
+ if (IS_DEV || !Updates.isEnabled) {
336
+ Alert.alert(
337
+ "Development Mode",
338
+ "Updates are not available in development."
339
+ );
340
+ return;
341
+ }
342
+
343
+ try {
344
+ const update = await Updates.checkForUpdateAsync();
345
+ if (update.isAvailable) {
346
+ Alert.alert("Updating...", "Downloading the latest version.");
347
+ await Updates.fetchUpdateAsync();
348
+ await Updates.reloadAsync();
349
+ } else {
350
+ Alert.alert("Up to Date", "You're running the latest version.");
351
+ }
352
+ } catch {
353
+ Alert.alert(
354
+ "Update Failed",
355
+ "Could not check for updates. Please try again."
356
+ );
357
+ }
358
+ }