@croacroa/react-native-template 2.1.0 → 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 -21
  10. package/README.md +446 -402
  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 -418
  69. package/components/ui/UploadProgress.tsx +189 -0
  70. package/components/ui/VirtualizedList.tsx +288 -285
  71. package/components/ui/index.ts +28 -30
  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 -40
  89. package/hooks/useAnimatedEntry.ts +204 -0
  90. package/hooks/useApi.ts +5 -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 -375
  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 -176
  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,446 +1,446 @@
1
- import { AccessibilityInfo, AccessibilityRole } from "react-native";
2
- import { useEffect, useState } from "react";
3
-
4
- // ============================================================================
5
- // Types
6
- // ============================================================================
7
-
8
- export interface AccessibilityProps {
9
- /**
10
- * A brief description of the element
11
- */
12
- accessibilityLabel?: string;
13
-
14
- /**
15
- * Additional context about what will happen when the element is activated
16
- */
17
- accessibilityHint?: string;
18
-
19
- /**
20
- * The role of the element (button, link, header, etc.)
21
- */
22
- accessibilityRole?: AccessibilityRole;
23
-
24
- /**
25
- * State of the element (selected, disabled, checked, etc.)
26
- */
27
- accessibilityState?: {
28
- disabled?: boolean;
29
- selected?: boolean;
30
- checked?: boolean | "mixed";
31
- busy?: boolean;
32
- expanded?: boolean;
33
- };
34
-
35
- /**
36
- * Value for sliders, progress bars, etc.
37
- */
38
- accessibilityValue?: {
39
- min?: number;
40
- max?: number;
41
- now?: number;
42
- text?: string;
43
- };
44
-
45
- /**
46
- * Whether the element is accessible
47
- */
48
- accessible?: boolean;
49
-
50
- /**
51
- * Test ID for testing
52
- */
53
- testID?: string;
54
- }
55
-
56
- // ============================================================================
57
- // Accessibility Builders
58
- // ============================================================================
59
-
60
- /**
61
- * Build accessibility props for a button
62
- */
63
- export function buttonA11y(
64
- label: string,
65
- options?: {
66
- hint?: string;
67
- disabled?: boolean;
68
- loading?: boolean;
69
- testID?: string;
70
- }
71
- ): AccessibilityProps {
72
- return {
73
- accessible: true,
74
- accessibilityRole: "button",
75
- accessibilityLabel: label,
76
- accessibilityHint: options?.hint,
77
- accessibilityState: {
78
- disabled: options?.disabled || options?.loading,
79
- busy: options?.loading,
80
- },
81
- testID: options?.testID,
82
- };
83
- }
84
-
85
- /**
86
- * Build accessibility props for a link
87
- */
88
- export function linkA11y(
89
- label: string,
90
- options?: {
91
- hint?: string;
92
- testID?: string;
93
- }
94
- ): AccessibilityProps {
95
- return {
96
- accessible: true,
97
- accessibilityRole: "link",
98
- accessibilityLabel: label,
99
- accessibilityHint: options?.hint || "Double tap to open",
100
- testID: options?.testID,
101
- };
102
- }
103
-
104
- /**
105
- * Build accessibility props for a text input
106
- */
107
- export function inputA11y(
108
- label: string,
109
- options?: {
110
- hint?: string;
111
- error?: string;
112
- required?: boolean;
113
- testID?: string;
114
- }
115
- ): AccessibilityProps {
116
- let accessibilityLabel = label;
117
- if (options?.required) {
118
- accessibilityLabel += ", required";
119
- }
120
- if (options?.error) {
121
- accessibilityLabel += `, error: ${options.error}`;
122
- }
123
-
124
- return {
125
- accessible: true,
126
- accessibilityLabel,
127
- accessibilityHint: options?.hint || "Double tap to edit",
128
- testID: options?.testID,
129
- };
130
- }
131
-
132
- /**
133
- * Build accessibility props for a checkbox/switch
134
- */
135
- export function toggleA11y(
136
- label: string,
137
- checked: boolean,
138
- options?: {
139
- hint?: string;
140
- disabled?: boolean;
141
- testID?: string;
142
- }
143
- ): AccessibilityProps {
144
- return {
145
- accessible: true,
146
- accessibilityRole: "checkbox",
147
- accessibilityLabel: label,
148
- accessibilityHint:
149
- options?.hint || `Double tap to ${checked ? "uncheck" : "check"}`,
150
- accessibilityState: {
151
- checked,
152
- disabled: options?.disabled,
153
- },
154
- testID: options?.testID,
155
- };
156
- }
157
-
158
- /**
159
- * Build accessibility props for a header
160
- */
161
- export function headerA11y(
162
- label: string,
163
- level: 1 | 2 | 3 | 4 | 5 | 6 = 1
164
- ): AccessibilityProps {
165
- return {
166
- accessible: true,
167
- accessibilityRole: "header",
168
- accessibilityLabel: `${label}, heading level ${level}`,
169
- };
170
- }
171
-
172
- /**
173
- * Build accessibility props for an image
174
- */
175
- export function imageA11y(
176
- description: string,
177
- options?: {
178
- isDecorative?: boolean;
179
- testID?: string;
180
- }
181
- ): AccessibilityProps {
182
- if (options?.isDecorative) {
183
- return {
184
- accessible: false,
185
- accessibilityElementsHidden: true,
186
- } as AccessibilityProps;
187
- }
188
-
189
- return {
190
- accessible: true,
191
- accessibilityRole: "image",
192
- accessibilityLabel: description,
193
- testID: options?.testID,
194
- };
195
- }
196
-
197
- /**
198
- * Build accessibility props for a list item
199
- */
200
- export function listItemA11y(
201
- label: string,
202
- position: number,
203
- total: number,
204
- options?: {
205
- hint?: string;
206
- selected?: boolean;
207
- testID?: string;
208
- }
209
- ): AccessibilityProps {
210
- return {
211
- accessible: true,
212
- accessibilityLabel: `${label}, ${position} of ${total}`,
213
- accessibilityHint: options?.hint,
214
- accessibilityState: {
215
- selected: options?.selected,
216
- },
217
- testID: options?.testID,
218
- };
219
- }
220
-
221
- /**
222
- * Build accessibility props for a progress indicator
223
- */
224
- export function progressA11y(
225
- label: string,
226
- value: number,
227
- options?: {
228
- min?: number;
229
- max?: number;
230
- testID?: string;
231
- }
232
- ): AccessibilityProps {
233
- const min = options?.min ?? 0;
234
- const max = options?.max ?? 100;
235
- const percentage = Math.round(((value - min) / (max - min)) * 100);
236
-
237
- return {
238
- accessible: true,
239
- accessibilityRole: "progressbar",
240
- accessibilityLabel: `${label}, ${percentage}% complete`,
241
- accessibilityValue: {
242
- min,
243
- max,
244
- now: value,
245
- text: `${percentage}%`,
246
- },
247
- testID: options?.testID,
248
- };
249
- }
250
-
251
- /**
252
- * Build accessibility props for a tab
253
- */
254
- export function tabA11y(
255
- label: string,
256
- selected: boolean,
257
- position: number,
258
- total: number,
259
- options?: {
260
- hint?: string;
261
- testID?: string;
262
- }
263
- ): AccessibilityProps {
264
- return {
265
- accessible: true,
266
- accessibilityRole: "tab",
267
- accessibilityLabel: `${label}, tab ${position} of ${total}`,
268
- accessibilityHint: options?.hint,
269
- accessibilityState: {
270
- selected,
271
- },
272
- testID: options?.testID,
273
- };
274
- }
275
-
276
- /**
277
- * Build accessibility props for an alert/notification
278
- */
279
- export function alertA11y(
280
- message: string,
281
- options?: {
282
- type?: "info" | "success" | "warning" | "error";
283
- testID?: string;
284
- }
285
- ): AccessibilityProps {
286
- const typeLabel = options?.type ? `${options.type}: ` : "";
287
-
288
- return {
289
- accessible: true,
290
- accessibilityRole: "alert",
291
- accessibilityLabel: `${typeLabel}${message}`,
292
- accessibilityLiveRegion: "polite",
293
- testID: options?.testID,
294
- } as AccessibilityProps;
295
- }
296
-
297
- // ============================================================================
298
- // Hooks
299
- // ============================================================================
300
-
301
- /**
302
- * Hook to check if screen reader is enabled
303
- */
304
- export function useScreenReader(): boolean {
305
- const [isEnabled, setIsEnabled] = useState(false);
306
-
307
- useEffect(() => {
308
- AccessibilityInfo.isScreenReaderEnabled().then(setIsEnabled);
309
-
310
- const subscription = AccessibilityInfo.addEventListener(
311
- "screenReaderChanged",
312
- setIsEnabled
313
- );
314
-
315
- return () => subscription.remove();
316
- }, []);
317
-
318
- return isEnabled;
319
- }
320
-
321
- /**
322
- * Hook to check if reduce motion is enabled
323
- */
324
- export function useReduceMotion(): boolean {
325
- const [isEnabled, setIsEnabled] = useState(false);
326
-
327
- useEffect(() => {
328
- AccessibilityInfo.isReduceMotionEnabled().then(setIsEnabled);
329
-
330
- const subscription = AccessibilityInfo.addEventListener(
331
- "reduceMotionChanged",
332
- setIsEnabled
333
- );
334
-
335
- return () => subscription.remove();
336
- }, []);
337
-
338
- return isEnabled;
339
- }
340
-
341
- /**
342
- * Hook to check if bold text is enabled
343
- */
344
- export function useBoldText(): boolean {
345
- const [isEnabled, setIsEnabled] = useState(false);
346
-
347
- useEffect(() => {
348
- AccessibilityInfo.isBoldTextEnabled().then(setIsEnabled);
349
-
350
- const subscription = AccessibilityInfo.addEventListener(
351
- "boldTextChanged",
352
- setIsEnabled
353
- );
354
-
355
- return () => subscription.remove();
356
- }, []);
357
-
358
- return isEnabled;
359
- }
360
-
361
- /**
362
- * Hook to get all accessibility preferences
363
- */
364
- export function useAccessibilityPreferences() {
365
- const isScreenReaderEnabled = useScreenReader();
366
- const isReduceMotionEnabled = useReduceMotion();
367
- const isBoldTextEnabled = useBoldText();
368
-
369
- return {
370
- isScreenReaderEnabled,
371
- isReduceMotionEnabled,
372
- isBoldTextEnabled,
373
- };
374
- }
375
-
376
- // ============================================================================
377
- // Utilities
378
- // ============================================================================
379
-
380
- /**
381
- * Announce a message to screen readers
382
- */
383
- export function announce(message: string): void {
384
- AccessibilityInfo.announceForAccessibility(message);
385
- }
386
-
387
- /**
388
- * Set focus to a specific element (requires a ref)
389
- */
390
- export function setAccessibilityFocus(ref: React.RefObject<unknown>): void {
391
- if (ref.current) {
392
- AccessibilityInfo.setAccessibilityFocus(ref.current);
393
- }
394
- }
395
-
396
- /**
397
- * Format a price for accessibility
398
- */
399
- export function formatPriceA11y(
400
- amount: number,
401
- currency = "EUR",
402
- locale = "fr-FR"
403
- ): string {
404
- const formatted = new Intl.NumberFormat(locale, {
405
- style: "currency",
406
- currency,
407
- }).format(amount);
408
-
409
- // Convert to speakable format
410
- return formatted
411
- .replace("€", "euros")
412
- .replace("$", "dollars")
413
- .replace("£", "pounds");
414
- }
415
-
416
- /**
417
- * Format a date for accessibility
418
- */
419
- export function formatDateA11y(date: Date | string, locale = "fr-FR"): string {
420
- const d = typeof date === "string" ? new Date(date) : date;
421
-
422
- return d.toLocaleDateString(locale, {
423
- weekday: "long",
424
- year: "numeric",
425
- month: "long",
426
- day: "numeric",
427
- });
428
- }
429
-
430
- /**
431
- * Format a duration for accessibility
432
- */
433
- export function formatDurationA11y(seconds: number): string {
434
- const hours = Math.floor(seconds / 3600);
435
- const minutes = Math.floor((seconds % 3600) / 60);
436
- const secs = seconds % 60;
437
-
438
- const parts = [];
439
- if (hours > 0) parts.push(`${hours} hour${hours !== 1 ? "s" : ""}`);
440
- if (minutes > 0) parts.push(`${minutes} minute${minutes !== 1 ? "s" : ""}`);
441
- if (secs > 0 || parts.length === 0) {
442
- parts.push(`${secs} second${secs !== 1 ? "s" : ""}`);
443
- }
444
-
445
- return parts.join(", ");
446
- }
1
+ import { AccessibilityInfo, AccessibilityRole } from "react-native";
2
+ import { useEffect, useState } from "react";
3
+
4
+ // ============================================================================
5
+ // Types
6
+ // ============================================================================
7
+
8
+ export interface AccessibilityProps {
9
+ /**
10
+ * A brief description of the element
11
+ */
12
+ accessibilityLabel?: string;
13
+
14
+ /**
15
+ * Additional context about what will happen when the element is activated
16
+ */
17
+ accessibilityHint?: string;
18
+
19
+ /**
20
+ * The role of the element (button, link, header, etc.)
21
+ */
22
+ accessibilityRole?: AccessibilityRole;
23
+
24
+ /**
25
+ * State of the element (selected, disabled, checked, etc.)
26
+ */
27
+ accessibilityState?: {
28
+ disabled?: boolean;
29
+ selected?: boolean;
30
+ checked?: boolean | "mixed";
31
+ busy?: boolean;
32
+ expanded?: boolean;
33
+ };
34
+
35
+ /**
36
+ * Value for sliders, progress bars, etc.
37
+ */
38
+ accessibilityValue?: {
39
+ min?: number;
40
+ max?: number;
41
+ now?: number;
42
+ text?: string;
43
+ };
44
+
45
+ /**
46
+ * Whether the element is accessible
47
+ */
48
+ accessible?: boolean;
49
+
50
+ /**
51
+ * Test ID for testing
52
+ */
53
+ testID?: string;
54
+ }
55
+
56
+ // ============================================================================
57
+ // Accessibility Builders
58
+ // ============================================================================
59
+
60
+ /**
61
+ * Build accessibility props for a button
62
+ */
63
+ export function buttonA11y(
64
+ label: string,
65
+ options?: {
66
+ hint?: string;
67
+ disabled?: boolean;
68
+ loading?: boolean;
69
+ testID?: string;
70
+ }
71
+ ): AccessibilityProps {
72
+ return {
73
+ accessible: true,
74
+ accessibilityRole: "button",
75
+ accessibilityLabel: label,
76
+ accessibilityHint: options?.hint,
77
+ accessibilityState: {
78
+ disabled: options?.disabled || options?.loading,
79
+ busy: options?.loading,
80
+ },
81
+ testID: options?.testID,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * Build accessibility props for a link
87
+ */
88
+ export function linkA11y(
89
+ label: string,
90
+ options?: {
91
+ hint?: string;
92
+ testID?: string;
93
+ }
94
+ ): AccessibilityProps {
95
+ return {
96
+ accessible: true,
97
+ accessibilityRole: "link",
98
+ accessibilityLabel: label,
99
+ accessibilityHint: options?.hint || "Double tap to open",
100
+ testID: options?.testID,
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Build accessibility props for a text input
106
+ */
107
+ export function inputA11y(
108
+ label: string,
109
+ options?: {
110
+ hint?: string;
111
+ error?: string;
112
+ required?: boolean;
113
+ testID?: string;
114
+ }
115
+ ): AccessibilityProps {
116
+ let accessibilityLabel = label;
117
+ if (options?.required) {
118
+ accessibilityLabel += ", required";
119
+ }
120
+ if (options?.error) {
121
+ accessibilityLabel += `, error: ${options.error}`;
122
+ }
123
+
124
+ return {
125
+ accessible: true,
126
+ accessibilityLabel,
127
+ accessibilityHint: options?.hint || "Double tap to edit",
128
+ testID: options?.testID,
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Build accessibility props for a checkbox/switch
134
+ */
135
+ export function toggleA11y(
136
+ label: string,
137
+ checked: boolean,
138
+ options?: {
139
+ hint?: string;
140
+ disabled?: boolean;
141
+ testID?: string;
142
+ }
143
+ ): AccessibilityProps {
144
+ return {
145
+ accessible: true,
146
+ accessibilityRole: "checkbox",
147
+ accessibilityLabel: label,
148
+ accessibilityHint:
149
+ options?.hint || `Double tap to ${checked ? "uncheck" : "check"}`,
150
+ accessibilityState: {
151
+ checked,
152
+ disabled: options?.disabled,
153
+ },
154
+ testID: options?.testID,
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Build accessibility props for a header
160
+ */
161
+ export function headerA11y(
162
+ label: string,
163
+ level: 1 | 2 | 3 | 4 | 5 | 6 = 1
164
+ ): AccessibilityProps {
165
+ return {
166
+ accessible: true,
167
+ accessibilityRole: "header",
168
+ accessibilityLabel: `${label}, heading level ${level}`,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Build accessibility props for an image
174
+ */
175
+ export function imageA11y(
176
+ description: string,
177
+ options?: {
178
+ isDecorative?: boolean;
179
+ testID?: string;
180
+ }
181
+ ): AccessibilityProps {
182
+ if (options?.isDecorative) {
183
+ return {
184
+ accessible: false,
185
+ accessibilityElementsHidden: true,
186
+ } as AccessibilityProps;
187
+ }
188
+
189
+ return {
190
+ accessible: true,
191
+ accessibilityRole: "image",
192
+ accessibilityLabel: description,
193
+ testID: options?.testID,
194
+ };
195
+ }
196
+
197
+ /**
198
+ * Build accessibility props for a list item
199
+ */
200
+ export function listItemA11y(
201
+ label: string,
202
+ position: number,
203
+ total: number,
204
+ options?: {
205
+ hint?: string;
206
+ selected?: boolean;
207
+ testID?: string;
208
+ }
209
+ ): AccessibilityProps {
210
+ return {
211
+ accessible: true,
212
+ accessibilityLabel: `${label}, ${position} of ${total}`,
213
+ accessibilityHint: options?.hint,
214
+ accessibilityState: {
215
+ selected: options?.selected,
216
+ },
217
+ testID: options?.testID,
218
+ };
219
+ }
220
+
221
+ /**
222
+ * Build accessibility props for a progress indicator
223
+ */
224
+ export function progressA11y(
225
+ label: string,
226
+ value: number,
227
+ options?: {
228
+ min?: number;
229
+ max?: number;
230
+ testID?: string;
231
+ }
232
+ ): AccessibilityProps {
233
+ const min = options?.min ?? 0;
234
+ const max = options?.max ?? 100;
235
+ const percentage = Math.round(((value - min) / (max - min)) * 100);
236
+
237
+ return {
238
+ accessible: true,
239
+ accessibilityRole: "progressbar",
240
+ accessibilityLabel: `${label}, ${percentage}% complete`,
241
+ accessibilityValue: {
242
+ min,
243
+ max,
244
+ now: value,
245
+ text: `${percentage}%`,
246
+ },
247
+ testID: options?.testID,
248
+ };
249
+ }
250
+
251
+ /**
252
+ * Build accessibility props for a tab
253
+ */
254
+ export function tabA11y(
255
+ label: string,
256
+ selected: boolean,
257
+ position: number,
258
+ total: number,
259
+ options?: {
260
+ hint?: string;
261
+ testID?: string;
262
+ }
263
+ ): AccessibilityProps {
264
+ return {
265
+ accessible: true,
266
+ accessibilityRole: "tab",
267
+ accessibilityLabel: `${label}, tab ${position} of ${total}`,
268
+ accessibilityHint: options?.hint,
269
+ accessibilityState: {
270
+ selected,
271
+ },
272
+ testID: options?.testID,
273
+ };
274
+ }
275
+
276
+ /**
277
+ * Build accessibility props for an alert/notification
278
+ */
279
+ export function alertA11y(
280
+ message: string,
281
+ options?: {
282
+ type?: "info" | "success" | "warning" | "error";
283
+ testID?: string;
284
+ }
285
+ ): AccessibilityProps {
286
+ const typeLabel = options?.type ? `${options.type}: ` : "";
287
+
288
+ return {
289
+ accessible: true,
290
+ accessibilityRole: "alert",
291
+ accessibilityLabel: `${typeLabel}${message}`,
292
+ accessibilityLiveRegion: "polite",
293
+ testID: options?.testID,
294
+ } as AccessibilityProps;
295
+ }
296
+
297
+ // ============================================================================
298
+ // Hooks
299
+ // ============================================================================
300
+
301
+ /**
302
+ * Hook to check if screen reader is enabled
303
+ */
304
+ export function useScreenReader(): boolean {
305
+ const [isEnabled, setIsEnabled] = useState(false);
306
+
307
+ useEffect(() => {
308
+ AccessibilityInfo.isScreenReaderEnabled().then(setIsEnabled);
309
+
310
+ const subscription = AccessibilityInfo.addEventListener(
311
+ "screenReaderChanged",
312
+ setIsEnabled
313
+ );
314
+
315
+ return () => subscription.remove();
316
+ }, []);
317
+
318
+ return isEnabled;
319
+ }
320
+
321
+ /**
322
+ * Hook to check if reduce motion is enabled
323
+ */
324
+ export function useReduceMotion(): boolean {
325
+ const [isEnabled, setIsEnabled] = useState(false);
326
+
327
+ useEffect(() => {
328
+ AccessibilityInfo.isReduceMotionEnabled().then(setIsEnabled);
329
+
330
+ const subscription = AccessibilityInfo.addEventListener(
331
+ "reduceMotionChanged",
332
+ setIsEnabled
333
+ );
334
+
335
+ return () => subscription.remove();
336
+ }, []);
337
+
338
+ return isEnabled;
339
+ }
340
+
341
+ /**
342
+ * Hook to check if bold text is enabled
343
+ */
344
+ export function useBoldText(): boolean {
345
+ const [isEnabled, setIsEnabled] = useState(false);
346
+
347
+ useEffect(() => {
348
+ AccessibilityInfo.isBoldTextEnabled().then(setIsEnabled);
349
+
350
+ const subscription = AccessibilityInfo.addEventListener(
351
+ "boldTextChanged",
352
+ setIsEnabled
353
+ );
354
+
355
+ return () => subscription.remove();
356
+ }, []);
357
+
358
+ return isEnabled;
359
+ }
360
+
361
+ /**
362
+ * Hook to get all accessibility preferences
363
+ */
364
+ export function useAccessibilityPreferences() {
365
+ const isScreenReaderEnabled = useScreenReader();
366
+ const isReduceMotionEnabled = useReduceMotion();
367
+ const isBoldTextEnabled = useBoldText();
368
+
369
+ return {
370
+ isScreenReaderEnabled,
371
+ isReduceMotionEnabled,
372
+ isBoldTextEnabled,
373
+ };
374
+ }
375
+
376
+ // ============================================================================
377
+ // Utilities
378
+ // ============================================================================
379
+
380
+ /**
381
+ * Announce a message to screen readers
382
+ */
383
+ export function announce(message: string): void {
384
+ AccessibilityInfo.announceForAccessibility(message);
385
+ }
386
+
387
+ /**
388
+ * Set focus to a specific element (requires a ref)
389
+ */
390
+ export function setAccessibilityFocus(ref: React.RefObject<unknown>): void {
391
+ if (ref.current) {
392
+ AccessibilityInfo.setAccessibilityFocus(ref.current);
393
+ }
394
+ }
395
+
396
+ /**
397
+ * Format a price for accessibility
398
+ */
399
+ export function formatPriceA11y(
400
+ amount: number,
401
+ currency = "EUR",
402
+ locale = "fr-FR"
403
+ ): string {
404
+ const formatted = new Intl.NumberFormat(locale, {
405
+ style: "currency",
406
+ currency,
407
+ }).format(amount);
408
+
409
+ // Convert to speakable format
410
+ return formatted
411
+ .replace("€", "euros")
412
+ .replace("$", "dollars")
413
+ .replace("£", "pounds");
414
+ }
415
+
416
+ /**
417
+ * Format a date for accessibility
418
+ */
419
+ export function formatDateA11y(date: Date | string, locale = "fr-FR"): string {
420
+ const d = typeof date === "string" ? new Date(date) : date;
421
+
422
+ return d.toLocaleDateString(locale, {
423
+ weekday: "long",
424
+ year: "numeric",
425
+ month: "long",
426
+ day: "numeric",
427
+ });
428
+ }
429
+
430
+ /**
431
+ * Format a duration for accessibility
432
+ */
433
+ export function formatDurationA11y(seconds: number): string {
434
+ const hours = Math.floor(seconds / 3600);
435
+ const minutes = Math.floor((seconds % 3600) / 60);
436
+ const secs = seconds % 60;
437
+
438
+ const parts = [];
439
+ if (hours > 0) parts.push(`${hours} hour${hours !== 1 ? "s" : ""}`);
440
+ if (minutes > 0) parts.push(`${minutes} minute${minutes !== 1 ? "s" : ""}`);
441
+ if (secs > 0 || parts.length === 0) {
442
+ parts.push(`${secs} second${secs !== 1 ? "s" : ""}`);
443
+ }
444
+
445
+ return parts.join(", ");
446
+ }