@incodetech/prizma-core-screens 0.1.2 → 0.2.3

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 (84) hide show
  1. package/dist/DemoScreensConfigScreen.d.ts +36 -0
  2. package/dist/DemoScreensConfigScreen.js +30 -0
  3. package/dist/DemoScreensConfigSheetScreen.d.ts +39 -0
  4. package/dist/DemoScreensConfigSheetScreen.js +33 -0
  5. package/dist/DemoScreensEditSheetScreen.d.ts +30 -0
  6. package/dist/DemoScreensEditSheetScreen.js +28 -0
  7. package/dist/DemoScreensEndScreen.d.ts +9 -21
  8. package/dist/DemoScreensEndScreen.js +9 -14
  9. package/dist/DemoScreensLaunchScreen.d.ts +28 -0
  10. package/dist/DemoScreensLaunchScreen.js +26 -0
  11. package/dist/DemoScreensLayout.css +542 -0
  12. package/dist/DemoScreensRedirectScreen.d.ts +50 -0
  13. package/dist/DemoScreensRedirectScreen.js +38 -0
  14. package/dist/DemoScreensResultScreen.d.ts +52 -0
  15. package/dist/DemoScreensResultScreen.js +46 -0
  16. package/dist/DemoScreensRunningScreen.d.ts +22 -0
  17. package/dist/DemoScreensRunningScreen.js +16 -0
  18. package/dist/DemoScreensSettingsScreen.d.ts +37 -0
  19. package/dist/DemoScreensSettingsScreen.js +40 -0
  20. package/dist/DemoScreensSplashScreen.d.ts +27 -0
  21. package/dist/DemoScreensSplashScreen.js +25 -0
  22. package/dist/DemoScreensStartScreen.d.ts +38 -0
  23. package/dist/DemoScreensStartScreen.js +37 -0
  24. package/dist/DemoScreensStrings.d.ts +192 -0
  25. package/dist/DemoScreensStrings.js +11 -11
  26. package/dist/DemoScreensTermsScreen.d.ts +28 -0
  27. package/dist/DemoScreensTermsScreen.js +26 -0
  28. package/dist/EkybAddressDetailsScreen.js +9 -5
  29. package/dist/EkybBusinessDetailsScreen.js +6 -3
  30. package/dist/EkybOwnersScreen.d.ts +4 -0
  31. package/dist/EkybOwnersScreen.js +13 -5
  32. package/dist/EkybStrings.d.ts +12 -1
  33. package/dist/EkycCountryCatalog.d.ts +46 -0
  34. package/dist/EkycCountryCatalog.js +139 -0
  35. package/dist/EkycCountryFormScreen.d.ts +36 -0
  36. package/dist/EkycCountryFormScreen.js +94 -0
  37. package/dist/EkycDetailsScreen.js +11 -4
  38. package/dist/EkycLayout.css +23 -0
  39. package/dist/EkycLicenseScreen.js +7 -3
  40. package/dist/EkycStrings.d.ts +26 -0
  41. package/dist/EmailInputStrings.d.ts +11 -1
  42. package/dist/FaceMatchComparisonScreen.d.ts +4 -2
  43. package/dist/FaceMatchComparisonScreen.js +3 -1
  44. package/dist/FaceMatchLayout.css +53 -3
  45. package/dist/FormScreen.css +74 -0
  46. package/dist/FormScreen.d.ts +17 -0
  47. package/dist/FormScreen.js +12 -1
  48. package/dist/PhoneInputStrings.d.ts +10 -1
  49. package/dist/index.d.ts +15 -2
  50. package/dist/index.js +13 -0
  51. package/dist/styles.css +1 -0
  52. package/package.json +62 -1
  53. package/src/DemoScreensConfigScreen.tsx +101 -0
  54. package/src/DemoScreensConfigSheetScreen.tsx +120 -0
  55. package/src/DemoScreensEditSheetScreen.tsx +103 -0
  56. package/src/DemoScreensEndScreen.tsx +14 -38
  57. package/src/DemoScreensLaunchScreen.tsx +96 -0
  58. package/src/DemoScreensLayout.css +542 -0
  59. package/src/DemoScreensRedirectScreen.tsx +190 -0
  60. package/src/DemoScreensResultScreen.tsx +115 -0
  61. package/src/DemoScreensRunningScreen.tsx +45 -0
  62. package/src/DemoScreensSettingsScreen.tsx +101 -0
  63. package/src/DemoScreensSplashScreen.tsx +47 -0
  64. package/src/DemoScreensStartScreen.tsx +105 -0
  65. package/src/DemoScreensStrings.ts +204 -11
  66. package/src/DemoScreensTermsScreen.tsx +82 -0
  67. package/src/EkybAddressDetailsScreen.tsx +7 -1
  68. package/src/EkybBusinessDetailsScreen.tsx +6 -1
  69. package/src/EkybOwnersScreen.tsx +46 -2
  70. package/src/EkybStrings.ts +12 -0
  71. package/src/EkycCountryCatalog.ts +199 -0
  72. package/src/EkycCountryFormScreen.tsx +184 -0
  73. package/src/EkycDetailsScreen.tsx +29 -20
  74. package/src/EkycLayout.css +23 -0
  75. package/src/EkycLicenseScreen.tsx +22 -16
  76. package/src/EkycStrings.ts +25 -0
  77. package/src/EmailInputStrings.ts +11 -1
  78. package/src/FaceMatchComparisonScreen.tsx +11 -3
  79. package/src/FaceMatchLayout.css +53 -3
  80. package/src/FormScreen.css +74 -0
  81. package/src/FormScreen.tsx +27 -1
  82. package/src/PhoneInputStrings.ts +10 -1
  83. package/src/index.ts +65 -1
  84. package/src/styles.css +1 -0
