@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
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { forwardRef } from 'react';
4
4
  import { PrizmaDropdown } from '@incodetech/prizma-core-web/dropdown';
5
+ import { PrizmaCountryFlag } from '@incodetech/prizma-core-web/combined';
5
6
  import { PrizmaFormScreen } from './FormScreen';
6
7
  import './ScreenLayout.css';
7
8
  /**
@@ -26,33 +27,63 @@ import './ScreenLayout.css';
26
27
  * live interaction) — the same mechanism Phone Input's `countryCode` and
27
28
  * CURP's `Gene.dropdown` sections use. The search bar Figma draws inside the
28
29
  * menu is Core's own `searchable`.
29
- * 3. The closed field and the menu row are DIFFERENT faces of one option:
30
- * 5707:54174 draws "🇺🇸 United States" in the field while the rows read
31
- * "🇺🇸 +1 United States". That is Core's `PrizmaDropdownOption.fieldLabel`,
32
- * so nothing is hand-composed. The flag travels inside the label text
33
- * because Core still ships no country-flag primitive (#PI-3).
30
+ * 3. Menu rows and the closed field read the same flag + "United States",
31
+ * no dial code. Figma 5707:54174 draws the rows with Phone Input's "+1"
32
+ * prefixes, dropped by the owner (Elena, 2026-09-28) because a country of
33
+ * registration has no phone prefix. The flag is Core's raster in the
34
+ * dropdown's leading slot, not an emoji in the label (Elena, 2026-10-23;
35
+ * #PI-3, the WatchlistBusinessFormScreen precedent).
34
36
  *
35
37
  * This page is the only eKYB form page with NO "Back" secondary and NO Stepper —
36
38
  * it is step 0, before the three-step wizard starts.
39
+ *
40
+ * `emptyError` is Continue pressed with no country chosen (Elena, 2026-09-30,
41
+ * GUIDELINES §6-E empty-submit rule; precedent `FormsFieldScreen`'s
42
+ * `emptyError`): the dropdown renders empty, with no placeholder, in Core's
43
+ * `error` state with `requiredError` under it, and Continue is disabled. The
44
+ * field draws no `*`, but it is the page's only input and the flow cannot go on
45
+ * without it, so it is treated as required. `empty` keeps Continue enabled.
37
46
  */
