@croacroa/react-native-template 1.0.0 → 2.0.1

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 (70) 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 +21 -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 +60 -6
  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 +286 -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/validation.ts +2 -1
  70. package/utils/withAccessibility.tsx +272 -0
@@ -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
+ }