@incodetech/prizma-core-screens 0.2.4 → 0.2.12

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 (243) hide show
  1. package/dist/BiometricConsentScreen.d.ts +95 -0
  2. package/dist/BiometricConsentScreen.js +91 -0
  3. package/dist/BiometricConsentStrings.d.ts +84 -0
  4. package/dist/BiometricConsentStrings.js +31 -0
  5. package/dist/CaptureScreen.d.ts +13 -0
  6. package/dist/CaptureScreen.js +2 -2
  7. package/dist/CurpEnterScreen.d.ts +7 -0
  8. package/dist/CurpEnterScreen.js +17 -3
  9. package/dist/CurpGenerateScreen.d.ts +8 -0
  10. package/dist/CurpGenerateScreen.js +28 -13
  11. package/dist/CurpLayout.css +8 -34
  12. package/dist/CurpStrings.d.ts +16 -2
  13. package/dist/DemoScreensLayout.css +16 -0
  14. package/dist/DemoScreensRedirectScreen.d.ts +6 -1
  15. package/dist/DemoScreensRedirectScreen.js +3 -2
  16. package/dist/DemoScreensStrings.d.ts +5 -0
  17. package/dist/DigitalIdUploadErrorScreen.d.ts +31 -0
  18. package/dist/DigitalIdUploadErrorScreen.js +26 -0
  19. package/dist/DigitalIdUploadFileTooLargeScreen.d.ts +27 -0
  20. package/dist/DigitalIdUploadFileTooLargeScreen.js +21 -0
  21. package/dist/DigitalIdUploadReviewScreen.d.ts +57 -0
  22. package/dist/DigitalIdUploadReviewScreen.js +44 -0
  23. package/dist/DigitalIdUploadStrings.d.ts +64 -0
  24. package/dist/DigitalIdUploadStrings.js +19 -0
  25. package/dist/DigitalIdUploadTutorialScreen.d.ts +28 -0
  26. package/dist/DigitalIdUploadTutorialScreen.js +23 -0
  27. package/dist/DocumentCaptureIllustration.d.ts +1 -1
  28. package/dist/EkybAddressDetailsScreen.d.ts +7 -0
  29. package/dist/EkybAddressDetailsScreen.js +25 -7
  30. package/dist/EkybBusinessDetailsScreen.d.ts +6 -0
  31. package/dist/EkybBusinessDetailsScreen.js +23 -6
  32. package/dist/EkybCountryScreen.d.ts +13 -5
  33. package/dist/EkybCountryScreen.js +41 -10
  34. package/dist/EkybOwnersScreen.d.ts +8 -0
  35. package/dist/EkybOwnersScreen.js +26 -3
  36. package/dist/EkybStrings.d.ts +32 -10
  37. package/dist/EkycCountryCatalog.d.ts +4 -1
  38. package/dist/EkycCountryCatalog.js +5 -3
  39. package/dist/EkycCountryFormScreen.d.ts +12 -3
  40. package/dist/EkycCountryFormScreen.js +28 -10
  41. package/dist/EkycDetailsScreen.d.ts +7 -0
  42. package/dist/EkycDetailsScreen.js +21 -4
  43. package/dist/EkycStrings.d.ts +14 -2
  44. package/dist/ElectronicSignatureLayout.css +6 -0
  45. package/dist/EmailInputStrings.d.ts +11 -1
  46. package/dist/EmailInputVerifyScreen.d.ts +7 -0
  47. package/dist/EmailInputVerifyScreen.js +17 -3
  48. package/dist/ErrorScreensScreen.d.ts +5 -2
  49. package/dist/ErrorScreensScreen.js +15 -4
  50. package/dist/ErrorScreensStrings.d.ts +17 -2
  51. package/dist/ErrorScreensStrings.js +18 -10
  52. package/dist/FaceCaptureCapturingScreen.d.ts +63 -0
  53. package/dist/FaceCaptureCapturingScreen.js +62 -2
  54. package/dist/FaceCaptureLayout.css +69 -4
  55. package/dist/FaceCaptureStrings.d.ts +23 -0
  56. package/dist/FaceMatchComparisonScreen.d.ts +3 -3
  57. package/dist/FaceMatchLayout.css +21 -12
  58. package/dist/FormScreen.css +30 -0
  59. package/dist/FormScreen.d.ts +28 -0
  60. package/dist/FormScreen.js +3 -2
  61. package/dist/FormsLongFormScreen.d.ts +8 -0
  62. package/dist/FormsLongFormScreen.js +34 -13
  63. package/dist/FormsStrings.d.ts +11 -1
  64. package/dist/GeolocationBrowserStepsScreen.d.ts +6 -2
  65. package/dist/GeolocationBrowserStepsScreen.js +7 -3
  66. package/dist/GeolocationStrings.d.ts +3 -0
  67. package/dist/IdCaptureLayout.css +205 -2
  68. package/dist/IdCaptureStageScreen.d.ts +47 -0
  69. package/dist/IdCaptureStageScreen.js +39 -3
  70. package/dist/IdCaptureStrings.d.ts +19 -0
  71. package/dist/ListScreen.d.ts +21 -0
  72. package/dist/ListScreen.js +7 -2
  73. package/dist/LoaderScreen.d.ts +7 -5
  74. package/dist/LoaderScreen.js +7 -5
  75. package/dist/LoadersStrings.d.ts +18 -5
  76. package/dist/OcrReviewErrorScreen.d.ts +30 -0
  77. package/dist/OcrReviewErrorScreen.js +24 -0
  78. package/dist/OcrReviewLayout.css +24 -0
  79. package/dist/OcrReviewLoadingScreen.d.ts +25 -0
  80. package/dist/OcrReviewLoadingScreen.js +19 -0
  81. package/dist/OcrReviewScreen.d.ts +76 -0
  82. package/dist/OcrReviewScreen.js +65 -0
  83. package/dist/OcrReviewStrings.d.ts +47 -0
  84. package/dist/OcrReviewStrings.js +17 -0
  85. package/dist/OcrReviewSuccessScreen.d.ts +30 -0
  86. package/dist/OcrReviewSuccessScreen.js +24 -0
  87. package/dist/PermissionsAllowScreen.d.ts +8 -11
  88. package/dist/PermissionsAllowScreen.js +8 -11
  89. package/dist/PermissionsAskScreen.d.ts +41 -0
  90. package/dist/PermissionsAskScreen.js +43 -0
  91. package/dist/PermissionsBrowserStepsScreen.d.ts +9 -2
  92. package/dist/PermissionsBrowserStepsScreen.js +12 -3
  93. package/dist/PermissionsLayout.css +47 -0
  94. package/dist/PermissionsStrings.d.ts +20 -9
  95. package/dist/PermissionsStrings.js +10 -4
  96. package/dist/PhoneInputLayout.css +10 -9
  97. package/dist/PhoneInputOtpScreen.d.ts +8 -0
  98. package/dist/PhoneInputOtpScreen.js +26 -19
  99. package/dist/PhoneInputStrings.d.ts +22 -4
  100. package/dist/PhoneInputVerifyScreen.d.ts +8 -0
  101. package/dist/PhoneInputVerifyScreen.js +36 -7
  102. package/dist/QualifiedSignatureAcceptScreen.js +6 -1
  103. package/dist/QualifiedSignatureDocumentScreen.d.ts +17 -10
  104. package/dist/QualifiedSignatureDocumentScreen.js +22 -17
  105. package/dist/QualifiedSignatureLayout.css +76 -0
  106. package/dist/QualifiedSignatureResultScreen.d.ts +16 -13
  107. package/dist/QualifiedSignatureResultScreen.js +17 -18
  108. package/dist/QualifiedSignatureStrings.d.ts +19 -2
  109. package/dist/ResultScreen.d.ts +8 -0
  110. package/dist/ResultScreen.js +14 -3
  111. package/dist/ReusabilityCredentialSelectionScreen.d.ts +95 -0
  112. package/dist/ReusabilityCredentialSelectionScreen.js +94 -0
  113. package/dist/ReusabilityLayout.css +241 -0
  114. package/dist/ReusabilityPrivacyPolicyScreen.d.ts +55 -0
  115. package/dist/ReusabilityPrivacyPolicyScreen.js +44 -0
  116. package/dist/ReusabilityStrings.d.ts +85 -0
  117. package/dist/ReusabilityStrings.js +37 -0
  118. package/dist/ReviewScreen.d.ts +38 -12
  119. package/dist/ReviewScreen.js +41 -8
  120. package/dist/ScreenLayout.css +382 -6
  121. package/dist/ScreenNav.css +8 -0
  122. package/dist/ScreenNav.d.ts +15 -2
  123. package/dist/ScreenNav.js +10 -3
  124. package/dist/VideoSelfieErrorScreen.js +1 -1
  125. package/dist/VideoSelfieIdCaptureScreen.d.ts +10 -3
  126. package/dist/VideoSelfieIdCaptureScreen.js +45 -14
  127. package/dist/VideoSelfieLayout.css +467 -34
  128. package/dist/VideoSelfieSelfieCaptureScreen.d.ts +44 -27
  129. package/dist/VideoSelfieSelfieCaptureScreen.js +65 -39
  130. package/dist/VideoSelfieSpeakScreen.js +19 -2
  131. package/dist/VideoSelfieStrings.d.ts +82 -13
  132. package/dist/VideoSelfieTurnHeadScreen.d.ts +51 -0
  133. package/dist/VideoSelfieTurnHeadScreen.js +66 -0
  134. package/dist/VideoSelfieTutorialScreen.d.ts +9 -0
  135. package/dist/VideoSelfieTutorialScreen.js +22 -5
  136. package/dist/VideoSelfieWaveScreen.d.ts +53 -0
  137. package/dist/VideoSelfieWaveScreen.js +62 -0
  138. package/dist/ViewfinderScreen.css +36 -0
  139. package/dist/WatchlistBusinessFormScreen.d.ts +11 -7
  140. package/dist/WatchlistBusinessFormScreen.js +27 -13
  141. package/dist/WatchlistBusinessStrings.d.ts +21 -7
  142. package/dist/flows.d.ts +8 -0
  143. package/dist/flows.js +17 -0
  144. package/dist/index.d.ts +19 -1
  145. package/dist/index.js +14 -0
  146. package/dist/styles.css +3 -0
  147. package/dist/types.d.ts +3 -1
  148. package/package.json +79 -3
  149. package/samples/video-selfie-camera-wide.jpg +0 -0
  150. package/samples/video-selfie-room-wide.jpg +0 -0
  151. package/samples/video-selfie-room.png +0 -0
  152. package/src/BiometricConsentScreen.tsx +192 -0
  153. package/src/BiometricConsentStrings.ts +119 -0
  154. package/src/CaptureScreen.tsx +17 -0
  155. package/src/CurpEnterScreen.tsx +21 -4
  156. package/src/CurpGenerateScreen.tsx +46 -22
  157. package/src/CurpLayout.css +8 -34
  158. package/src/CurpStrings.ts +22 -2
  159. package/src/DemoScreensLayout.css +16 -0
  160. package/src/DemoScreensRedirectScreen.tsx +14 -6
  161. package/src/DemoScreensStrings.ts +5 -0
  162. package/src/DigitalIdUploadErrorScreen.tsx +83 -0
  163. package/src/DigitalIdUploadFileTooLargeScreen.tsx +67 -0
  164. package/src/DigitalIdUploadReviewScreen.tsx +119 -0
  165. package/src/DigitalIdUploadStrings.ts +88 -0
  166. package/src/DigitalIdUploadTutorialScreen.tsx +59 -0
  167. package/src/DocumentCaptureIllustration.tsx +1 -1
  168. package/src/EkybAddressDetailsScreen.tsx +32 -4
  169. package/src/EkybBusinessDetailsScreen.tsx +23 -5
  170. package/src/EkybCountryScreen.tsx +45 -7
  171. package/src/EkybOwnersScreen.tsx +34 -3
  172. package/src/EkybStrings.ts +34 -10
  173. package/src/EkycCountryCatalog.ts +9 -4
  174. package/src/EkycCountryFormScreen.tsx +51 -12
  175. package/src/EkycDetailsScreen.tsx +22 -1
  176. package/src/EkycStrings.ts +14 -2
  177. package/src/ElectronicSignatureLayout.css +6 -0
  178. package/src/EmailInputStrings.ts +12 -1
  179. package/src/EmailInputVerifyScreen.tsx +21 -4
  180. package/src/ErrorScreensScreen.tsx +15 -4
  181. package/src/ErrorScreensStrings.ts +37 -12
  182. package/src/FaceCaptureCapturingScreen.tsx +102 -13
  183. package/src/FaceCaptureLayout.css +69 -4
  184. package/src/FaceCaptureStrings.ts +24 -0
  185. package/src/FaceMatchComparisonScreen.tsx +3 -3
  186. package/src/FaceMatchLayout.css +21 -12
  187. package/src/FormScreen.css +30 -0
  188. package/src/FormScreen.tsx +34 -0
  189. package/src/FormsLongFormScreen.tsx +41 -13
  190. package/src/FormsStrings.ts +12 -1
  191. package/src/GeolocationBrowserStepsScreen.tsx +6 -3
  192. package/src/GeolocationStrings.ts +3 -0
  193. package/src/IdCaptureLayout.css +205 -2
  194. package/src/IdCaptureStageScreen.tsx +75 -0
  195. package/src/IdCaptureStrings.ts +19 -0
  196. package/src/ListScreen.tsx +58 -10
  197. package/src/LoaderScreen.tsx +7 -5
  198. package/src/LoadersStrings.ts +18 -5
  199. package/src/OcrReviewErrorScreen.tsx +53 -0
  200. package/src/OcrReviewLayout.css +24 -0
  201. package/src/OcrReviewLoadingScreen.tsx +48 -0
  202. package/src/OcrReviewScreen.tsx +190 -0
  203. package/src/OcrReviewStrings.ts +69 -0
  204. package/src/OcrReviewSuccessScreen.tsx +52 -0
  205. package/src/PermissionsAllowScreen.tsx +9 -10
  206. package/src/PermissionsAskScreen.tsx +105 -0
  207. package/src/PermissionsBrowserStepsScreen.tsx +11 -3
  208. package/src/PermissionsLayout.css +47 -0
  209. package/src/PermissionsStrings.ts +42 -12
  210. package/src/PhoneInputLayout.css +10 -9
  211. package/src/PhoneInputOtpScreen.tsx +23 -3
  212. package/src/PhoneInputStrings.ts +23 -4
  213. package/src/PhoneInputVerifyScreen.tsx +41 -5
  214. package/src/QualifiedSignatureAcceptScreen.tsx +12 -1
  215. package/src/QualifiedSignatureDocumentScreen.tsx +31 -19
  216. package/src/QualifiedSignatureLayout.css +76 -0
  217. package/src/QualifiedSignatureResultScreen.tsx +28 -26
  218. package/src/QualifiedSignatureStrings.ts +19 -2
  219. package/src/ResultScreen.tsx +33 -2
  220. package/src/ReusabilityCredentialSelectionScreen.tsx +246 -0
  221. package/src/ReusabilityLayout.css +241 -0
  222. package/src/ReusabilityPrivacyPolicyScreen.tsx +109 -0
  223. package/src/ReusabilityStrings.ts +127 -0
  224. package/src/ReviewScreen.tsx +96 -27
  225. package/src/ScreenLayout.css +382 -6
  226. package/src/ScreenNav.css +8 -0
  227. package/src/ScreenNav.tsx +35 -5
  228. package/src/VideoSelfieErrorScreen.tsx +1 -1
  229. package/src/VideoSelfieIdCaptureScreen.tsx +125 -31
  230. package/src/VideoSelfieLayout.css +467 -34
  231. package/src/VideoSelfieSelfieCaptureScreen.tsx +110 -57
  232. package/src/VideoSelfieSpeakScreen.tsx +34 -15
  233. package/src/VideoSelfieStrings.ts +89 -14
  234. package/src/VideoSelfieTurnHeadScreen.tsx +162 -0
  235. package/src/VideoSelfieTutorialScreen.tsx +45 -23
  236. package/src/VideoSelfieWaveScreen.tsx +143 -0
  237. package/src/ViewfinderScreen.css +36 -0
  238. package/src/WatchlistBusinessFormScreen.tsx +35 -16
  239. package/src/WatchlistBusinessStrings.ts +21 -7
  240. package/src/flows.ts +25 -0
  241. package/src/index.ts +88 -3
  242. package/src/styles.css +3 -0
  243. package/src/types.ts +3 -1