@@ -1,31 +1,224 @@
1
1
  // Demo screens — copy contract for the GENERIC family.
2
2
  //
3
- // Figma: `Module Generic Screens` (qGJjfL4O0JSjz1x4tOjJn8), frame `63:399`.
3
+ // Figma, two sources:
4
+ // · `Demo App - Latest Design` (FA4ZXKH3meKQnFmDsBsMdk), page "Full flow map",
5
+ // section `Web` (2010:5731) — the flow the Demo App runs in production on
6
+ // the web, phone and desktop: splash → start → (settings) → running →
7
+ // approved / rejected. Traced 2026-09-24.
8
+ // · `Module Generic Screens` (qGJjfL4O0JSjz1x4tOjJn8), frame `63:399`, and the
9
+ // same file's "DemoApp: End Screens" thread — the two end screens that are
10
+ // not a verdict: the result is with a human (`end`), or the flow broke
11
+ // before it reached one (`notCompleted`).
4
12
  //
5
13
  // The screens that bracket a DEMO session rather than a real one — what a
6
- // prospect sees at the two ends of a sample flow. They are generic by
7
- // definition: the demo does not belong to any module, it strings several of
14
+ // prospect sees before the first module and after the last. They are generic
15
+ // by definition: the demo does not belong to any module, it strings several of
8
16
  // them together.
9
17
  //
10
- // ONLY the end screen exists so far. The owner is supplying the start screen
11
- // separately (Camila 2026-09-03: "that's the end screen but we need the start
12
- // too, I will add it some other time"), so `start` is deliberately absent from
13
- // this contract rather than guessed — an invented start screen would be a
14
- // second source of truth for a frame that already exists in someone's file.
15
- //
16
- // The family has no flow of its own. Core Lab lists it as a library of states.
17
- //
18
18
  // No hardcoded English anywhere in a screen: every visible string arrives here.
19
19
 
