@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,357 +1,359 @@
1
- /**
2
- * @fileoverview React 19-ready Suspense and Error Boundary components
3
- * Provides production-ready async UI patterns compatible with React 18 and 19.
4
- * @module components/providers/SuspenseBoundary
5
- */
6
-
7
- import React, {
8
- Suspense,
9
- Component,
10
- ReactNode,
11
- ErrorInfo,
12
- createContext,
13
- useContext,
14
- useCallback,
15
- useState,
16
- } from "react";
17
- import { View, Text, ActivityIndicator, Pressable } from "react-native";
18
- import { cn } from "@/utils/cn";
19
-
20
- // ============================================================================
21
- // Error Boundary
22
- // ============================================================================
23
-
24
- interface ErrorBoundaryState {
25
- hasError: boolean;
26
- error: Error | null;
27
- errorInfo: ErrorInfo | null;
28
- }
29
-
30
- interface ErrorBoundaryProps {
31
- children: ReactNode;
32
- /** Custom fallback component when error occurs */
33
- fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
34
- /** Callback when error is caught */
35
- onError?: (error: Error, errorInfo: ErrorInfo) => void;
36
- /** Whether to show error details in development */
37
- showDetails?: boolean;
38
- }
39
-
40
- /**
41
- * Error Boundary component for catching and handling React errors.
42
- * Compatible with React 18 and ready for React 19's improved error handling.
43
- *
44
- * @example
45
- * ```tsx
46
- * <ErrorBoundary
47
- * fallback={(error, reset) => (
48
- * <View>
49
- * <Text>Something went wrong</Text>
50
- * <Button onPress={reset}>Try Again</Button>
51
- * </View>
52
- * )}
53
- * onError={(error) => logToSentry(error)}
54
- * >
55
- * <MyComponent />
56
- * </ErrorBoundary>
57
- * ```
58
- */
59
- export class LocalErrorBoundary extends Component<
60
- ErrorBoundaryProps,
61
- ErrorBoundaryState
62
- > {
63
- constructor(props: ErrorBoundaryProps) {
64
- super(props);
65
- this.state = { hasError: false, error: null, errorInfo: null };
66
- }
67
-
68
- static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
69
- return { hasError: true, error };
70
- }
71
-
72
- componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
73
- this.setState({ errorInfo });
74
- this.props.onError?.(error, errorInfo);
75
-
76
- // Log to console in development
77
- if (__DEV__) {
78
- console.error("[ErrorBoundary] Caught error:", error);
79
- console.error("[ErrorBoundary] Component stack:", errorInfo.componentStack);
80
- }
81
- }
82
-
83
- reset = (): void => {
84
- this.setState({ hasError: false, error: null, errorInfo: null });
85
- };
86
-
87
- render(): ReactNode {
88
- const { hasError, error, errorInfo } = this.state;
89
- const { children, fallback, showDetails = __DEV__ } = this.props;
90
-
91
- if (hasError && error) {
92
- // Custom fallback
93
- if (fallback) {
94
- if (typeof fallback === "function") {
95
- return fallback(error, this.reset);
96
- }
97
- return fallback;
98
- }
99
-
100
- // Default fallback
101
- return (
102
- <View className="flex-1 items-center justify-center p-6 bg-red-50 dark:bg-red-900/20">
103
- <Text className="text-xl font-bold text-red-600 dark:text-red-400 mb-2">
104
- Something went wrong
105
- </Text>
106
- <Text className="text-sm text-red-500 dark:text-red-300 text-center mb-4">
107
- {error.message}
108
- </Text>
109
- {showDetails && errorInfo && (
110
- <View className="bg-white dark:bg-gray-800 p-3 rounded-lg mb-4 max-h-40">
111
- <Text className="text-xs font-mono text-gray-600 dark:text-gray-300">
112
- {errorInfo.componentStack?.slice(0, 500)}
113
- </Text>
114
- </View>
115
- )}
116
- <Pressable
117
- onPress={this.reset}
118
- className="bg-red-600 px-6 py-3 rounded-xl"
119
- accessibilityLabel="Try again"
120
- accessibilityRole="button"
121
- >
122
- <Text className="text-white font-semibold">Try Again</Text>
123
- </Pressable>
124
- </View>
125
- );
126
- }
127
-
128
- return children;
129
- }
130
- }
131
-
132
- // ============================================================================
133
- // Suspense Boundary
134
- // ============================================================================
135
-
136
- interface SuspenseBoundaryProps {
137
- children: ReactNode;
138
- /** Loading fallback component */
139
- fallback?: ReactNode;
140
- /** Minimum time to show loading state (prevents flash) */
141
- minLoadingMs?: number;
142
- /** Custom loading component */
143
- LoadingComponent?: React.ComponentType<{ message?: string }>;
144
- /** Loading message */
145
- loadingMessage?: string;
146
- }
147
-
148
- /**
149
- * Default loading component
150
- */
151
- function DefaultLoadingFallback({ message }: { message?: string }) {
152
- return (
153
- <View
154
- className="flex-1 items-center justify-center p-6"
155
- accessibilityLabel={message || "Loading"}
156
- accessibilityRole="progressbar"
157
- >
158
- <ActivityIndicator size="large" color="#3b82f6" />
159
- {message && (
160
- <Text className="mt-4 text-sm text-muted-light dark:text-muted-dark">
161
- {message}
162
- </Text>
163
- )}
164
- </View>
165
- );
166
- }
167
-
168
- /**
169
- * Enhanced Suspense Boundary with loading state management.
170
- * Ready for React 19's improved Suspense features.
171
- *
172
- * @example
173
- * ```tsx
174
- * <SuspenseBoundary
175
- * loadingMessage="Loading profile..."
176
- * minLoadingMs={300}
177
- * >
178
- * <ProfileContent />
179
- * </SuspenseBoundary>
180
- * ```
181
- */
182
- export function SuspenseBoundary({
183
- children,
184
- fallback,
185
- minLoadingMs,
186
- LoadingComponent = DefaultLoadingFallback,
187
- loadingMessage,
188
- }: SuspenseBoundaryProps) {
189
- const loadingFallback = fallback || (
190
- <LoadingComponent message={loadingMessage} />
191
- );
192
-
193
- // Note: minLoadingMs would require a custom implementation
194
- // React 19 may provide better APIs for this
195
-
196
- return <Suspense fallback={loadingFallback}>{children}</Suspense>;
197
- }
198
-
199
- // ============================================================================
200
- // Combined Boundary
201
- // ============================================================================
202
-
203
- interface AsyncBoundaryProps extends SuspenseBoundaryProps, ErrorBoundaryProps {
204
- /** Unique key to reset boundary on navigation */
205
- resetKey?: string | number;
206
- }
207
-
208
- /**
209
- * Combined Error + Suspense boundary for async components.
210
- * The recommended pattern for data fetching components.
211
- *
212
- * @example
213
- * ```tsx
214
- * <AsyncBoundary
215
- * loadingMessage="Loading data..."
216
- * fallback={(error, reset) => <ErrorView error={error} onRetry={reset} />}
217
- * onError={logError}
218
- * >
219
- * <DataFetchingComponent />
220
- * </AsyncBoundary>
221
- * ```
222
- */
223
- export function AsyncBoundary({
224
- children,
225
- fallback: errorFallback,
226
- onError,
227
- showDetails,
228
- loadingMessage,
229
- minLoadingMs,
230
- LoadingComponent,
231
- resetKey,
232
- fallback: loadingFallback,
233
- }: AsyncBoundaryProps) {
234
- return (
235
- <LocalErrorBoundary
236
- key={resetKey}
237
- fallback={errorFallback}
238
- onError={onError}
239
- showDetails={showDetails}
240
- >
241
- <SuspenseBoundary
242
- fallback={loadingFallback}
243
- loadingMessage={loadingMessage}
244
- minLoadingMs={minLoadingMs}
245
- LoadingComponent={LoadingComponent}
246
- >
247
- {children}
248
- </SuspenseBoundary>
249
- </LocalErrorBoundary>
250
- );
251
- }
252
-
253
- // ============================================================================
254
- // Query Boundary (React Query + Suspense)
255
- // ============================================================================
256
-
257
- interface QueryBoundaryProps {
258
- children: ReactNode;
259
- /** Loading state */
260
- loadingFallback?: ReactNode;
261
- /** Error state */
262
- errorFallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
263
- /** Empty state */
264
- emptyFallback?: ReactNode;
265
- /** Check if data is empty */
266
- isEmpty?: boolean;
267
- }
268
-
269
- /**
270
- * Specialized boundary for React Query with Suspense mode.
271
- * Handles loading, error, and empty states.
272
- *
273
- * @example
274
- * ```tsx
275
- * function UserList() {
276
- * const { data } = useSuspenseQuery(userQueryOptions);
277
- *
278
- * return (
279
- * <QueryBoundary
280
- * isEmpty={data.length === 0}
281
- * emptyFallback={<EmptyState message="No users found" />}
282
- * >
283
- * {data.map(user => <UserCard key={user.id} user={user} />)}
284
- * </QueryBoundary>
285
- * );
286
- * }
287
- * ```
288
- */
289
- export function QueryBoundary({
290
- children,
291
- loadingFallback,
292
- errorFallback,
293
- emptyFallback,
294
- isEmpty = false,
295
- }: QueryBoundaryProps) {
296
- if (isEmpty && emptyFallback) {
297
- return <>{emptyFallback}</>;
298
- }
299
-
300
- return (
301
- <LocalErrorBoundary fallback={errorFallback}>
302
- <Suspense fallback={loadingFallback || <DefaultLoadingFallback />}>
303
- {children}
304
- </Suspense>
305
- </LocalErrorBoundary>
306
- );
307
- }
308
-
309
- // ============================================================================
310
- // Context for Boundary Control
311
- // ============================================================================
312
-
313
- interface BoundaryContextValue {
314
- /** Reset all boundaries */
315
- resetAll: () => void;
316
- /** Report an error to parent boundaries */
317
- reportError: (error: Error) => void;
318
- }
319
-
320
- const BoundaryContext = createContext<BoundaryContextValue | null>(null);
321
-
322
- /**
323
- * Hook to access boundary controls from child components
324
- */
325
- export function useBoundary(): BoundaryContextValue {
326
- const context = useContext(BoundaryContext);
327
- if (!context) {
328
- // Return no-op functions if not in a boundary
329
- return {
330
- resetAll: () => {},
331
- reportError: () => {},
332
- };
333
- }
334
- return context;
335
- }
336
-
337
- /**
338
- * Provider for boundary controls
339
- */
340
- export function BoundaryProvider({ children }: { children: ReactNode }) {
341
- const [resetKey, setResetKey] = useState(0);
342
-
343
- const resetAll = useCallback(() => {
344
- setResetKey((k) => k + 1);
345
- }, []);
346
-
347
- const reportError = useCallback((error: Error) => {
348
- // Could be used to propagate errors to error tracking
349
- console.error("[BoundaryProvider] Error reported:", error);
350
- }, []);
351
-
352
- return (
353
- <BoundaryContext.Provider value={{ resetAll, reportError }}>
354
- <LocalErrorBoundary key={resetKey}>{children}</LocalErrorBoundary>
355
- </BoundaryContext.Provider>
356
- );
357
- }
1
+ /**
2
+ * @fileoverview React 19-ready Suspense and Error Boundary components
3
+ * Provides production-ready async UI patterns compatible with React 18 and 19.
4
+ * @module components/providers/SuspenseBoundary
5
+ */
6
+
7
+ import React, {
8
+ Suspense,
9
+ Component,
10
+ ReactNode,
11
+ ErrorInfo,
12
+ createContext,
13
+ useContext,
14
+ useCallback,
15
+ useState,
16
+ } from "react";
17
+ import { View, Text, ActivityIndicator, Pressable } from "react-native";
18
+
19
+ // ============================================================================
20
+ // Error Boundary
21
+ // ============================================================================
22
+
23
+ interface ErrorBoundaryState {
24
+ hasError: boolean;
25
+ error: Error | null;
26
+ errorInfo: ErrorInfo | null;
27
+ }
28
+
29
+ interface ErrorBoundaryProps {
30
+ children: ReactNode;
31
+ /** Custom fallback component when error occurs */
32
+ fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
33
+ /** Callback when error is caught */
34
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
35
+ /** Whether to show error details in development */
36
+ showDetails?: boolean;
37
+ }
38
+
39
+ /**
40
+ * Error Boundary component for catching and handling React errors.
41
+ * Compatible with React 18 and ready for React 19's improved error handling.
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * <ErrorBoundary
46
+ * fallback={(error, reset) => (
47
+ * <View>
48
+ * <Text>Something went wrong</Text>
49
+ * <Button onPress={reset}>Try Again</Button>
50
+ * </View>
51
+ * )}
52
+ * onError={(error) => logToSentry(error)}
53
+ * >
54
+ * <MyComponent />
55
+ * </ErrorBoundary>
56
+ * ```
57
+ */
58
+ export class LocalErrorBoundary extends Component<
59
+ ErrorBoundaryProps,
60
+ ErrorBoundaryState
61
+ > {
62
+ constructor(props: ErrorBoundaryProps) {
63
+ super(props);
64
+ this.state = { hasError: false, error: null, errorInfo: null };
65
+ }
66
+
67
+ static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
68
+ return { hasError: true, error };
69
+ }
70
+
71
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
72
+ this.setState({ errorInfo });
73
+ this.props.onError?.(error, errorInfo);
74
+
75
+ // Log to console in development
76
+ if (__DEV__) {
77
+ console.error("[ErrorBoundary] Caught error:", error);
78
+ console.error(
79
+ "[ErrorBoundary] Component stack:",
80
+ errorInfo.componentStack
81
+ );
82
+ }
83
+ }
84
+
85
+ reset = (): void => {
86
+ this.setState({ hasError: false, error: null, errorInfo: null });
87
+ };
88
+
89
+ render(): ReactNode {
90
+ const { hasError, error, errorInfo } = this.state;
91
+ const { children, fallback, showDetails = __DEV__ } = this.props;
92
+
93
+ if (hasError && error) {
94
+ // Custom fallback
95
+ if (fallback) {
96
+ if (typeof fallback === "function") {
97
+ return fallback(error, this.reset);
98
+ }
99
+ return fallback;
100
+ }
101
+
102
+ // Default fallback
103
+ return (
104
+ <View className="flex-1 items-center justify-center p-6 bg-red-50 dark:bg-red-900/20">
105
+ <Text className="text-xl font-bold text-red-600 dark:text-red-400 mb-2">
106
+ Something went wrong
107
+ </Text>
108
+ <Text className="text-sm text-red-500 dark:text-red-300 text-center mb-4">
109
+ {error.message}
110
+ </Text>
111
+ {showDetails && errorInfo && (
112
+ <View className="bg-white dark:bg-gray-800 p-3 rounded-lg mb-4 max-h-40">
113
+ <Text className="text-xs font-mono text-gray-600 dark:text-gray-300">
114
+ {errorInfo.componentStack?.slice(0, 500)}
115
+ </Text>
116
+ </View>
117
+ )}
118
+ <Pressable
119
+ onPress={this.reset}
120
+ className="bg-red-600 px-6 py-3 rounded-xl"
121
+ accessibilityLabel="Try again"
122
+ accessibilityRole="button"
123
+ >
124
+ <Text className="text-white font-semibold">Try Again</Text>
125
+ </Pressable>
126
+ </View>
127
+ );
128
+ }
129
+
130
+ return children;
131
+ }
132
+ }
133
+
134
+ // ============================================================================
135
+ // Suspense Boundary
136
+ // ============================================================================
137
+
138
+ interface SuspenseBoundaryProps {
139
+ children: ReactNode;
140
+ /** Loading fallback component */
141
+ fallback?: ReactNode;
142
+ /** Minimum time to show loading state (prevents flash) */
143
+ minLoadingMs?: number;
144
+ /** Custom loading component */
145
+ LoadingComponent?: React.ComponentType<{ message?: string }>;
146
+ /** Loading message */
147
+ loadingMessage?: string;
148
+ }
149
+
150
+ /**
151
+ * Default loading component
152
+ */
153
+ function DefaultLoadingFallback({ message }: { message?: string }) {
154
+ return (
155
+ <View
156
+ className="flex-1 items-center justify-center p-6"
157
+ accessibilityLabel={message || "Loading"}
158
+ accessibilityRole="progressbar"
159
+ >
160
+ <ActivityIndicator size="large" color="#3b82f6" />
161
+ {message && (
162
+ <Text className="mt-4 text-sm text-muted-light dark:text-muted-dark">
163
+ {message}
164
+ </Text>
165
+ )}
166
+ </View>
167
+ );
168
+ }
169
+
170
+ /**
171
+ * Enhanced Suspense Boundary with loading state management.
172
+ * Ready for React 19's improved Suspense features.
173
+ *
174
+ * @example
175
+ * ```tsx
176
+ * <SuspenseBoundary
177
+ * loadingMessage="Loading profile..."
178
+ * minLoadingMs={300}
179
+ * >
180
+ * <ProfileContent />
181
+ * </SuspenseBoundary>
182
+ * ```
183
+ */
184
+ export function SuspenseBoundary({
185
+ children,
186
+ fallback,
187
+ minLoadingMs: _minLoadingMs,
188
+ LoadingComponent = DefaultLoadingFallback,
189
+ loadingMessage,
190
+ }: SuspenseBoundaryProps) {
191
+ const loadingFallback = fallback || (
192
+ <LoadingComponent message={loadingMessage} />
193
+ );
194
+
195
+ // Note: minLoadingMs would require a custom implementation
196
+ // React 19 may provide better APIs for this
197
+
198
+ return <Suspense fallback={loadingFallback}>{children}</Suspense>;
199
+ }
200
+
201
+ // ============================================================================
202
+ // Combined Boundary
203
+ // ============================================================================
204
+
205
+ interface AsyncBoundaryProps extends SuspenseBoundaryProps, ErrorBoundaryProps {
206
+ /** Unique key to reset boundary on navigation */
207
+ resetKey?: string | number;
208
+ }
209
+
210
+ /**
211
+ * Combined Error + Suspense boundary for async components.
212
+ * The recommended pattern for data fetching components.
213
+ *
214
+ * @example
215
+ * ```tsx
216
+ * <AsyncBoundary
217
+ * loadingMessage="Loading data..."
218
+ * fallback={(error, reset) => <ErrorView error={error} onRetry={reset} />}
219
+ * onError={logError}
220
+ * >
221
+ * <DataFetchingComponent />
222
+ * </AsyncBoundary>
223
+ * ```
224
+ */
225
+ export function AsyncBoundary({
226
+ children,
227
+ fallback: errorFallback,
228
+ onError,
229
+ showDetails,
230
+ loadingMessage,
231
+ minLoadingMs,
232
+ LoadingComponent,
233
+ resetKey,
234
+ fallback: loadingFallback,
235
+ }: AsyncBoundaryProps) {
236
+ return (
237
+ <LocalErrorBoundary
238
+ key={resetKey}
239
+ fallback={errorFallback}
240
+ onError={onError}
241
+ showDetails={showDetails}
242
+ >
243
+ <SuspenseBoundary
244
+ fallback={loadingFallback}
245
+ loadingMessage={loadingMessage}
246
+ minLoadingMs={minLoadingMs}
247
+ LoadingComponent={LoadingComponent}
248
+ >
249
+ {children}
250
+ </SuspenseBoundary>
251
+ </LocalErrorBoundary>
252
+ );
253
+ }
254
+
255
+ // ============================================================================
256
+ // Query Boundary (React Query + Suspense)
257
+ // ============================================================================
258
+
259
+ interface QueryBoundaryProps {
260
+ children: ReactNode;
261
+ /** Loading state */
262
+ loadingFallback?: ReactNode;
263
+ /** Error state */
264
+ errorFallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
265
+ /** Empty state */
266
+ emptyFallback?: ReactNode;
267
+ /** Check if data is empty */
268
+ isEmpty?: boolean;
269
+ }
270
+
271
+ /**
272
+ * Specialized boundary for React Query with Suspense mode.
273
+ * Handles loading, error, and empty states.
274
+ *
275
+ * @example
276
+ * ```tsx
277
+ * function UserList() {
278
+ * const { data } = useSuspenseQuery(userQueryOptions);
279
+ *
280
+ * return (
281
+ * <QueryBoundary
282
+ * isEmpty={data.length === 0}
283
+ * emptyFallback={<EmptyState message="No users found" />}
284
+ * >
285
+ * {data.map(user => <UserCard key={user.id} user={user} />)}
286
+ * </QueryBoundary>
287
+ * );
288
+ * }
289
+ * ```
290
+ */
291
+ export function QueryBoundary({
292
+ children,
293
+ loadingFallback,
294
+ errorFallback,
295
+ emptyFallback,
296
+ isEmpty = false,
297
+ }: QueryBoundaryProps) {
298
+ if (isEmpty && emptyFallback) {
299
+ return <>{emptyFallback}</>;
300
+ }
301
+
302
+ return (
303
+ <LocalErrorBoundary fallback={errorFallback}>
304
+ <Suspense fallback={loadingFallback || <DefaultLoadingFallback />}>
305
+ {children}
306
+ </Suspense>
307
+ </LocalErrorBoundary>
308
+ );
309
+ }
310
+
311
+ // ============================================================================
312
+ // Context for Boundary Control
313
+ // ============================================================================
314
+
315
+ interface BoundaryContextValue {
316
+ /** Reset all boundaries */
317
+ resetAll: () => void;
318
+ /** Report an error to parent boundaries */
319
+ reportError: (error: Error) => void;
320
+ }
321
+
322
+ const BoundaryContext = createContext<BoundaryContextValue | null>(null);
323
+
324
+ /**
325
+ * Hook to access boundary controls from child components
326
+ */
327
+ export function useBoundary(): BoundaryContextValue {
328
+ const context = useContext(BoundaryContext);
329
+ if (!context) {
330
+ // Return no-op functions if not in a boundary
331
+ return {
332
+ resetAll: () => {},
333
+ reportError: () => {},
334
+ };
335
+ }
336
+ return context;
337
+ }
338
+
339
+ /**
340
+ * Provider for boundary controls
341
+ */
342
+ export function BoundaryProvider({ children }: { children: ReactNode }) {
343
+ const [resetKey, setResetKey] = useState(0);
344
+
345
+ const resetAll = useCallback(() => {
346
+ setResetKey((k) => k + 1);
347
+ }, []);
348
+
349
+ const reportError = useCallback((error: Error) => {
350
+ // Could be used to propagate errors to error tracking
351
+ console.error("[BoundaryProvider] Error reported:", error);
352
+ }, []);
353
+
354
+ return (
355
+ <BoundaryContext.Provider value={{ resetAll, reportError }}>
356
+ <LocalErrorBoundary key={resetKey}>{children}</LocalErrorBoundary>
357
+ </BoundaryContext.Provider>
358
+ );
359
+ }