@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.
- package/dist/DemoScreensConfigScreen.d.ts +36 -0
- package/dist/DemoScreensConfigScreen.js +30 -0
- package/dist/DemoScreensConfigSheetScreen.d.ts +39 -0
- package/dist/DemoScreensConfigSheetScreen.js +33 -0
- package/dist/DemoScreensEditSheetScreen.d.ts +30 -0
- package/dist/DemoScreensEditSheetScreen.js +28 -0
- package/dist/DemoScreensEndScreen.d.ts +9 -21
- package/dist/DemoScreensEndScreen.js +9 -14
- package/dist/DemoScreensLaunchScreen.d.ts +28 -0
- package/dist/DemoScreensLaunchScreen.js +26 -0
- package/dist/DemoScreensLayout.css +542 -0
- package/dist/DemoScreensRedirectScreen.d.ts +50 -0
- package/dist/DemoScreensRedirectScreen.js +38 -0
- package/dist/DemoScreensResultScreen.d.ts +52 -0
- package/dist/DemoScreensResultScreen.js +46 -0
- package/dist/DemoScreensRunningScreen.d.ts +22 -0
- package/dist/DemoScreensRunningScreen.js +16 -0
- package/dist/DemoScreensSettingsScreen.d.ts +37 -0
- package/dist/DemoScreensSettingsScreen.js +40 -0
- package/dist/DemoScreensSplashScreen.d.ts +27 -0
- package/dist/DemoScreensSplashScreen.js +25 -0
- package/dist/DemoScreensStartScreen.d.ts +38 -0
- package/dist/DemoScreensStartScreen.js +37 -0
- package/dist/DemoScreensStrings.d.ts +192 -0
- package/dist/DemoScreensStrings.js +11 -11
- package/dist/DemoScreensTermsScreen.d.ts +28 -0
- package/dist/DemoScreensTermsScreen.js +26 -0
- package/dist/EkybAddressDetailsScreen.js +9 -5
- package/dist/EkybBusinessDetailsScreen.js +6 -3
- package/dist/EkybOwnersScreen.d.ts +4 -0
- package/dist/EkybOwnersScreen.js +13 -5
- package/dist/EkybStrings.d.ts +12 -1
- package/dist/EkycCountryCatalog.d.ts +46 -0
- package/dist/EkycCountryCatalog.js +139 -0
- package/dist/EkycCountryFormScreen.d.ts +36 -0
- package/dist/EkycCountryFormScreen.js +94 -0
- package/dist/EkycDetailsScreen.js +11 -4
- package/dist/EkycLayout.css +23 -0
- package/dist/EkycLicenseScreen.js +7 -3
- package/dist/EkycStrings.d.ts +26 -0
- package/dist/EmailInputStrings.d.ts +11 -1
- package/dist/FaceMatchComparisonScreen.d.ts +4 -2
- package/dist/FaceMatchComparisonScreen.js +3 -1
- package/dist/FaceMatchLayout.css +53 -3
- package/dist/FormScreen.css +74 -0
- package/dist/FormScreen.d.ts +17 -0
- package/dist/FormScreen.js +12 -1
- package/dist/PhoneInputStrings.d.ts +10 -1
- package/dist/index.d.ts +15 -2
- package/dist/index.js +13 -0
- package/dist/styles.css +1 -0
- package/package.json +62 -1
- package/src/DemoScreensConfigScreen.tsx +101 -0
- package/src/DemoScreensConfigSheetScreen.tsx +120 -0
- package/src/DemoScreensEditSheetScreen.tsx +103 -0
- package/src/DemoScreensEndScreen.tsx +14 -38
- package/src/DemoScreensLaunchScreen.tsx +96 -0
- package/src/DemoScreensLayout.css +542 -0
- package/src/DemoScreensRedirectScreen.tsx +190 -0
- package/src/DemoScreensResultScreen.tsx +115 -0
- package/src/DemoScreensRunningScreen.tsx +45 -0
- package/src/DemoScreensSettingsScreen.tsx +101 -0
- package/src/DemoScreensSplashScreen.tsx +47 -0
- package/src/DemoScreensStartScreen.tsx +105 -0
- package/src/DemoScreensStrings.ts +204 -11
- package/src/DemoScreensTermsScreen.tsx +82 -0
- package/src/EkybAddressDetailsScreen.tsx +7 -1
- package/src/EkybBusinessDetailsScreen.tsx +6 -1
- package/src/EkybOwnersScreen.tsx +46 -2
- package/src/EkybStrings.ts +12 -0
- package/src/EkycCountryCatalog.ts +199 -0
- package/src/EkycCountryFormScreen.tsx +184 -0
- package/src/EkycDetailsScreen.tsx +29 -20
- package/src/EkycLayout.css +23 -0
- package/src/EkycLicenseScreen.tsx +22 -16
- package/src/EkycStrings.ts +25 -0
- package/src/EmailInputStrings.ts +11 -1
- package/src/FaceMatchComparisonScreen.tsx +11 -3
- package/src/FaceMatchLayout.css +53 -3
- package/src/FormScreen.css +74 -0
- package/src/FormScreen.tsx +27 -1
- package/src/PhoneInputStrings.ts +10 -1
- package/src/index.ts +65 -1
- package/src/styles.css +1 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// eKYC — the field catalogue by country and data provider.
|
|
2
|
+
//
|
|
3
|
+
// Figma: `👁️ eKYC Initial screen options` (`TWgYZVhurQCizfC4tfLEut`), section
|
|
4
|
+
// `2098:20867` "1-1-1-eKYC-empty (all options)", read on 2026-09-24. Fifteen
|
|
5
|
+
// step-1 frames, one per country and provider, eight of them with a step 2 and
|
|
6
|
+
// US Telco 1 with a step 3. Every frame is archetype E on the same skeleton;
|
|
7
|
+
// what changes is the LIST OF FIELDS per step. So a country is data, not a
|
|
8
|
+
// screen: `EkycCountryFormScreen` renders any entry of this catalogue.
|
|
9
|
+
//
|
|
10
|
+
// Labels are copy and live in `EkycStrings.country.fields`, keyed by
|
|
11
|
+
// `EkycFieldKey`, so a locale pack translates them once. The catalogue only
|
|
12
|
+
// says WHICH fields, in WHICH order, with WHICH kind of Core input.
|
|
13
|
+
//
|
|
14
|
+
// Node ids per variant are the frame ids in that section; a step's id is the
|
|
15
|
+
// frame that draws it. Nothing here is invented: a field appears because the
|
|
16
|
+
// frame draws it, with the label the frame draws (DEVIATIONS #EKC-1). Two
|
|
17
|
+
// things the frames draw unevenly were ALIGNED by the owner (Elena,
|
|
18
|
+
// 2026-09-24): every step uses the module's "Enter your details" title and
|
|
19
|
+
// subtitle (the US licence frame's copy) rather than a bare "eKYC
|
|
20
|
+
// Verification", and no variant draws a close in the nav; steps after the
|
|
21
|
+
// first get the archetype's Back button instead of a back arrow (#EKC-2).
|
|
22
|
+
|
|
23
|
+
export type EkycFieldKind = 'text' | 'date' | 'phone' | 'dropdown' | 'radio';
|
|
24
|
+
|
|
25
|
+
export type EkycFieldKey =
|
|
26
|
+
| 'firstName' | 'middleName' | 'middleNameOptional' | 'lastName' | 'surname' | 'maternalSurname'
|
|
27
|
+
| 'dateOfBirth' | 'email' | 'phone' | 'ssnLast4' | 'ssnOrItin' | 'nationality' | 'cpf'
|
|
28
|
+
| 'nationalId' | 'taxId' | 'gender'
|
|
29
|
+
| 'licenseNumber' | 'licenseState' | 'licenseExpiration'
|
|
30
|
+
| 'street' | 'houseNumber' | 'city' | 'state' | 'stateCode' | 'zipCode' | 'postalCode';
|
|
31
|
+
|
|
32
|
+
export interface EkycField {
|
|
33
|
+
key: EkycFieldKey;
|
|
34
|
+
kind: EkycFieldKind;
|
|
35
|
+
/** Draws the `*` after the label, as the US driver's licence frame does. */
|
|
36
|
+
required?: boolean;
|
|
37
|
+
/** Placeholder key into `EkycStrings.country.placeholders`, when the frame draws one. */
|
|
38
|
+
placeholder?: string;
|
|
39
|
+
/** For `phone`: the ISO code of the dial code the frame shows (+1, +52, +34…). */
|
|
40
|
+
dialCountry?: string;
|
|
41
|
+
/**
|
|
42
|
+
* Desktop and tablet pairing (DEVIATIONS #EKC-5). Two ADJACENT fields with
|
|
43
|
+
* the same `row` share a `PrizmaFormRow`: two lanes of 202 on a wide frame,
|
|
44
|
+
* the ordinary stack on the phone (the archetype's container query, 420 and
|
|
45
|
+
* up, decides; nothing here re-declares a width). A field without a `row`
|
|
46
|
+
* spans the column. The rule mirrors eKYB's frames: a name pair shares
|
|
47
|
+
* (first | last, first | surname, surname | maternal surname), nationality |
|
|
48
|
+
* CPF share, State | Postal or Zip code share; email, date of birth, the
|
|
49
|
+
* phone (a Combined field never shares a row, #EKC-7), every identifier
|
|
50
|
+
* (SSN, ITIN, national ID, tax ID, CPF alone), the gender pair and street,
|
|
51
|
+
* house number, city span.
|
|
52
|
+
*/
|
|
53
|
+
row?: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface EkycCountryStep {
|
|
57
|
+
/** Figma frame id. */
|
|
58
|
+
node: string;
|
|
59
|
+
/** A section heading under the title ("Address details", "Driver's License details"), when drawn. */
|
|
60
|
+
section?: 'address' | 'license';
|
|
61
|
+
fields: EkycField[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface EkycCountryVariant {
|
|
65
|
+
/** Stable id; screen ids are `ekyc-<id>-step<n>`. */
|
|
66
|
+
id: string;
|
|
67
|
+
/** ISO 3166-1 alpha-2 of the country; `XX` for a country-agnostic provider (IPQS). */
|
|
68
|
+
country: string;
|
|
69
|
+
/** Country name and provider, as the frame names them. */
|
|
70
|
+
label: string;
|
|
71
|
+
provider: string;
|
|
72
|
+
/** Segments the frame's stepper draws; 0 when it draws none. */
|
|
73
|
+
stepper: number;
|
|
74
|
+
steps: EkycCountryStep[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const t = (key: EkycFieldKey, extra: Partial<EkycField> = {}): EkycField => ({ key, kind: 'text', ...extra });
|
|
78
|
+
const date = (key: EkycFieldKey = 'dateOfBirth', extra: Partial<EkycField> = {}): EkycField => ({ key, kind: 'date', ...extra });
|
|
79
|
+
// Required fields carry Core's asterisk (`required` + `requiredHint`), the
|
|
80
|
+
// owner's rule (Elena, 2026-09-24, DEVIATIONS #EKC-6): first name, last name or
|
|
81
|
+
// surname, email and phone on every variant. Middle name (plain or "Optional")
|
|
82
|
+
// and maternal surname stay optional; date of birth, identifiers, address and
|
|
83
|
+
// licence fields keep what their frame draws (only the US licence step 1 draws
|
|
84
|
+
// asterisks on them). `showErrors` flags required empty fields only.
|
|
85
|
+
const phone = (dialCountry: string, extra: Partial<EkycField> = {}): EkycField => ({ key: 'phone', kind: 'phone', dialCountry, required: true, ...extra });
|
|
86
|
+
const email = (extra: Partial<EkycField> = {}): EkycField => t('email', { required: true, ...extra });
|
|
87
|
+
|
|
88
|
+
// The wide-frame pairs (`row`, DEVIATIONS #EKC-5). A row number is only a
|
|
89
|
+
// grouping id, local to its step; 1 is the name pair, 2 Brazil's nationality |
|
|
90
|
+
// CPF, 3 the State | Postal pair of an address. The phone field NEVER shares a
|
|
91
|
+
// row: Core's Combined (dial code + number) collapses to a three-digit stub in
|
|
92
|
+
// a 202 lane (Elena, 2026-09-24, #EKC-7), and date of birth spans with it.
|
|
93
|
+
const NAME_PAIR = (last: EkycFieldKey = 'lastName'): EkycField[] => [t('firstName', { required: true, row: 1 }), t(last, { required: true, row: 1 })];
|
|
94
|
+
const DOB_PHONE = (dialCountry: string): EkycField[] => [date(), phone(dialCountry)];
|
|
95
|
+
|
|
96
|
+
const US_ADDRESS: EkycField[] = [t('street'), t('houseNumber'), t('city'), { key: 'state', kind: 'dropdown', placeholder: 'selectState', row: 3 }, t('zipCode', { row: 3 })];
|
|
97
|
+
const CODE_ADDRESS = (placeholder?: string): EkycField[] => [t('street'), t('houseNumber'), t('city'), t('stateCode', { row: 3, ...(placeholder ? { placeholder } : {}) }), t('postalCode', { row: 3 })];
|
|
98
|
+
const LICENSE: EkycField[] = [t('licenseNumber'), { key: 'licenseState', kind: 'dropdown', placeholder: 'selectState', row: 1 }, date('licenseExpiration', { row: 1 })];
|
|
99
|
+
|
|
100
|
+
export const EKYC_COUNTRY_CATALOG: EkycCountryVariant[] = [
|
|
101
|
+
{
|
|
102
|
+
id: 'us-drivers-license', country: 'US', label: 'United States', provider: "Driver's license", stepper: 2,
|
|
103
|
+
steps: [
|
|
104
|
+
/* First | Last share the wide row, so Middle name follows the pair here
|
|
105
|
+
and in `EkycDetailsScreen` (the same frame). On the phone the stack
|
|
106
|
+
reads First, Last, Middle, Date of birth — the one order change the
|
|
107
|
+
desktop pairing costs (#EKC-5). */
|
|
108
|
+
{ node: '2101:25871', fields: [t('firstName', { required: true, row: 1 }), t('lastName', { required: true, row: 1 }), t('middleName'), date('dateOfBirth', { required: true })] },
|
|
109
|
+
{ node: '5849:73280', section: 'license', fields: LICENSE },
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'us-telco-1', country: 'US', label: 'United States', provider: 'Telco 1', stepper: 3,
|
|
114
|
+
steps: [
|
|
115
|
+
{ node: '2098:9575', fields: [...NAME_PAIR(), email(), ...DOB_PHONE('US'), t('ssnLast4')] },
|
|
116
|
+
{ node: '5854:76222', section: 'license', fields: [t('licenseNumber'), { key: 'licenseState', kind: 'dropdown', placeholder: 'selectState' }] },
|
|
117
|
+
{ node: '5854:76221', section: 'address', fields: US_ADDRESS },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'us-telco-2', country: 'US', label: 'United States', provider: 'Telco 2', stepper: 2,
|
|
122
|
+
steps: [
|
|
123
|
+
{ node: '2098:9578', fields: [...NAME_PAIR(), ...DOB_PHONE('US')] },
|
|
124
|
+
{ node: '5857:33074', section: 'address', fields: US_ADDRESS },
|
|
125
|
+
],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'us-credit-bureau', country: 'US', label: 'United States', provider: 'Credit bureau', stepper: 2,
|
|
129
|
+
steps: [
|
|
130
|
+
/* Same First | Last pair as the licence variant; Middle name (Optional)
|
|
131
|
+
follows it and spans (#EKC-5). */
|
|
132
|
+
{ node: '2098:9577', fields: [...NAME_PAIR(), t('middleNameOptional'), ...DOB_PHONE('US'), email(), t('ssnOrItin')] },
|
|
133
|
+
{ node: '5857:34094', section: 'address', fields: US_ADDRESS },
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'mx-consumer', country: 'MX', label: 'Mexico', provider: 'Consumer', stepper: 0,
|
|
138
|
+
/* Mexico pairs the two surnames, so First name spans on its own. */
|
|
139
|
+
steps: [{ node: '2098:9572', fields: [t('firstName', { required: true }), t('surname', { required: true, row: 1 }), t('maternalSurname', { row: 1 }), phone('MX'), email()] }],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
id: 'br-govt', country: 'BR', label: 'Brazil', provider: 'Government', stepper: 2,
|
|
143
|
+
steps: [
|
|
144
|
+
{ node: '2098:9574', fields: [...NAME_PAIR('surname'), email(), date(), { key: 'nationality', kind: 'dropdown', row: 2 }, t('cpf', { row: 2 })] },
|
|
145
|
+
{ node: '5857:34935', section: 'address', fields: CODE_ADDRESS('stateCodeBr') },
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: 'ca-res-credit', country: 'CA', label: 'Canada', provider: 'Residential credit', stepper: 2,
|
|
150
|
+
steps: [
|
|
151
|
+
{ node: '2098:9570', fields: [...NAME_PAIR(), email(), ...DOB_PHONE('CA'), t('ssnOrItin')] },
|
|
152
|
+
{ node: '5857:36450', section: 'address', fields: CODE_ADDRESS('stateCodeCa') },
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: 'uk-credit-bureau', country: 'GB', label: 'United Kingdom', provider: 'Credit bureau', stepper: 2,
|
|
157
|
+
steps: [
|
|
158
|
+
{ node: '2098:9569', fields: [...NAME_PAIR('surname'), date()] },
|
|
159
|
+
{ node: '5857:36888', section: 'address', fields: CODE_ADDRESS() },
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'es-telephone-register', country: 'ES', label: 'Spain', provider: 'Telephone register', stepper: 2,
|
|
164
|
+
steps: [
|
|
165
|
+
{ node: '2098:9573', fields: [...NAME_PAIR('surname'), ...DOB_PHONE('ES'), t('nationalId')] },
|
|
166
|
+
{ node: '5857:37341', section: 'address', fields: CODE_ADDRESS() },
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'cl-official-census', country: 'CL', label: 'Chile', provider: 'Official census', stepper: 0,
|
|
171
|
+
steps: [{ node: '2098:9571', fields: [...NAME_PAIR('surname'), date(), t('nationalId')] }],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: 'ar-civil-register', country: 'AR', label: 'Argentina', provider: 'Official civil register', stepper: 2,
|
|
175
|
+
steps: [
|
|
176
|
+
{ node: '2098:9568', fields: [...NAME_PAIR('surname'), { key: 'gender', kind: 'radio' }, email(), date(), t('nationalId'), t('taxId')] },
|
|
177
|
+
{ node: '5857:38015', section: 'address', fields: [t('street'), t('houseNumber'), t('city'), { key: 'state', kind: 'dropdown', placeholder: 'selectState', row: 3 }, t('postalCode', { row: 3 })] },
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: 'gt', country: 'GT', label: 'Guatemala', provider: 'Registry', stepper: 0,
|
|
182
|
+
steps: [{ node: '2098:9566', fields: [...NAME_PAIR('surname'), date(), t('nationalId')] }],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'co', country: 'CO', label: 'Colombia', provider: 'Registry', stepper: 0,
|
|
186
|
+
steps: [{ node: '2098:9567', fields: [...NAME_PAIR('surname'), t('nationalId')] }],
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'cr', country: 'CR', label: 'Costa Rica', provider: 'Registry', stepper: 0,
|
|
190
|
+
steps: [{ node: '2098:9565', fields: [...NAME_PAIR('surname'), t('nationalId')] }],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: 'ipqs', country: 'XX', label: 'Any country', provider: 'IPQS', stepper: 0,
|
|
194
|
+
steps: [{ node: '2098:9564', fields: [email(), phone('US')] }],
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
|
|
198
|
+
export const ekycCountryVariant = (id: string): EkycCountryVariant | undefined =>
|
|
199
|
+
EKYC_COUNTRY_CATALOG.find(v => v.id === id);
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import React, { forwardRef } from 'react';
|
|
2
|
+
import { PrizmaTextField } from '@incodetech/prizma-core-web/text-field';
|
|
3
|
+
import { PrizmaDateField } from '@incodetech/prizma-core-web/date-field';
|
|
4
|
+
import { PrizmaDropdown } from '@incodetech/prizma-core-web/dropdown';
|
|
5
|
+
import { PrizmaCombined, type PrizmaCountry } from '@incodetech/prizma-core-web/combined';
|
|
6
|
+
import { PrizmaRadioGroup } from '@incodetech/prizma-core-web/radio-group';
|
|
7
|
+
import { PrizmaRadio } from '@incodetech/prizma-core-web/radio';
|
|
8
|
+
import { PrizmaStepper, type PrizmaStepperState } from '@incodetech/prizma-core-web/stepper';
|
|
9
|
+
import { PrizmaButton } from '@incodetech/prizma-core-web/button';
|
|
10
|
+
import { PrizmaFormScreen, PrizmaFormRow, type PrizmaFormScreenProps } from './FormScreen';
|
|
11
|
+
import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
|
|
12
|
+
import type { EkycCountryValues, EkycStrings } from './EkycStrings';
|
|
13
|
+
import { ekycCountryVariant, type EkycCountryVariant, type EkycField } from './EkycCountryCatalog';
|
|
14
|
+
import './ScreenLayout.css';
|
|
15
|
+
import './EkycLayout.css';
|
|
16
|
+
|
|
17
|
+
export interface EkycCountryFormScreenProps
|
|
18
|
+
extends Omit<
|
|
19
|
+
PrizmaFormScreenProps,
|
|
20
|
+
'title' | 'subtitle' | 'ctaLabel' | 'ctaDisabled' | 'verifiedLabel' | 'children' | 'navLabel' | 'progress' | 'scrollFields' | 'linkContent'
|
|
21
|
+
> {
|
|
22
|
+
strings: EkycStrings;
|
|
23
|
+
/** A catalogue id (`us-telco-1`) or the variant itself. */
|
|
24
|
+
variant: string | EkycCountryVariant;
|
|
25
|
+
/** 1-based step of the variant. Default 1. */
|
|
26
|
+
step?: number;
|
|
27
|
+
/** Starting values by field key. Empty renders the resting empty field. */
|
|
28
|
+
values?: EkycCountryValues;
|
|
29
|
+
/**
|
|
30
|
+
* 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.
|
|
34
|
+
*/
|
|
35
|
+
showErrors?: boolean;
|
|
36
|
+
/** Dial codes the phone field offers. Runtime data, never copy. */
|
|
37
|
+
phoneCountries?: PrizmaCountry[];
|
|
38
|
+
/** Options for the dropdown fields, keyed by field key. Runtime data. */
|
|
39
|
+
options?: Record<string, { value: string; label: string }[]>;
|
|
40
|
+
screenId?: string;
|
|
41
|
+
overrides?: PrizmaScreenOverrides;
|
|
42
|
+
onAction?: PrizmaScreenActionHandler;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* eKYC — one step of one by-country variant, built on the Form archetype.
|
|
47
|
+
*
|
|
48
|
+
* The section `2098:20867` draws fifteen variants on the same skeleton (nav →
|
|
49
|
+
* optional stepper → title → fields → Continue → tag) and differs only in the
|
|
50
|
+
* list of fields. This screen reads that list from `EkycCountryCatalog.ts` and
|
|
51
|
+
* composes Core's own inputs for it — text, date, phone (`PrizmaCombined`),
|
|
52
|
+
* dropdown, radio pair — so a new country is a catalogue entry, never a file.
|
|
53
|
+
*
|
|
54
|
+
* 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).
|
|
59
|
+
* - ONE title per step (Elena, 2026-09-24): the personal-data step is "Enter
|
|
60
|
+
* your details" with its subtitle; a licence step is titled "Driver's
|
|
61
|
+
* License details" and an address step "Address details", with nothing
|
|
62
|
+
* above them. The frames drew "eKYC Verification" over a section heading;
|
|
63
|
+
* the owner aligned the copy to one H2 per form.
|
|
64
|
+
* - No close in the nav (Core's nav has none, GUIDELINES §3, and the owner
|
|
65
|
+
* asked for none). A step after the first gets the archetype's Back button,
|
|
66
|
+
* as eKYB does, instead of the back arrow four frames drew. DEVIATIONS #EKC-2.
|
|
67
|
+
* - Desktop and tablet take the eKYB shape (`responsive`, exploration
|
|
68
|
+
* `ekyb-desktop`): two adjacent catalogue fields with the same `row` share a
|
|
69
|
+
* `PrizmaFormRow`, so on a wide frame they sit in two lanes and on the phone
|
|
70
|
+
* they stack exactly as before. Which fields pair is the catalogue's call,
|
|
71
|
+
* never this screen's (DEVIATIONS #EKC-5).
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/** Adjacent fields sharing a `row` become one group; the rest stand alone. */
|
|
75
|
+
const groupRows = (fields: EkycField[]): EkycField[][] => {
|
|
76
|
+
const groups: EkycField[][] = [];
|
|
77
|
+
for (const f of fields) {
|
|
78
|
+
const last = groups[groups.length - 1];
|
|
79
|
+
if (f.row != null && last && last[0].row === f.row) last.push(f);
|
|
80
|
+
else groups.push([f]);
|
|
81
|
+
}
|
|
82
|
+
return groups;
|
|
83
|
+
};
|
|
84
|
+
export const EkycCountryFormScreen = forwardRef<HTMLDivElement, EkycCountryFormScreenProps>(
|
|
85
|
+
({ strings, variant, step = 1, values, showErrors = false, phoneCountries, options, screenId, overrides, onAction, ...shell }, ref) => {
|
|
86
|
+
const v = typeof variant === 'string' ? ekycCountryVariant(variant) : variant;
|
|
87
|
+
if (!v) return null;
|
|
88
|
+
const s = v.steps[Math.min(Math.max(step, 1), v.steps.length) - 1];
|
|
89
|
+
const c = strings.country;
|
|
90
|
+
const stepperState: PrizmaStepperState = step <= 1 ? 'Start' : (Math.min(step, 6) as PrizmaStepperState);
|
|
91
|
+
|
|
92
|
+
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]);
|
|
94
|
+
|
|
95
|
+
const renderField = (f: EkycField) => {
|
|
96
|
+
// 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];
|
|
99
|
+
const common = {
|
|
100
|
+
key: f.key, label: label(f), required: f.required, requiredHint: strings.details.requiredHint,
|
|
101
|
+
error: missing || undefined, errorText: missing ? c.requiredError : undefined,
|
|
102
|
+
} as const;
|
|
103
|
+
switch (f.kind) {
|
|
104
|
+
case 'date':
|
|
105
|
+
return <PrizmaDateField {...common} placeholder={placeholder(f)} defaultValue={values?.[f.key] ?? ''} />;
|
|
106
|
+
case 'phone': {
|
|
107
|
+
const country = phoneCountries?.find(p => p.code === f.dialCountry) ?? phoneCountries?.[0];
|
|
108
|
+
return (
|
|
109
|
+
<PrizmaCombined
|
|
110
|
+
{...common}
|
|
111
|
+
countries={phoneCountries}
|
|
112
|
+
defaultCountry={country}
|
|
113
|
+
countryListLabel={c.countryListLabel}
|
|
114
|
+
placeholder={placeholder(f)}
|
|
115
|
+
defaultValue={values?.[f.key] ?? ''}
|
|
116
|
+
autoComplete="off"
|
|
117
|
+
/>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
case 'dropdown':
|
|
121
|
+
return (
|
|
122
|
+
<PrizmaDropdown
|
|
123
|
+
{...common}
|
|
124
|
+
placeholder={placeholder(f)}
|
|
125
|
+
options={options?.[f.key] ?? []}
|
|
126
|
+
defaultValue={values?.[f.key] ?? null}
|
|
127
|
+
/>
|
|
128
|
+
);
|
|
129
|
+
case 'radio':
|
|
130
|
+
return (
|
|
131
|
+
<PrizmaRadioGroup key={f.key} name={f.key} legend={label(f)} defaultValue={values?.[f.key]} className="ekyc-country__radios">
|
|
132
|
+
{c.genderOptions.map(o => (
|
|
133
|
+
<PrizmaRadio key={o.value} value={o.value} textSize="m" showText text={o.label} />
|
|
134
|
+
))}
|
|
135
|
+
</PrizmaRadioGroup>
|
|
136
|
+
);
|
|
137
|
+
default:
|
|
138
|
+
return <PrizmaTextField {...common} placeholder={placeholder(f)} autoComplete="off" defaultValue={values?.[f.key] ?? ''} />;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<PrizmaFormScreen
|
|
144
|
+
ref={ref}
|
|
145
|
+
scrollFields
|
|
146
|
+
progress={
|
|
147
|
+
v.stepper > 0 ? (
|
|
148
|
+
<PrizmaStepper steps={v.stepper as 2 | 3} currentState={stepperState} showIcons={false} aria-label={strings.stepperLabel} />
|
|
149
|
+
) : undefined
|
|
150
|
+
}
|
|
151
|
+
title={s.section === 'address' ? c.addressSection : s.section === 'license' ? c.licenseSection : strings.details.title}
|
|
152
|
+
subtitle={s.section ? undefined : strings.details.subtitle}
|
|
153
|
+
linkContent={
|
|
154
|
+
step > 1 ? (
|
|
155
|
+
<PrizmaButton variant="secondary" size="L" onClick={() => onAction?.({ type: 'secondary', screenId })}>
|
|
156
|
+
{c.backLabel}
|
|
157
|
+
</PrizmaButton>
|
|
158
|
+
) : undefined
|
|
159
|
+
}
|
|
160
|
+
ctaLabel={c.ctaLabel}
|
|
161
|
+
verifiedLabel={strings.verifiedLabel}
|
|
162
|
+
navLabel={strings.navLabel}
|
|
163
|
+
screenId={screenId}
|
|
164
|
+
overrides={{ align: overrides?.align ?? 'left', logo: overrides?.logo }}
|
|
165
|
+
onAction={onAction}
|
|
166
|
+
/* The eKYB desktop shape: catalogue pairs become two lanes at 420 and
|
|
167
|
+
up, Back | Continue share a row. The phone stack is untouched. */
|
|
168
|
+
responsive
|
|
169
|
+
{...shell}
|
|
170
|
+
>
|
|
171
|
+
{groupRows(s.fields).map(group =>
|
|
172
|
+
group.length > 1 ? (
|
|
173
|
+
<PrizmaFormRow key={group.map(f => f.key).join('+')}>{group.map(renderField)}</PrizmaFormRow>
|
|
174
|
+
) : (
|
|
175
|
+
renderField(group[0])
|
|
176
|
+
),
|
|
177
|
+
)}
|
|
178
|
+
</PrizmaFormScreen>
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
EkycCountryFormScreen.displayName = 'EkycCountryFormScreen';
|
|
183
|
+
|
|
184
|
+
export default EkycCountryFormScreen;
|
|
@@ -2,7 +2,7 @@ import React, { forwardRef } from 'react';
|
|
|
2
2
|
import { PrizmaTextField } from '@incodetech/prizma-core-web/text-field';
|
|
3
3
|
import { PrizmaDateField } from '@incodetech/prizma-core-web/date-field';
|
|
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 { EkycDetailsState, EkycDetailsValues, EkycStrings } from './EkycStrings';
|
|
8
8
|
import './ScreenLayout.css';
|
|
@@ -131,33 +131,42 @@ export const EkycDetailsScreen = forwardRef<HTMLDivElement, EkycDetailsScreenPro
|
|
|
131
131
|
that passes its own `align` still wins. */
|
|
132
132
|
overrides={{ align: overrides?.align ?? 'left', logo: overrides?.logo }}
|
|
133
133
|
onAction={onAction}
|
|
134
|
+
/* Exploration ekyb-desktop, applied to eKYC: on a tablet or desktop
|
|
135
|
+
frame First name | Last name share a row; Middle name and Date of
|
|
136
|
+
birth span the column. So that the pair is adjacent, Middle name
|
|
137
|
+
now FOLLOWS Last name, on the phone too (First, Last, Middle, Date of
|
|
138
|
+
birth), the one order change the pairing costs: the frames drew
|
|
139
|
+
First, Middle, Last. DEVIATIONS #EKC-5. */
|
|
140
|
+
responsive
|
|
134
141
|
{...shell}
|
|
135
142
|
>
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
143
|
+
<PrizmaFormRow>
|
|
144
|
+
<PrizmaTextField
|
|
145
|
+
className={focused ? 'prizma-input-textfield--focused' : undefined}
|
|
146
|
+
label={strings.details.firstNameLabel}
|
|
147
|
+
required
|
|
148
|
+
requiredHint={strings.details.requiredHint}
|
|
149
|
+
defaultValue={values?.firstName ?? ''}
|
|
150
|
+
autoComplete="off"
|
|
151
|
+
/* Only 2101:8159 draws the ✕, and only in this field. */
|
|
152
|
+
clearable={focused}
|
|
153
|
+
clearLabel={strings.details.clearLabel}
|
|
154
|
+
/>
|
|
155
|
+
<PrizmaTextField
|
|
156
|
+
label={strings.details.lastNameLabel}
|
|
157
|
+
required
|
|
158
|
+
requiredHint={strings.details.requiredHint}
|
|
159
|
+
defaultValue={values?.lastName ?? ''}
|
|
160
|
+
autoComplete="off"
|
|
161
|
+
clearable={false}
|
|
162
|
+
/>
|
|
163
|
+
</PrizmaFormRow>
|
|
147
164
|
<PrizmaTextField
|
|
148
165
|
label={strings.details.middleNameLabel}
|
|
149
166
|
defaultValue={values?.middleName ?? ''}
|
|
150
167
|
autoComplete="off"
|
|
151
168
|
clearable={false}
|
|
152
169
|
/>
|
|
153
|
-
<PrizmaTextField
|
|
154
|
-
label={strings.details.lastNameLabel}
|
|
155
|
-
required
|
|
156
|
-
requiredHint={strings.details.requiredHint}
|
|
157
|
-
defaultValue={values?.lastName ?? ''}
|
|
158
|
-
autoComplete="off"
|
|
159
|
-
clearable={false}
|
|
160
|
-
/>
|
|
161
170
|
<PrizmaDateField
|
|
162
171
|
label={strings.details.dateOfBirthLabel}
|
|
163
172
|
required
|
package/src/EkycLayout.css
CHANGED
|
@@ -37,3 +37,26 @@
|
|
|
37
37
|
.prizma-input-textfield__clear--show {
|
|
38
38
|
display: inline-flex;
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
/* ── The gender pair sits on one row ──────────────────────────────────────────
|
|
42
|
+
* Argentina's frame (2098:9568) draws "Female Male" side by side under the
|
|
43
|
+
* "Gender" label. Core's `PrizmaRadioGroup` stacks its radios in a column and
|
|
44
|
+
* has no row option, so this releases that one direction for the pair: the
|
|
45
|
+
* legend keeps the whole first line, the two radios share the next one at
|
|
46
|
+
* Spacing/24. Core's radio, its label, its size and colours are untouched.
|
|
47
|
+
* CORE REQUEST: a `direction="row"` on `PrizmaRadioGroup` would retire this
|
|
48
|
+
* rule (DEVIATIONS #EKC-3). */
|
|
49
|
+
.ekyc-country__radios {
|
|
50
|
+
flex-direction: row;
|
|
51
|
+
flex-wrap: wrap;
|
|
52
|
+
column-gap: var(--prizma-spacing-24);
|
|
53
|
+
}
|
|
54
|
+
.ekyc-country__radios .prizma-radio-group__legend {
|
|
55
|
+
flex-basis: 100%;
|
|
56
|
+
}
|
|
57
|
+
/* Core's radio is FILL by design (radio.md §3); on the row each one hugs its
|
|
58
|
+
* label so the pair reads "Female Male" as drawn. Same release, same request. */
|
|
59
|
+
.ekyc-country__radios .prizma-radio {
|
|
60
|
+
width: auto;
|
|
61
|
+
flex: 0 0 auto;
|
|
62
|
+
}
|
|
@@ -3,7 +3,7 @@ import { PrizmaTextField } from '@incodetech/prizma-core-web/text-field';
|
|
|
3
3
|
import { PrizmaDateField } from '@incodetech/prizma-core-web/date-field';
|
|
4
4
|
import { PrizmaDropdown } from '@incodetech/prizma-core-web/dropdown';
|
|
5
5
|
import { PrizmaStepper, type PrizmaStepperState } 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
|
EkycLicenseState,
|
|
@@ -129,6 +129,10 @@ export const EkycLicenseScreen = forwardRef<HTMLDivElement, EkycLicenseScreenPro
|
|
|
129
129
|
/* Left-aligned title block, same as page 1; a host `align` still wins. */
|
|
130
130
|
overrides={{ align: overrides?.align ?? 'left', logo: overrides?.logo }}
|
|
131
131
|
onAction={onAction}
|
|
132
|
+
/* Exploration ekyb-desktop, applied to eKYC: on a wide frame the
|
|
133
|
+
licence number spans and State | Expiration date share a row; the
|
|
134
|
+
phone stack is untouched. DEVIATIONS #EKC-5. */
|
|
135
|
+
responsive
|
|
132
136
|
{...shell}
|
|
133
137
|
>
|
|
134
138
|
<PrizmaTextField
|
|
@@ -140,21 +144,23 @@ export const EkycLicenseScreen = forwardRef<HTMLDivElement, EkycLicenseScreenPro
|
|
|
140
144
|
/* No section on this page draws the clear ✕. */
|
|
141
145
|
clearable={false}
|
|
142
146
|
/>
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
147
|
+
<PrizmaFormRow>
|
|
148
|
+
<PrizmaDropdown
|
|
149
|
+
label={strings.license.stateLabel}
|
|
150
|
+
options={stateOptions}
|
|
151
|
+
defaultValue={values?.state ?? null}
|
|
152
|
+
disabled={busy}
|
|
153
|
+
/>
|
|
154
|
+
<PrizmaDateField
|
|
155
|
+
label={strings.license.expirationLabel}
|
|
156
|
+
defaultValue={values?.expirationDate ?? ''}
|
|
157
|
+
autoComplete="off"
|
|
158
|
+
disabled={busy}
|
|
159
|
+
/* No `onCalendarClick` — see the note in `EkycDetailsScreen`: Core
|
|
160
|
+
renders the calendar as a decorative aria-hidden glyph without
|
|
161
|
+
one, so `calendarLabel` would do nothing and is not passed. */
|
|
162
|
+
/>
|
|
163
|
+
</PrizmaFormRow>
|
|
158
164
|
</PrizmaFormScreen>
|
|
159
165
|
);
|
|
160
166
|
},
|
package/src/EkycStrings.ts
CHANGED
|
@@ -57,6 +57,9 @@ export type EkycDetailsState = 'default' | 'focused' | 'filled';
|
|
|
57
57
|
*/
|
|
58
58
|
export type EkycLicenseState = 'default' | 'filled' | 'loading';
|
|
59
59
|
|
|
60
|
+
/** Values for a by-country step, keyed by `EkycFieldKey`. Empty renders the resting empty field. */
|
|
61
|
+
export type EkycCountryValues = Partial<Record<string, string>>;
|
|
62
|
+
|
|
60
63
|
/** One row of the licence-state chooser. Runtime data, never copy — the host
|
|
61
64
|
* supplies the jurisdictions its tenant accepts, exactly as Phone Input's
|
|
62
65
|
* `PhoneInputCountry` supplies dial codes. */
|
|
@@ -139,6 +142,28 @@ export interface EkycStrings {
|
|
|
139
142
|
ctaLoadingLabel: string;
|
|
140
143
|
};
|
|
141
144
|
|
|
145
|
+
/**
|
|
146
|
+
* The by-country variants (`EkycCountryCatalog.ts`, Figma section
|
|
147
|
+
* 2098:20867). Field labels keyed by `EkycFieldKey`, placeholders by the
|
|
148
|
+
* catalogue's `placeholder` keys; one map, translated once.
|
|
149
|
+
*/
|
|
150
|
+
country: {
|
|
151
|
+
/** Section headings drawn under the title on a step 2 / 3. */
|
|
152
|
+
addressSection: string;
|
|
153
|
+
licenseSection: string;
|
|
154
|
+
/** Accessible name of the phone field's dial-code list. */
|
|
155
|
+
countryListLabel: string;
|
|
156
|
+
/** The gender radio pair, Argentina. */
|
|
157
|
+
genderOptions: { value: string; label: string }[];
|
|
158
|
+
fields: Record<string, string>;
|
|
159
|
+
placeholders: Record<string, string>;
|
|
160
|
+
ctaLabel: string;
|
|
161
|
+
/** The secondary button on a step after the first. */
|
|
162
|
+
backLabel: string;
|
|
163
|
+
/** Under every empty field once Continue was pressed (`showErrors`). */
|
|
164
|
+
requiredError: string;
|
|
165
|
+
};
|
|
166
|
+
|
|
142
167
|
/** `eKYC.processing` — spinner over the status line, no button. */
|
|
143
168
|
processing: {
|
|
144
169
|
title: string;
|
package/src/EmailInputStrings.ts
CHANGED
|
@@ -63,7 +63,17 @@ export interface EmailInputStrings {
|
|
|
63
63
|
verify: {
|
|
64
64
|
/** H2 ("Verify your email"). */
|
|
65
65
|
title: string;
|
|
66
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* H5 ("We'll use it to verify it's yours").
|
|
68
|
+
*
|
|
69
|
+
* DELIBERATELY SILENT ABOUT THE CODE, for the same reason as Phone Input's
|
|
70
|
+
* twin line. This module ships in TWO configurations and the `Without a
|
|
71
|
+
* one-time code` one never asks for a code at all (see the `noOtp` variant
|
|
72
|
+
* in `flow/graphs/email-input.ts`: the spinner runs straight into the
|
|
73
|
+
* verdict). One screen serves both, and which configuration the session
|
|
74
|
+
* wants is not something this line can know. It answers "why do you want my
|
|
75
|
+
* address?" instead, which holds in both.
|
|
76
|
+
*/
|
|
67
77
|
subtitle: string;
|
|
68
78
|
/** Field placeholder ("Email"). */
|
|
69
79
|
placeholder: string;
|
|
@@ -16,8 +16,10 @@ import './FaceMatchLayout.css';
|
|
|
16
16
|
* contract as `camera` on the capture screens) — Face Match never captures. */
|
|
17
17
|
export type FaceMatchPhotoRole = 'selfie' | 'id' | 'nfc' | 'secondId';
|
|
18
18
|
|
|
19
|
-
/** Host-supplied portraits, one per role the chosen variant asks for.
|
|
20
|
-
* left out renders
|
|
19
|
+
/** Host-supplied portraits, one per role the chosen variant asks for. A role
|
|
20
|
+
* left out renders the empty state: a Brand/50 disc, marked with
|
|
21
|
+
* `.face-match-cluster__photo--empty`. Interim treatment (2026-09-25) while
|
|
22
|
+
* the designed no-photo state is drawn; see FaceMatchLayout.css. */
|
|
21
23
|
export type FaceMatchComparisonPhotos = Partial<Record<FaceMatchPhotoRole, ReactNode>>;
|
|
22
24
|
|
|
23
25
|
/**
|
|
@@ -179,7 +181,13 @@ export const FaceMatchComparisonScreen = forwardRef<
|
|
|
179
181
|
{label}
|
|
180
182
|
</PrizmaTooltip>
|
|
181
183
|
) : null}
|
|
182
|
-
<div
|
|
184
|
+
<div
|
|
185
|
+
className={
|
|
186
|
+
photos?.[role]
|
|
187
|
+
? 'face-match-cluster__photo'
|
|
188
|
+
: 'face-match-cluster__photo face-match-cluster__photo--empty'
|
|
189
|
+
}
|
|
190
|
+
>
|
|
183
191
|
{photos?.[role]}
|
|
184
192
|
</div>
|
|
185
193
|
</div>
|