20
+ export interface DemoScreensLanguage {
21
+ /** The dropdown option value — a BCP 47 tag (`en-US`, `es-MX`). */
22
+ value: string;
23
+ /** The row label, exactly as the app lists it ("English (US)"). */
24
+ label: string;
25
+ /** ISO 3166-1 alpha-2 code for the canonical flag bank (`US`, `RS`). Absent
26
+ * when the bank has no raster for that country yet — the row then draws
27
+ * no flag rather than an emoji. */
28
+ countryCode?: string;
29
+ }
30
+
31
+ /** One row of a picker: an environment, a flow. */
32
+ export interface DemoScreensOption {
33
+ value: string;
34
+ label: string;
35
+ }
36
+
20
37
  export interface DemoScreensStrings {
21
38
  /** Accessible name for the logo nav region (i18n — no baked-in English). */
22
39
  navLabel?: string;
23
40
  /** "verified by" attribution copy for the footer tag. */
24
41
  verifiedLabel: string;
42
+ /** The caption under the wordmark on the splash and the start screen. */
43
+ appName: string;
44
+
45
+ /** NATIVE · `Configuration` (2010:4704) — Client ID + Environment before the
46
+ * app knows who you are. `demo_2` empty, `demo_4` filled, `demo_3` custom
47
+ * URL, `Error Client ID` (2116:10697). */
48
+ config: {
49
+ clientIdLabel: string;
50
+ clientIdPlaceholder: string;
51
+ /** The value the filled states show. */
52
+ clientIdValue: string;
53
+ /** Error line under a Client ID the backend rejected. */
54
+ clientIdError: string;
55
+ environmentLabel: string;
56
+ environmentPlaceholder: string;
57
+ environments: DemoScreensOption[];
58
+ /** The environment whose choice reveals the URL field. */
59
+ customEnvironment: string;
60
+ urlLabel: string;
61
+ urlValue: string;
62
+ ctaLabel: string;
63
+ };
64
+
65
+ /** NATIVE · `Accept Terms` (2010:4923 · 4012:8731) — two documents, each
66
+ * with its consent checkbox. */
67
+ terms: {
68
+ /** "Last Modified: August 5, 2024" — the line under the logo. */
69
+ lastModified: string;
70
+ documents: Record<'terms' | 'privacy', { consentLabel: string; body: string }>;
71
+ ctaLabel: string;
72
+ };
73
+
74
+ /** NATIVE · `Launch screen` (2010:5024) · `Loading` (2082:8004) — the app
75
+ * is configured; one CTA runs the flow. */
76
+ launch: {
77
+ /** "Flow: Digital ID Flow" — the header's left text. */
78
+ flowLabel: string;
79
+ menuLabel: string;
80
+ ctaLabel: string;
81
+ /** "Environment:" — the caption under the CTA … */
82
+ environmentCaption: string;
83
+ /** … and its highlighted value ("Stage"). */
84
+ environmentValue: string;
85
+ /** Snackbar after the configuration sheet saved ("Flow saved"). */
86
+ savedSnackbar: string;
87
+ };
88
+
89
+ /** NATIVE · `Configuration › Demo config screen` (2118:20999) — the sheet
90
+ * the menu opens: pick a flow, or go edit Client ID / Environment. */
91
+ configSheet: {
92
+ title: string;
93
+ subtitle: string;
94
+ closeLabel: string;
95
+ flowLabel: string;
96
+ /** The flow shown before any change ("Build Predefined"). */
97
+ flowValue: string;
98
+ /** The flow the `selected` state shows ("uxv2"). */
99
+ flowSelected: string;
100
+ flows: DemoScreensOption[];
101
+ searchPlaceholder: string;
102
+ clearSearchLabel: string;
103
+ /** The two read-only lines under the picker. */
104
+ clientIdLine: string;
105
+ environmentLine: string;
106
+ saveLabel: string;
107
+ editLinkLabel: string;
108
+ /** "Build: 2020" — the caption at the foot of both sheets. */
109
+ buildLabel: string;
110
+ savedSnackbar: string;
111
+ };
112
+
113
+ /** NATIVE · `Edit Client ID or Environment` (2118:21000 …) — the second
114
+ * sheet, reached from the first. */
115
+ editSheet: {
116
+ title: string;
117
+ subtitle: string;
118
+ backLabel: string;
119
+ closeLabel: string;
120
+ clientIdLabel: string;
121
+ clientIdValue: string;
122
+ clientIdError: string;
123
+ environmentLabel: string;
124
+ /** The environment shown before any change ("Kubernetes Stage"). */
125
+ environmentValue: string;
126
+ /** The environment the `filled` state shows ("Demo"). */
127
+ environmentSelected: string;
128
+ environments: DemoScreensOption[];
129
+ ctaLabel: string;
130
+ buildLabel: string;
131
+ };
132
+
133
+ /** DESKTOP · `Redirect` (4272:11166) — the browser page that hands the
134
+ * session to a phone: a QR, or a link by SMS, or carry on right here. */
135
+ redirect: {
136
+ title: string;
137
+ /** Under the title when both QR and SMS are offered. */
138
+ subtitle: string;
139
+ /** Under the title when only the QR is offered, with the desktop option … */
140
+ subtitleQrDesktop: string;
141
+ /** … and without it. */
142
+ subtitleQrOnly: string;
143
+ tabs: { qr: string; sms: string };
144
+ /** "Scan the QR code to verify on your mobile device." */
145
+ qrHint: string;
146
+ /** "Enter your phone number to receive a link to verify via SMS" */
147
+ smsHint: string;
148
+ phoneLabel: string;
149
+ phonePlaceholder: string;
150
+ /** The typed number the filled state shows. */
151
+ phoneValue: string;
152
+ /** ISO code of the country the picker opens on (`RS`). */
153
+ countryCode: string;
154
+ sendLabel: string;
155
+ /** "or" — between the phone leg and the desktop option. */
156
+ orLabel: string;
157
+ desktopLabel: string;
158
+ sent: {
159
+ title: string;
160
+ /** "To" — followed by the dial code and number. */
161
+ toPrefix: string;
162
+ /** "Resend in 30s" — the countdown state. */
163
+ resendIn: string;
164
+ /** "Resend" — once the countdown is over. */
165
+ resend: string;
166
+ changeLabel: string;
167
+ resentSnackbar: string;
168
+ };
169
+ tooMany: {
170
+ title: string;
171
+ subtitle: string;
172
+ changeLabel: string;
173
+ };
174
+ };
175
+
176
+ /** WEB · `demo_2` — the front door: language, the wordmark, one CTA. */
177
+ start: {
178
+ /** Small label over the current language, top left. */
179
+ languageLabel: string;
180
+ /** The current language as the front door shows it ("English (US)"). */
181
+ languageValue: string;
182
+ /** Accessible name for the menu affordance, top right. */
183
+ menuLabel: string;
184
+ ctaLabel: string;
185
+ };
186
+
187
+ /** `demo_3` … `demo_5` — the settings sheet: one dropdown, Done. */
188
+ settings: {
189
+ title: string;
190
+ closeLabel: string;
191
+ languageLabel: string;
192
+ languages: DemoScreensLanguage[];
193
+ searchPlaceholder: string;
194
+ clearSearchLabel: string;
195
+ ctaLabel: string;
196
+ };
197
+
198
+ /** `demo_6` — the wait while the session is scored. */
199
+ running: {
200
+ title: string;
201
+ subtitle?: string;
202
+ };
203
+
204
+ /** `demo_7` / `demo_8` — the two verdicts the production app renders. */
205
+ result: {
206
+ approved: { title: string; subtitle?: string };
207
+ rejected: { title: string; subtitle?: string };
208
+ /** The CTA under a verdict ("Restart onboarding"). */
209
+ restartLabel: string;
210
+ };
25
211
 
26
212
  /** `63:399` — the demo is over and the result is with a human. */
27
213
  end: {
28
214
  title: string;
29
215
  subtitle?: string;
30
216
  };
217
+
218
+ /** "DemoApp: End Screens" · `Processnotcompleted` — the flow broke before a
219
+ * verdict; the app currently shows this title over the error's message. */
220
+ notCompleted: {
221
+ title: string;
222
+ subtitle?: string;
223
+ };
31
224
  }
