@croacroa/react-native-template 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/.github/workflows/ci.yml +187 -184
  2. package/.github/workflows/eas-build.yml +55 -55
  3. package/.github/workflows/eas-update.yml +50 -50
  4. package/CHANGELOG.md +106 -106
  5. package/CONTRIBUTING.md +377 -377
  6. package/README.md +399 -399
  7. package/__tests__/components/snapshots.test.tsx +131 -0
  8. package/__tests__/integration/auth-api.test.tsx +227 -0
  9. package/__tests__/performance/VirtualizedList.perf.test.tsx +362 -0
  10. package/app/(public)/onboarding.tsx +5 -5
  11. package/app.config.ts +45 -2
  12. package/assets/images/.gitkeep +7 -7
  13. package/components/onboarding/OnboardingScreen.tsx +370 -370
  14. package/components/onboarding/index.ts +2 -2
  15. package/components/providers/SuspenseBoundary.tsx +357 -0
  16. package/components/providers/index.ts +13 -0
  17. package/components/ui/Avatar.tsx +316 -316
  18. package/components/ui/Badge.tsx +416 -416
  19. package/components/ui/BottomSheet.tsx +307 -307
  20. package/components/ui/Checkbox.tsx +261 -261
  21. package/components/ui/OptimizedImage.tsx +369 -369
  22. package/components/ui/Select.tsx +240 -240
  23. package/components/ui/VirtualizedList.tsx +285 -0
  24. package/components/ui/index.ts +23 -18
  25. package/constants/config.ts +97 -54
  26. package/docs/adr/001-state-management.md +79 -79
  27. package/docs/adr/002-styling-approach.md +130 -130
  28. package/docs/adr/003-data-fetching.md +155 -155
  29. package/docs/adr/004-auth-adapter-pattern.md +144 -144
  30. package/docs/adr/README.md +78 -78
  31. package/hooks/index.ts +27 -25
  32. package/hooks/useApi.ts +102 -5
  33. package/hooks/useAuth.tsx +82 -0
  34. package/hooks/useBiometrics.ts +295 -295
  35. package/hooks/useDeepLinking.ts +256 -256
  36. package/hooks/useMFA.ts +499 -0
  37. package/hooks/useNotifications.ts +39 -0
  38. package/hooks/useOffline.ts +32 -2
  39. package/hooks/usePerformance.ts +434 -434
  40. package/hooks/useTheme.tsx +76 -0
  41. package/hooks/useUpdates.ts +358 -358
  42. package/i18n/index.ts +194 -77
  43. package/i18n/locales/ar.json +101 -0
  44. package/i18n/locales/de.json +101 -0
  45. package/i18n/locales/en.json +101 -101
  46. package/i18n/locales/es.json +101 -0
  47. package/i18n/locales/fr.json +101 -101
  48. package/jest.config.js +4 -4
  49. package/maestro/README.md +113 -113
  50. package/maestro/config.yaml +35 -35
  51. package/maestro/flows/login.yaml +62 -62
  52. package/maestro/flows/mfa-login.yaml +92 -0
  53. package/maestro/flows/mfa-setup.yaml +86 -0
  54. package/maestro/flows/navigation.yaml +68 -68
  55. package/maestro/flows/offline-conflict.yaml +101 -0
  56. package/maestro/flows/offline-sync.yaml +128 -0
  57. package/maestro/flows/offline.yaml +60 -60
  58. package/maestro/flows/register.yaml +94 -94
  59. package/package.json +175 -170
  60. package/services/analytics.ts +428 -428
  61. package/services/api.ts +340 -340
  62. package/services/authAdapter.ts +333 -333
  63. package/services/backgroundSync.ts +626 -0
  64. package/services/index.ts +54 -22
  65. package/services/security.ts +229 -0
  66. package/tailwind.config.js +47 -47
  67. package/utils/accessibility.ts +446 -446
  68. package/utils/index.ts +52 -43
  69. package/utils/withAccessibility.tsx +272 -0
@@ -1,2 +1,2 @@
1
- export { OnboardingScreen } from "./OnboardingScreen";
2
- export type { OnboardingSlide } from "./OnboardingScreen";
1
+ export { OnboardingScreen } from "./OnboardingScreen";
2
+ export type { OnboardingSlide } from "./OnboardingScreen";
@@ -0,0 +1,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
+ 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 ErrorBoundary 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
+ <ErrorBoundary
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
+ </ErrorBoundary>
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
+ <ErrorBoundary fallback={errorFallback}>
302
+ <Suspense fallback={loadingFallback || <DefaultLoadingFallback />}>
303
+ {children}
304
+ </Suspense>
305
+ </ErrorBoundary>
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
+ <ErrorBoundary key={resetKey}>{children}</ErrorBoundary>
355
+ </BoundaryContext.Provider>
356
+ );
357
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @fileoverview Provider components for app-wide functionality
3
+ * @module components/providers
4
+ */
5
+
6
+ export {
7
+ ErrorBoundary,
8
+ SuspenseBoundary,
9
+ AsyncBoundary,
10
+ QueryBoundary,
11
+ BoundaryProvider,
12
+ useBoundary,
13
+ } from "./SuspenseBoundary";