@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
@@ -0,0 +1,626 @@
1
+ /**
2
+ * @fileoverview Background sync service for offline mutation queue
3
+ * Uses expo-task-manager to process queued mutations when the app is in background.
4
+ * @module services/backgroundSync
5
+ */
6
+
7
+ import * as TaskManager from "expo-task-manager";
8
+ import * as BackgroundFetch from "expo-background-fetch";
9
+ import { storage } from "./storage";
10
+ import { api } from "./api";
11
+ import { IS_DEV } from "@/constants/config";
12
+
13
+ // Task names
14
+ const BACKGROUND_SYNC_TASK = "BACKGROUND_SYNC_TASK";
15
+ const MUTATION_QUEUE_KEY = "offline_mutation_queue";
16
+
17
+ /**
18
+ * Conflict resolution strategies
19
+ */
20
+ export type ConflictResolutionStrategy =
21
+ | "last-write-wins"
22
+ | "first-write-wins"
23
+ | "merge"
24
+ | "manual"
25
+ | "server-wins"
26
+ | "client-wins";
27
+
28
+ /**
29
+ * Conflict information when a sync conflict is detected
30
+ */
31
+ export interface SyncConflict {
32
+ /** The local mutation that caused the conflict */
33
+ localMutation: QueuedMutation;
34
+ /** Server data at the time of conflict (if available) */
35
+ serverData?: Record<string, unknown>;
36
+ /** Error message from the server */
37
+ errorMessage: string;
38
+ /** HTTP status code */
39
+ statusCode: number;
40
+ /** Timestamp when conflict was detected */
41
+ detectedAt: number;
42
+ }
43
+
44
+ /**
45
+ * Conflict resolver function type
46
+ */
47
+ export type ConflictResolver = (
48
+ conflict: SyncConflict
49
+ ) => Promise<{
50
+ action: "retry" | "discard" | "merge";
51
+ mergedData?: Record<string, unknown>;
52
+ }>;
53
+
54
+ /**
55
+ * Queued mutation structure
56
+ */
57
+ export interface QueuedMutation {
58
+ /** Unique identifier for the mutation */
59
+ id: string;
60
+ /** Timestamp when the mutation was queued */
61
+ timestamp: number;
62
+ /** HTTP method */
63
+ method: "POST" | "PUT" | "PATCH" | "DELETE";
64
+ /** API endpoint */
65
+ endpoint: string;
66
+ /** Request body */
67
+ body?: Record<string, unknown>;
68
+ /** Number of retry attempts */
69
+ retryCount: number;
70
+ /** Maximum retries before giving up */
71
+ maxRetries: number;
72
+ /** Conflict resolution strategy for this mutation */
73
+ conflictStrategy?: ConflictResolutionStrategy;
74
+ /** Version/ETag for optimistic locking */
75
+ version?: string | number;
76
+ /** Optional metadata for tracking */
77
+ metadata?: {
78
+ type: string;
79
+ entityId?: string;
80
+ description?: string;
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Sync result for a single mutation
86
+ */
87
+ interface SyncResult {
88
+ id: string;
89
+ success: boolean;
90
+ error?: string;
91
+ conflict?: SyncConflict;
92
+ statusCode?: number;
93
+ }
94
+
95
+ /**
96
+ * Background sync configuration
97
+ */
98
+ const SYNC_CONFIG = {
99
+ /** Minimum interval between background fetches (in seconds) */
100
+ MINIMUM_INTERVAL: 15 * 60, // 15 minutes
101
+ /** Maximum retries for a single mutation */
102
+ MAX_RETRIES: 5,
103
+ /** Timeout for the entire sync task (in seconds) */
104
+ TASK_TIMEOUT: 30,
105
+ /** Default conflict resolution strategy */
106
+ DEFAULT_CONFLICT_STRATEGY: "last-write-wins" as ConflictResolutionStrategy,
107
+ };
108
+
109
+ // Store for conflict handlers
110
+ const conflictHandlers: Map<string, ConflictResolver> = new Map();
111
+ const pendingConflicts: SyncConflict[] = [];
112
+
113
+ // ============================================================================
114
+ // Mutation Queue Management
115
+ // ============================================================================
116
+
117
+ /**
118
+ * Add a mutation to the offline queue
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * // In your mutation hook
123
+ * const createPost = useMutation({
124
+ * mutationFn: async (data) => {
125
+ * try {
126
+ * return await api.post('/posts', data);
127
+ * } catch (error) {
128
+ * if (isNetworkError(error)) {
129
+ * await queueMutation({
130
+ * method: 'POST',
131
+ * endpoint: '/posts',
132
+ * body: data,
133
+ * metadata: { type: 'create_post' },
134
+ * });
135
+ * throw new Error('Queued for sync');
136
+ * }
137
+ * throw error;
138
+ * }
139
+ * },
140
+ * });
141
+ * ```
142
+ */
143
+ export async function queueMutation(
144
+ mutation: Omit<QueuedMutation, "id" | "timestamp" | "retryCount" | "maxRetries">
145
+ ): Promise<string> {
146
+ const queue = await getMutationQueue();
147
+
148
+ const queuedMutation: QueuedMutation = {
149
+ ...mutation,
150
+ id: generateId(),
151
+ timestamp: Date.now(),
152
+ retryCount: 0,
153
+ maxRetries: SYNC_CONFIG.MAX_RETRIES,
154
+ };
155
+
156
+ queue.push(queuedMutation);
157
+ await saveMutationQueue(queue);
158
+
159
+ if (IS_DEV) {
160
+ console.log("[BackgroundSync] Queued mutation:", queuedMutation.id);
161
+ }
162
+
163
+ return queuedMutation.id;
164
+ }
165
+
166
+ /**
167
+ * Get the current mutation queue
168
+ */
169
+ export async function getMutationQueue(): Promise<QueuedMutation[]> {
170
+ const queue = await storage.get<QueuedMutation[]>(MUTATION_QUEUE_KEY);
171
+ return queue || [];
172
+ }
173
+
174
+ /**
175
+ * Save the mutation queue
176
+ */
177
+ async function saveMutationQueue(queue: QueuedMutation[]): Promise<void> {
178
+ await storage.set(MUTATION_QUEUE_KEY, queue);
179
+ }
180
+
181
+ /**
182
+ * Remove a mutation from the queue
183
+ */
184
+ export async function removeMutation(id: string): Promise<void> {
185
+ const queue = await getMutationQueue();
186
+ const filtered = queue.filter((m) => m.id !== id);
187
+ await saveMutationQueue(filtered);
188
+ }
189
+
190
+ /**
191
+ * Clear all mutations from the queue
192
+ */
193
+ export async function clearMutationQueue(): Promise<void> {
194
+ await storage.set(MUTATION_QUEUE_KEY, []);
195
+ }
196
+
197
+ /**
198
+ * Get the number of pending mutations
199
+ */
200
+ export async function getPendingMutationCount(): Promise<number> {
201
+ const queue = await getMutationQueue();
202
+ return queue.length;
203
+ }
204
+
205
+ // ============================================================================
206
+ // Conflict Resolution
207
+ // ============================================================================
208
+
209
+ /**
210
+ * Register a custom conflict resolver for a mutation type
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * registerConflictResolver('update_profile', async (conflict) => {
215
+ * // Custom merge logic
216
+ * const merged = {
217
+ * ...conflict.serverData,
218
+ * ...conflict.localMutation.body,
219
+ * updatedAt: Date.now(),
220
+ * };
221
+ * return { action: 'merge', mergedData: merged };
222
+ * });
223
+ * ```
224
+ */
225
+ export function registerConflictResolver(
226
+ mutationType: string,
227
+ resolver: ConflictResolver
228
+ ): void {
229
+ conflictHandlers.set(mutationType, resolver);
230
+ }
231
+
232
+ /**
233
+ * Unregister a conflict resolver
234
+ */
235
+ export function unregisterConflictResolver(mutationType: string): void {
236
+ conflictHandlers.delete(mutationType);
237
+ }
238
+
239
+ /**
240
+ * Get pending conflicts that need manual resolution
241
+ */
242
+ export function getPendingConflicts(): SyncConflict[] {
243
+ return [...pendingConflicts];
244
+ }
245
+
246
+ /**
247
+ * Clear a pending conflict after manual resolution
248
+ */
249
+ export function clearPendingConflict(mutationId: string): void {
250
+ const index = pendingConflicts.findIndex(
251
+ (c) => c.localMutation.id === mutationId
252
+ );
253
+ if (index !== -1) {
254
+ pendingConflicts.splice(index, 1);
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Resolve a conflict using the default strategy
260
+ */
261
+ async function resolveConflictWithStrategy(
262
+ conflict: SyncConflict,
263
+ strategy: ConflictResolutionStrategy
264
+ ): Promise<{ action: "retry" | "discard" | "merge"; mergedData?: Record<string, unknown> }> {
265
+ switch (strategy) {
266
+ case "last-write-wins":
267
+ case "client-wins":
268
+ // Client data takes precedence, retry with same data
269
+ return { action: "retry" };
270
+
271
+ case "first-write-wins":
272
+ case "server-wins":
273
+ // Server data takes precedence, discard local changes
274
+ return { action: "discard" };
275
+
276
+ case "merge":
277
+ // Attempt automatic merge
278
+ if (conflict.serverData && conflict.localMutation.body) {
279
+ const mergedData = {
280
+ ...conflict.serverData,
281
+ ...conflict.localMutation.body,
282
+ _mergedAt: Date.now(),
283
+ _conflictResolved: true,
284
+ };
285
+ return { action: "merge", mergedData };
286
+ }
287
+ return { action: "retry" };
288
+
289
+ case "manual":
290
+ // Add to pending conflicts for user resolution
291
+ pendingConflicts.push(conflict);
292
+ return { action: "discard" }; // Don't retry automatically
293
+
294
+ default:
295
+ return { action: "retry" };
296
+ }
297
+ }
298
+
299
+ /**
300
+ * Check if an error indicates a conflict (409 or version mismatch)
301
+ */
302
+ function isConflictError(error: unknown): { isConflict: boolean; statusCode?: number; serverData?: Record<string, unknown> } {
303
+ if (error && typeof error === "object" && "status" in error) {
304
+ const status = (error as { status: number }).status;
305
+ if (status === 409 || status === 412) {
306
+ // 409 Conflict or 412 Precondition Failed
307
+ const serverData = "data" in error ? (error as { data?: Record<string, unknown> }).data : undefined;
308
+ return { isConflict: true, statusCode: status, serverData };
309
+ }
310
+ }
311
+ return { isConflict: false };
312
+ }
313
+
314
+ // ============================================================================
315
+ // Sync Processing
316
+ // ============================================================================
317
+
318
+ /**
319
+ * Process a single mutation with conflict handling
320
+ */
321
+ async function processMutation(mutation: QueuedMutation): Promise<SyncResult> {
322
+ try {
323
+ // Add version header if available for optimistic locking
324
+ const headers: Record<string, string> = {};
325
+ if (mutation.version) {
326
+ headers["If-Match"] = String(mutation.version);
327
+ }
328
+
329
+ switch (mutation.method) {
330
+ case "POST":
331
+ await api.post(mutation.endpoint, mutation.body);
332
+ break;
333
+ case "PUT":
334
+ await api.put(mutation.endpoint, mutation.body);
335
+ break;
336
+ case "PATCH":
337
+ await api.patch(mutation.endpoint, mutation.body);
338
+ break;
339
+ case "DELETE":
340
+ await api.delete(mutation.endpoint);
341
+ break;
342
+ }
343
+
344
+ return { id: mutation.id, success: true };
345
+ } catch (error) {
346
+ const message = error instanceof Error ? error.message : "Unknown error";
347
+ const conflictCheck = isConflictError(error);
348
+
349
+ if (conflictCheck.isConflict) {
350
+ const conflict: SyncConflict = {
351
+ localMutation: mutation,
352
+ serverData: conflictCheck.serverData,
353
+ errorMessage: message,
354
+ statusCode: conflictCheck.statusCode || 409,
355
+ detectedAt: Date.now(),
356
+ };
357
+
358
+ return {
359
+ id: mutation.id,
360
+ success: false,
361
+ error: message,
362
+ conflict,
363
+ statusCode: conflictCheck.statusCode,
364
+ };
365
+ }
366
+
367
+ return { id: mutation.id, success: false, error: message };
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Process all pending mutations with conflict resolution
373
+ * Called by background task and can also be triggered manually
374
+ */
375
+ export async function processQueue(): Promise<{
376
+ processed: number;
377
+ succeeded: number;
378
+ failed: number;
379
+ conflicts: number;
380
+ remaining: number;
381
+ }> {
382
+ const queue = await getMutationQueue();
383
+
384
+ if (queue.length === 0) {
385
+ return { processed: 0, succeeded: 0, failed: 0, conflicts: 0, remaining: 0 };
386
+ }
387
+
388
+ if (IS_DEV) {
389
+ console.log(`[BackgroundSync] Processing ${queue.length} mutations`);
390
+ }
391
+
392
+ const results: SyncResult[] = [];
393
+ const updatedQueue: QueuedMutation[] = [];
394
+ let conflictCount = 0;
395
+
396
+ for (const mutation of queue) {
397
+ const result = await processMutation(mutation);
398
+ results.push(result);
399
+
400
+ if (!result.success) {
401
+ // Check if this is a conflict
402
+ if (result.conflict) {
403
+ conflictCount++;
404
+ const strategy =
405
+ mutation.conflictStrategy || SYNC_CONFIG.DEFAULT_CONFLICT_STRATEGY;
406
+
407
+ // Check for custom resolver first
408
+ const customResolver = mutation.metadata?.type
409
+ ? conflictHandlers.get(mutation.metadata.type)
410
+ : null;
411
+
412
+ let resolution: { action: "retry" | "discard" | "merge"; mergedData?: Record<string, unknown> };
413
+
414
+ if (customResolver) {
415
+ resolution = await customResolver(result.conflict);
416
+ } else {
417
+ resolution = await resolveConflictWithStrategy(result.conflict, strategy);
418
+ }
419
+
420
+ if (IS_DEV) {
421
+ console.log(
422
+ `[BackgroundSync] Conflict for ${mutation.id} resolved with action: ${resolution.action}`
423
+ );
424
+ }
425
+
426
+ switch (resolution.action) {
427
+ case "retry":
428
+ if (mutation.retryCount < mutation.maxRetries) {
429
+ updatedQueue.push({
430
+ ...mutation,
431
+ retryCount: mutation.retryCount + 1,
432
+ });
433
+ }
434
+ break;
435
+ case "merge":
436
+ if (resolution.mergedData && mutation.retryCount < mutation.maxRetries) {
437
+ updatedQueue.push({
438
+ ...mutation,
439
+ body: resolution.mergedData,
440
+ retryCount: mutation.retryCount + 1,
441
+ });
442
+ }
443
+ break;
444
+ case "discard":
445
+ // Don't add to queue, effectively discarding
446
+ break;
447
+ }
448
+ } else {
449
+ // Regular failure, check if we should retry
450
+ if (mutation.retryCount < mutation.maxRetries) {
451
+ updatedQueue.push({
452
+ ...mutation,
453
+ retryCount: mutation.retryCount + 1,
454
+ });
455
+ } else {
456
+ // Max retries reached, log and discard
457
+ console.warn(
458
+ `[BackgroundSync] Mutation ${mutation.id} failed after ${mutation.maxRetries} retries:`,
459
+ result.error
460
+ );
461
+ }
462
+ }
463
+ }
464
+ }
465
+
466
+ // Save remaining mutations
467
+ await saveMutationQueue(updatedQueue);
468
+
469
+ const succeeded = results.filter((r) => r.success).length;
470
+ const failed = results.filter((r) => !r.success).length;
471
+
472
+ if (IS_DEV) {
473
+ console.log(
474
+ `[BackgroundSync] Processed: ${results.length}, Succeeded: ${succeeded}, Failed: ${failed}, Conflicts: ${conflictCount}, Remaining: ${updatedQueue.length}`
475
+ );
476
+ }
477
+
478
+ return {
479
+ processed: results.length,
480
+ succeeded,
481
+ failed,
482
+ conflicts: conflictCount,
483
+ remaining: updatedQueue.length,
484
+ };
485
+ }
486
+
487
+ // ============================================================================
488
+ // Background Task Definition
489
+ // ============================================================================
490
+
491
+ /**
492
+ * Define the background sync task
493
+ */
494
+ TaskManager.defineTask(BACKGROUND_SYNC_TASK, async () => {
495
+ try {
496
+ const result = await processQueue();
497
+
498
+ if (result.processed === 0) {
499
+ return BackgroundFetch.BackgroundFetchResult.NoData;
500
+ }
501
+
502
+ if (result.failed > 0 && result.succeeded === 0) {
503
+ return BackgroundFetch.BackgroundFetchResult.Failed;
504
+ }
505
+
506
+ return BackgroundFetch.BackgroundFetchResult.NewData;
507
+ } catch (error) {
508
+ console.error("[BackgroundSync] Task error:", error);
509
+ return BackgroundFetch.BackgroundFetchResult.Failed;
510
+ }
511
+ });
512
+
513
+ // ============================================================================
514
+ // Registration Functions
515
+ // ============================================================================
516
+
517
+ /**
518
+ * Register the background sync task
519
+ * Call this during app initialization
520
+ *
521
+ * @example
522
+ * ```tsx
523
+ * // In your app root or initialization
524
+ * useEffect(() => {
525
+ * registerBackgroundSync();
526
+ * }, []);
527
+ * ```
528
+ */
529
+ export async function registerBackgroundSync(): Promise<boolean> {
530
+ try {
531
+ // Check if background fetch is available
532
+ const status = await BackgroundFetch.getStatusAsync();
533
+
534
+ if (status === BackgroundFetch.BackgroundFetchStatus.Restricted) {
535
+ console.warn("[BackgroundSync] Background fetch is restricted");
536
+ return false;
537
+ }
538
+
539
+ if (status === BackgroundFetch.BackgroundFetchStatus.Denied) {
540
+ console.warn("[BackgroundSync] Background fetch is denied");
541
+ return false;
542
+ }
543
+
544
+ // Register the task
545
+ await BackgroundFetch.registerTaskAsync(BACKGROUND_SYNC_TASK, {
546
+ minimumInterval: SYNC_CONFIG.MINIMUM_INTERVAL,
547
+ stopOnTerminate: false,
548
+ startOnBoot: true,
549
+ });
550
+
551
+ if (IS_DEV) {
552
+ console.log("[BackgroundSync] Background sync registered");
553
+ }
554
+
555
+ return true;
556
+ } catch (error) {
557
+ console.error("[BackgroundSync] Registration failed:", error);
558
+ return false;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Unregister the background sync task
564
+ */
565
+ export async function unregisterBackgroundSync(): Promise<void> {
566
+ try {
567
+ await BackgroundFetch.unregisterTaskAsync(BACKGROUND_SYNC_TASK);
568
+ if (IS_DEV) {
569
+ console.log("[BackgroundSync] Background sync unregistered");
570
+ }
571
+ } catch (error) {
572
+ console.error("[BackgroundSync] Unregistration failed:", error);
573
+ }
574
+ }
575
+
576
+ /**
577
+ * Check if background sync is registered
578
+ */
579
+ export async function isBackgroundSyncRegistered(): Promise<boolean> {
580
+ return TaskManager.isTaskRegisteredAsync(BACKGROUND_SYNC_TASK);
581
+ }
582
+
583
+ /**
584
+ * Get background fetch status
585
+ */
586
+ export async function getBackgroundSyncStatus(): Promise<{
587
+ isAvailable: boolean;
588
+ isRegistered: boolean;
589
+ status: BackgroundFetch.BackgroundFetchStatus;
590
+ }> {
591
+ const status = await BackgroundFetch.getStatusAsync();
592
+ const isRegistered = await isBackgroundSyncRegistered();
593
+
594
+ return {
595
+ isAvailable: status === BackgroundFetch.BackgroundFetchStatus.Available,
596
+ isRegistered,
597
+ status,
598
+ };
599
+ }
600
+
601
+ // ============================================================================
602
+ // Utilities
603
+ // ============================================================================
604
+
605
+ /**
606
+ * Generate a unique ID for mutations
607
+ */
608
+ function generateId(): string {
609
+ return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
610
+ }
611
+
612
+ /**
613
+ * Check if an error is a network error
614
+ */
615
+ export function isNetworkError(error: unknown): boolean {
616
+ if (error instanceof Error) {
617
+ const message = error.message.toLowerCase();
618
+ return (
619
+ message.includes("network") ||
620
+ message.includes("fetch") ||
621
+ message.includes("timeout") ||
622
+ message.includes("offline")
623
+ );
624
+ }
625
+ return false;
626
+ }
package/services/index.ts CHANGED
@@ -1,22 +1,54 @@
1
- export { api, ApiClient } from "./api";
2
- export { storage, secureStorage } from "./storage";
3
- export {
4
- initSentry,
5
- captureException,
6
- captureMessage,
7
- setUser,
8
- addBreadcrumb,
9
- } from "./sentry";
10
- export { authAdapter, mockAuthAdapter } from "./authAdapter";
11
- export type { AuthAdapter, AuthResult, AuthError } from "./authAdapter";
12
- export {
13
- analytics,
14
- track,
15
- identify,
16
- screen,
17
- resetAnalytics,
18
- setUserProperties,
19
- trackRevenue,
20
- AnalyticsEvents,
21
- } from "./analytics";
22
- export type { AnalyticsAdapter, AnalyticsEvent } from "./analytics";
1
+ export { api, ApiClient } from "./api";
2
+ export { storage, secureStorage } from "./storage";
3
+ export {
4
+ initSentry,
5
+ captureException,
6
+ captureMessage,
7
+ setUser,
8
+ addBreadcrumb,
9
+ } from "./sentry";
10
+ export { authAdapter, mockAuthAdapter } from "./authAdapter";
11
+ export type { AuthAdapter, AuthResult, AuthError } from "./authAdapter";
12
+ export {
13
+ analytics,
14
+ track,
15
+ identify,
16
+ screen,
17
+ resetAnalytics,
18
+ setUserProperties,
19
+ trackRevenue,
20
+ AnalyticsEvents,
21
+ } from "./analytics";
22
+ export type { AnalyticsAdapter, AnalyticsEvent } from "./analytics";
23
+ export {
24
+ getCertificatePins,
25
+ isSslPinningEnabled,
26
+ getSecurityHeaders,
27
+ isUrlAllowed,
28
+ sanitizeInput,
29
+ checkSecurityEnvironment,
30
+ SSL_PINNING_CONFIG,
31
+ } from "./security";
32
+ export {
33
+ queueMutation,
34
+ getMutationQueue,
35
+ removeMutation,
36
+ clearMutationQueue,
37
+ getPendingMutationCount,
38
+ processQueue,
39
+ registerBackgroundSync,
40
+ unregisterBackgroundSync,
41
+ isBackgroundSyncRegistered,
42
+ getBackgroundSyncStatus,
43
+ isNetworkError,
44
+ registerConflictResolver,
45
+ unregisterConflictResolver,
46
+ getPendingConflicts,
47
+ clearPendingConflict,
48
+ } from "./backgroundSync";
49
+ export type {
50
+ QueuedMutation,
51
+ ConflictResolutionStrategy,
52
+ SyncConflict,
53
+ ConflictResolver,
54
+ } from "./backgroundSync";