@@ -0,0 +1,82 @@
1
+ import React, { forwardRef } from 'react';
2
+ import { PrizmaButton } from '@incodetech/prizma-core-web/button';
3
+ import { PrizmaCheckbox } from '@incodetech/prizma-core-web/checkbox';
4
+ import { PrizmaLogo } from '@incodetech/prizma-core-web/logo';
5
+ import { PrizmaScreenShell, type PrizmaScreenShellProps } from './ScreenShell';
6
+ import type { PrizmaScreenActionHandler } from './types';
7
+ import type { DemoScreensStrings } from './DemoScreensStrings';
8
+ import './ScreenLayout.css';
9
+ import './DemoScreensLayout.css';
10
+
11
+ export interface DemoScreensTermsScreenProps
12
+ extends Omit<PrizmaScreenShellProps, 'dark' | 'title' | 'children'> {
13
+ /** Family copy — this screen reads the `terms` slice. */
14
+ strings: DemoScreensStrings;
15
+ /** Which document is on screen. Default `terms`. */
16
+ document?: 'terms' | 'privacy';
17
+ /** The consent box is ticked and Continue is live. */
18
+ accepted?: boolean;
19
+ screenId?: string;
20
+ onAction?: PrizmaScreenActionHandler;
21
+ }
22
+
23
+ /**
24
+ * Demo screens · NATIVE — `Accept Terms` (2010:4923 · 4012:8731): the logo on
25
+ * the left, the "Last Modified" line, the document scrolling in the middle,
26
+ * and the consent checkbox over the CTA. Two documents, Terms of Use then
27
+ * Privacy Policy, each in an unchecked and a checked state.
28
+ *
29
+ * No archetype has a left-aligned logo or a scrolling document body, so the
30
+ * screen composes Core's Logo, Checkbox and Button over the layout classes.
31
+ * The document text arrives through `strings`, as every other string does.
32
+ */
33
+ export const DemoScreensTermsScreen = forwardRef<HTMLDivElement, DemoScreensTermsScreenProps>(
34
+ (
35
+ {
36
+ strings,
37
+ document = 'terms',
38
+ accepted = false,
39
+ screenId = `demo-screens-${document}${accepted ? '-accepted' : ''}`,
40
+ onAction,
41
+ ...shell
42
+ },
43
+ ref,
44
+ ) => {
45
+ const t = strings.terms;
46
+ const doc = t.documents[document];
47
+ return (
48
+ <PrizmaScreenShell ref={ref} data-screen-id={screenId} {...shell}>
49
+ <header className="demo-screens-terms__header" aria-label={strings.navLabel}>
50
+ <PrizmaLogo variant="incode" />
51
+ </header>
52
+ <div className="prizma-screen-layout__content">
53
+ <p className="demo-screens-terms__meta">{t.lastModified}</p>
54
+ <div className="demo-screens-terms__body">{doc.body}</div>
55
+ </div>
56
+ <div className="prizma-screen-layout__band prizma-screen-layout__band--bare demo-screens-band">
57
+ <div className="demo-screens-terms__consent">
58
+ <PrizmaCheckbox
59
+ size="M"
60
+ checked={accepted}
61
+ label={doc.consentLabel}
62
+ onCheckedChange={() => onAction?.({ type: 'secondary', screenId })}
63
+ />
64
+ </div>
65
+ <div className="prizma-screen-layout__band__ctas">
66
+ <PrizmaButton
67
+ variant="primary"
68
+ size="L"
69
+ disabled={!accepted}
70
+ onClick={() => onAction?.({ type: 'cta', screenId })}
71
+ >
72
+ {t.ctaLabel}
73
+ </PrizmaButton>
74
+ </div>
75
+ </div>
76
+ </PrizmaScreenShell>
77
+ );
78
+ },
79
+ );
80
+ DemoScreensTermsScreen.displayName = 'DemoScreensTermsScreen';
81
+
82
+ export default DemoScreensTermsScreen;
@@ -3,7 +3,7 @@ import { PrizmaTextField } from '@incodetech/prizma-core-web/text-field';
3
3
  import { PrizmaDropdown } from '@incodetech/prizma-core-web/dropdown';
