@incodetech/prizma-core-screens 0.2.4 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/BiometricConsentScreen.d.ts +95 -0
- package/dist/BiometricConsentScreen.js +91 -0
- package/dist/BiometricConsentStrings.d.ts +84 -0
- package/dist/BiometricConsentStrings.js +31 -0
- package/dist/CaptureScreen.d.ts +13 -0
- package/dist/CaptureScreen.js +2 -2
- package/dist/CurpEnterScreen.d.ts +7 -0
- package/dist/CurpEnterScreen.js +17 -3
- package/dist/CurpGenerateScreen.d.ts +8 -0
- package/dist/CurpGenerateScreen.js +28 -13
- package/dist/CurpLayout.css +8 -34
- package/dist/CurpStrings.d.ts +16 -2
- package/dist/DemoScreensLayout.css +16 -0
- package/dist/DemoScreensRedirectScreen.d.ts +6 -1
- package/dist/DemoScreensRedirectScreen.js +3 -2
- package/dist/DemoScreensStrings.d.ts +5 -0
- package/dist/DigitalIdUploadErrorScreen.d.ts +31 -0
- package/dist/DigitalIdUploadErrorScreen.js +26 -0
- package/dist/DigitalIdUploadFileTooLargeScreen.d.ts +27 -0
- package/dist/DigitalIdUploadFileTooLargeScreen.js +21 -0
- package/dist/DigitalIdUploadReviewScreen.d.ts +57 -0
- package/dist/DigitalIdUploadReviewScreen.js +44 -0
- package/dist/DigitalIdUploadStrings.d.ts +64 -0
- package/dist/DigitalIdUploadStrings.js +19 -0
- package/dist/DigitalIdUploadTutorialScreen.d.ts +28 -0
- package/dist/DigitalIdUploadTutorialScreen.js +23 -0
- package/dist/DocumentCaptureIllustration.d.ts +1 -1
- package/dist/EkybAddressDetailsScreen.d.ts +7 -0
- package/dist/EkybAddressDetailsScreen.js +25 -7
- package/dist/EkybBusinessDetailsScreen.d.ts +6 -0
- package/dist/EkybBusinessDetailsScreen.js +23 -6
- package/dist/EkybCountryScreen.d.ts +13 -5
- package/dist/EkybCountryScreen.js +41 -10
- package/dist/EkybOwnersScreen.d.ts +8 -0
- package/dist/EkybOwnersScreen.js +26 -3
- package/dist/EkybStrings.d.ts +32 -10
- package/dist/EkycCountryCatalog.d.ts +4 -1
- package/dist/EkycCountryCatalog.js +5 -3
- package/dist/EkycCountryFormScreen.d.ts +12 -3
- package/dist/EkycCountryFormScreen.js +28 -10
- package/dist/EkycDetailsScreen.d.ts +7 -0
- package/dist/EkycDetailsScreen.js +21 -4
- package/dist/EkycStrings.d.ts +14 -2
- package/dist/ElectronicSignatureLayout.css +6 -0
- package/dist/EmailInputStrings.d.ts +11 -1
- package/dist/EmailInputVerifyScreen.d.ts +7 -0
- package/dist/EmailInputVerifyScreen.js +17 -3
- package/dist/ErrorScreensScreen.d.ts +5 -2
- package/dist/ErrorScreensScreen.js +15 -4
- package/dist/ErrorScreensStrings.d.ts +17 -2
- package/dist/ErrorScreensStrings.js +18 -10
- package/dist/FaceCaptureCapturingScreen.d.ts +63 -0
- package/dist/FaceCaptureCapturingScreen.js +62 -2
- package/dist/FaceCaptureLayout.css +69 -4
- package/dist/FaceCaptureStrings.d.ts +23 -0
- package/dist/FaceMatchComparisonScreen.d.ts +3 -3
- package/dist/FaceMatchLayout.css +21 -12
- package/dist/FormScreen.css +30 -0
- package/dist/FormScreen.d.ts +28 -0
- package/dist/FormScreen.js +3 -2
- package/dist/FormsLongFormScreen.d.ts +8 -0
- package/dist/FormsLongFormScreen.js +34 -13
- package/dist/FormsStrings.d.ts +11 -1
- package/dist/GeolocationBrowserStepsScreen.d.ts +6 -2
- package/dist/GeolocationBrowserStepsScreen.js +7 -3
- package/dist/GeolocationStrings.d.ts +3 -0
- package/dist/IdCaptureLayout.css +205 -2
- package/dist/IdCaptureStageScreen.d.ts +47 -0
- package/dist/IdCaptureStageScreen.js +39 -3
- package/dist/IdCaptureStrings.d.ts +19 -0
- package/dist/ListScreen.d.ts +21 -0
- package/dist/ListScreen.js +7 -2
- package/dist/LoaderScreen.d.ts +7 -5
- package/dist/LoaderScreen.js +7 -5
- package/dist/LoadersStrings.d.ts +18 -5
- package/dist/OcrReviewErrorScreen.d.ts +30 -0
- package/dist/OcrReviewErrorScreen.js +24 -0
- package/dist/OcrReviewLayout.css +24 -0
- package/dist/OcrReviewLoadingScreen.d.ts +25 -0
- package/dist/OcrReviewLoadingScreen.js +19 -0
- package/dist/OcrReviewScreen.d.ts +76 -0
- package/dist/OcrReviewScreen.js +65 -0
- package/dist/OcrReviewStrings.d.ts +47 -0
- package/dist/OcrReviewStrings.js +17 -0
- package/dist/OcrReviewSuccessScreen.d.ts +30 -0
- package/dist/OcrReviewSuccessScreen.js +24 -0
- package/dist/PermissionsAllowScreen.d.ts +8 -11
- package/dist/PermissionsAllowScreen.js +8 -11
- package/dist/PermissionsAskScreen.d.ts +41 -0
- package/dist/PermissionsAskScreen.js +43 -0
- package/dist/PermissionsBrowserStepsScreen.d.ts +9 -2
- package/dist/PermissionsBrowserStepsScreen.js +12 -3
- package/dist/PermissionsLayout.css +47 -0
- package/dist/PermissionsStrings.d.ts +20 -9
- package/dist/PermissionsStrings.js +10 -4
- package/dist/PhoneInputLayout.css +10 -9
- package/dist/PhoneInputOtpScreen.d.ts +8 -0
- package/dist/PhoneInputOtpScreen.js +26 -19
- package/dist/PhoneInputStrings.d.ts +22 -4
- package/dist/PhoneInputVerifyScreen.d.ts +8 -0
- package/dist/PhoneInputVerifyScreen.js +36 -7
- package/dist/QualifiedSignatureAcceptScreen.js +6 -1
- package/dist/QualifiedSignatureDocumentScreen.d.ts +17 -10
- package/dist/QualifiedSignatureDocumentScreen.js +22 -17
- package/dist/QualifiedSignatureLayout.css +76 -0
- package/dist/QualifiedSignatureResultScreen.d.ts +16 -13
- package/dist/QualifiedSignatureResultScreen.js +17 -18
- package/dist/QualifiedSignatureStrings.d.ts +19 -2
- package/dist/ResultScreen.d.ts +8 -0
- package/dist/ResultScreen.js +14 -3
- package/dist/ReusabilityCredentialSelectionScreen.d.ts +95 -0
- package/dist/ReusabilityCredentialSelectionScreen.js +94 -0
- package/dist/ReusabilityLayout.css +241 -0
- package/dist/ReusabilityPrivacyPolicyScreen.d.ts +55 -0
- package/dist/ReusabilityPrivacyPolicyScreen.js +44 -0
- package/dist/ReusabilityStrings.d.ts +85 -0
- package/dist/ReusabilityStrings.js +37 -0
- package/dist/ReviewScreen.d.ts +38 -12
- package/dist/ReviewScreen.js +41 -8
- package/dist/ScreenLayout.css +382 -6
- package/dist/ScreenNav.css +8 -0
- package/dist/ScreenNav.d.ts +15 -2
- package/dist/ScreenNav.js +10 -3
- package/dist/VideoSelfieErrorScreen.js +1 -1
- package/dist/VideoSelfieIdCaptureScreen.d.ts +10 -3
- package/dist/VideoSelfieIdCaptureScreen.js +45 -14
- package/dist/VideoSelfieLayout.css +467 -34
- package/dist/VideoSelfieSelfieCaptureScreen.d.ts +44 -27
- package/dist/VideoSelfieSelfieCaptureScreen.js +65 -39
- package/dist/VideoSelfieSpeakScreen.js +19 -2
- package/dist/VideoSelfieStrings.d.ts +82 -13
- package/dist/VideoSelfieTurnHeadScreen.d.ts +51 -0
- package/dist/VideoSelfieTurnHeadScreen.js +66 -0
- package/dist/VideoSelfieTutorialScreen.d.ts +9 -0
- package/dist/VideoSelfieTutorialScreen.js +22 -5
- package/dist/VideoSelfieWaveScreen.d.ts +53 -0
- package/dist/VideoSelfieWaveScreen.js +62 -0
- package/dist/ViewfinderScreen.css +36 -0
- package/dist/WatchlistBusinessFormScreen.d.ts +11 -7
- package/dist/WatchlistBusinessFormScreen.js +27 -13
- package/dist/WatchlistBusinessStrings.d.ts +21 -7
- package/dist/flows.d.ts +8 -0
- package/dist/flows.js +17 -0
- package/dist/index.d.ts +19 -1
- package/dist/index.js +14 -0
- package/dist/styles.css +3 -0
- package/dist/types.d.ts +3 -1
- package/package.json +79 -3
- package/samples/video-selfie-camera-wide.jpg +0 -0
- package/samples/video-selfie-room-wide.jpg +0 -0
- package/samples/video-selfie-room.png +0 -0
- package/src/BiometricConsentScreen.tsx +192 -0
- package/src/BiometricConsentStrings.ts +119 -0
- package/src/CaptureScreen.tsx +17 -0
- package/src/CurpEnterScreen.tsx +21 -4
- package/src/CurpGenerateScreen.tsx +46 -22
- package/src/CurpLayout.css +8 -34
- package/src/CurpStrings.ts +22 -2
- package/src/DemoScreensLayout.css +16 -0
- package/src/DemoScreensRedirectScreen.tsx +14 -6
- package/src/DemoScreensStrings.ts +5 -0
- package/src/DigitalIdUploadErrorScreen.tsx +83 -0
- package/src/DigitalIdUploadFileTooLargeScreen.tsx +67 -0
- package/src/DigitalIdUploadReviewScreen.tsx +119 -0
- package/src/DigitalIdUploadStrings.ts +88 -0
- package/src/DigitalIdUploadTutorialScreen.tsx +59 -0
- package/src/DocumentCaptureIllustration.tsx +1 -1
- package/src/EkybAddressDetailsScreen.tsx +32 -4
- package/src/EkybBusinessDetailsScreen.tsx +23 -5
- package/src/EkybCountryScreen.tsx +45 -7
- package/src/EkybOwnersScreen.tsx +34 -3
- package/src/EkybStrings.ts +34 -10
- package/src/EkycCountryCatalog.ts +9 -4
- package/src/EkycCountryFormScreen.tsx +51 -12
- package/src/EkycDetailsScreen.tsx +22 -1
- package/src/EkycStrings.ts +14 -2
- package/src/ElectronicSignatureLayout.css +6 -0
- package/src/EmailInputStrings.ts +12 -1
- package/src/EmailInputVerifyScreen.tsx +21 -4
- package/src/ErrorScreensScreen.tsx +15 -4
- package/src/ErrorScreensStrings.ts +37 -12
- package/src/FaceCaptureCapturingScreen.tsx +102 -13
- package/src/FaceCaptureLayout.css +69 -4
- package/src/FaceCaptureStrings.ts +24 -0
- package/src/FaceMatchComparisonScreen.tsx +3 -3
- package/src/FaceMatchLayout.css +21 -12
- package/src/FormScreen.css +30 -0
- package/src/FormScreen.tsx +34 -0
- package/src/FormsLongFormScreen.tsx +41 -13
- package/src/FormsStrings.ts +12 -1
- package/src/GeolocationBrowserStepsScreen.tsx +6 -3
- package/src/GeolocationStrings.ts +3 -0
- package/src/IdCaptureLayout.css +205 -2
- package/src/IdCaptureStageScreen.tsx +75 -0
- package/src/IdCaptureStrings.ts +19 -0
- package/src/ListScreen.tsx +58 -10
- package/src/LoaderScreen.tsx +7 -5
- package/src/LoadersStrings.ts +18 -5
- package/src/OcrReviewErrorScreen.tsx +53 -0
- package/src/OcrReviewLayout.css +24 -0
- package/src/OcrReviewLoadingScreen.tsx +48 -0
- package/src/OcrReviewScreen.tsx +190 -0
- package/src/OcrReviewStrings.ts +69 -0
- package/src/OcrReviewSuccessScreen.tsx +52 -0
- package/src/PermissionsAllowScreen.tsx +9 -10
- package/src/PermissionsAskScreen.tsx +105 -0
- package/src/PermissionsBrowserStepsScreen.tsx +11 -3
- package/src/PermissionsLayout.css +47 -0
- package/src/PermissionsStrings.ts +42 -12
- package/src/PhoneInputLayout.css +10 -9
- package/src/PhoneInputOtpScreen.tsx +23 -3
- package/src/PhoneInputStrings.ts +23 -4
- package/src/PhoneInputVerifyScreen.tsx +41 -5
- package/src/QualifiedSignatureAcceptScreen.tsx +12 -1
- package/src/QualifiedSignatureDocumentScreen.tsx +31 -19
- package/src/QualifiedSignatureLayout.css +76 -0
- package/src/QualifiedSignatureResultScreen.tsx +28 -26
- package/src/QualifiedSignatureStrings.ts +19 -2
- package/src/ResultScreen.tsx +33 -2
- package/src/ReusabilityCredentialSelectionScreen.tsx +246 -0
- package/src/ReusabilityLayout.css +241 -0
- package/src/ReusabilityPrivacyPolicyScreen.tsx +109 -0
- package/src/ReusabilityStrings.ts +127 -0
- package/src/ReviewScreen.tsx +96 -27
- package/src/ScreenLayout.css +382 -6
- package/src/ScreenNav.css +8 -0
- package/src/ScreenNav.tsx +35 -5
- package/src/VideoSelfieErrorScreen.tsx +1 -1
- package/src/VideoSelfieIdCaptureScreen.tsx +125 -31
- package/src/VideoSelfieLayout.css +467 -34
- package/src/VideoSelfieSelfieCaptureScreen.tsx +110 -57
- package/src/VideoSelfieSpeakScreen.tsx +34 -15
- package/src/VideoSelfieStrings.ts +89 -14
- package/src/VideoSelfieTurnHeadScreen.tsx +162 -0
- package/src/VideoSelfieTutorialScreen.tsx +45 -23
- package/src/VideoSelfieWaveScreen.tsx +143 -0
- package/src/ViewfinderScreen.css +36 -0
- package/src/WatchlistBusinessFormScreen.tsx +35 -16
- package/src/WatchlistBusinessStrings.ts +21 -7
- package/src/flows.ts +25 -0
- package/src/index.ts +88 -3
- package/src/styles.css +3 -0
- package/src/types.ts +3 -1
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
// Verification", and no variant draws a close in the nav; steps after the
|
|
21
21
|
// first get the archetype's Back button instead of a back arrow (#EKC-2).
|
|
22
22
|
|
|
23
|
-
|
|
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'),
|
|
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(),
|
|
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'),
|
|
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
|
|
32
|
-
* Continue
|
|
33
|
-
*
|
|
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
|
|
56
|
-
* most frames draw): pressing it with empty fields is the
|
|
57
|
-
* Core's error ring and message under each empty
|
|
58
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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 :
|
|
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
|
package/src/EkycStrings.ts
CHANGED
|
@@ -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
|
|
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
|
package/src/EmailInputStrings.ts
CHANGED
|
@@ -27,13 +27,18 @@
|
|
|
27
27
|
* - `filled` — focused field with a complete address
|
|
28
28
|
* - `error` — negative field + clear ✕ + the invalid-format row
|
|
29
29
|
* - `loading` — field greyed while the code is requested, spinner in the CTA
|
|
30
|
+
* - `emptyError` — "Continue" pressed with the field still empty: the field
|
|
31
|
+
* stays empty (no placeholder) in Core's error state with `requiredError`,
|
|
32
|
+
* and the CTA is disabled. GUIDELINES §6-E empty-submit rule (Elena,
|
|
33
|
+
* 2026-09-30).
|
|
30
34
|
*/
|
|
31
35
|
export type EmailInputVerifyState =
|
|
32
36
|
| 'default'
|
|
33
37
|
| 'typing'
|
|
34
38
|
| 'filled'
|
|
35
39
|
| 'error'
|
|
36
|
-
| 'loading'
|
|
40
|
+
| 'loading'
|
|
41
|
+
| 'emptyError';
|
|
37
42
|
|
|
38
43
|
/**
|
|
39
44
|
* The `email.OTP.*` sections — same layout, different code-field state:
|
|
@@ -58,6 +63,12 @@ export interface EmailInputStrings {
|
|
|
58
63
|
verifiedLabel: string;
|
|
59
64
|
/** Accessible name for the logo nav row. */
|
|
60
65
|
navLabel?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Error row under the required field when the primary CTA is pressed with it
|
|
68
|
+
* still empty (the verify screen's `emptyError` state): "This field is
|
|
69
|
+
* required". SDK key `dynamicForms.fieldRequired`.
|
|
70
|
+
*/
|
|
71
|
+
requiredError: string;
|
|
61
72
|
|
|
62
73
|
/** email.verifyEmail.* */
|
|
63
74
|
verify: {
|
|
@@ -48,6 +48,13 @@ export interface EmailInputVerifyScreenProps
|
|
|
48
48
|
* paints the DISABLED surface (owner definition 2026-07-12), which would grey
|
|
49
49
|
* the four resting states Figma draws white. The field is uncontrolled instead
|
|
50
50
|
* (DEVIATIONS #PI-6).
|
|
51
|
+
*
|
|
52
|
+
* `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
|
|
53
|
+
* GUIDELINES §6-E empty-submit rule): "Continue" was pressed with nothing
|
|
54
|
+
* typed. The field renders empty with no placeholder, in Core's error state
|
|
55
|
+
* with `requiredError`, and the primary CTA is disabled, following the drawn
|
|
56
|
+
* precedent in `FormsFieldScreen`. The resting `default` keeps the CTA enabled
|
|
57
|
+
* so the user can reach this state.
|
|
51
58
|
*/
|
|
52
59
|
export const EmailInputVerifyScreen = forwardRef<HTMLDivElement, EmailInputVerifyScreenProps>(
|
|
53
60
|
(
|
|
@@ -64,6 +71,9 @@ export const EmailInputVerifyScreen = forwardRef<HTMLDivElement, EmailInputVerif
|
|
|
64
71
|
) => {
|
|
65
72
|
const busy = state === 'loading';
|
|
66
73
|
const invalid = state === 'error';
|
|
74
|
+
// Empty submit: the field drops its placeholder and takes Core's error
|
|
75
|
+
// state with the shared "required" row; the CTA greys.
|
|
76
|
+
const emptyError = state === 'emptyError';
|
|
67
77
|
const focused = state === 'typing' || state === 'filled';
|
|
68
78
|
|
|
69
79
|
return (
|
|
@@ -74,6 +84,7 @@ export const EmailInputVerifyScreen = forwardRef<HTMLDivElement, EmailInputVerif
|
|
|
74
84
|
ctaLabel={strings.verify.ctaLabel}
|
|
75
85
|
/* Figma's `loading` section swaps the CTA label for Core's spinner. */
|
|
76
86
|
ctaLoading={busy}
|
|
87
|
+
ctaDisabled={emptyError}
|
|
77
88
|
ctaLoadingLabel={strings.verify.ctaLoadingLabel}
|
|
78
89
|
verifiedLabel={strings.verifiedLabel}
|
|
79
90
|
navLabel={strings.navLabel}
|
|
@@ -97,13 +108,19 @@ export const EmailInputVerifyScreen = forwardRef<HTMLDivElement, EmailInputVerif
|
|
|
97
108
|
/* Figma's field has no visible label row — the placeholder carries
|
|
98
109
|
the meaning — so the copy rides `aria-label` (DEVIATIONS #PI-4). */
|
|
99
110
|
aria-label={strings.verify.fieldLabel}
|
|
100
|
-
placeholder={strings.verify.placeholder}
|
|
111
|
+
placeholder={emptyError ? undefined : strings.verify.placeholder}
|
|
101
112
|
/* Uncontrolled: a screen is one static spec state, so no `onChange`
|
|
102
113
|
is needed and no warning is raised. */
|
|
103
|
-
defaultValue={value ?? ''}
|
|
114
|
+
defaultValue={emptyError ? '' : value ?? ''}
|
|
104
115
|
disabled={busy}
|
|
105
|
-
error={invalid}
|
|
106
|
-
errorText={
|
|
116
|
+
error={invalid || emptyError}
|
|
117
|
+
errorText={
|
|
118
|
+
invalid
|
|
119
|
+
? strings.verify.errorText
|
|
120
|
+
: emptyError
|
|
121
|
+
? strings.requiredError
|
|
122
|
+
: undefined
|
|
123
|
+
}
|
|
107
124
|
/* The clear ✕ is drawn on the `error` section only. */
|
|
108
125
|
clearable={invalid}
|
|
109
126
|
clearLabel={strings.verify.clearLabel}
|
|
@@ -38,7 +38,7 @@ export interface ErrorScreensScreenProps
|
|
|
38
38
|
* Error screens — the session-level failures, built on the Result archetype
|
|
39
39
|
* (GUIDELINES.md §6-F, `PrizmaResultScreen`).
|
|
40
40
|
*
|
|
41
|
-
* ONE screen for all
|
|
41
|
+
* ONE screen for all four generic sections. They draw the identical skeleton —
|
|
42
42
|
* a centred mark + title + supporting line over a CTA band — and differ only in
|
|
43
43
|
* copy, in which mark leads, and in what the band carries:
|
|
44
44
|
*
|
|
@@ -46,8 +46,11 @@ export interface ErrorScreensScreenProps
|
|
|
46
46
|
* - `noConnection` — Wi-Fi glyph, "Refresh"
|
|
47
47
|
* - `unsupportedBrowser` — canonical negative mark, the session URL above
|
|
48
48
|
* "Copy link", with "Continue anyways" under it
|
|
49
|
+
* - `somethingWentWrong` — canonical negative mark, "Try again" when the copy
|
|
50
|
+
* carries a CTA, "Quit process" under it when it
|
|
51
|
+
* carries a quit label; a dead end when it has neither
|
|
49
52
|
*
|
|
50
|
-
* Chrome is CONSTANT. All
|
|
53
|
+
* Chrome is CONSTANT. All the frames are drawn without the logo header and
|
|
51
54
|
* without the verified tag; both are kept here, because the owner rule of
|
|
52
55
|
* 2026-08-18 (#F-30, #F-34) makes the chrome the same on every screen so
|
|
53
56
|
* nothing shifts between states, and every module's connectivity screen must
|
|
@@ -91,11 +94,19 @@ export const ErrorScreensScreen = forwardRef<HTMLDivElement, ErrorScreensScreenP
|
|
|
91
94
|
) => {
|
|
92
95
|
const unsupported = variant === 'unsupportedBrowser';
|
|
93
96
|
const noConnection = variant === 'noConnection';
|
|
97
|
+
const generic = variant === 'somethingWentWrong';
|
|
94
98
|
const copy = unsupported
|
|
95
99
|
? strings.unsupportedBrowser
|
|
96
100
|
: noConnection
|
|
97
101
|
? strings.noConnection
|
|
98
|
-
:
|
|
102
|
+
: generic
|
|
103
|
+
? strings.somethingWentWrong
|
|
104
|
+
: strings.sessionExpired;
|
|
105
|
+
const linkLabel = unsupported
|
|
106
|
+
? strings.unsupportedBrowser.continueLabel
|
|
107
|
+
: generic
|
|
108
|
+
? strings.somethingWentWrong.quitLabel
|
|
109
|
+
: undefined;
|
|
99
110
|
|
|
100
111
|
return (
|
|
101
112
|
<PrizmaResultScreen
|
|
@@ -133,7 +144,7 @@ export const ErrorScreensScreen = forwardRef<HTMLDivElement, ErrorScreensScreenP
|
|
|
133
144
|
: undefined
|
|
134
145
|
}
|
|
135
146
|
ctaLabel={copy.ctaLabel}
|
|
136
|
-
linkLabel={
|
|
147
|
+
linkLabel={linkLabel}
|
|
137
148
|
verifiedLabel={strings.verifiedLabel}
|
|
138
149
|
navLabel={strings.navLabel}
|
|
139
150
|
screenId={screenId}
|
|
@@ -6,19 +6,27 @@
|
|
|
6
6
|
// 214:3357 Geolocation.error.noNetwork → `noConnection`
|
|
7
7
|
// 240:154 Error screen - Unsupported browser → `unsupportedBrowser`
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// plus the one failure every module draws for itself and the file never did:
|
|
10
|
+
//
|
|
11
|
+
// — <module>.error "Something went wrong" → `somethingWentWrong`
|
|
12
|
+
//
|
|
13
|
+
// The failures that belong to the SESSION rather than to any module: the link
|
|
14
|
+
// has expired, the network went away, the browser cannot run the SDK, the
|
|
15
|
+
// server said no for a reason the user cannot act on. Every module can hit all
|
|
16
|
+
// four, which is why they are recorded once in the generic family — a user who
|
|
17
|
+
// sees three different "no internet" screens in one journey reads them as three
|
|
18
|
+
// different products.
|
|
14
19
|
//
|
|
15
20
|
// `noConnection` is deliberately the same screen every module already ships
|
|
16
21
|
// (`FaceCaptureNoConnectionScreen`, `IdCaptureNoConnectionScreen`,
|
|
17
|
-
// `DocumentCaptureNoConnectionScreen`, `GeolocationErrorScreen`'s `noNetwork
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
+
// `DocumentCaptureNoConnectionScreen`, `GeolocationErrorScreen`'s `noNetwork`,
|
|
23
|
+
// Phone Input's and Video Selfie's `noConnection`). `somethingWentWrong` is the
|
|
24
|
+
// same screen eKYC, eKYB, Forms, Email Input, Phone Input, Certificate
|
|
25
|
+
// Issuance, both signatures and Video Selfie already ship. The generic entry is
|
|
26
|
+
// where the shape and the copy are DOCUMENTED; the module copies stay put until
|
|
27
|
+
// someone retires them deliberately, because deleting exported components to
|
|
28
|
+
// save one screen is a breaking change for every host that imports them.
|
|
29
|
+
// Flagged for the owner (DEVIATIONS #GEN-7, #GEN-18).
|
|
22
30
|
//
|
|
23
31
|
// The family has no flow of its own: each of these is entered from wherever the
|
|
24
32
|
// session broke and exits to a retry or to nothing. Core Lab lists it as a
|
|
@@ -27,15 +35,17 @@
|
|
|
27
35
|
// No hardcoded English anywhere in a screen: every visible string arrives here.
|
|
28
36
|
|
|
29
37
|
/**
|
|
30
|
-
* The
|
|
38
|
+
* The four sections.
|
|
31
39
|
* - `sessionExpired` — the link is dead; the user must go back to the provider
|
|
32
40
|
* - `noConnection` — the network went away; retry when it returns
|
|
33
41
|
* - `unsupportedBrowser` — the SDK cannot run here; copy the link and move
|
|
42
|
+
* - `somethingWentWrong` — the generic failure; try again, or nothing, or quit
|
|
34
43
|
*/
|
|
35
44
|
export type ErrorScreensVariant =
|
|
36
45
|
| 'sessionExpired'
|
|
37
46
|
| 'noConnection'
|
|
38
|
-
| 'unsupportedBrowser'
|
|
47
|
+
| 'unsupportedBrowser'
|
|
48
|
+
| 'somethingWentWrong';
|
|
39
49
|
|
|
40
50
|
export interface ErrorScreensCopy {
|
|
41
51
|
title: string;
|
|
@@ -63,4 +73,19 @@ export interface ErrorScreensStrings {
|
|
|
63
73
|
/** Snackbar copy after the link is copied — the frame's second state. */
|
|
64
74
|
copiedLabel: string;
|
|
65
75
|
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The generic failure. Three ways the modules fill it, all the same screen:
|
|
79
|
+
* `ctaLabel` "Try again" (eKYC, eKYB, Forms, Email Input);
|
|
80
|
+
* no `ctaLabel`, a dead end (Certificate Issuance, Qualified Signature);
|
|
81
|
+
* `ctaLabel` plus `quitLabel`, the way out under the retry (Video Selfie).
|
|
82
|
+
* Phone Input's `unknown` fills it with "Continue" instead, because a retry
|
|
83
|
+
* may not be possible there (owner, 2026-10-09) — the CTA is the module's
|
|
84
|
+
* call; the shape and the title are what the family fixes.
|
|
85
|
+
* The title never carries an ellipsis — a failure is not in progress.
|
|
86
|
+
*/
|
|
87
|
+
somethingWentWrong: ErrorScreensCopy & {
|
|
88
|
+
/** Tertiary link under the CTA — "Quit process". Omit for no way out. */
|
|
89
|
+
quitLabel?: string;
|
|
90
|
+
};
|
|
66
91
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { forwardRef, type ReactNode } from 'react';
|
|
2
2
|
import type { PrizmaSelfieCaptureState } from '@incodetech/prizma-core-web/selfie-capture';
|
|
3
|
+
import { PrizmaSnackbar } from '@incodetech/prizma-core-web/snackbar';
|
|
3
4
|
import { PrizmaCaptureScreen, type PrizmaCaptureScreenProps } from './CaptureScreen';
|
|
4
5
|
import type {
|
|
5
6
|
FaceCaptureCapturingState,
|
|
@@ -46,6 +47,19 @@ export interface FaceCaptureCapturingScreenProps
|
|
|
46
47
|
state?: FaceCaptureCapturingState;
|
|
47
48
|
/** Live camera feed (<video>) or sample still (<img>) for the ring window. */
|
|
48
49
|
camera?: ReactNode;
|
|
50
|
+
/**
|
|
51
|
+
* Raise the "Need more time?" offer over the feed — the accessibility
|
|
52
|
+
* notification shown 20 s before auto capture falls back to manual.
|
|
53
|
+
*
|
|
54
|
+
* It is a MODIFIER of whatever capturing state is already on screen, not a
|
|
55
|
+
* sixteenth state: the timer runs underneath the guidance, so the user can be
|
|
56
|
+
* told to move closer and offered more time in the same frame. Needs
|
|
57
|
+
* `strings.extendTime`; without it the flag is a no-op, which is what a host
|
|
58
|
+
* that has not localized the copy should get rather than English.
|
|
59
|
+
*/
|
|
60
|
+
extendTime?: boolean;
|
|
61
|
+
/** Tapping "Extend auto-capture". The host resets its own fallback timer. */
|
|
62
|
+
onExtendTime?: () => void;
|
|
49
63
|
}
|
|
50
64
|
|
|
51
65
|
/**
|
|
@@ -65,6 +79,56 @@ export interface FaceCaptureCapturingScreenProps
|
|
|
65
79
|
*
|
|
66
80
|
* The archetype adds no offsets of its own — the component owns that spacing
|
|
67
81
|
* (same deviation row).
|
|
82
|
+
*
|
|
83
|
+
* ── The "Need more time?" offer (`extendTime`) ──────────────────────────────
|
|
84
|
+
*
|
|
85
|
+
* Auto capture gives up after a configured timeout and drops the user into
|
|
86
|
+
* manual capture. Someone who is struggling — a tremor, low vision, bad light,
|
|
87
|
+
* an unfamiliar phone — gets moved off the EASIER path precisely because they
|
|
88
|
+
* were slow, which is backwards. So 20 s before the switch, the screen offers
|
|
89
|
+
* to stay on auto. A client turns it on per flow from the dashboard; with it
|
|
90
|
+
* off, nothing about today's capture changes.
|
|
91
|
+
*
|
|
92
|
+
* NOT a nicety — it is the conformance route for WCAG 2.2.1 Timing Adjustable
|
|
93
|
+
* via the *Extend* clause (warn before the limit, ≥20 s to act, ≥10 extensions).
|
|
94
|
+
* Two things follow that the visuals cannot settle: the notice must be able to
|
|
95
|
+
* come back ten times, and it must be ANNOUNCED early enough to be acted on.
|
|
96
|
+
*
|
|
97
|
+
* ── Live-region politeness: `role="alert"`, not the default ────────────────
|
|
98
|
+
*
|
|
99
|
+
* SETTLED 2026-10-05 (Camila). The Snackbar computes `role="status"` (polite)
|
|
100
|
+
* for every state but error, and this instance overrides it to `alert`.
|
|
101
|
+
*
|
|
102
|
+
* A polite region is allowed to wait until the screen reader finishes whatever
|
|
103
|
+
* it is already saying — and on this screen it always IS saying something: the
|
|
104
|
+
* capture instruction under the ring changes as the face moves, so the queue
|
|
105
|
+
* is rarely empty. The offer has a 20 s life. Queued behind two or three
|
|
106
|
+
* guidance announcements it can be read out after it has already expired,
|
|
107
|
+
* which turns the one affordance a struggling user needs into an
|
|
108
|
+
* announcement that the limit has passed.
|
|
109
|
+
*
|
|
110
|
+
* That is not a polish issue, it is the conformance failing: WCAG 2.2.1's
|
|
111
|
+
* Extend clause requires the warning to be given in time to ACT on it. An
|
|
112
|
+
* announcement the user cannot act on does not satisfy it, however correct the
|
|
113
|
+
* visuals. `alert` is assertive, so it interrupts the guidance and is heard
|
|
114
|
+
* while the 20 s is still running.
|
|
115
|
+
*
|
|
116
|
+
* The usual objection to `alert` is that interrupting is rude. It applies to
|
|
117
|
+
* notices that can wait; this one cannot, and it is strictly better to
|
|
118
|
+
* interrupt "Move closer" than to let the window close in silence. It is also
|
|
119
|
+
* why the offer must not be re-raised on a timer — each new `alert` interrupts
|
|
120
|
+
* again. It appears once per countdown, and WCAG allows that up to ten times.
|
|
121
|
+
*
|
|
122
|
+
* ⚠ NEW, not yet authored in Core. Two parts of this notice have no Figma
|
|
123
|
+
* variant behind them and are drawn from the exploration, not from the SoT:
|
|
124
|
+
* • the bold title line — the Snackbar has no `title` slot, so it rides
|
|
125
|
+
* inside `children` as a <strong>. Core to decide whether to adopt a real
|
|
126
|
+
* slot, at which point this moves onto it.
|
|
127
|
+
* • the state — it is `neutral` (blue surface, ⓘ glyph), Camila's call
|
|
128
|
+
* 2026-10-05. `warning` would signal attention more sharply but turns the
|
|
129
|
+
* surface amber, and the exploration's open question is whether amber
|
|
130
|
+
* reads as "hurry up" to someone who was already struggling. The icon
|
|
131
|
+
* cannot be chosen independently: `ICON_BY_STATE` binds it to `state`.
|
|
68
132
|
*/
|
|
69
133
|
export const FaceCaptureCapturingScreen = forwardRef<
|
|
70
134
|
HTMLDivElement,
|
|
@@ -75,24 +139,49 @@ export const FaceCaptureCapturingScreen = forwardRef<
|
|
|
75
139
|
strings,
|
|
76
140
|
state = 'initiating',
|
|
77
141
|
camera,
|
|
142
|
+
extendTime,
|
|
143
|
+
onExtendTime,
|
|
78
144
|
screenId = 'face-capture-capturing',
|
|
79
145
|
className,
|
|
80
146
|
...rest
|
|
81
147
|
},
|
|
82
148
|
ref,
|
|
83
|
-
) =>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
149
|
+
) => {
|
|
150
|
+
const offer = strings.extendTime;
|
|
151
|
+
// No copy, no notice: a host that has not localized this must not get
|
|
152
|
+
// English, and the flag alone is not permission to invent it.
|
|
153
|
+
const notice =
|
|
154
|
+
extendTime && offer ? (
|
|
155
|
+
<PrizmaSnackbar
|
|
156
|
+
state="neutral"
|
|
157
|
+
isMultiline
|
|
158
|
+
// Assertive on purpose — see the live-region note in this file's
|
|
159
|
+
// header. The component default here would be `status`.
|
|
160
|
+
role="alert"
|
|
161
|
+
linkLabel={offer.linkLabel}
|
|
162
|
+
onLinkClick={onExtendTime}
|
|
163
|
+
>
|
|
164
|
+
<strong>{offer.title}</strong>
|
|
165
|
+
<br />
|
|
166
|
+
{offer.description}
|
|
167
|
+
</PrizmaSnackbar>
|
|
168
|
+
) : undefined;
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<PrizmaCaptureScreen
|
|
172
|
+
ref={ref}
|
|
173
|
+
className={className}
|
|
174
|
+
state={SELFIE_STATE[state]}
|
|
175
|
+
camera={camera}
|
|
176
|
+
feedbackText={strings.capturing[state]}
|
|
177
|
+
notice={notice}
|
|
178
|
+
verifiedLabel={strings.verifiedLabel}
|
|
179
|
+
navLabel={strings.navLabel}
|
|
180
|
+
screenId={screenId}
|
|
181
|
+
{...rest}
|
|
182
|
+
/>
|
|
183
|
+
);
|
|
184
|
+
},
|
|
96
185
|
);
|
|
97
186
|
FaceCaptureCapturingScreen.displayName = 'FaceCaptureCapturingScreen';
|
|
98
187
|
|