@incodetech/prizma-core-screens 0.2.3 → 0.2.7

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 (116) hide show
  1. package/dist/CurpEnterScreen.d.ts +7 -0
  2. package/dist/CurpEnterScreen.js +17 -3
  3. package/dist/CurpGenerateScreen.d.ts +8 -0
  4. package/dist/CurpGenerateScreen.js +28 -13
  5. package/dist/CurpLayout.css +8 -34
  6. package/dist/CurpStrings.d.ts +16 -2
  7. package/dist/EkybAddressDetailsScreen.d.ts +7 -0
  8. package/dist/EkybAddressDetailsScreen.js +25 -7
  9. package/dist/EkybBusinessDetailsScreen.d.ts +6 -0
  10. package/dist/EkybBusinessDetailsScreen.js +23 -6
  11. package/dist/EkybCountryScreen.d.ts +13 -5
  12. package/dist/EkybCountryScreen.js +27 -9
  13. package/dist/EkybOwnersScreen.d.ts +8 -0
  14. package/dist/EkybOwnersScreen.js +26 -3
  15. package/dist/EkybStrings.d.ts +27 -10
  16. package/dist/EkycCountryCatalog.d.ts +4 -1
  17. package/dist/EkycCountryCatalog.js +5 -3
  18. package/dist/EkycCountryFormScreen.d.ts +12 -3
  19. package/dist/EkycCountryFormScreen.js +28 -10
  20. package/dist/EkycDetailsScreen.d.ts +7 -0
  21. package/dist/EkycDetailsScreen.js +21 -4
  22. package/dist/EkycStrings.d.ts +14 -2
  23. package/dist/ElectronicSignatureLayout.css +6 -0
  24. package/dist/EmailInputStrings.d.ts +11 -1
  25. package/dist/EmailInputVerifyScreen.d.ts +7 -0
  26. package/dist/EmailInputVerifyScreen.js +17 -3
  27. package/dist/FaceMatchComparisonScreen.d.ts +3 -3
  28. package/dist/FaceMatchLayout.css +21 -12
  29. package/dist/FormScreen.css +16 -0
  30. package/dist/FormsLongFormScreen.d.ts +8 -0
  31. package/dist/FormsLongFormScreen.js +34 -13
  32. package/dist/FormsStrings.d.ts +11 -1
  33. package/dist/GeolocationBrowserStepsScreen.d.ts +6 -2
  34. package/dist/GeolocationBrowserStepsScreen.js +7 -3
  35. package/dist/GeolocationStrings.d.ts +3 -0
  36. package/dist/IdCaptureLayout.css +40 -0
  37. package/dist/ListScreen.d.ts +2 -0
  38. package/dist/ListScreen.js +1 -1
  39. package/dist/PhoneInputLayout.css +10 -9
  40. package/dist/PhoneInputStrings.d.ts +11 -1
  41. package/dist/PhoneInputVerifyScreen.d.ts +8 -0
  42. package/dist/PhoneInputVerifyScreen.js +19 -6
  43. package/dist/QualifiedSignatureAcceptScreen.js +6 -1
  44. package/dist/QualifiedSignatureDocumentScreen.d.ts +13 -9
  45. package/dist/QualifiedSignatureDocumentScreen.js +18 -16
  46. package/dist/QualifiedSignatureLayout.css +76 -0
  47. package/dist/QualifiedSignatureStrings.d.ts +10 -0
  48. package/dist/ScreenNav.css +8 -0
  49. package/dist/ScreenNav.d.ts +15 -2
  50. package/dist/ScreenNav.js +10 -3
  51. package/dist/VideoSelfieIdCaptureScreen.d.ts +10 -3
  52. package/dist/VideoSelfieIdCaptureScreen.js +44 -13
  53. package/dist/VideoSelfieLayout.css +164 -33
  54. package/dist/VideoSelfieSelfieCaptureScreen.d.ts +44 -27
  55. package/dist/VideoSelfieSelfieCaptureScreen.js +65 -39
  56. package/dist/VideoSelfieSpeakScreen.js +18 -1
  57. package/dist/VideoSelfieStrings.d.ts +78 -12
  58. package/dist/VideoSelfieTurnHeadScreen.d.ts +51 -0
  59. package/dist/VideoSelfieTurnHeadScreen.js +63 -0
  60. package/dist/VideoSelfieTutorialScreen.js +13 -5
  61. package/dist/VideoSelfieWaveScreen.d.ts +53 -0
  62. package/dist/VideoSelfieWaveScreen.js +62 -0
  63. package/dist/WatchlistBusinessFormScreen.d.ts +11 -7
  64. package/dist/WatchlistBusinessFormScreen.js +27 -13
  65. package/dist/WatchlistBusinessStrings.d.ts +21 -7
  66. package/dist/index.d.ts +3 -1
  67. package/dist/index.js +2 -0
  68. package/dist/types.d.ts +3 -1
  69. package/package.json +14 -3
  70. package/samples/video-selfie-room.png +0 -0
  71. package/src/CurpEnterScreen.tsx +21 -4
  72. package/src/CurpGenerateScreen.tsx +46 -22
  73. package/src/CurpLayout.css +8 -34
  74. package/src/CurpStrings.ts +22 -2
  75. package/src/EkybAddressDetailsScreen.tsx +32 -4
  76. package/src/EkybBusinessDetailsScreen.tsx +23 -5
  77. package/src/EkybCountryScreen.tsx +28 -7
  78. package/src/EkybOwnersScreen.tsx +34 -3
  79. package/src/EkybStrings.ts +29 -10
  80. package/src/EkycCountryCatalog.ts +9 -4
  81. package/src/EkycCountryFormScreen.tsx +51 -12
  82. package/src/EkycDetailsScreen.tsx +22 -1
  83. package/src/EkycStrings.ts +14 -2
  84. package/src/ElectronicSignatureLayout.css +6 -0
  85. package/src/EmailInputStrings.ts +12 -1
  86. package/src/EmailInputVerifyScreen.tsx +21 -4
  87. package/src/FaceMatchComparisonScreen.tsx +3 -3
  88. package/src/FaceMatchLayout.css +21 -12
  89. package/src/FormScreen.css +16 -0
  90. package/src/FormsLongFormScreen.tsx +41 -13
  91. package/src/FormsStrings.ts +12 -1
  92. package/src/GeolocationBrowserStepsScreen.tsx +6 -3
  93. package/src/GeolocationStrings.ts +3 -0
  94. package/src/IdCaptureLayout.css +40 -0
  95. package/src/ListScreen.tsx +10 -1
  96. package/src/PhoneInputLayout.css +10 -9
  97. package/src/PhoneInputStrings.ts +12 -1
  98. package/src/PhoneInputVerifyScreen.tsx +21 -5
  99. package/src/QualifiedSignatureAcceptScreen.tsx +12 -1
  100. package/src/QualifiedSignatureDocumentScreen.tsx +27 -17
  101. package/src/QualifiedSignatureLayout.css +76 -0
  102. package/src/QualifiedSignatureStrings.ts +10 -0
  103. package/src/ScreenNav.css +8 -0
  104. package/src/ScreenNav.tsx +35 -5
  105. package/src/VideoSelfieIdCaptureScreen.tsx +116 -30
  106. package/src/VideoSelfieLayout.css +164 -33
  107. package/src/VideoSelfieSelfieCaptureScreen.tsx +104 -59
  108. package/src/VideoSelfieSpeakScreen.tsx +25 -14
  109. package/src/VideoSelfieStrings.ts +85 -12
  110. package/src/VideoSelfieTurnHeadScreen.tsx +151 -0
  111. package/src/VideoSelfieTutorialScreen.tsx +34 -23
  112. package/src/VideoSelfieWaveScreen.tsx +135 -0
  113. package/src/WatchlistBusinessFormScreen.tsx +35 -16
  114. package/src/WatchlistBusinessStrings.ts +21 -7
  115. package/src/index.ts +14 -0
  116. package/src/types.ts +3 -1