4
4
  import { PrizmaButton } from '@incodetech/prizma-core-web/button';
5
5
  import { PrizmaStepper } from '@incodetech/prizma-core-web/stepper';
6
- import { PrizmaFormScreen, type PrizmaFormScreenProps } from './FormScreen';
6
+ import { PrizmaFormScreen, PrizmaFormRow, type PrizmaFormScreenProps } from './FormScreen';
7
7
  import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
8
8
  import type {
9
9
  EkybAddressState,
@@ -136,6 +136,10 @@ export const EkybAddressDetailsScreen = forwardRef<
136
136
  screenId={screenId}
137
137
  overrides={{ align: overrides?.align ?? 'left', logo: overrides?.logo }}
138
138
  onAction={onAction}
139
+ /* Exploration ekyb-desktop: on a wide frame only State + Postal code
140
+ share a row (Figma 6407:43637); the four text fields above span the
141
+ column. On the phone nothing changes. */
142
+ responsive
139
143
  {...shell}
140
144
  >
141
145
  <PrizmaTextField
@@ -169,6 +173,7 @@ export const EkybAddressDetailsScreen = forwardRef<
169
173
  autoComplete="off"
170
174
  defaultValue={values?.city ?? ''}
171
175
  />
176
+ <PrizmaFormRow>
172
177
  <PrizmaDropdown
173
178
  label={copy.state.label}
174
179
  placeholder={copy.state.placeholder}
@@ -191,6 +196,7 @@ export const EkybAddressDetailsScreen = forwardRef<
191
196
  autoComplete="off"
192
197
  defaultValue={values?.postalCode ?? ''}
193
198
  />
199
+ </PrizmaFormRow>
194
200
  </PrizmaFormScreen>
195
201
  );
196
202
  },
