@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.
- package/.github/workflows/ci.yml +187 -184
- package/.github/workflows/eas-build.yml +55 -55
- package/.github/workflows/eas-update.yml +50 -50
- package/CHANGELOG.md +106 -106
- package/CONTRIBUTING.md +377 -377
- package/README.md +399 -399
- package/__tests__/components/snapshots.test.tsx +131 -0
- package/__tests__/integration/auth-api.test.tsx +227 -0
- package/__tests__/performance/VirtualizedList.perf.test.tsx +362 -0
- package/app/(public)/onboarding.tsx +5 -5
- package/app.config.ts +45 -2
- package/assets/images/.gitkeep +7 -7
- package/components/onboarding/OnboardingScreen.tsx +370 -370
- package/components/onboarding/index.ts +2 -2
- package/components/providers/SuspenseBoundary.tsx +357 -0
- package/components/providers/index.ts +13 -0
- package/components/ui/Avatar.tsx +316 -316
- package/components/ui/Badge.tsx +416 -416
- package/components/ui/BottomSheet.tsx +307 -307
- package/components/ui/Checkbox.tsx +261 -261
- package/components/ui/OptimizedImage.tsx +369 -369
- package/components/ui/Select.tsx +240 -240
- package/components/ui/VirtualizedList.tsx +285 -0
- package/components/ui/index.ts +23 -18
- package/constants/config.ts +97 -54
- package/docs/adr/001-state-management.md +79 -79
- package/docs/adr/002-styling-approach.md +130 -130
- package/docs/adr/003-data-fetching.md +155 -155
- package/docs/adr/004-auth-adapter-pattern.md +144 -144
- package/docs/adr/README.md +78 -78
- package/hooks/index.ts +27 -25
- package/hooks/useApi.ts +102 -5
- package/hooks/useAuth.tsx +82 -0
- package/hooks/useBiometrics.ts +295 -295
- package/hooks/useDeepLinking.ts +256 -256
- package/hooks/useMFA.ts +499 -0
- package/hooks/useNotifications.ts +39 -0
- package/hooks/useOffline.ts +32 -2
- package/hooks/usePerformance.ts +434 -434
- package/hooks/useTheme.tsx +76 -0
- package/hooks/useUpdates.ts +358 -358
- package/i18n/index.ts +194 -77
- package/i18n/locales/ar.json +101 -0
- package/i18n/locales/de.json +101 -0
- package/i18n/locales/en.json +101 -101
- package/i18n/locales/es.json +101 -0
- package/i18n/locales/fr.json +101 -101
- package/jest.config.js +4 -4
- package/maestro/README.md +113 -113
- package/maestro/config.yaml +35 -35
- package/maestro/flows/login.yaml +62 -62
- package/maestro/flows/mfa-login.yaml +92 -0
- package/maestro/flows/mfa-setup.yaml +86 -0
- package/maestro/flows/navigation.yaml +68 -68
- package/maestro/flows/offline-conflict.yaml +101 -0
- package/maestro/flows/offline-sync.yaml +128 -0
- package/maestro/flows/offline.yaml +60 -60
- package/maestro/flows/register.yaml +94 -94
- package/package.json +175 -170
- package/services/analytics.ts +428 -428
- package/services/api.ts +340 -340
- package/services/authAdapter.ts +333 -333
- package/services/backgroundSync.ts +626 -0
- package/services/index.ts +54 -22
- package/services/security.ts +229 -0
- package/tailwind.config.js +47 -47
- package/utils/accessibility.ts +446 -446
- package/utils/index.ts +52 -43
- 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";
|