@@ -2,7 +2,7 @@ import React, { forwardRef } from 'react';
2
2
  import { PrizmaTextField } from '@incodetech/prizma-core-web/text-field';
3
3
  import { PrizmaButton } from '@incodetech/prizma-core-web/button';
4
4
  import { PrizmaStepper } from '@incodetech/prizma-core-web/stepper';
5
- import { PrizmaFormScreen, PrizmaFormRow, type PrizmaFormScreenProps } from './FormScreen';
5
+ import { PrizmaFormScreen, type PrizmaFormScreenProps } from './FormScreen';
6
6
  import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
7
7
  import type { EkybBusinessState, EkybBusinessValues, EkybStrings } from './EkybStrings';
8
8
  import './ScreenLayout.css';
@@ -69,6 +69,12 @@ export interface EkybBusinessDetailsScreenProps
69
69
  * bar is at `currentState="Start"`: segment 1 filled, segments 2 and 3 grey,
70
70
  * exactly what 5919:68599 renders. Both offsets around it live in
71
71
  * `ScreenLayout.css`, never here.
72
+ *
73
+ * `emptyError` is Continue pressed with the fields still empty (Elena,
74
+ * 2026-09-30, GUIDELINES §6-E empty-submit rule; precedent `FormsFieldScreen`'s
75
+ * `emptyError`): both REQUIRED fields render empty in Core's `error` state with
76
+ * `requiredError` under them, and Continue is disabled. `empty` keeps Continue
77
+ * enabled so the press can happen.
72
78
  */