@@ -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, type PrizmaFormScreenProps } from './FormScreen';
5
+ import { PrizmaFormScreen, PrizmaFormRow, 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';
@@ -120,8 +120,12 @@ export const EkybBusinessDetailsScreen = forwardRef<
120
120
  `align` still wins (#GEO-2). */
121
121
  overrides={{ align: overrides?.align ?? 'left', logo: overrides?.logo }}
122
122
  onAction={onAction}
123
+ /* Exploration ekyb-desktop: on a tablet or desktop frame the two
124
+ fields share a row; on the phone nothing changes. */
125
+ responsive
123
126
  {...shell}
124
127
  >
128
+ <PrizmaFormRow>
125
129
  <PrizmaTextField
126
130
  label={copy.businessName.label}
127
131
  placeholder={copy.businessName.placeholder}
@@ -138,6 +142,7 @@ export const EkybBusinessDetailsScreen = forwardRef<
138
142
  autoComplete="off"
139
143
  defaultValue={values?.taxId ?? ''}
140
144
  />
145
+ </PrizmaFormRow>
141
146
  </PrizmaFormScreen>
142
147
  );
143
148
  },
@@ -3,7 +3,7 @@ import { PrizmaTextField } from '@incodetech/prizma-core-web/text-field';
3
3
  import { PrizmaTitle } from '@incodetech/prizma-core-web/title';
4
4
  import { PrizmaButton } from '@incodetech/prizma-core-web/button';
5
5
  import { PrizmaStepper } from '@incodetech/prizma-core-web/stepper';
6
- import { PrizmaFormScreen, type PrizmaFormScreenProps } from './FormScreen';
6
+ import { PrizmaFormScreen, PrizmaFormRow, type PrizmaFormScreenProps } from './FormScreen';
7
7
  import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
8
8
  import type { EkybOwnersState, EkybOwnersValues, EkybStrings } from './EkybStrings';
9
9
  import './ScreenLayout.css';
@@ -52,6 +52,10 @@ export interface EkybOwnersScreenProps
52
52
  * - `uboFilled` 5707:49608 — UBO block only, both fields carrying a value
53
53
  * - `directorEmpty` 5655:56032 — UBO block + Director block, all four blank
54
54
  * - `directorFilled` 5707:49782 — UBO block + Director block, all four filled
55
+ * - `uboAdded` (no frame) — `directorFilled` after "Add another UBO": a
56
+ * second, blank UBO pair under the first with its own "Remove UBO" link,
57
+ * then the same "Add another UBO" link and the Director block. Elena asked
58
+ * for the screen the link leads to (2026-09-24); the frames stop at the link.
55
59
  * - `loading` 5663:94390 — `directorFilled` with the submission in
56
60
  * flight: every field, both "Add another" links and "Back" go inert, and the