38
- export const EkybCountryScreen = forwardRef(({ strings, state = 'empty', countries = [], value, screenId = `ekyb-country-${state}`, overrides, onAction, ...shell }, ref) => {
47
+ export const EkybCountryScreen = forwardRef(({ strings, state = 'empty', countries = [], value, screenId = `ekyb-country-${state}`, ctaDisabled, overrides, onAction, ...shell }, ref) => {
39
48
  const copy = strings.country;
40
49
  // `empty` is the only section drawn on the placeholder; both other sections
41
50
  // draw a chosen country in the closed field.
42
- const selected = state === 'empty' ? null : (value ?? countries[0]?.value ?? null);
43
- return (_jsx(PrizmaFormScreen, { ref: ref, title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId,
51
+ const emptyError = state === 'emptyError';
52
+ const selected = state === 'empty' || emptyError ? null : (value ?? countries[0]?.value ?? null);
53
+ return (_jsx(PrizmaFormScreen, { ref: ref, title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel,
54
+ /* Disabled only after the empty submit; `empty` keeps it enabled. */
55
+ ctaDisabled: emptyError || ctaDisabled, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId,
44
56
  /* Every eKYB form frame LEFT-aligns its title block, while the archetype
45
57
  defaults to centred. The module supplies its designed alignment
46
58
  through the sanctioned override rather than declaring any layout of
47
59
  its own, and a host that passes `align` still wins (#GEO-2). */
48
- overrides: { align: overrides?.align ?? 'left', logo: overrides?.logo }, onAction: onAction, ...shell, children: _jsx(PrizmaDropdown, { label: copy.field.label, placeholder: copy.field.placeholder, options: countries,
60
+ overrides: { align: overrides?.align ?? 'left', logo: overrides?.logo }, onAction: onAction, ...shell, children: _jsx(PrizmaDropdown, { label: copy.field.label,
61
+ /* Required, but UNMARKED: a single-input screen shows no asterisk
62
+ and no legend (Core pattern "Optimistic submission", single-input
63
+ screens; Elena 2026-09-30). The requirement surfaces on submit,
64
+ through the empty-submit error. */
65
+ /* The error state draws the field with nothing in it, not even the
66
+ placeholder (the `FormsFieldScreen` precedent). */
67
+ placeholder: emptyError ? undefined : copy.field.placeholder, error: emptyError, errorText: emptyError ? strings.requiredError : undefined, options: countries,
49
68
  /* Uncontrolled: a screen is one static spec state, so no `onChange` is
50
69
  needed. Core's `readOnly`/`disabled` are NOT used to freeze it —
51
70
  they paint the DISABLED grey surface on a section Figma draws white
52
71
  (DEVIATIONS #PI-6). */
53
72
  defaultValue: selected, searchable: true, searchPlaceholder: copy.searchPlaceholder, clearSearchLabel: copy.clearSearchLabel,
54
73
  /* The `dropdown` section IS the open-menu spec state. */
55
- forceOpen: state === 'dropdown' }) }));
74
+ forceOpen: state === 'dropdown',
75
+ /* Core's canonical flag raster in the closed field and every menu
76
+ row (see note 3). A country outside the bank falls back to its
77
+ `flag` emoji. */
78
+ renderLeading: (option) => {
79
+ const match = countries.find((c) => c.value === option.value);
80
+ return match ? (_jsx(PrizmaCountryFlag, { country: {
81
+ code: match.value,
82
+ name: match.label,
83
+ dialCode: '',
84
+ flag: match.flag ?? '',
85
+ } })) : null;
86
+ } }) }));
56
87
  });
57
88
  EkybCountryScreen.displayName = 'EkybCountryScreen';
58
89
  export default EkybCountryScreen;
@@ -38,6 +38,14 @@ export interface EkybOwnersScreenProps extends Omit<PrizmaFormScreenProps, 'titl
38
38
  * - `loading` 5663:94390 — `directorFilled` with the submission in
39
39
  * flight: every field, both "Add another" links and "Back" go inert, and the
40
40
  * spinner rides the CTA, never a field (the Phone Input lesson, #PI-5)
41
+ * - `uboEmptyError` / `directorEmptyError` (no frame) — `uboEmpty` /
42
+ * `directorEmpty` after Continue was pressed with the fields still empty
43
+ * (Elena, 2026-09-30, GUIDELINES §6-E empty-submit rule; precedent
44
+ * `FormsFieldScreen`'s `emptyError`): every field renders empty, the two
45
+ * REQUIRED UBO fields in Core's `error` state with `requiredError` and no
46
+ * placeholder, the optional Director fields unflagged, and Continue
47
+ * disabled. The `*Empty` states keep Continue enabled so the press can
48
+ * happen.
41
49
  *
42
50
  * Four things are deliberate:
43
51
  *
@@ -30,6 +30,14 @@ import './ScreenLayout.css';
30
30
  * - `loading` 5663:94390 — `directorFilled` with the submission in
31
31
  * flight: every field, both "Add another" links and "Back" go inert, and the
32
32
  * spinner rides the CTA, never a field (the Phone Input lesson, #PI-5)
33
+ * - `uboEmptyError` / `directorEmptyError` (no frame) — `uboEmpty` /
34
+ * `directorEmpty` after Continue was pressed with the fields still empty
35
+ * (Elena, 2026-09-30, GUIDELINES §6-E empty-submit rule; precedent
36
+ * `FormsFieldScreen`'s `emptyError`): every field renders empty, the two
37
+ * REQUIRED UBO fields in Core's `error` state with `requiredError` and no
38
+ * placeholder, the optional Director fields unflagged, and Continue
39
+ * disabled. The `*Empty` states keep Continue enabled so the press can
40
+ * happen.
33
41
  *
34
42
  * Four things are deliberate:
35
43
  *
@@ -65,24 +73,39 @@ import './ScreenLayout.css';
65
73
  * distinguishes `loading` from `directorFilled` above the fold, so it is not
66
74
  * optional chrome.
67
75
  */
68
- export const EkybOwnersScreen = forwardRef(({ strings, state = 'uboEmpty', values, screenId = `ekyb-owners-${state}`, className, overrides, onAction, ...shell }, ref) => {
76
+ export const EkybOwnersScreen = forwardRef(({ strings, state = 'uboEmpty', values: valuesProp, screenId = `ekyb-owners-${state}`, ctaDisabled, className, overrides, onAction, ...shell }, ref) => {
69
77
  const copy = strings.owners;
70
78
  const busy = state === 'loading';
71
79
  const secondUbo = state === 'uboAdded';
72
- const showDirector = state === 'directorEmpty' || state === 'directorFilled' || secondUbo || busy;
80
+ const emptyError = state === 'uboEmptyError' || state === 'directorEmptyError';
81
+ const values = emptyError ? undefined : valuesProp;
82
+ const showDirector = state === 'directorEmpty' ||
83
+ state === 'directorFilled' ||
84
+ state === 'directorEmptyError' ||
85
+ secondUbo ||
86
+ busy;
87
+ // Core's own error treatment, spread AFTER each required UBO field's own
88
+ // placeholder so the error state shows none (the `FormsFieldScreen`
89
+ // precedent). The Director fields are optional and never receive it.
90
+ const missing = emptyError
91
+ ? { error: true, errorText: strings.requiredError, placeholder: undefined }
92
+ : {};
73
93
  return (_jsxs(PrizmaFormScreen, { ref: ref, className: ['ekyb-owners', className].filter(Boolean).join(' '),
74
94
  /* The archetype's own scrolling field stack — two titled blocks, four
75
95
  fields and two links do not fit a 844 frame. No module CSS. */
76
96
  scrollFields: true,
77
97
  /* Step 3 of 3, and GREEN across all three once the submission runs. */
78
98
  progress: _jsx(PrizmaStepper, { steps: 3, currentState: busy ? 'Completed' : 3, showIcons: false, "aria-label": strings.stepperLabel, completedValueText: strings.stepperCompletedLabel }), title: copy.title, subtitle: copy.subtitle, ctaLabel: copy.ctaLabel,
99
+ /* Disabled only after the empty submit; the `*Empty` states keep it
100
+ enabled. */
101
+ ctaDisabled: emptyError || ctaDisabled,
79
102
  /* `loading` swaps the CTA label for Core's designed spinner. */
80
103
  ctaLoading: busy, ctaLoadingLabel: copy.ctaLoadingLabel, linkContent: _jsx(PrizmaButton, { variant: "secondary", size: "L",
81
104
  /* 5663:94390 greys "Back" while the submission runs. */
82
105
  disabled: busy, onClick: () => onAction?.({ type: 'secondary', screenId }), children: strings.backLabel }), verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: { align: overrides?.align ?? 'left', logo: overrides?.logo }, onAction: onAction,
83
106
  /* Exploration ekyb-desktop: name + surname share a row per block on a
84
107
  wide frame; on the phone nothing changes. */
85
- responsive: true, ...shell, children: [_jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { label: copy.uboName.label, placeholder: copy.uboName.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.uboName ?? '', disabled: busy }), _jsx(PrizmaTextField, { label: copy.uboSurname.label, placeholder: copy.uboSurname.placeholder, autoComplete: "off", defaultValue: values?.uboSurname ?? '', disabled: busy })] }), secondUbo ? (_jsxs(_Fragment, { children: [_jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { label: copy.uboName.label, placeholder: copy.uboName.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.secondUboName ?? '' }), _jsx(PrizmaTextField, { label: copy.uboSurname.label, placeholder: copy.uboSurname.placeholder, autoComplete: "off", defaultValue: values?.secondUboSurname ?? '' })] }), copy.removeUboLabel ? (_jsx(PrizmaButton, { variant: "tertiary-link", size: "L", onClick: () => onAction?.({ type: 'tertiary', screenId: `${screenId}-remove-ubo` }), children: copy.removeUboLabel })) : null] })) : null, _jsx(PrizmaButton, { variant: "tertiary-link", size: "L", disabled: busy,
108
+ responsive: true, ...shell, children: [_jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { label: copy.uboName.label, placeholder: copy.uboName.placeholder, required: true, requiredHint: strings.requiredHint, ...missing, autoComplete: "off", defaultValue: values?.uboName ?? '', disabled: busy }), _jsx(PrizmaTextField, { label: copy.uboSurname.label, placeholder: copy.uboSurname.placeholder, required: true, requiredHint: strings.requiredHint, ...missing, autoComplete: "off", defaultValue: values?.uboSurname ?? '', disabled: busy })] }), secondUbo ? (_jsxs(_Fragment, { children: [_jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { label: copy.uboName.label, placeholder: copy.uboName.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.secondUboName ?? '' }), _jsx(PrizmaTextField, { label: copy.uboSurname.label, placeholder: copy.uboSurname.placeholder, required: true, requiredHint: strings.requiredHint, autoComplete: "off", defaultValue: values?.secondUboSurname ?? '' })] }), copy.removeUboLabel ? (_jsx(PrizmaButton, { variant: "tertiary-link", size: "L", onClick: () => onAction?.({ type: 'tertiary', screenId: `${screenId}-remove-ubo` }), children: copy.removeUboLabel })) : null] })) : null, _jsx(PrizmaButton, { variant: "tertiary-link", size: "L", disabled: busy,
86
109
  /* Action-qualified id: the two "Add another…" links are different
87
110
  flows and must stay distinguishable (#PI-10), but the action
88
111
  contract has no per-action discriminator yet. */
@@ -7,13 +7,19 @@
7
7
  * - `dropdown` — the same selection with the MENU OPEN over the form: search
8
8
  * bar plus the country rows (Core's documentation-only `forceOpen`, since a
9
9
  * screen is one static spec state and never a live interaction)
10
+ * - `emptyError` — Continue was pressed with no country chosen (GUIDELINES
11
+ * §6-E empty-submit rule, Elena 2026-09-30): the dropdown in Core's error
12
+ * state with `requiredError`, Continue disabled. Not a Figma frame.
10
13
  */
11
- export type EkybCountryState = 'empty' | 'filled' | 'dropdown';
14
+ export type EkybCountryState = 'empty' | 'filled' | 'dropdown' | 'emptyError';
12
15
  /** The two `eKYB.Bdetails` (business identity) sections: every field on its
13
- * resting empty surface, or every field carrying a value. */
14
- export type EkybBusinessState = 'empty' | 'filled';
15
- /** The two address sections (the SECOND `eKYB.Bdetails` parent). Same axis. */
16
- export type EkybAddressState = 'empty' | 'filled';
16
+ * resting empty surface, or every field carrying a value — plus `emptyError`,
17
+ * Continue pressed with the fields empty (empty-submit rule, 2026-09-30):
18
+ * every required field in Core's error state, Continue disabled. */
19
+ export type EkybBusinessState = 'empty' | 'filled' | 'emptyError';
20
+ /** The two address sections (the SECOND `eKYB.Bdetails` parent). Same axis,
21
+ * same `emptyError` (the optional Address line 2 shows no error). */
22
+ export type EkybAddressState = 'empty' | 'filled' | 'emptyError';
17
23
  /**
18
24
  * The `eKYB.UBO.*` and `eKYB.director.*` sections — one screen family, because
19
25
  * measured against the 390 frames the director frames ARE the UBO frames with
@@ -33,8 +39,13 @@ export type EkybAddressState = 'empty' | 'filled';
33
39
  * flight, so EVERY field, both "Add another" links and "Back" go inert and
34
40
  * the spinner rides the CTA, never a field (the Phone Input lesson,
35
41
  * DEVIATIONS #PI-5)
42
+ * - `uboEmptyError` / `directorEmptyError` — `uboEmpty` / `directorEmpty` after
43
+ * Continue was pressed with the fields empty (GUIDELINES §6-E empty-submit
44
+ * rule, Elena 2026-09-30): the two REQUIRED UBO fields in Core's error state
45
+ * with `requiredError`, the optional Director fields unflagged, Continue
46
+ * disabled. Not Figma frames.
36
47
  */
37
- export type EkybOwnersState = 'uboEmpty' | 'uboFilled' | 'directorEmpty' | 'directorFilled' | 'uboAdded' | 'loading';
48
+ export type EkybOwnersState = 'uboEmpty' | 'uboFilled' | 'directorEmpty' | 'directorFilled' | 'uboAdded' | 'loading' | 'uboEmptyError' | 'directorEmptyError';
38
49
  /**
39
50
  * One selectable row in the country / state menus. Runtime data, not copy — the
40
51
  * host owns the catalogue (an ISO country list is ~250 entries and a state list
@@ -47,10 +58,14 @@ export interface EkybOption {
47
58
  /** The row label as drawn in the open menu. */
48
59
  label: string;
49
60
  /** Compact face for the CLOSED field when it differs from the menu row —
50
- * Core's own `PrizmaDropdownOption.fieldLabel`. `eKYB.country.dropdown`
51
- * draws "🇺🇸 +1 United States" in the row and "🇺🇸 United States" in the
52
- * field. */
61
+ * Core's own `PrizmaDropdownOption.fieldLabel`. The eKYB country list
62
+ * does not need it: rows and field both read "United States" after the
63
+ * flag. */
53
64
  fieldLabel?: string;
65
+ /** Country menus only: the emoji fallback for a `value` outside Core's
66
+ * flag raster bank. The country screen draws the bank's flag by `value`,
67
+ * so this never travels inside `label`. */
68
+ flag?: string;
54
69
  }
55
70
  /** The values the business-identity page carries. Runtime data, never copy. */
56
71
  export interface EkybBusinessValues {
@@ -97,6 +112,13 @@ export interface EkybStrings {
97
112
  * localizable and never hardcoded in a screen.
98
113
  */
99
114
  requiredHint?: string;
115
+ /**
116
+ * Under every empty REQUIRED field once the primary was pressed with it
117
+ * still empty — the `emptyError` states (GUIDELINES §6-E empty-submit rule,
118
+ * Elena 2026-09-30). Core's own `errorText` slot; SDK key
119
+ * `dynamicForms.fieldRequired`.
120
+ */
121
+ requiredError: string;
100
122
  /** Shared "Back" secondary action drawn under "Continue" on every page except
101
123
  * `country`. Emits `secondary`. */
102
124
  backLabel: string;
@@ -170,7 +192,7 @@ export interface EkybStrings {
170
192
  title: string;
171
193
  subtitle?: string;
172
194
  uboName: EkybFieldStrings;
173
- /** Figma draws NO `*` on the surname — it is optional. */
195
+ /** Required, like the name (Elena, 2026-09-28); the frames drew no `*`. */
174
196
  uboSurname: EkybFieldStrings;
175
197
  /** The tertiary link under the UBO block ("Add another UBO (Optional)"). */
176
198
  addUboLabel: string;
@@ -1,4 +1,7 @@
1
- export type EkycFieldKind = 'text' | 'date' | 'phone' | 'dropdown' | 'radio';
1
+ /** `password`: Core's `PrizmaPassword`, the field with the eye that hides the
2
+ * typed value. For the identifiers a stranger must not read over a shoulder:
3
+ * the SSN / ITIN fields (Elena, 2026-09-28). No requirement checklist. */
4
+ export type EkycFieldKind = 'text' | 'date' | 'phone' | 'dropdown' | 'radio' | 'password';
2
5
  export type EkycFieldKey = 'firstName' | 'middleName' | 'middleNameOptional' | 'lastName' | 'surname' | 'maternalSurname' | 'dateOfBirth' | 'email' | 'phone' | 'ssnLast4' | 'ssnOrItin' | 'nationality' | 'cpf' | 'nationalId' | 'taxId' | 'gender' | 'licenseNumber' | 'licenseState' | 'licenseExpiration' | 'street' | 'houseNumber' | 'city' | 'state' | 'stateCode' | 'zipCode' | 'postalCode';
3
6
  export interface EkycField {
4
7
  key: EkycFieldKey;
@@ -20,6 +20,8 @@
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
  const t = (key, extra = {}) => ({ key, kind: 'text', ...extra });
23
+ /** A hidden-by-default identifier, with the eye to reveal it. */
24
+ const secret = (key, extra = {}) => ({ key, kind: 'password', ...extra });
23
25
  const date = (key = 'dateOfBirth', extra = {}) => ({ key, kind: 'date', ...extra });
24
26
  // Required fields carry Core's asterisk (`required` + `requiredHint`), the
25
27
  // owner's rule (Elena, 2026-09-24, DEVIATIONS #EKC-6): first name, last name or
@@ -54,7 +56,7 @@ export const EKYC_COUNTRY_CATALOG = [
54
56
  {
55
57
  id: 'us-telco-1', country: 'US', label: 'United States', provider: 'Telco 1', stepper: 3,
56
58
  steps: [
57
- { node: '2098:9575', fields: [...NAME_PAIR(), email(), ...DOB_PHONE('US'), t('ssnLast4')] },
59
+ { node: '2098:9575', fields: [...NAME_PAIR(), email(), ...DOB_PHONE('US'), secret('ssnLast4')] },
58
60
  { node: '5854:76222', section: 'license', fields: [t('licenseNumber'), { key: 'licenseState', kind: 'dropdown', placeholder: 'selectState' }] },
59
61
  { node: '5854:76221', section: 'address', fields: US_ADDRESS },
60
62
  ],
@@ -71,7 +73,7 @@ export const EKYC_COUNTRY_CATALOG = [
71
73
  steps: [
72
74
  /* Same First | Last pair as the licence variant; Middle name (Optional)
73
75
  follows it and spans (#EKC-5). */
74
- { node: '2098:9577', fields: [...NAME_PAIR(), t('middleNameOptional'), ...DOB_PHONE('US'), email(), t('ssnOrItin')] },
76
+ { node: '2098:9577', fields: [...NAME_PAIR(), t('middleNameOptional'), ...DOB_PHONE('US'), email(), secret('ssnOrItin')] },
75
77
  { node: '5857:34094', section: 'address', fields: US_ADDRESS },
76
78
  ],
77
79
  },
@@ -90,7 +92,7 @@ export const EKYC_COUNTRY_CATALOG = [
90
92
  {
91
93
  id: 'ca-res-credit', country: 'CA', label: 'Canada', provider: 'Residential credit', stepper: 2,
92
94
  steps: [
93
- { node: '2098:9570', fields: [...NAME_PAIR(), email(), ...DOB_PHONE('CA'), t('ssnOrItin')] },
95
+ { node: '2098:9570', fields: [...NAME_PAIR(), email(), ...DOB_PHONE('CA'), secret('ssnOrItin')] },
94
96
  { node: '5857:36450', section: 'address', fields: CODE_ADDRESS('stateCodeCa') },
95
97
  ],
96
98
  },
@@ -16,11 +16,20 @@ export interface EkycCountryFormScreenProps extends Omit<PrizmaFormScreenProps,
16
16
  values?: EkycCountryValues;
17
17
  /**
18
18
  * The state after Continue was pressed with fields still empty: every empty
19
- * field shows Core's error state and `country.requiredError` under it.
20
- * Continue itself is ALWAYS enabled (Elena, 2026-09-24): validation happens
21
- * on press, not by greying the button.
19
+ * REQUIRED field shows Core's error state and `country.requiredError` under
20
+ * it, and Continue is disabled (GUIDELINES §6-E empty-submit rule, Elena
21
+ * 2026-09-30). Without it Continue stays enabled: validation happens on
22
+ * press, the resting page never greys the button.
22
23
  */
23
24
  showErrors?: boolean;
25
+ /**
26
+ * Field keys whose typed value failed the format check on Continue (an
27
+ * invalid CPF, say). Each keeps its value, shows Core's error state and
28
+ * `country.invalidErrors[key]` under it; Continue stays enabled so the user
29
+ * corrects it and presses again, the CURP precedent (`CurpEnterScreen`'s
30
+ * `invalid`). A key with no message in `invalidErrors` shows no error.
31
+ */
32
+ invalidFields?: string[];
24
33
  /** Dial codes the phone field offers. Runtime data, never copy. */
25
34
  phoneCountries?: PrizmaCountry[];
26
35
  /** Options for the dropdown fields, keyed by field key. Runtime data. */
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { forwardRef } from 'react';
4
4
  import { PrizmaTextField } from '@incodetech/prizma-core-web/text-field';
5
+ import { PrizmaPassword } from '@incodetech/prizma-core-web/password';
5
6
  import { PrizmaDateField } from '@incodetech/prizma-core-web/date-field';
6
7
  import { PrizmaDropdown } from '@incodetech/prizma-core-web/dropdown';
7
8
  import { PrizmaCombined } from '@incodetech/prizma-core-web/combined';
@@ -23,10 +24,13 @@ import './EkycLayout.css';
23
24
  * dropdown, radio pair — so a new country is a catalogue entry, never a file.
24
25
  *
25
26
  * Deliberate, per frame and per owner:
26
- * - Continue is ALWAYS enabled (Elena, 2026-09-24, overriding the grey button
27
- * most frames draw): pressing it with empty fields is the `showErrors` state,
28
- * Core's error ring and message under each empty field. The radio pair has
29
- * no error state in Core, so an empty gender shows none (DEVIATIONS #EKC-4).
27
+ * - Continue is enabled on the resting page (Elena, 2026-09-24, overriding the
28
+ * grey button most frames draw): pressing it with empty fields is the
29
+ * `showErrors` state, Core's error ring and message under each empty
30
+ * required field, and THEN Continue is disabled. The "always enabled" half
31
+ * of DEVIATIONS #EKC-4 is superseded by the 2026-09-30 empty-submit rule
32
+ * (GUIDELINES §6-E). The radio pair has no error state in Core, so an empty
33
+ * gender still shows none.
30
34
  * - ONE title per step (Elena, 2026-09-24): the personal-data step is "Enter
31
35
  * your details" with its subtitle; a licence step is titled "Driver's
32
36
  * License details" and an address step "Address details", with nothing
@@ -53,7 +57,7 @@ const groupRows = (fields) => {
53
57
  }
54
58
  return groups;
55
59
  };
56
- export const EkycCountryFormScreen = forwardRef(({ strings, variant, step = 1, values, showErrors = false, phoneCountries, options, screenId, overrides, onAction, ...shell }, ref) => {
60
+ export const EkycCountryFormScreen = forwardRef(({ strings, variant, step = 1, values, showErrors = false, invalidFields, phoneCountries, options, screenId, overrides, onAction, ...shell }, ref) => {
57
61
  const v = typeof variant === 'string' ? ekycCountryVariant(variant) : variant;
58
62
  if (!v)
59
63
  return null;
@@ -61,14 +65,21 @@ export const EkycCountryFormScreen = forwardRef(({ strings, variant, step = 1, v
61
65
  const c = strings.country;
62
66
  const stepperState = step <= 1 ? 'Start' : Math.min(step, 6);
63
67
  const label = (f) => c.fields[f.key] ?? f.key;
64
- const placeholder = (f) => (f.placeholder ? c.placeholders[f.placeholder] : c.placeholders[f.key]);
68
+ // A lone field in its empty-submit error drops its placeholder, as the
69
+ // single-input screens do (Phone, Email and CURP's `emptyError`).
70
+ const lone = s.fields.length === 1;
71
+ const placeholder = (f) => lone && showErrors && !values?.[f.key] ? undefined : f.placeholder ? c.placeholders[f.placeholder] : c.placeholders[f.key];
65
72
  const renderField = (f) => {
66
73
  // Only a REQUIRED empty field is an error on press (Elena, 2026-09-24,
67
- // DEVIATIONS #EKC-6): an optional field left blank shows nothing.
68
- const missing = showErrors && Boolean(f.required) && !values?.[f.key];
74
+ // DEVIATIONS #EKC-6): an optional field left blank shows nothing. A
75
+ // step's lone field is required by definition, asterisk or not (the
76
+ // single-input rule, as Phone and Email Input: Elena, 2026-10-02).
77
+ const required = Boolean(f.required) || lone;
78
+ const missing = showErrors && required && !values?.[f.key];
79
+ const invalidText = invalidFields?.includes(f.key) ? c.invalidErrors?.[f.key] : undefined;
69
80
  const common = {
70
81
  key: f.key, label: label(f), required: f.required, requiredHint: strings.details.requiredHint,
71
- error: missing || undefined, errorText: missing ? c.requiredError : undefined,
82
+ error: missing || Boolean(invalidText) || undefined, errorText: missing ? c.requiredError : invalidText,
72
83
  };
73
84
  switch (f.kind) {
74
85
  case 'date':
@@ -79,13 +90,20 @@ export const EkycCountryFormScreen = forwardRef(({ strings, variant, step = 1, v
79
90
  }
80
91
  case 'dropdown':
81
92
  return (_jsx(PrizmaDropdown, { ...common, placeholder: placeholder(f), options: options?.[f.key] ?? [], defaultValue: values?.[f.key] ?? null }));
93
+ case 'password':
94
+ /* The SSN / ITIN fields: Core's Password field, hidden by default, the
95
+ eye reveals it. No requirement checklist — this is an identifier,
96
+ not a password to compose (Elena, 2026-09-28). */
97
+ return (_jsx(PrizmaPassword, { ...common, placeholder: placeholder(f), autoComplete: "off", defaultValue: values?.[f.key] ?? '', showPasswordLabel: c.showValueLabel, hidePasswordLabel: c.hideValueLabel, showHelper: false, showRequirements: false }));
82
98
  case 'radio':
83
99
  return (_jsx(PrizmaRadioGroup, { name: f.key, legend: label(f), defaultValue: values?.[f.key], className: "ekyc-country__radios", children: c.genderOptions.map(o => (_jsx(PrizmaRadio, { value: o.value, textSize: "m", showText: true, text: o.label }, o.value))) }, f.key));
84
100
  default:
85
101
  return _jsx(PrizmaTextField, { ...common, placeholder: placeholder(f), autoComplete: "off", defaultValue: values?.[f.key] ?? '' });
86
102
  }
87
103
  };
88
- return (_jsx(PrizmaFormScreen, { ref: ref, scrollFields: true, progress: v.stepper > 0 ? (_jsx(PrizmaStepper, { steps: v.stepper, currentState: stepperState, showIcons: false, "aria-label": strings.stepperLabel })) : undefined, title: s.section === 'address' ? c.addressSection : s.section === 'license' ? c.licenseSection : strings.details.title, subtitle: s.section ? undefined : strings.details.subtitle, linkContent: step > 1 ? (_jsx(PrizmaButton, { variant: "secondary", size: "L", onClick: () => onAction?.({ type: 'secondary', screenId }), children: c.backLabel })) : undefined, ctaLabel: c.ctaLabel, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: { align: overrides?.align ?? 'left', logo: overrides?.logo }, onAction: onAction,
104
+ return (_jsx(PrizmaFormScreen, { ref: ref, scrollFields: true, progress: v.stepper > 0 ? (_jsx(PrizmaStepper, { steps: v.stepper, currentState: stepperState, showIcons: false, "aria-label": strings.stepperLabel })) : undefined, title: s.section === 'address' ? c.addressSection : s.section === 'license' ? c.licenseSection : strings.details.title, subtitle: s.section ? undefined : strings.details.subtitle, linkContent: step > 1 ? (_jsx(PrizmaButton, { variant: "secondary", size: "L", onClick: () => onAction?.({ type: 'secondary', screenId }), children: c.backLabel })) : undefined, ctaLabel: c.ctaLabel,
105
+ /* Greyed only after the empty submit (2026-09-30 empty-submit rule). */
106
+ ctaDisabled: showErrors, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: { align: overrides?.align ?? 'left', logo: overrides?.logo }, onAction: onAction,
89
107
  /* The eKYB desktop shape: catalogue pairs become two lanes at 420 and
90
108
  up, Back | Continue share a row. The phone stack is untouched. */
91
109
  responsive: true, ...shell, children: groupRows(s.fields).map(group => group.length > 1 ? (_jsx(PrizmaFormRow, { children: group.map(renderField) }, group.map(f => f.key).join('+'))) : (renderField(group[0]))) }));
@@ -52,6 +52,13 @@ export interface EkycDetailsScreenProps extends Omit<PrizmaFormScreenProps, 'tit
52
52
  * the label row Figma draws as "First name *" — so Middle name, the one
53
53
  * optional field, correctly renders none.
54
54
  *
55
+ * `emptyError` is the state after Continue was pressed with the fields still
56
+ * empty (Elena, 2026-09-30, GUIDELINES §6-E empty-submit rule; drawn precedent
57
+ * `FormsFieldScreen`'s `emptyError`): every field renders empty, the three
58
+ * REQUIRED ones carry Core's `error` + `country.requiredError`, Middle name
59
+ * stays unflagged (DEVIATIONS #EKC-6), and Continue is disabled. The resting
60
+ * `default` keeps Continue enabled so the press can happen at all.
61
+ *
55
62
  * Figma draws a two-segment journey bar between the nav and the title on every
56
63
  * page-1 frame, and it is the only chrome that separates page 1 from page 2.
57
64
  * Archetype E had no slot above the title; the gap is now CLOSED in the
@@ -44,6 +44,13 @@ import './EkycLayout.css';
44
44
  * the label row Figma draws as "First name *" — so Middle name, the one
45
45
  * optional field, correctly renders none.
46
46
  *
47
+ * `emptyError` is the state after Continue was pressed with the fields still
48
+ * empty (Elena, 2026-09-30, GUIDELINES §6-E empty-submit rule; drawn precedent
49
+ * `FormsFieldScreen`'s `emptyError`): every field renders empty, the three
50
+ * REQUIRED ones carry Core's `error` + `country.requiredError`, Middle name
51
+ * stays unflagged (DEVIATIONS #EKC-6), and Continue is disabled. The resting
52
+ * `default` keeps Continue enabled so the press can happen at all.
53
+ *
47
54
  * Figma draws a two-segment journey bar between the nav and the title on every
48
55
  * page-1 frame, and it is the only chrome that separates page 1 from page 2.
49
56
  * Archetype E had no slot above the title; the gap is now CLOSED in the
@@ -53,11 +60,18 @@ import './EkycLayout.css';
53
60
  * solid brand blue and segment 2 grey, i.e. `steps={2} currentState="Start"`,
54
61
  * with the icons row off (the frames draw the bar alone). DEVIATIONS #EK-1.
55
62
  */
56
- export const EkycDetailsScreen = forwardRef(({ strings, state = 'default', values, screenId = `ekyc-details-${state}`, className, overrides, onAction, ...shell }, ref) => {
63
+ export const EkycDetailsScreen = forwardRef(({ strings, state = 'default', values: valuesProp, screenId = `ekyc-details-${state}`, ctaDisabled, className, overrides, onAction, ...shell }, ref) => {
57
64
  // The one field treatment that varies across the three sections: 2101:8159
58
65
  // rings First name blue and shows its clear ✕; the other two draw every
59
66
  // field resting.
60
67
  const focused = state === 'focused';
68
+ // The empty-submit state: every field empty, required ones in error.
69
+ const emptyError = state === 'emptyError';
70
+ const values = emptyError ? undefined : valuesProp;
71
+ // Core's own error treatment, spread onto the REQUIRED fields only.
72
+ const missing = emptyError
73
+ ? { error: true, errorText: strings.country.requiredError }
74
+ : {};
61
75
  return (_jsxs(PrizmaFormScreen, { ref: ref,
62
76
  /* Scopes the single rule that lets the focused field keep the clear ✕
63
77
  Figma draws inside it. See EkycLayout.css. */
@@ -68,7 +82,10 @@ export const EkycDetailsScreen = forwardRef(({ strings, state = 'default', value
68
82
  `Start` state at `steps={2}`. `showIcons={false}` because the frames
69
83
  draw the bar with no glyph row (and it keeps the icon lookup, which
70
84
  has no id+2 sequence, out of the render entirely). */
71
- progress: _jsx(PrizmaStepper, { steps: 2, currentState: "Start", showIcons: false, "aria-label": strings.stepperLabel }), title: strings.details.title, subtitle: strings.details.subtitle, ctaLabel: strings.details.ctaLabel, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId,
85
+ progress: _jsx(PrizmaStepper, { steps: 2, currentState: "Start", showIcons: false, "aria-label": strings.stepperLabel }), title: strings.details.title, subtitle: strings.details.subtitle, ctaLabel: strings.details.ctaLabel,
86
+ /* Disabled only after the empty submit; the resting page keeps it
87
+ enabled so the press can happen. */
88
+ ctaDisabled: emptyError || ctaDisabled, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId,
72
89
  /* Every eKYC form frame LEFT-aligns its title block, while the archetype
73
90
  defaults to centred. The module supplies its designed alignment through
74
91
  the sanctioned override rather than declaring any layout, and a host
@@ -80,9 +97,9 @@ export const EkycDetailsScreen = forwardRef(({ strings, state = 'default', value
80
97
  now FOLLOWS Last name, on the phone too (First, Last, Middle, Date of
81
98
  birth), the one order change the pairing costs: the frames drew
82
99
  First, Middle, Last. DEVIATIONS #EKC-5. */
83
- responsive: true, ...shell, children: [_jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { className: focused ? 'prizma-input-textfield--focused' : undefined, label: strings.details.firstNameLabel, required: true, requiredHint: strings.details.requiredHint, defaultValue: values?.firstName ?? '', autoComplete: "off",
100
+ responsive: true, ...shell, children: [_jsxs(PrizmaFormRow, { children: [_jsx(PrizmaTextField, { className: focused ? 'prizma-input-textfield--focused' : undefined, label: strings.details.firstNameLabel, required: true, requiredHint: strings.details.requiredHint, ...missing, defaultValue: values?.firstName ?? '', autoComplete: "off",
84
101
  /* Only 2101:8159 draws the ✕, and only in this field. */
85
- clearable: focused, clearLabel: strings.details.clearLabel }), _jsx(PrizmaTextField, { label: strings.details.lastNameLabel, required: true, requiredHint: strings.details.requiredHint, defaultValue: values?.lastName ?? '', autoComplete: "off", clearable: false })] }), _jsx(PrizmaTextField, { label: strings.details.middleNameLabel, defaultValue: values?.middleName ?? '', autoComplete: "off", clearable: false }), _jsx(PrizmaDateField, { label: strings.details.dateOfBirthLabel, required: true, requiredHint: strings.details.requiredHint, defaultValue: values?.dateOfBirth ?? '', autoComplete: "off" })] }));
102
+ clearable: focused, clearLabel: strings.details.clearLabel }), _jsx(PrizmaTextField, { label: strings.details.lastNameLabel, required: true, requiredHint: strings.details.requiredHint, ...missing, defaultValue: values?.lastName ?? '', autoComplete: "off", clearable: false })] }), _jsx(PrizmaTextField, { label: strings.details.middleNameLabel, defaultValue: values?.middleName ?? '', autoComplete: "off", clearable: false }), _jsx(PrizmaDateField, { label: strings.details.dateOfBirthLabel, required: true, requiredHint: strings.details.requiredHint, ...missing, defaultValue: values?.dateOfBirth ?? '', autoComplete: "off" })] }));
86
103
  });
87
104
  EkycDetailsScreen.displayName = 'EkycDetailsScreen';
88
105
  export default EkycDetailsScreen;
@@ -9,8 +9,12 @@
9
9
  * the clear ✕ (2101:8159 draws the OS keyboard over the CTA; the
10
10
  * keyboard is OS chrome, never a screen element)
11
11
  * - `filled` — all four fields resting with their values
12
+ * - `emptyError` — Continue was pressed with the fields empty (GUIDELINES §6-E
13
+ * empty-submit rule, Elena 2026-09-30): the three REQUIRED fields
14
+ * in Core's error state with `country.requiredError`, Middle name
15
+ * (optional) unflagged, and Continue disabled. Not a Figma frame.
12
16
  */
13
- export type EkycDetailsState = 'default' | 'focused' | 'filled';
17
+ export type EkycDetailsState = 'default' | 'focused' | 'filled' | 'emptyError';
14
18
  /**
15
19
  * The page-2 sections (`eKYC.default` / `eKYC.filled` / `eKYC.loading`), same
16
20
  * layout with the document field set:
@@ -109,6 +113,10 @@ export interface EkycStrings {
109
113
  licenseSection: string;
110
114
  /** Accessible name of the phone field's dial-code list. */
111
115
  countryListLabel: string;
116
+ /** Accessible names for the eye on a `password` field (the SSN / ITIN
117
+ * fields): while the value is hidden, and while it is shown. */
118
+ showValueLabel: string;
119
+ hideValueLabel: string;
112
120
  /** The gender radio pair, Argentina. */
113
121
  genderOptions: {
114
122
  value: string;
@@ -119,8 +127,12 @@ export interface EkycStrings {
119
127
  ctaLabel: string;
120
128
  /** The secondary button on a step after the first. */
121
129
  backLabel: string;
122
- /** Under every empty field once Continue was pressed (`showErrors`). */
130
+ /** Under every empty REQUIRED field once Continue was pressed: the
131
+ * by-country `showErrors` state and page 1's `emptyError` both read it. */
123
132
  requiredError: string;
133
+ /** Under a field whose value failed the format check on Continue, by
134
+ * field key (`invalidFields`). `cpf` is SDK key `dynamicForms.invalidCpf`. */
135
+ invalidErrors?: Record<string, string>;
124
136
  };
125
137
  /** `eKYC.processing` — spinner over the status line, no button. */
126
138
  processing: {
@@ -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
@@ -8,8 +8,12 @@
8
8
  * - `filled` — focused field with a complete address
9
9
  * - `error` — negative field + clear ✕ + the invalid-format row
10
10
  * - `loading` — field greyed while the code is requested, spinner in the CTA
11
+ * - `emptyError` — "Continue" pressed with the field still empty: the field
12
+ * stays empty (no placeholder) in Core's error state with `requiredError`,
13
+ * and the CTA is disabled. GUIDELINES §6-E empty-submit rule (Elena,
14
+ * 2026-09-30).
11
15
  */
12
- export type EmailInputVerifyState = 'default' | 'typing' | 'filled' | 'error' | 'loading';
16
+ export type EmailInputVerifyState = 'default' | 'typing' | 'filled' | 'error' | 'loading' | 'emptyError';
13
17
  /**
14
18
  * The `email.OTP.*` sections — same layout, different code-field state:
15
19
  * - `typing` — focused field, partial code, CTA enabled, and the line
@@ -27,6 +31,12 @@ export interface EmailInputStrings {
27
31
  verifiedLabel: string;
28
32
  /** Accessible name for the logo nav row. */
29
33
  navLabel?: string;
34
+ /**
35
+ * Error row under the required field when the primary CTA is pressed with it
36
+ * still empty (the verify screen's `emptyError` state): "This field is
37
+ * required". SDK key `dynamicForms.fieldRequired`.
38
+ */
39
+ requiredError: string;
30
40
  /** email.verifyEmail.* */
31
41
  verify: {
32
42
  /** H2 ("Verify your email"). */
@@ -41,6 +41,13 @@ export interface EmailInputVerifyScreenProps extends Omit<PrizmaFormScreenProps,
41
41
  * paints the DISABLED surface (owner definition 2026-07-12), which would grey
42
42
  * the four resting states Figma draws white. The field is uncontrolled instead
43
43
  * (DEVIATIONS #PI-6).
44
+ *
45
+ * `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
46
+ * GUIDELINES §6-E empty-submit rule): "Continue" was pressed with nothing
47
+ * typed. The field renders empty with no placeholder, in Core's error state
48
+ * with `requiredError`, and the primary CTA is disabled, following the drawn
49
+ * precedent in `FormsFieldScreen`. The resting `default` keeps the CTA enabled
50
+ * so the user can reach this state.
44
51
  */
45
52
  export declare const EmailInputVerifyScreen: React.ForwardRefExoticComponent<EmailInputVerifyScreenProps & React.RefAttributes<HTMLDivElement>>;
46
53
  export default EmailInputVerifyScreen;
@@ -31,23 +31,37 @@ import './ScreenLayout.css';
31
31
  * paints the DISABLED surface (owner definition 2026-07-12), which would grey
32
32
  * the four resting states Figma draws white. The field is uncontrolled instead
33
33
  * (DEVIATIONS #PI-6).
34
+ *
35
+ * `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
36
+ * GUIDELINES §6-E empty-submit rule): "Continue" was pressed with nothing
37
+ * typed. The field renders empty with no placeholder, in Core's error state
38
+ * with `requiredError`, and the primary CTA is disabled, following the drawn
39
+ * precedent in `FormsFieldScreen`. The resting `default` keeps the CTA enabled
40
+ * so the user can reach this state.
34
41
  */
35
42
  export const EmailInputVerifyScreen = forwardRef(({ strings, state = 'default', value, screenId = `email-input-verify-${state}`, overrides, onAction, ...shell }, ref) => {
36
43
  const busy = state === 'loading';
37
44
  const invalid = state === 'error';
45
+ // Empty submit: the field drops its placeholder and takes Core's error
46
+ // state with the shared "required" row; the CTA greys.
47
+ const emptyError = state === 'emptyError';
38
48
  const focused = state === 'typing' || state === 'filled';
39
49
  return (_jsx(PrizmaFormScreen, { ref: ref, title: strings.verify.title, subtitle: strings.verify.subtitle, ctaLabel: strings.verify.ctaLabel,
40
50
  /* Figma's `loading` section swaps the CTA label for Core's spinner. */
41
- ctaLoading: busy, ctaLoadingLabel: strings.verify.ctaLoadingLabel, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: overrides, onAction: onAction, ...shell, children: _jsx(PrizmaTextField, { className:
51
+ ctaLoading: busy, ctaDisabled: emptyError, ctaLoadingLabel: strings.verify.ctaLoadingLabel, verifiedLabel: strings.verifiedLabel, navLabel: strings.navLabel, screenId: screenId, overrides: overrides, onAction: onAction, ...shell, children: _jsx(PrizmaTextField, { className:
42
52
  /* Core's OWN static-focus modifier — the class its CSS already
43
53
  implements beside `:focus-within`. `PrizmaTextField` has no
44
54
  `forceState` prop the way `PrizmaDropdown` does, so this is how a
45
55
  static spec state draws the focus ring without redeclaring any
46
56
  styling (DEVIATIONS #PI-7). */
47
- focused ? 'prizma-input-textfield--focused' : undefined, type: "email", inputMode: "email", autoComplete: "email", "aria-label": strings.verify.fieldLabel, placeholder: strings.verify.placeholder,
57
+ focused ? 'prizma-input-textfield--focused' : undefined, type: "email", inputMode: "email", autoComplete: "email", "aria-label": strings.verify.fieldLabel, placeholder: emptyError ? undefined : strings.verify.placeholder,
48
58
  /* Uncontrolled: a screen is one static spec state, so no `onChange`
49
59
  is needed and no warning is raised. */
50
- defaultValue: value ?? '', disabled: busy, error: invalid, errorText: invalid ? strings.verify.errorText : undefined,
60
+ defaultValue: emptyError ? '' : value ?? '', disabled: busy, error: invalid || emptyError, errorText: invalid
61
+ ? strings.verify.errorText
62
+ : emptyError
63
+ ? strings.requiredError
64
+ : undefined,
51
65
  /* The clear ✕ is drawn on the `error` section only. */
52
66
  clearable: invalid, clearLabel: strings.verify.clearLabel }) }));
53
67
  });