@@ -20,7 +20,10 @@
20
20
  // Verification", and no variant draws a close in the nav; steps after the
21
21
  // first get the archetype's Back button instead of a back arrow (#EKC-2).
22
22
 
23
- export type EkycFieldKind = 'text' | 'date' | 'phone' | 'dropdown' | 'radio';
23
+ /** `password`: Core's `PrizmaPassword`, the field with the eye that hides the
24
+ * typed value. For the identifiers a stranger must not read over a shoulder:
25
+ * the SSN / ITIN fields (Elena, 2026-09-28). No requirement checklist. */
26
+ export type EkycFieldKind = 'text' | 'date' | 'phone' | 'dropdown' | 'radio' | 'password';
24
27
 
25
28
  export type EkycFieldKey =
26
29
  | 'firstName' | 'middleName' | 'middleNameOptional' | 'lastName' | 'surname' | 'maternalSurname'
@@ -75,6 +78,8 @@ export interface EkycCountryVariant {
75
78
  }
76
79
 
77
80
  const t = (key: EkycFieldKey, extra: Partial<EkycField> = {}): EkycField => ({ key, kind: 'text', ...extra });
81
+ /** A hidden-by-default identifier, with the eye to reveal it. */
82
+ const secret = (key: EkycFieldKey, extra: Partial<EkycField> = {}): EkycField => ({ key, kind: 'password', ...extra });
78
83
  const date = (key: EkycFieldKey = 'dateOfBirth', extra: Partial<EkycField> = {}): EkycField => ({ key, kind: 'date', ...extra });