57
61
  * spinner rides the CTA, never a field (the Phone Input lesson, #PI-5)
@@ -106,8 +110,9 @@ export const EkybOwnersScreen = forwardRef<HTMLDivElement, EkybOwnersScreenProps
106
110
  ) => {
107
111
  const copy = strings.owners;
108
112
  const busy = state === 'loading';
113
+ const secondUbo = state === 'uboAdded';
109
114
  const showDirector =
110
- state === 'directorEmpty' || state === 'directorFilled' || busy;
115
+ state === 'directorEmpty' || state === 'directorFilled' || secondUbo || busy;
111
116
 
112
117
  return (
113
118
  <PrizmaFormScreen
@@ -148,8 +153,12 @@ export const EkybOwnersScreen = forwardRef<HTMLDivElement, EkybOwnersScreenProps
148
153
  screenId={screenId}
149
154
  overrides={{ align: overrides?.align ?? 'left', logo: overrides?.logo }}
150
155
  onAction={onAction}
156
+ /* Exploration ekyb-desktop: name + surname share a row per block on a
157
+ wide frame; on the phone nothing changes. */
158
+ responsive
151
159
  {...shell}
152
160
  >
161
+ <PrizmaFormRow>
153
162
  <PrizmaTextField
154
163
  label={copy.uboName.label}
155
164
  placeholder={copy.uboName.placeholder}
@@ -167,6 +176,39 @@ export const EkybOwnersScreen = forwardRef<HTMLDivElement, EkybOwnersScreenProps
167
176
  defaultValue={values?.uboSurname ?? ''}
168
177
  disabled={busy}
169
178
  />
179
+ </PrizmaFormRow>
180
+ {secondUbo ? (
181
+ <>
182
+ {/* The second UBO: the same two fields, blank, and the way back. */}
183
+ <PrizmaFormRow>
184
+ <PrizmaTextField
185
+ label={copy.uboName.label}
186
+ placeholder={copy.uboName.placeholder}
187
+ required
188
+ requiredHint={strings.requiredHint}
189
+ autoComplete="off"
190
+ defaultValue={values?.secondUboName ?? ''}
191
+ />
192
+ <PrizmaTextField
193
+ label={copy.uboSurname.label}
194
+ placeholder={copy.uboSurname.placeholder}
195
+ autoComplete="off"
196
+ defaultValue={values?.secondUboSurname ?? ''}
197
+ />
198
+ </PrizmaFormRow>
199
+ {copy.removeUboLabel ? (
200
+ <PrizmaButton
201
+ variant="tertiary-link"
202
+ size="L"
203
+ onClick={() =>
204
+ onAction?.({ type: 'tertiary', screenId: `${screenId}-remove-ubo` })
205
+ }
206
+ >
207
+ {copy.removeUboLabel}
208
+ </PrizmaButton>
209
+ ) : null}
210
+ </>
211
+ ) : null}
170
212
  <PrizmaButton
171
213
  variant="tertiary-link"
172
214
  size="L"
@@ -192,6 +234,7 @@ export const EkybOwnersScreen = forwardRef<HTMLDivElement, EkybOwnersScreenProps
192
234
  showSubtitle={Boolean(copy.directorSubtitle)}
193
235
  subtitle={copy.directorSubtitle}
194
236
  />
237
+ <PrizmaFormRow>
195
238
  <PrizmaTextField
196
239
  label={copy.directorName.label}
197
240
  placeholder={copy.directorName.placeholder}
@@ -206,6 +249,7 @@ export const EkybOwnersScreen = forwardRef<HTMLDivElement, EkybOwnersScreenProps
206
249
  defaultValue={values?.directorSurname ?? ''}
207
250
  disabled={busy}
208
251
  />
252
+ </PrizmaFormRow>
209
253
  <PrizmaButton
210
254
  variant="tertiary-link"
211
255
  size="L"
@@ -55,6 +55,10 @@ export type EkybAddressState = 'empty' | 'filled';
55
55
  * - `directorEmpty` — `eKYB.director.empty` (5655:56032): UBO block plus the
56
56
  * Director block, every field blank
57
57
  * - `directorFilled`— `eKYB.directorFilled` (5707:49782): all four filled
58
+ * - `uboAdded` — what "Add another UBO" does: a second UBO name + surname
59
+ * pair opens under the first, blank, with a "Remove" link of its own, and the
60
+ * Director block below as before. Not a Figma frame — the frames draw the
61
+ * link and stop there; Elena asked for the screen it leads to (2026-09-24).
58
62
  * - `loading` — `eKYB.directorloading` (5663:94390): the submission is in
59
63
  * flight, so EVERY field, both "Add another" links and "Back" go inert and
60
64
  * the spinner rides the CTA, never a field (the Phone Input lesson,
@@ -65,6 +69,7 @@ export type EkybOwnersState =
65
69
  | 'uboFilled'
66
70
  | 'directorEmpty'
67
71
  | 'directorFilled'
72
+ | 'uboAdded'
68
73
  | 'loading';
69
74
 
70
75
  /**
@@ -106,6 +111,9 @@ export interface EkybAddressValues {
106
111
  export interface EkybOwnersValues {
107
112
  uboName?: string;
108
113
  uboSurname?: string;
114
+ /** The second UBO, present only in `uboAdded`. */
115
+ secondUboName?: string;
116
+ secondUboSurname?: string;
109
117
  directorName?: string;
110
118
  directorSurname?: string;
111
119
  }
@@ -213,6 +221,10 @@ export interface EkybStrings {
213
221
  uboSurname: EkybFieldStrings;
214
222
  /** The tertiary link under the UBO block ("Add another UBO (Optional)"). */
215
223
  addUboLabel: string;
224
+ /** The tertiary link under a second UBO's pair ("Remove UBO"), `uboAdded`
225
+ * only. Optional so existing hosts keep compiling; without it the second
226
+ * pair cannot be taken away again. */
227
+ removeUboLabel?: string;
216
228
  /** The Director block's own heading + description. Rendered by Core's
217
229
  * `PrizmaTitle` inside the field stack; see the screen's docstring and the
218
230
  * module's reported deviation about the second heading. */