73
79
  export const EkybBusinessDetailsScreen = forwardRef<
74
80
  HTMLDivElement,
@@ -78,8 +84,9 @@ export const EkybBusinessDetailsScreen = forwardRef<
78
84
  {
79
85
  strings,
80
86
  state = 'empty',
81
- values,
87
+ values: valuesProp,
82
88
  screenId = `ekyb-business-${state}`,
89
+ ctaDisabled,
83
90
  overrides,
84
91
  onAction,
85
92
  ...shell
@@ -87,6 +94,13 @@ export const EkybBusinessDetailsScreen = forwardRef<
87
94
  ref,
88
95
  ) => {
89
96
  const copy = strings.business;
97
+ const emptyError = state === 'emptyError';
98
+ const values = emptyError ? undefined : valuesProp;
99
+ // Core's own error treatment, spread onto the REQUIRED fields only; the
100
+ // error state shows no placeholder (the `FormsFieldScreen` precedent).
101
+ const missing = emptyError
102
+ ? { error: true, errorText: strings.requiredError, placeholder: undefined }
103
+ : {};
90
104
 
91
105
  return (
92
106
  <PrizmaFormScreen
@@ -104,6 +118,8 @@ export const EkybBusinessDetailsScreen = forwardRef<
104
118
  title={copy.title}
105
119
  subtitle={copy.subtitle}
106
120
  ctaLabel={copy.ctaLabel}
121
+ /* Disabled only after the empty submit; `empty` keeps it enabled. */
122
+ ctaDisabled={emptyError || ctaDisabled}
107
123
  linkContent={
108
124
  <PrizmaButton
109
125
  variant="secondary"
@@ -121,16 +137,18 @@ export const EkybBusinessDetailsScreen = forwardRef<
121
137
  overrides={{ align: overrides?.align ?? 'left', logo: overrides?.logo }}
122
138
  onAction={onAction}
123
139
  /* Exploration ekyb-desktop: on a tablet or desktop frame the two
124
- fields share a row; on the phone nothing changes. */
140
+ fields stay on two rows, each spanning the column (Elena,
141
+ 2026-09-28); `responsive` still pairs Continue and Back. On the
142
+ phone nothing changes. */
125
143
  responsive
126
144
  {...shell}
127
145
  >
128
- <PrizmaFormRow>
129
146
  <PrizmaTextField
130
147
  label={copy.businessName.label}
131
148
  placeholder={copy.businessName.placeholder}
132
149
  required
133
150
  requiredHint={strings.requiredHint}
151
+ {...missing}
134
152
  autoComplete="off"
135
153
  defaultValue={values?.businessName ?? ''}
136
154
  />
@@ -139,10 +157,10 @@ export const EkybBusinessDetailsScreen = forwardRef<
139
157
  placeholder={copy.taxId.placeholder}
140
158
  required
141
159
  requiredHint={strings.requiredHint}
160
+ {...missing}
142
161
  autoComplete="off"
143
162
  defaultValue={values?.taxId ?? ''}
144
163
  />
145
- </PrizmaFormRow>
146
164
  </PrizmaFormScreen>
147
165
  );
148
166
  },
@@ -55,14 +55,22 @@ export interface EkybCountryScreenProps
55
55
  * live interaction) — the same mechanism Phone Input's `countryCode` and