79
84
  // Required fields carry Core's asterisk (`required` + `requiredHint`), the
80
85
  // owner's rule (Elena, 2026-09-24, DEVIATIONS #EKC-6): first name, last name or
@@ -112,7 +117,7 @@ export const EKYC_COUNTRY_CATALOG: EkycCountryVariant[] = [
112
117
  {
113
118
  id: 'us-telco-1', country: 'US', label: 'United States', provider: 'Telco 1', stepper: 3,
114
119
  steps: [
115
- { node: '2098:9575', fields: [...NAME_PAIR(), email(), ...DOB_PHONE('US'), t('ssnLast4')] },
120
+ { node: '2098:9575', fields: [...NAME_PAIR(), email(), ...DOB_PHONE('US'), secret('ssnLast4')] },
116
121
  { node: '5854:76222', section: 'license', fields: [t('licenseNumber'), { key: 'licenseState', kind: 'dropdown', placeholder: 'selectState' }] },
117
122
  { node: '5854:76221', section: 'address', fields: US_ADDRESS },
118
123
  ],
@@ -129,7 +134,7 @@ export const EKYC_COUNTRY_CATALOG: EkycCountryVariant[] = [
129
134
  steps: [
130
135
  /* Same First | Last pair as the licence variant; Middle name (Optional)
131
136
  follows it and spans (#EKC-5). */
132
- { node: '2098:9577', fields: [...NAME_PAIR(), t('middleNameOptional'), ...DOB_PHONE('US'), email(), t('ssnOrItin')] },
137
+ { node: '2098:9577', fields: [...NAME_PAIR(), t('middleNameOptional'), ...DOB_PHONE('US'), email(), secret('ssnOrItin')] },
133
138
  { node: '5857:34094', section: 'address', fields: US_ADDRESS },
134
139
  ],
135
140
  },
@@ -148,7 +153,7 @@ export const EKYC_COUNTRY_CATALOG: EkycCountryVariant[] = [
148
153
  {
149
154
  id: 'ca-res-credit', country: 'CA', label: 'Canada', provider: 'Residential credit', stepper: 2,
150
155
  steps: [
151
- { node: '2098:9570', fields: [...NAME_PAIR(), email(), ...DOB_PHONE('CA'), t('ssnOrItin')] },
156
+ { node: '2098:9570', fields: [...NAME_PAIR(), email(), ...DOB_PHONE('CA'), secret('ssnOrItin')] },
152
157
  { node: '5857:36450', section: 'address', fields: CODE_ADDRESS('stateCodeCa') },
153
158
  ],
154
159
  },
@@ -1,5 +1,6 @@
1
1
  import React, { forwardRef } from 'react';
2
2
  import { PrizmaTextField } from '@incodetech/prizma-core-web/text-field';
3
+ import { PrizmaPassword } from '@incodetech/prizma-core-web/password';
3
4
  import { PrizmaDateField } from '@incodetech/prizma-core-web/date-field';
4
5
  import { PrizmaDropdown } from '@incodetech/prizma-core-web/dropdown';
5
6
  import { PrizmaCombined, type PrizmaCountry } from '@incodetech/prizma-core-web/combined';
@@ -28,11 +29,20 @@ export interface EkycCountryFormScreenProps
28
29
  values?: EkycCountryValues;
29
30
  /**
30
31
  * The state after Continue was pressed with fields still empty: every empty
31
- * field shows Core's error state and `country.requiredError` under it.
32
- * Continue itself is ALWAYS enabled (Elena, 2026-09-24): validation happens
33
- * on press, not by greying the button.
32
+ * REQUIRED field shows Core's error state and `country.requiredError` under
33
+ * it, and Continue is disabled (GUIDELINES §6-E empty-submit rule, Elena
34
+ * 2026-09-30). Without it Continue stays enabled: validation happens on
35
+ * press, the resting page never greys the button.
34
36
  */
35
37
  showErrors?: boolean;
38
+ /**
39
+ * Field keys whose typed value failed the format check on Continue (an
40
+ * invalid CPF, say). Each keeps its value, shows Core's error state and
41
+ * `country.invalidErrors[key]` under it; Continue stays enabled so the user
42
+ * corrects it and presses again, the CURP precedent (`CurpEnterScreen`'s
43
+ * `invalid`). A key with no message in `invalidErrors` shows no error.
44
+ */
45
+ invalidFields?: string[];
36
46
  /** Dial codes the phone field offers. Runtime data, never copy. */
37
47
  phoneCountries?: PrizmaCountry[];
38
48
  /** Options for the dropdown fields, keyed by field key. Runtime data. */
@@ -52,10 +62,13 @@ export interface EkycCountryFormScreenProps
52
62
  * dropdown, radio pair — so a new country is a catalogue entry, never a file.
53
63
  *
54
64
  * Deliberate, per frame and per owner:
55
- * - Continue is ALWAYS enabled (Elena, 2026-09-24, overriding the grey button
56
- * most frames draw): pressing it with empty fields is the `showErrors` state,
57
- * Core's error ring and message under each empty field. The radio pair has
58
- * no error state in Core, so an empty gender shows none (DEVIATIONS #EKC-4).
65
+ * - Continue is enabled on the resting page (Elena, 2026-09-24, overriding the
66
+ * grey button most frames draw): pressing it with empty fields is the
67
+ * `showErrors` state, Core's error ring and message under each empty
68
+ * required field, and THEN Continue is disabled. The "always enabled" half
69
+ * of DEVIATIONS #EKC-4 is superseded by the 2026-09-30 empty-submit rule
70
+ * (GUIDELINES §6-E). The radio pair has no error state in Core, so an empty
71
+ * gender still shows none.
59
72
  * - ONE title per step (Elena, 2026-09-24): the personal-data step is "Enter
60
73
  * your details" with its subtitle; a licence step is titled "Driver's
61
74
  * License details" and an address step "Address details", with nothing
@@ -82,7 +95,7 @@ const groupRows = (fields: EkycField[]): EkycField[][] => {
82
95
  return groups;
83
96
  };
84
97
  export const EkycCountryFormScreen = forwardRef<HTMLDivElement, EkycCountryFormScreenProps>(
85
- ({ strings, variant, step = 1, values, showErrors = false, phoneCountries, options, screenId, overrides, onAction, ...shell }, ref) => {
98
+ ({ strings, variant, step = 1, values, showErrors = false, invalidFields, phoneCountries, options, screenId, overrides, onAction, ...shell }, ref) => {
86
99
  const v = typeof variant === 'string' ? ekycCountryVariant(variant) : variant;
87
100
  if (!v) return null;
88
101
  const s = v.steps[Math.min(Math.max(step, 1), v.steps.length) - 1];
@@ -90,15 +103,23 @@ export const EkycCountryFormScreen = forwardRef<HTMLDivElement, EkycCountryFormS
90
103
  const stepperState: PrizmaStepperState = step <= 1 ? 'Start' : (Math.min(step, 6) as PrizmaStepperState);
91
104
 
92
105
  const label = (f: EkycField) => c.fields[f.key] ?? f.key;
93
- const placeholder = (f: EkycField) => (f.placeholder ? c.placeholders[f.placeholder] : c.placeholders[f.key]);
106
+ // A lone field in its empty-submit error drops its placeholder, as the
107
+ // single-input screens do (Phone, Email and CURP's `emptyError`).
108
+ const lone = s.fields.length === 1;
109
+ const placeholder = (f: EkycField) =>
110
+ lone && showErrors && !values?.[f.key] ? undefined : f.placeholder ? c.placeholders[f.placeholder] : c.placeholders[f.key];
94
111
 
95
112
  const renderField = (f: EkycField) => {
96
113
  // Only a REQUIRED empty field is an error on press (Elena, 2026-09-24,
97
- // DEVIATIONS #EKC-6): an optional field left blank shows nothing.
98
- const missing = showErrors && Boolean(f.required) && !values?.[f.key];
114
+ // DEVIATIONS #EKC-6): an optional field left blank shows nothing. A
115
+ // step's lone field is required by definition, asterisk or not (the
116
+ // single-input rule, as Phone and Email Input: Elena, 2026-10-02).
117
+ const required = Boolean(f.required) || lone;
118
+ const missing = showErrors && required && !values?.[f.key];
119
+ const invalidText = invalidFields?.includes(f.key) ? c.invalidErrors?.[f.key] : undefined;
99
120
  const common = {
100
121
  key: f.key, label: label(f), required: f.required, requiredHint: strings.details.requiredHint,
101
- error: missing || undefined, errorText: missing ? c.requiredError : undefined,
122
+ error: missing || Boolean(invalidText) || undefined, errorText: missing ? c.requiredError : invalidText,
102
123
  } as const;
103
124
  switch (f.kind) {
104
125
  case 'date':
@@ -126,6 +147,22 @@ export const EkycCountryFormScreen = forwardRef<HTMLDivElement, EkycCountryFormS
126
147
  defaultValue={values?.[f.key] ?? null}
127
148
  />
128
149
  );
150
+ case 'password':
151
+ /* The SSN / ITIN fields: Core's Password field, hidden by default, the
152
+ eye reveals it. No requirement checklist — this is an identifier,
153
+ not a password to compose (Elena, 2026-09-28). */
154
+ return (
155
+ <PrizmaPassword
156
+ {...common}
157
+ placeholder={placeholder(f)}
158
+ autoComplete="off"
159
+ defaultValue={values?.[f.key] ?? ''}
160
+ showPasswordLabel={c.showValueLabel}
161
+ hidePasswordLabel={c.hideValueLabel}
162
+ showHelper={false}
163
+ showRequirements={false}
164
+ />
165
+ );
129
166
  case 'radio':
130
167
  return (
131
168
  <PrizmaRadioGroup key={f.key} name={f.key} legend={label(f)} defaultValue={values?.[f.key]} className="ekyc-country__radios">
@@ -158,6 +195,8 @@ export const EkycCountryFormScreen = forwardRef<HTMLDivElement, EkycCountryFormS
158
195
  ) : undefined
159
196
  }
160
197
  ctaLabel={c.ctaLabel}
198
+ /* Greyed only after the empty submit (2026-09-30 empty-submit rule). */
199
+ ctaDisabled={showErrors}
161
200
  verifiedLabel={strings.verifiedLabel}
162
201
  navLabel={strings.navLabel}
163
202
  screenId={screenId}
@@ -71,6 +71,13 @@ export interface EkycDetailsScreenProps
71
71
  * the label row Figma draws as "First name *" — so Middle name, the one
72
72
  * optional field, correctly renders none.
73
73
  *
74
+ * `emptyError` is the state after Continue was pressed with the fields still
75
+ * empty (Elena, 2026-09-30, GUIDELINES §6-E empty-submit rule; drawn precedent
76
+ * `FormsFieldScreen`'s `emptyError`): every field renders empty, the three
77
+ * REQUIRED ones carry Core's `error` + `country.requiredError`, Middle name
78
+ * stays unflagged (DEVIATIONS #EKC-6), and Continue is disabled. The resting
79
+ * `default` keeps Continue enabled so the press can happen at all.
80
+ *
74
81
  * Figma draws a two-segment journey bar between the nav and the title on every
75
82
  * page-1 frame, and it is the only chrome that separates page 1 from page 2.
76
83
  * Archetype E had no slot above the title; the gap is now CLOSED in the
@@ -85,8 +92,9 @@ export const EkycDetailsScreen = forwardRef<HTMLDivElement, EkycDetailsScreenPro
85
92
  {
86
93
  strings,
87
94
  state = 'default',
88
- values,
95
+ values: valuesProp,
89
96
  screenId = `ekyc-details-${state}`,
97
+ ctaDisabled,
90
98
  className,
91
99
  overrides,
92
100
  onAction,
@@ -98,6 +106,13 @@ export const EkycDetailsScreen = forwardRef<HTMLDivElement, EkycDetailsScreenPro
98
106
  // rings First name blue and shows its clear ✕; the other two draw every
99
107
  // field resting.
100
108
  const focused = state === 'focused';
109
+ // The empty-submit state: every field empty, required ones in error.
110
+ const emptyError = state === 'emptyError';
111
+ const values = emptyError ? undefined : valuesProp;
112
+ // Core's own error treatment, spread onto the REQUIRED fields only.
113
+ const missing = emptyError
114
+ ? { error: true, errorText: strings.country.requiredError }
115
+ : {};
101
116
 
102
117
  return (
103
118
  <PrizmaFormScreen
@@ -122,6 +137,9 @@ export const EkycDetailsScreen = forwardRef<HTMLDivElement, EkycDetailsScreenPro
122
137
  title={strings.details.title}
123
138
  subtitle={strings.details.subtitle}
124
139
  ctaLabel={strings.details.ctaLabel}
140
+ /* Disabled only after the empty submit; the resting page keeps it
141
+ enabled so the press can happen. */
142
+ ctaDisabled={emptyError || ctaDisabled}
125
143
  verifiedLabel={strings.verifiedLabel}
126
144
  navLabel={strings.navLabel}
127
145
  screenId={screenId}
@@ -146,6 +164,7 @@ export const EkycDetailsScreen = forwardRef<HTMLDivElement, EkycDetailsScreenPro
146
164
  label={strings.details.firstNameLabel}
147
165
  required
148
166
  requiredHint={strings.details.requiredHint}
167
+ {...missing}
149
168
  defaultValue={values?.firstName ?? ''}
150
169
  autoComplete="off"
151
170
  /* Only 2101:8159 draws the ✕, and only in this field. */
@@ -156,6 +175,7 @@ export const EkycDetailsScreen = forwardRef<HTMLDivElement, EkycDetailsScreenPro
156
175
  label={strings.details.lastNameLabel}
157
176
  required
158
177
  requiredHint={strings.details.requiredHint}
178
+ {...missing}
159
179
  defaultValue={values?.lastName ?? ''}
160
180
  autoComplete="off"
161
181
  clearable={false}
@@ -171,6 +191,7 @@ export const EkycDetailsScreen = forwardRef<HTMLDivElement, EkycDetailsScreenPro
171
191
  label={strings.details.dateOfBirthLabel}
172
192
  required
173
193
  requiredHint={strings.details.requiredHint}
194
+ {...missing}
174
195
  defaultValue={values?.dateOfBirth ?? ''}
175
196
  autoComplete="off"
176
197
  /* No `onCalendarClick`: a screen is a static spec state with no
@@ -43,8 +43,12 @@
43
43
  * the clear ✕ (2101:8159 draws the OS keyboard over the CTA; the
44
44
  * keyboard is OS chrome, never a screen element)
45
45
  * - `filled` — all four fields resting with their values
46
+ * - `emptyError` — Continue was pressed with the fields empty (GUIDELINES §6-E
47
+ * empty-submit rule, Elena 2026-09-30): the three REQUIRED fields
48
+ * in Core's error state with `country.requiredError`, Middle name
49
+ * (optional) unflagged, and Continue disabled. Not a Figma frame.
46
50
  */
47
- export type EkycDetailsState = 'default' | 'focused' | 'filled';
51
+ export type EkycDetailsState = 'default' | 'focused' | 'filled' | 'emptyError';
48
52
 
49
53
  /**
50
54
  * The page-2 sections (`eKYC.default` / `eKYC.filled` / `eKYC.loading`), same
@@ -153,6 +157,10 @@ export interface EkycStrings {
153
157
  licenseSection: string;
154
158
  /** Accessible name of the phone field's dial-code list. */
155
159
  countryListLabel: string;
160
+ /** Accessible names for the eye on a `password` field (the SSN / ITIN
161
+ * fields): while the value is hidden, and while it is shown. */
162
+ showValueLabel: string;
163
+ hideValueLabel: string;
156
164
  /** The gender radio pair, Argentina. */
157
165
  genderOptions: { value: string; label: string }[];
158
166
  fields: Record<string, string>;
@@ -160,8 +168,12 @@ export interface EkycStrings {
160
168
  ctaLabel: string;
161
169
  /** The secondary button on a step after the first. */
162
170
  backLabel: string;
163
- /** Under every empty field once Continue was pressed (`showErrors`). */
171
+ /** Under every empty REQUIRED field once Continue was pressed: the
172
+ * by-country `showErrors` state and page 1's `emptyError` both read it. */
164
173
  requiredError: string;
174
+ /** Under a field whose value failed the format check on Continue, by
175
+ * field key (`invalidFields`). `cpf` is SDK key `dynamicForms.invalidCpf`. */
176
+ invalidErrors?: Record<string, string>;
165
177
  };
166
178
 
167
179
  /** `eKYC.processing` — spinner over the status line, no button. */
@@ -47,6 +47,12 @@
47
47
  border: var(--prizma-border-width-regular) solid var(--prizma-border-neutral-100);
48
48
  border-radius: var(--prizma-border-radius-small);
49
49
  text-align: start;
50
+ /* Air between the frame and the CTA band. Archetype G's list middle carries
51
+ no bottom gap (its content normally ends well above the band), so a frame
52
+ that fills the middle sat flush against Continue (owner-reported
53
+ 2026-09-29). Spacing/16 is the archetype's own scrolling-middle → band gap
54
+ (`.prizma-screen-layout__list--scroll`, ScreenLayout.css). */
55
+ margin-bottom: var(--prizma-spacing-16);
50
56
  }
51
57
 
52
58
  /* The agreement's own typography — Body/M Regular on Text/Body/500, the pair
@@ -27,13 +27,18 @@
27
27
  * - `filled` — focused field with a complete address
28
28
  * - `error` — negative field + clear ✕ + the invalid-format row
29
29
  * - `loading` — field greyed while the code is requested, spinner in the CTA
30
+ * - `emptyError` — "Continue" pressed with the field still empty: the field
31
+ * stays empty (no placeholder) in Core's error state with `requiredError`,
32
+ * and the CTA is disabled. GUIDELINES §6-E empty-submit rule (Elena,
33
+ * 2026-09-30).
30
34
  */
31
35
  export type EmailInputVerifyState =
32
36
  | 'default'
33
37
  | 'typing'
34
38
  | 'filled'
35
39
  | 'error'
36
- | 'loading';
40
+ | 'loading'
41
+ | 'emptyError';
37
42
 
38
43
  /**
39
44
  * The `email.OTP.*` sections — same layout, different code-field state:
@@ -58,6 +63,12 @@ export interface EmailInputStrings {
58
63
  verifiedLabel: string;
59
64
  /** Accessible name for the logo nav row. */
60
65
  navLabel?: string;
66
+ /**
67
+ * Error row under the required field when the primary CTA is pressed with it
68
+ * still empty (the verify screen's `emptyError` state): "This field is
69
+ * required". SDK key `dynamicForms.fieldRequired`.
70
+ */
71
+ requiredError: string;
61
72
 
62
73
  /** email.verifyEmail.* */
63
74
  verify: {
@@ -48,6 +48,13 @@ export interface EmailInputVerifyScreenProps
48
48
  * paints the DISABLED surface (owner definition 2026-07-12), which would grey
49
49
  * the four resting states Figma draws white. The field is uncontrolled instead
50
50
  * (DEVIATIONS #PI-6).
51
+ *
52
+ * `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
53
+ * GUIDELINES §6-E empty-submit rule): "Continue" was pressed with nothing
54
+ * typed. The field renders empty with no placeholder, in Core's error state
55
+ * with `requiredError`, and the primary CTA is disabled, following the drawn
56
+ * precedent in `FormsFieldScreen`. The resting `default` keeps the CTA enabled
57
+ * so the user can reach this state.
51
58
  */
52
59
  export const EmailInputVerifyScreen = forwardRef<HTMLDivElement, EmailInputVerifyScreenProps>(
53
60
  (
@@ -64,6 +71,9 @@ export const EmailInputVerifyScreen = forwardRef<HTMLDivElement, EmailInputVerif
64
71
  ) => {
65
72
  const busy = state === 'loading';
66
73
  const invalid = state === 'error';
74
+ // Empty submit: the field drops its placeholder and takes Core's error
75
+ // state with the shared "required" row; the CTA greys.
76
+ const emptyError = state === 'emptyError';
67
77
  const focused = state === 'typing' || state === 'filled';
68
78
 
69
79
  return (
@@ -74,6 +84,7 @@ export const EmailInputVerifyScreen = forwardRef<HTMLDivElement, EmailInputVerif
74
84
  ctaLabel={strings.verify.ctaLabel}
75
85
  /* Figma's `loading` section swaps the CTA label for Core's spinner. */
76
86
  ctaLoading={busy}
87
+ ctaDisabled={emptyError}
77
88
  ctaLoadingLabel={strings.verify.ctaLoadingLabel}
78
89
  verifiedLabel={strings.verifiedLabel}
79
90
  navLabel={strings.navLabel}
@@ -97,13 +108,19 @@ export const EmailInputVerifyScreen = forwardRef<HTMLDivElement, EmailInputVerif
97
108
  /* Figma's field has no visible label row — the placeholder carries
98
109
  the meaning — so the copy rides `aria-label` (DEVIATIONS #PI-4). */
99
110
  aria-label={strings.verify.fieldLabel}
100
- placeholder={strings.verify.placeholder}
111
+ placeholder={emptyError ? undefined : strings.verify.placeholder}
101
112
  /* Uncontrolled: a screen is one static spec state, so no `onChange`
102
113
  is needed and no warning is raised. */
103
- defaultValue={value ?? ''}
114
+ defaultValue={emptyError ? '' : value ?? ''}
104
115
  disabled={busy}
105
- error={invalid}
106
- errorText={invalid ? strings.verify.errorText : undefined}
116
+ error={invalid || emptyError}
117
+ errorText={
118
+ invalid
119
+ ? strings.verify.errorText
120
+ : emptyError
121
+ ? strings.requiredError
122
+ : undefined
123
+ }
107
124
  /* The clear ✕ is drawn on the `error` section only. */
108
125
  clearable={invalid}
109
126
  clearLabel={strings.verify.clearLabel}
@@ -38,7 +38,7 @@ export interface ErrorScreensScreenProps
38
38
  * Error screens — the session-level failures, built on the Result archetype
39
39
  * (GUIDELINES.md §6-F, `PrizmaResultScreen`).
40
40
  *
41
- * ONE screen for all three generic sections. They draw the identical skeleton —
41
+ * ONE screen for all four generic sections. They draw the identical skeleton —
42
42
  * a centred mark + title + supporting line over a CTA band — and differ only in
43
43
  * copy, in which mark leads, and in what the band carries:
44
44
  *
@@ -46,8 +46,11 @@ export interface ErrorScreensScreenProps
46
46
  * - `noConnection` — Wi-Fi glyph, "Refresh"
47
47
  * - `unsupportedBrowser` — canonical negative mark, the session URL above
48
48
  * "Copy link", with "Continue anyways" under it
49
+ * - `somethingWentWrong` — canonical negative mark, "Try again" when the copy
50
+ * carries a CTA, "Quit process" under it when it
51
+ * carries a quit label; a dead end when it has neither
49
52
  *
50
- * Chrome is CONSTANT. All three frames are drawn without the logo header and
53
+ * Chrome is CONSTANT. All the frames are drawn without the logo header and
51
54
  * without the verified tag; both are kept here, because the owner rule of
52
55
  * 2026-08-18 (#F-30, #F-34) makes the chrome the same on every screen so
53
56
  * nothing shifts between states, and every module's connectivity screen must
@@ -91,11 +94,19 @@ export const ErrorScreensScreen = forwardRef<HTMLDivElement, ErrorScreensScreenP
91
94
  ) => {
92
95
  const unsupported = variant === 'unsupportedBrowser';
93
96
  const noConnection = variant === 'noConnection';
97
+ const generic = variant === 'somethingWentWrong';
94
98
  const copy = unsupported
95
99
  ? strings.unsupportedBrowser
96
100
  : noConnection
97
101
  ? strings.noConnection
98
- : strings.sessionExpired;
102
+ : generic
103
+ ? strings.somethingWentWrong
104
+ : strings.sessionExpired;
105
+ const linkLabel = unsupported
106
+ ? strings.unsupportedBrowser.continueLabel
107
+ : generic
108
+ ? strings.somethingWentWrong.quitLabel
109
+ : undefined;
99
110
 
100
111
  return (
101
112
  <PrizmaResultScreen
@@ -133,7 +144,7 @@ export const ErrorScreensScreen = forwardRef<HTMLDivElement, ErrorScreensScreenP
133
144
  : undefined
134
145
  }
135
146
  ctaLabel={copy.ctaLabel}
136
- linkLabel={unsupported ? strings.unsupportedBrowser.continueLabel : undefined}
147
+ linkLabel={linkLabel}
137
148
  verifiedLabel={strings.verifiedLabel}
138
149
  navLabel={strings.navLabel}
139
150
  screenId={screenId}
@@ -6,19 +6,27 @@
6
6
  // 214:3357 Geolocation.error.noNetwork → `noConnection`
7
7
  // 240:154 Error screen - Unsupported browser → `unsupportedBrowser`
8
8
  //
9
- // The three failures that belong to the SESSION rather than to any module: the
10
- // link has expired, the network went away, the browser cannot run the SDK.
11
- // Every module can hit all three, which is why they are drawn once in the
12
- // generic file — a user who sees three different "no internet" screens in one
13
- // journey reads them as three different products.
9
+ // plus the one failure every module draws for itself and the file never did:
10
+ //
11
+ // — <module>.error "Something went wrong" → `somethingWentWrong`
12
+ //
13
+ // The failures that belong to the SESSION rather than to any module: the link
14
+ // has expired, the network went away, the browser cannot run the SDK, the
15
+ // server said no for a reason the user cannot act on. Every module can hit all
16
+ // four, which is why they are recorded once in the generic family — a user who
17
+ // sees three different "no internet" screens in one journey reads them as three
18
+ // different products.
14
19
  //
15
20
  // `noConnection` is deliberately the same screen every module already ships
16
21
  // (`FaceCaptureNoConnectionScreen`, `IdCaptureNoConnectionScreen`,
17
- // `DocumentCaptureNoConnectionScreen`, `GeolocationErrorScreen`'s `noNetwork`).
18
- // The generic entry is where the shape is DOCUMENTED; the module copies stay
19
- // put until someone retires them deliberately, because deleting four exported
20
- // components to save one screen is a breaking change for every host that
21
- // imports them. Flagged for the owner.
22
+ // `DocumentCaptureNoConnectionScreen`, `GeolocationErrorScreen`'s `noNetwork`,
23
+ // Phone Input's and Video Selfie's `noConnection`). `somethingWentWrong` is the
24
+ // same screen eKYC, eKYB, Forms, Email Input, Phone Input, Certificate
25
+ // Issuance, both signatures and Video Selfie already ship. The generic entry is
26
+ // where the shape and the copy are DOCUMENTED; the module copies stay put until
27
+ // someone retires them deliberately, because deleting exported components to
28
+ // save one screen is a breaking change for every host that imports them.
29
+ // Flagged for the owner (DEVIATIONS #GEN-7, #GEN-18).
22
30
  //
23
31
  // The family has no flow of its own: each of these is entered from wherever the
24
32
  // session broke and exits to a retry or to nothing. Core Lab lists it as a
@@ -27,15 +35,17 @@
27
35
  // No hardcoded English anywhere in a screen: every visible string arrives here.
28
36
 
29
37
  /**
30
- * The three sections.
38
+ * The four sections.
31
39
  * - `sessionExpired` — the link is dead; the user must go back to the provider
32
40
  * - `noConnection` — the network went away; retry when it returns
33
41
  * - `unsupportedBrowser` — the SDK cannot run here; copy the link and move
42
+ * - `somethingWentWrong` — the generic failure; try again, or nothing, or quit
34
43
  */
35
44
  export type ErrorScreensVariant =
36
45
  | 'sessionExpired'
37
46
  | 'noConnection'
38
- | 'unsupportedBrowser';
47
+ | 'unsupportedBrowser'
48
+ | 'somethingWentWrong';
39
49
 
40
50
  export interface ErrorScreensCopy {
41
51
  title: string;
@@ -63,4 +73,19 @@ export interface ErrorScreensStrings {
63
73
  /** Snackbar copy after the link is copied — the frame's second state. */
64
74
  copiedLabel: string;
65
75
  };
76
+
77
+ /**
78
+ * The generic failure. Three ways the modules fill it, all the same screen:
79
+ * `ctaLabel` "Try again" (eKYC, eKYB, Forms, Email Input);
80
+ * no `ctaLabel`, a dead end (Certificate Issuance, Qualified Signature);
81
+ * `ctaLabel` plus `quitLabel`, the way out under the retry (Video Selfie).
82
+ * Phone Input's `unknown` fills it with "Continue" instead, because a retry
83
+ * may not be possible there (owner, 2026-10-09) — the CTA is the module's
84
+ * call; the shape and the title are what the family fixes.
85
+ * The title never carries an ellipsis — a failure is not in progress.
86
+ */
87
+ somethingWentWrong: ErrorScreensCopy & {
88
+ /** Tertiary link under the CTA — "Quit process". Omit for no way out. */
89
+ quitLabel?: string;
90
+ };
66
91
  }
@@ -1,5 +1,6 @@
1
1
  import React, { forwardRef, type ReactNode } from 'react';
2
2
  import type { PrizmaSelfieCaptureState } from '@incodetech/prizma-core-web/selfie-capture';
3
+ import { PrizmaSnackbar } from '@incodetech/prizma-core-web/snackbar';
3
4
  import { PrizmaCaptureScreen, type PrizmaCaptureScreenProps } from './CaptureScreen';
4
5
  import type {
5
6
  FaceCaptureCapturingState,
@@ -46,6 +47,19 @@ export interface FaceCaptureCapturingScreenProps
46
47
  state?: FaceCaptureCapturingState;
47
48
  /** Live camera feed (<video>) or sample still (<img>) for the ring window. */
48
49
  camera?: ReactNode;
50
+ /**
51
+ * Raise the "Need more time?" offer over the feed — the accessibility
52
+ * notification shown 20 s before auto capture falls back to manual.
53
+ *
54
+ * It is a MODIFIER of whatever capturing state is already on screen, not a
55
+ * sixteenth state: the timer runs underneath the guidance, so the user can be
56
+ * told to move closer and offered more time in the same frame. Needs
57
+ * `strings.extendTime`; without it the flag is a no-op, which is what a host
58
+ * that has not localized the copy should get rather than English.
59
+ */
60
+ extendTime?: boolean;
61
+ /** Tapping "Extend auto-capture". The host resets its own fallback timer. */
62
+ onExtendTime?: () => void;
49
63
  }
50
64
 
51
65
  /**
@@ -65,6 +79,56 @@ export interface FaceCaptureCapturingScreenProps
65
79
  *
66
80
  * The archetype adds no offsets of its own — the component owns that spacing
67
81
  * (same deviation row).
82
+ *
83
+ * ── The "Need more time?" offer (`extendTime`) ──────────────────────────────
84
+ *
85
+ * Auto capture gives up after a configured timeout and drops the user into
86
+ * manual capture. Someone who is struggling — a tremor, low vision, bad light,
87
+ * an unfamiliar phone — gets moved off the EASIER path precisely because they
88
+ * were slow, which is backwards. So 20 s before the switch, the screen offers
89
+ * to stay on auto. A client turns it on per flow from the dashboard; with it
90
+ * off, nothing about today's capture changes.
91
+ *
92
+ * NOT a nicety — it is the conformance route for WCAG 2.2.1 Timing Adjustable
93
+ * via the *Extend* clause (warn before the limit, ≥20 s to act, ≥10 extensions).
94
+ * Two things follow that the visuals cannot settle: the notice must be able to
95
+ * come back ten times, and it must be ANNOUNCED early enough to be acted on.
96
+ *
97
+ * ── Live-region politeness: `role="alert"`, not the default ────────────────
98
+ *
99
+ * SETTLED 2026-10-05 (Camila). The Snackbar computes `role="status"` (polite)
100
+ * for every state but error, and this instance overrides it to `alert`.
101
+ *
102
+ * A polite region is allowed to wait until the screen reader finishes whatever
103
+ * it is already saying — and on this screen it always IS saying something: the
104
+ * capture instruction under the ring changes as the face moves, so the queue
105
+ * is rarely empty. The offer has a 20 s life. Queued behind two or three
106
+ * guidance announcements it can be read out after it has already expired,
107
+ * which turns the one affordance a struggling user needs into an
108
+ * announcement that the limit has passed.
109
+ *
110
+ * That is not a polish issue, it is the conformance failing: WCAG 2.2.1's
111
+ * Extend clause requires the warning to be given in time to ACT on it. An
112
+ * announcement the user cannot act on does not satisfy it, however correct the
113
+ * visuals. `alert` is assertive, so it interrupts the guidance and is heard
114
+ * while the 20 s is still running.
115
+ *
116
+ * The usual objection to `alert` is that interrupting is rude. It applies to
117
+ * notices that can wait; this one cannot, and it is strictly better to
118
+ * interrupt "Move closer" than to let the window close in silence. It is also
119
+ * why the offer must not be re-raised on a timer — each new `alert` interrupts
120
+ * again. It appears once per countdown, and WCAG allows that up to ten times.
121
+ *
122
+ * ⚠ NEW, not yet authored in Core. Two parts of this notice have no Figma
123
+ * variant behind them and are drawn from the exploration, not from the SoT:
124
+ * • the bold title line — the Snackbar has no `title` slot, so it rides
125
+ * inside `children` as a <strong>. Core to decide whether to adopt a real
126
+ * slot, at which point this moves onto it.
127
+ * • the state — it is `neutral` (blue surface, ⓘ glyph), Camila's call
128
+ * 2026-10-05. `warning` would signal attention more sharply but turns the
129
+ * surface amber, and the exploration's open question is whether amber
130
+ * reads as "hurry up" to someone who was already struggling. The icon
131
+ * cannot be chosen independently: `ICON_BY_STATE` binds it to `state`.
68
132
  */
69
133
  export const FaceCaptureCapturingScreen = forwardRef<
70
134
  HTMLDivElement,
@@ -75,24 +139,49 @@ export const FaceCaptureCapturingScreen = forwardRef<
75
139
  strings,
76
140
  state = 'initiating',
77
141
  camera,
142
+ extendTime,
143
+ onExtendTime,
78
144
  screenId = 'face-capture-capturing',
79
145
  className,
80
146
  ...rest
81
147
  },
82
148
  ref,
83
- ) => (
84
- <PrizmaCaptureScreen
85
- ref={ref}
86
- className={className}
87
- state={SELFIE_STATE[state]}
88
- camera={camera}
89
- feedbackText={strings.capturing[state]}
90
- verifiedLabel={strings.verifiedLabel}
91
- navLabel={strings.navLabel}
92
- screenId={screenId}
93
- {...rest}
94
- />
95
- ),
149
+ ) => {
150
+ const offer = strings.extendTime;
151
+ // No copy, no notice: a host that has not localized this must not get
152
+ // English, and the flag alone is not permission to invent it.
153
+ const notice =
154
+ extendTime && offer ? (
155
+ <PrizmaSnackbar
156
+ state="neutral"
157
+ isMultiline
158
+ // Assertive on purpose — see the live-region note in this file's
159
+ // header. The component default here would be `status`.
160
+ role="alert"
161
+ linkLabel={offer.linkLabel}
162
+ onLinkClick={onExtendTime}
163
+ >
164
+ <strong>{offer.title}</strong>
165
+ <br />
166
+ {offer.description}
167
+ </PrizmaSnackbar>
168
+ ) : undefined;
169
+
170
+ return (
171
+ <PrizmaCaptureScreen
172
+ ref={ref}
173
+ className={className}
174
+ state={SELFIE_STATE[state]}
175
+ camera={camera}
176
+ feedbackText={strings.capturing[state]}
177
+ notice={notice}
178
+ verifiedLabel={strings.verifiedLabel}
179
+ navLabel={strings.navLabel}
180
+ screenId={screenId}
181
+ {...rest}
182
+ />
183
+ );
184
+ },
96
185
  );
97
186
  FaceCaptureCapturingScreen.displayName = 'FaceCaptureCapturingScreen';
98
187