56
56
  * CURP's `Gene.dropdown` sections use. The search bar Figma draws inside the
57
57
  * menu is Core's own `searchable`.
58
- * 3. The closed field and the menu row are DIFFERENT faces of one option:
59
- * 5707:54174 draws "🇺🇸 United States" in the field while the rows read
60
- * "🇺🇸 +1 United States". That is Core's `PrizmaDropdownOption.fieldLabel`,
61
- * so nothing is hand-composed. The flag travels inside the label text
62
- * because Core still ships no country-flag primitive (#PI-3).
58
+ * 3. Menu rows and the closed field read the same "🇺🇸 United States": flag
59
+ * and country name, no dial code. Figma 5707:54174 draws the rows with
60
+ * Phone Input's "+1" prefixes, dropped by the owner (Elena, 2026-09-28)
61
+ * because a country of registration has no phone prefix. The flag travels
62
+ * inside the label text because Core still ships no country-flag primitive
63
+ * (#PI-3).
63
64
  *
64
65
  * This page is the only eKYB form page with NO "Back" secondary and NO Stepper —
65
66
  * it is step 0, before the three-step wizard starts.
67
+ *
68
+ * `emptyError` is Continue pressed with no country chosen (Elena, 2026-09-30,
69
+ * GUIDELINES §6-E empty-submit rule; precedent `FormsFieldScreen`'s
70
+ * `emptyError`): the dropdown renders empty, with no placeholder, in Core's
71
+ * `error` state with `requiredError` under it, and Continue is disabled. The
72
+ * field draws no `*`, but it is the page's only input and the flow cannot go on
73
+ * without it, so it is treated as required. `empty` keeps Continue enabled.
66
74
  */
67
75
  export const EkybCountryScreen = forwardRef<HTMLDivElement, EkybCountryScreenProps>(
68
76
  (
@@ -72,6 +80,7 @@ export const EkybCountryScreen = forwardRef<HTMLDivElement, EkybCountryScreenPro
72
80
  countries = [],
73
81
  value,
74
82
  screenId = `ekyb-country-${state}`,
83
+ ctaDisabled,
75
84
  overrides,
76
85
  onAction,
77
86
  ...shell
@@ -81,7 +90,9 @@ export const EkybCountryScreen = forwardRef<HTMLDivElement, EkybCountryScreenPro
81
90
  const copy = strings.country;
82
91
  // `empty` is the only section drawn on the placeholder; both other sections
83
92
  // draw a chosen country in the closed field.
84
- const selected = state === 'empty' ? null : (value ?? countries[0]?.value ?? null);
93
+ const emptyError = state === 'emptyError';
94
+ const selected =
95
+ state === 'empty' || emptyError ? null : (value ?? countries[0]?.value ?? null);
85
96
 
86
97
  return (
87
98
  <PrizmaFormScreen
@@ -89,6 +100,8 @@ export const EkybCountryScreen = forwardRef<HTMLDivElement, EkybCountryScreenPro
89
100
  title={copy.title}
90
101
  subtitle={copy.subtitle}
91
102
  ctaLabel={copy.ctaLabel}
103
+ /* Disabled only after the empty submit; `empty` keeps it enabled. */
104
+ ctaDisabled={emptyError || ctaDisabled}
92
105
  verifiedLabel={strings.verifiedLabel}
93
106
  navLabel={strings.navLabel}
94
107
  screenId={screenId}
@@ -102,7 +115,15 @@ export const EkybCountryScreen = forwardRef<HTMLDivElement, EkybCountryScreenPro
102
115
  >
103
116
  <PrizmaDropdown
104
117
  label={copy.field.label}
105
- placeholder={copy.field.placeholder}
118
+ /* Required, but UNMARKED: a single-input screen shows no asterisk
119
+ and no legend (Core pattern "Optimistic submission", single-input
120
+ screens; Elena 2026-09-30). The requirement surfaces on submit,
121
+ through the empty-submit error. */
122
+ /* The error state draws the field with nothing in it, not even the
123
+ placeholder (the `FormsFieldScreen` precedent). */
124
+ placeholder={emptyError ? undefined : copy.field.placeholder}
125
+ error={emptyError}
126
+ errorText={emptyError ? strings.requiredError : undefined}
106
127
  options={countries}
107
128
  /* Uncontrolled: a screen is one static spec state, so no `onChange` is
108
129
  needed. Core's `readOnly`/`disabled` are NOT used to freeze it —
@@ -59,6 +59,14 @@ export interface EkybOwnersScreenProps
59
59
  * - `loading` 5663:94390 — `directorFilled` with the submission in
60
60
  * flight: every field, both "Add another" links and "Back" go inert, and the
61
61
  * spinner rides the CTA, never a field (the Phone Input lesson, #PI-5)
62
+ * - `uboEmptyError` / `directorEmptyError` (no frame) — `uboEmpty` /
63
+ * `directorEmpty` after Continue was pressed with the fields still empty
64
+ * (Elena, 2026-09-30, GUIDELINES §6-E empty-submit rule; precedent
65
+ * `FormsFieldScreen`'s `emptyError`): every field renders empty, the two
66
+ * REQUIRED UBO fields in Core's `error` state with `requiredError` and no
67
+ * placeholder, the optional Director fields unflagged, and Continue
68
+ * disabled. The `*Empty` states keep Continue enabled so the press can
69
+ * happen.
62
70
  *
63
71
  * Four things are deliberate:
64
72
  *
@@ -99,8 +107,9 @@ export const EkybOwnersScreen = forwardRef<HTMLDivElement, EkybOwnersScreenProps
99
107
  {
100
108
  strings,
101
109
  state = 'uboEmpty',
102
- values,
110
+ values: valuesProp,
103
111
  screenId = `ekyb-owners-${state}`,
112
+ ctaDisabled,
104
113
  className,
105
114
  overrides,
106
115
  onAction,
@@ -111,8 +120,20 @@ export const EkybOwnersScreen = forwardRef<HTMLDivElement, EkybOwnersScreenProps
111
120
  const copy = strings.owners;
112
121
  const busy = state === 'loading';
113
122
  const secondUbo = state === 'uboAdded';
123
+ const emptyError = state === 'uboEmptyError' || state === 'directorEmptyError';
124
+ const values = emptyError ? undefined : valuesProp;
114
125
  const showDirector =
115
- state === 'directorEmpty' || state === 'directorFilled' || secondUbo || busy;
126
+ state === 'directorEmpty' ||
127
+ state === 'directorFilled' ||
128
+ state === 'directorEmptyError' ||
129
+ secondUbo ||
130
+ busy;
131
+ // Core's own error treatment, spread AFTER each required UBO field's own
132
+ // placeholder so the error state shows none (the `FormsFieldScreen`
133
+ // precedent). The Director fields are optional and never receive it.
134
+ const missing = emptyError
135
+ ? { error: true, errorText: strings.requiredError, placeholder: undefined }
136
+ : {};
116
137
 
117
138
  return (
118
139
  <PrizmaFormScreen
@@ -134,6 +155,9 @@ export const EkybOwnersScreen = forwardRef<HTMLDivElement, EkybOwnersScreenProps
134
155
  title={copy.title}
135
156
  subtitle={copy.subtitle}
136
157
  ctaLabel={copy.ctaLabel}
158
+ /* Disabled only after the empty submit; the `*Empty` states keep it
159
+ enabled. */
160
+ ctaDisabled={emptyError || ctaDisabled}
137
161
  /* `loading` swaps the CTA label for Core's designed spinner. */
138
162
  ctaLoading={busy}
139
163
  ctaLoadingLabel={copy.ctaLoadingLabel}
@@ -164,14 +188,19 @@ export const EkybOwnersScreen = forwardRef<HTMLDivElement, EkybOwnersScreenProps
164
188
  placeholder={copy.uboName.placeholder}
165
189
  required
166
190
  requiredHint={strings.requiredHint}
191
+ {...missing}
167
192
  autoComplete="off"
168
193
  defaultValue={values?.uboName ?? ''}
169
194
  disabled={busy}
170
195
  />
171
- {/* Figma draws no asterisk on the surname — it is optional. */}
196
+ {/* Required since 2026-09-28 (Elena): the frames drew no asterisk on the
197
+ surname, but a beneficial owner needs both names. */}
172
198
  <PrizmaTextField
173
199
  label={copy.uboSurname.label}
174
200
  placeholder={copy.uboSurname.placeholder}
201
+ required
202
+ requiredHint={strings.requiredHint}
203
+ {...missing}
175
204
  autoComplete="off"
176
205
  defaultValue={values?.uboSurname ?? ''}
177
206
  disabled={busy}
@@ -192,6 +221,8 @@ export const EkybOwnersScreen = forwardRef<HTMLDivElement, EkybOwnersScreenProps
192
221
  <PrizmaTextField
193
222
  label={copy.uboSurname.label}
194
223
  placeholder={copy.uboSurname.placeholder}
224
+ required
225
+ requiredHint={strings.requiredHint}
195
226
  autoComplete="off"
196
227
  defaultValue={values?.secondUboSurname ?? ''}
197
228
  />
@@ -34,15 +34,21 @@
34
34
  * - `dropdown` — the same selection with the MENU OPEN over the form: search
35
35
  * bar plus the country rows (Core's documentation-only `forceOpen`, since a
36
36
  * screen is one static spec state and never a live interaction)
37
+ * - `emptyError` — Continue was pressed with no country chosen (GUIDELINES
38
+ * §6-E empty-submit rule, Elena 2026-09-30): the dropdown in Core's error
39
+ * state with `requiredError`, Continue disabled. Not a Figma frame.
37
40
  */
38
- export type EkybCountryState = 'empty' | 'filled' | 'dropdown';
41
+ export type EkybCountryState = 'empty' | 'filled' | 'dropdown' | 'emptyError';
39
42
 
40
43
  /** The two `eKYB.Bdetails` (business identity) sections: every field on its
41
- * resting empty surface, or every field carrying a value. */
42
- export type EkybBusinessState = 'empty' | 'filled';
44
+ * resting empty surface, or every field carrying a value — plus `emptyError`,
45
+ * Continue pressed with the fields empty (empty-submit rule, 2026-09-30):
46
+ * every required field in Core's error state, Continue disabled. */
47
+ export type EkybBusinessState = 'empty' | 'filled' | 'emptyError';
43
48
 
44
- /** The two address sections (the SECOND `eKYB.Bdetails` parent). Same axis. */
45
- export type EkybAddressState = 'empty' | 'filled';
49
+ /** The two address sections (the SECOND `eKYB.Bdetails` parent). Same axis,
50
+ * same `emptyError` (the optional Address line 2 shows no error). */
51
+ export type EkybAddressState = 'empty' | 'filled' | 'emptyError';
46
52
 
47
53
  /**
48
54
  * The `eKYB.UBO.*` and `eKYB.director.*` sections — one screen family, because
@@ -63,6 +69,11 @@ export type EkybAddressState = 'empty' | 'filled';
63
69
  * flight, so EVERY field, both "Add another" links and "Back" go inert and
64
70
  * the spinner rides the CTA, never a field (the Phone Input lesson,
65
71
  * DEVIATIONS #PI-5)
72
+ * - `uboEmptyError` / `directorEmptyError` — `uboEmpty` / `directorEmpty` after
73
+ * Continue was pressed with the fields empty (GUIDELINES §6-E empty-submit
74
+ * rule, Elena 2026-09-30): the two REQUIRED UBO fields in Core's error state
75
+ * with `requiredError`, the optional Director fields unflagged, Continue
76
+ * disabled. Not Figma frames.
66
77
  */
67
78
  export type EkybOwnersState =
68
79
  | 'uboEmpty'
@@ -70,7 +81,9 @@ export type EkybOwnersState =
70
81
  | 'directorEmpty'
71
82
  | 'directorFilled'
72
83
  | 'uboAdded'
73
- | 'loading';
84
+ | 'loading'
85
+ | 'uboEmptyError'
86
+ | 'directorEmptyError';
74
87
 
75
88
  /**
76
89
  * One selectable row in the country / state menus. Runtime data, not copy — the
@@ -84,9 +97,8 @@ export interface EkybOption {
84
97
  /** The row label as drawn in the open menu. */
85
98
  label: string;
86
99
  /** Compact face for the CLOSED field when it differs from the menu row —
87
- * Core's own `PrizmaDropdownOption.fieldLabel`. `eKYB.country.dropdown`
88
- * draws "🇺🇸 +1 United States" in the row and "🇺🇸 United States" in the
89
- * field. */
100
+ * Core's own `PrizmaDropdownOption.fieldLabel`. The eKYB country list
101
+ * does not need it: rows and field both read "🇺🇸 United States". */
90
102
  fieldLabel?: string;
91
103
  }
92
104
 
@@ -139,6 +151,13 @@ export interface EkybStrings {
139
151
  * localizable and never hardcoded in a screen.
140
152
  */
141
153
  requiredHint?: string;
154
+ /**
155
+ * Under every empty REQUIRED field once the primary was pressed with it
156
+ * still empty — the `emptyError` states (GUIDELINES §6-E empty-submit rule,
157
+ * Elena 2026-09-30). Core's own `errorText` slot; SDK key
158
+ * `dynamicForms.fieldRequired`.
159
+ */
160
+ requiredError: string;
142
161
  /** Shared "Back" secondary action drawn under "Continue" on every page except
143
162
  * `country`. Emits `secondary`. */
144
163
  backLabel: string;
@@ -217,7 +236,7 @@ export interface EkybStrings {
217
236
  title: string;
218
237
  subtitle?: string;
219
238
  uboName: EkybFieldStrings;
220
- /** Figma draws NO `*` on the surname — it is optional. */
239
+ /** Required, like the name (Elena, 2026-09-28); the frames drew no `*`. */
221
240
  uboSurname: EkybFieldStrings;
222
241
  /** The tertiary link under the UBO block ("Add another UBO (Optional)"). */
223
242
  addUboLabel: string;
@@ -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: {