@incodetech/prizma-core-screens 0.2.3 → 0.2.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +27 -9
- package/dist/EkybOwnersScreen.d.ts +8 -0
- package/dist/EkybOwnersScreen.js +26 -3
- package/dist/EkybStrings.d.ts +27 -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/FaceMatchComparisonScreen.d.ts +3 -3
- package/dist/FaceMatchLayout.css +21 -12
- package/dist/FormScreen.css +16 -0
- 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 +40 -0
- package/dist/ListScreen.d.ts +2 -0
- package/dist/ListScreen.js +1 -1
- package/dist/PhoneInputLayout.css +10 -9
- package/dist/PhoneInputStrings.d.ts +11 -1
- package/dist/PhoneInputVerifyScreen.d.ts +8 -0
- package/dist/PhoneInputVerifyScreen.js +19 -6
- package/dist/QualifiedSignatureAcceptScreen.js +6 -1
- package/dist/QualifiedSignatureDocumentScreen.d.ts +13 -9
- package/dist/QualifiedSignatureDocumentScreen.js +18 -16
- package/dist/QualifiedSignatureLayout.css +76 -0
- package/dist/QualifiedSignatureStrings.d.ts +10 -0
- package/dist/ScreenNav.css +8 -0
- package/dist/ScreenNav.d.ts +15 -2
- package/dist/ScreenNav.js +10 -3
- package/dist/VideoSelfieIdCaptureScreen.d.ts +10 -3
- package/dist/VideoSelfieIdCaptureScreen.js +44 -13
- package/dist/VideoSelfieLayout.css +164 -33
- package/dist/VideoSelfieSelfieCaptureScreen.d.ts +44 -27
- package/dist/VideoSelfieSelfieCaptureScreen.js +65 -39
- package/dist/VideoSelfieSpeakScreen.js +18 -1
- package/dist/VideoSelfieStrings.d.ts +78 -12
- package/dist/VideoSelfieTurnHeadScreen.d.ts +51 -0
- package/dist/VideoSelfieTurnHeadScreen.js +63 -0
- package/dist/VideoSelfieTutorialScreen.js +13 -5
- package/dist/VideoSelfieWaveScreen.d.ts +53 -0
- package/dist/VideoSelfieWaveScreen.js +62 -0
- package/dist/WatchlistBusinessFormScreen.d.ts +11 -7
- package/dist/WatchlistBusinessFormScreen.js +27 -13
- package/dist/WatchlistBusinessStrings.d.ts +21 -7
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/types.d.ts +3 -1
- package/package.json +14 -3
- package/samples/video-selfie-room.png +0 -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/EkybAddressDetailsScreen.tsx +32 -4
- package/src/EkybBusinessDetailsScreen.tsx +23 -5
- package/src/EkybCountryScreen.tsx +28 -7
- package/src/EkybOwnersScreen.tsx +34 -3
- package/src/EkybStrings.ts +29 -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/FaceMatchComparisonScreen.tsx +3 -3
- package/src/FaceMatchLayout.css +21 -12
- package/src/FormScreen.css +16 -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 +40 -0
- package/src/ListScreen.tsx +10 -1
- package/src/PhoneInputLayout.css +10 -9
- package/src/PhoneInputStrings.ts +12 -1
- package/src/PhoneInputVerifyScreen.tsx +21 -5
- package/src/QualifiedSignatureAcceptScreen.tsx +12 -1
- package/src/QualifiedSignatureDocumentScreen.tsx +27 -17
- package/src/QualifiedSignatureLayout.css +76 -0
- package/src/QualifiedSignatureStrings.ts +10 -0
- package/src/ScreenNav.css +8 -0
- package/src/ScreenNav.tsx +35 -5
- package/src/VideoSelfieIdCaptureScreen.tsx +116 -30
- package/src/VideoSelfieLayout.css +164 -33
- package/src/VideoSelfieSelfieCaptureScreen.tsx +104 -59
- package/src/VideoSelfieSpeakScreen.tsx +25 -14
- package/src/VideoSelfieStrings.ts +85 -12
- package/src/VideoSelfieTurnHeadScreen.tsx +151 -0
- package/src/VideoSelfieTutorialScreen.tsx +34 -23
- package/src/VideoSelfieWaveScreen.tsx +135 -0
- package/src/WatchlistBusinessFormScreen.tsx +35 -16
- package/src/WatchlistBusinessStrings.ts +21 -7
- package/src/index.ts +14 -0
- package/src/types.ts +3 -1
|
@@ -38,7 +38,10 @@ export type VideoSelfieTutorialRowKind =
|
|
|
38
38
|
| 'questions'
|
|
39
39
|
| 'speak';
|
|
40
40
|
|
|
41
|
-
/** The
|
|
41
|
+
/** The live-feedback messages of group 9, in Figma order. Fourteen since
|
|
42
|
+
* 2026-09-30: `9.12`–`9.14` (head cover · closed eyes · face occlusion) were
|
|
43
|
+
* added to the frames after the module's first build and are on the Web, iOS
|
|
44
|
+
* and Android full-flow maps. */
|
|
42
45
|
export type VideoSelfieFeedbackKind =
|
|
43
46
|
| 'dark'
|
|
44
47
|
| 'blurry'
|
|
@@ -50,19 +53,67 @@ export type VideoSelfieFeedbackKind =
|
|
|
50
53
|
| 'turnLeft'
|
|
51
54
|
| 'alignFace'
|
|
52
55
|
| 'moveCloser'
|
|
53
|
-
| 'moveAway'
|
|
56
|
+
| 'moveAway'
|
|
57
|
+
| 'headCover'
|
|
58
|
+
| 'closedEyes'
|
|
59
|
+
| 'faceOcclusion';
|
|
60
|
+
|
|
61
|
+
/** Which head-turn the liveness step is asking for (the `🙂↔️ Turn Head` band
|
|
62
|
+
* of the full-flow frames: Left · Right · Upward · Downward, each drawn as a
|
|
63
|
+
* guide beat and a captured beat). */
|
|
64
|
+
export type VideoSelfieTurnDirection = 'left' | 'right' | 'up' | 'down';
|
|
54
65
|
|
|
55
66
|
/** Which face of the document the ID stage is on (groups 3 and 5). */
|
|
56
67
|
export type VideoSelfieIdSide = 'front' | 'back';
|
|
57
68
|
|
|
58
|
-
/** Why an ID read was rejected (group 6, the `.1`–`.4` variants). */
|
|
59
69
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
70
|
+
* Why an ID read was rejected — the `.1`–`.4` suffix of every group 6 section.
|
|
71
|
+
*
|
|
72
|
+
* RESTORED 2026-09-30. It was removed on 2026-08-19 on the reading that "group
|
|
73
|
+
* 6 draws exactly four sections", because the `📐 Screens specs & behavior`
|
|
74
|
+
* page documents ONE representative per group (`6.1.1`, `6.2.1`, `6.3.1`,
|
|
75
|
+
* `6.4.1`) and the rest are component variants inside it. The `🔸 Full flow
|
|
76
|
+
* map` draws them all, and they carry DIFFERENT copy, not a translation of
|
|
77
|
+
* one line: `6.2.2 - name failed` reads "Name verification failed",
|
|
78
|
+
* `6.2.3 - photo failed` reads "ID photo verification failed",
|
|
79
|
+
* `6.1.4 - type mismatch try again` reads "ID type mismatch, show the same ID".
|
|
80
|
+
* Collapsing them to one line lost three rejections a user can actually hit.
|
|
81
|
+
* DEVIATIONS #VS-1.
|
|
65
82
|
*/
|
|
83
|
+
export type VideoSelfieIdErrorReason = 'scan' | 'name' | 'photo' | 'typeMismatch';
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* A live CONDITION the document stage can coach the user out of, as opposed
|
|
87
|
+
* to a rejection it has to report.
|
|
88
|
+
*
|
|
89
|
+
* BOTH legs run five of these before the identity checks — the frames draw
|
|
90
|
+
* them on the back only, and the owner extended them to the front on
|
|
91
|
+
* 2026-09-30 ("I think it would be nice to add them for the front as well").
|
|
92
|
+
* Each one draws its OWN screen: the frame in its
|
|
93
|
+
* matching red treatment with a `Capture feedback` bubble above it saying
|
|
94
|
+
* what to do. They were all wired to the single "ID scan failed, try again"
|
|
95
|
+
* card until 2026-09-30 — the five screens are `frame` nodes rather than
|
|
96
|
+
* component instances in the flow map, so the first trace walked straight
|
|
97
|
+
* past them (owner: "we do have those states in figma").
|
|
98
|
+
*
|
|
99
|
+
* All five map 1:1 onto states Core already ships: four onto
|
|
100
|
+
* `PrizmaIdCapture`'s own `error-*` treatments, and `blurry` onto the plain
|
|
101
|
+
* error border plus the bubble, which is the recipe the owner set for
|
|
102
|
+
* detection results on 2026-07-28 (the border is the state, the message is
|
|
103
|
+
* the bubble).
|
|
104
|
+
*/
|
|
105
|
+
/** One coaching bubble's copy: what is wrong, and what to do about it. */
|
|
106
|
+
export interface VideoSelfieConditionCopy {
|
|
107
|
+
title: string;
|
|
108
|
+
subtitle: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type VideoSelfieIdCondition =
|
|
112
|
+
| 'wrongSide'
|
|
113
|
+
| 'notAligned'
|
|
114
|
+
| 'blurry'
|
|
115
|
+
| 'glare'
|
|
116
|
+
| 'tooDark';
|
|
66
117
|
|
|
67
118
|
/** Whether the rejection invites a retry (`6.1` / `6.3`) or is terminal
|
|
68
119
|
* (`6.2` / `6.4`). */
|
|
@@ -172,9 +223,26 @@ export interface VideoSelfieStrings {
|
|
|
172
223
|
processingLabel: string;
|
|
173
224
|
/** Accepted label (`3.3` / `5.3`). */
|
|
174
225
|
successLabel: string;
|
|
175
|
-
/** Rejection copy, keyed reason → severity (group 6).
|
|
176
|
-
|
|
177
|
-
|
|
226
|
+
/** Rejection copy, keyed reason → severity (group 6). The two axes are
|
|
227
|
+
* independent: `6.1.x` / `6.3.x` are the try-again lines, `6.2.x` /
|
|
228
|
+
* `6.4.x` the terminal ones, and the `.1`–`.4` suffix is the reason. The
|
|
229
|
+
* two sides share the copy — only the stepper beat differs. */
|
|
230
|
+
errors: Record<VideoSelfieIdErrorReason, Record<VideoSelfieIdErrorSeverity, string>>;
|
|
231
|
+
/**
|
|
232
|
+
* The five coaching bubbles — title + what to do about it.
|
|
233
|
+
*
|
|
234
|
+
* Only `wrongSide` is keyed by side, because it is the only one that
|
|
235
|
+
* NAMES the face the user should be showing; the other four describe the
|
|
236
|
+
* shot, which is the same problem whichever face is up. Keying all five by
|
|
237
|
+
* side would duplicate four strings per language for nothing.
|
|
238
|
+
*/
|
|
239
|
+
conditions: {
|
|
240
|
+
wrongSide: Record<VideoSelfieIdSide, VideoSelfieConditionCopy>;
|
|
241
|
+
notAligned: VideoSelfieConditionCopy;
|
|
242
|
+
blurry: VideoSelfieConditionCopy;
|
|
243
|
+
glare: VideoSelfieConditionCopy;
|
|
244
|
+
tooDark: VideoSelfieConditionCopy;
|
|
245
|
+
};
|
|
178
246
|
};
|
|
179
247
|
|
|
180
248
|
/** Group 4 — the flip interstitial. */
|
|
@@ -193,8 +261,13 @@ export interface VideoSelfieStrings {
|
|
|
193
261
|
successLabel: string;
|
|
194
262
|
/** `8.3` rejected. */
|
|
195
263
|
failedLabel: string;
|
|
196
|
-
/** Group 9 — the
|
|
264
|
+
/** Group 9 — the fourteen live-feedback lines. */
|
|
197
265
|
feedback: Record<VideoSelfieFeedbackKind, string>;
|
|
266
|
+
/** The head-turn step's own instruction, one per direction (the
|
|
267
|
+
* `🙂↔️ Turn Head` band of the full-flow frames). */
|
|
268
|
+
turn: Record<VideoSelfieTurnDirection, string>;
|
|
269
|
+
/** The wave step's instruction and its three checkpoints (`👋 Wave`). */
|
|
270
|
+
wave: { instruction: string; steps: string[]; doneLabel: string };
|
|
198
271
|
};
|
|
199
272
|
|
|
200
273
|
/** Groups 10 · 11 — the spoken steps. */
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import React, { forwardRef, type ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
IconArrowDownCircle,
|
|
4
|
+
IconArrowLeftCircle,
|
|
5
|
+
IconArrowRightCircle,
|
|
6
|
+
IconArrowUpCircle,
|
|
7
|
+
} from '@incodetech/prizma-core-icons';
|
|
8
|
+
import { PrizmaSelfieCapture } from '@incodetech/prizma-core-web/selfie-capture';
|
|
9
|
+
import { PrizmaStepper } from '@incodetech/prizma-core-web/stepper';
|
|
10
|
+
import { PrizmaScreenShell, type PrizmaScreenShellProps } from './ScreenShell';
|
|
11
|
+
import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
|
|
12
|
+
import type {
|
|
13
|
+
VideoSelfieDocumentType,
|
|
14
|
+
VideoSelfieStrings,
|
|
15
|
+
VideoSelfieTurnDirection,
|
|
16
|
+
} from './VideoSelfieStrings';
|
|
17
|
+
import './ScreenLayout.css';
|
|
18
|
+
import './ViewfinderScreen.css';
|
|
19
|
+
import './VideoSelfieLayout.css';
|
|
20
|
+
|
|
21
|
+
/** The two beats each direction is drawn in: the stage ASKING (dashed guide
|
|
22
|
+
* ring) and the stage having SEEN it (the positive ring). */
|
|
23
|
+
export type VideoSelfieTurnStage = 'ask' | 'done';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Direction → the mark under the ring, from the canonical bank at 2xl (48) —
|
|
27
|
+
* the size the frames draw it (`element container` 342×48, the glyph centred).
|
|
28
|
+
*
|
|
29
|
+
* It is ONE glyph, a filled disc with the arrow knocked out of it, which is
|
|
30
|
+
* what `Left Circle - Iconly Pro` is. The first pass paired a 24px bare arrow
|
|
31
|
+
* with a hand-made disc behind it and read as a thin chevron floating on the
|
|
32
|
+
* camera (owner, 2026-09-30). The four circle arrows were extracted into the
|
|
33
|
+
* bank on the same day. Nothing is drawn here (§10).
|
|
34
|
+
*/
|
|
35
|
+
const TURN_ICON: Record<VideoSelfieTurnDirection, ReactNode> = {
|
|
36
|
+
left: <IconArrowLeftCircle size="2xl" />,
|
|
37
|
+
right: <IconArrowRightCircle size="2xl" />,
|
|
38
|
+
up: <IconArrowUpCircle size="2xl" />,
|
|
39
|
+
down: <IconArrowDownCircle size="2xl" />,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export interface VideoSelfieTurnHeadScreenProps
|
|
43
|
+
extends Omit<PrizmaScreenShellProps, 'dark' | 'title' | 'children'> {
|
|
44
|
+
/** Module copy — this screen reads `selfieCapture.turn`. */
|
|
45
|
+
strings: VideoSelfieStrings;
|
|
46
|
+
/** Which way the user is asked to turn. Default `left`. */
|
|
47
|
+
direction?: VideoSelfieTurnDirection;
|
|
48
|
+
/** Asking, or seen. Default `ask`. */
|
|
49
|
+
stage?: VideoSelfieTurnStage;
|
|
50
|
+
/** Which document the journey collects — the Stepper's own `documentType`. */
|
|
51
|
+
documentType?: VideoSelfieDocumentType;
|
|
52
|
+
/** Live camera feed (<video>) or sample still (<img>), full-bleed behind
|
|
53
|
+
* the ring. */
|
|
54
|
+
camera?: ReactNode;
|
|
55
|
+
screenId?: string;
|
|
56
|
+
overrides?: PrizmaScreenOverrides;
|
|
57
|
+
onAction?: PrizmaScreenActionHandler;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Video Selfie — the head-turn beats of the liveness sequence (Archetype D,
|
|
62
|
+
* GUIDELINES.md §6-D; always dark). NEW 2026-09-30: the `🙂↔️ Turn Head` band
|
|
63
|
+
* of the Web / iOS / Android full-flow frames draws four directions, each in
|
|
64
|
+
* two beats, and none of them existed in the screens package — the module
|
|
65
|
+
* jumped straight from the liveness ring to the spoken steps.
|
|
66
|
+
*
|
|
67
|
+
* The frames are the liveness stage with three changes, and nothing else:
|
|
68
|
+
*
|
|
69
|
+
* - the ring is Core's GUIDE stroke (`ring="dashed"`) while the stage waits
|
|
70
|
+
* for the turn, and the positive ring once it has seen it;
|
|
71
|
+
* - the silhouette guide is ON (Core's `empty`), because the user is being
|
|
72
|
+
* asked to move their face rather than hold it still;
|
|
73
|
+
* - a small arrow chip sits between the ring and the instruction, saying
|
|
74
|
+
* which way.
|
|
75
|
+
*
|
|
76
|
+
* The mark under the ring is a canonical bank glyph at 2xl — the disc is part
|
|
77
|
+
* of the drawing, not something this screen paints. `PrizmaTitle.icon` takes
|
|
78
|
+
* `status | spinner | none` only (DEVIATIONS #10), so the block below the ring
|
|
79
|
+
* is composed here; it declares a gap and nothing else. CORE REQUEST stands: a
|
|
80
|
+
* `glyph` slot on the capture stage, so this and Face Capture's future hint
|
|
81
|
+
* marks share one component. DEVIATIONS #VS-20.
|
|
82
|
+
*/
|
|
83
|
+
export const VideoSelfieTurnHeadScreen = forwardRef<
|
|
84
|
+
HTMLDivElement,
|
|
85
|
+
VideoSelfieTurnHeadScreenProps
|
|
86
|
+
>(
|
|
87
|
+
(
|
|
88
|
+
{
|
|
89
|
+
strings,
|
|
90
|
+
direction = 'left',
|
|
91
|
+
stage = 'ask',
|
|
92
|
+
documentType = 'id',
|
|
93
|
+
camera,
|
|
94
|
+
screenId = `video-selfie-turn-${direction}-${stage}`,
|
|
95
|
+
overrides,
|
|
96
|
+
onAction,
|
|
97
|
+
className,
|
|
98
|
+
...shell
|
|
99
|
+
},
|
|
100
|
+
ref,
|
|
101
|
+
) => (
|
|
102
|
+
<PrizmaScreenShell ref={ref} dark className={className} {...shell}>
|
|
103
|
+
<div className="prizma-viewfinder__surface">{camera}</div>
|
|
104
|
+
<div className="video-selfie-stepper">
|
|
105
|
+
<PrizmaStepper
|
|
106
|
+
steps={7}
|
|
107
|
+
currentState={4}
|
|
108
|
+
documentType={documentType}
|
|
109
|
+
completedValueText={strings.stepperCompletedLabel}
|
|
110
|
+
/>
|
|
111
|
+
</div>
|
|
112
|
+
<div
|
|
113
|
+
className={[
|
|
114
|
+
'prizma-viewfinder__content',
|
|
115
|
+
overrides?.align === 'left' ? 'prizma-viewfinder__content--left' : '',
|
|
116
|
+
]
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.join(' ')}
|
|
119
|
+
>
|
|
120
|
+
<div className="video-selfie-band video-selfie-band--ring">
|
|
121
|
+
<PrizmaSelfieCapture
|
|
122
|
+
mode="overlay"
|
|
123
|
+
scrim
|
|
124
|
+
/* The ring says WHICH WAY: the half on the side being asked for
|
|
125
|
+
is dashed while the stage waits and positive once it has seen
|
|
126
|
+
the turn, and the other half stays the neutral track. The state
|
|
127
|
+
is `filled` on both beats — the verdict lives in the arc, not in
|
|
128
|
+
the whole ring, so nothing else about the stage changes. */
|
|
129
|
+
guide={{ side: direction, state: stage === 'ask' ? 'waiting' : 'seen' }}
|
|
130
|
+
state="filled"
|
|
131
|
+
showFeedback={false}
|
|
132
|
+
/>
|
|
133
|
+
{/* Both beats carry the SAME chip and the same instruction — the
|
|
134
|
+
frames change only the ring. Nothing is announced on the turn
|
|
135
|
+
itself, which is why the stage is handed no `statusLabels`: in
|
|
136
|
+
overlay mode a verdict with no copy is a ring colour, not a
|
|
137
|
+
message. */}
|
|
138
|
+
<div className="video-selfie-turn">
|
|
139
|
+
<span className="video-selfie-turn__mark">{TURN_ICON[direction]}</span>
|
|
140
|
+
<p className="prizma-viewfinder__title">
|
|
141
|
+
{strings.selfieCapture.turn[direction]}
|
|
142
|
+
</p>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
</PrizmaScreenShell>
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
VideoSelfieTurnHeadScreen.displayName = 'VideoSelfieTurnHeadScreen';
|
|
150
|
+
|
|
151
|
+
export default VideoSelfieTurnHeadScreen;
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import React, { forwardRef, type ReactNode } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
IconBankCard,
|
|
4
|
-
IconIdFront,
|
|
5
4
|
IconMessages,
|
|
6
5
|
IconMicrophone,
|
|
7
6
|
IconNumberedList,
|
|
8
|
-
|
|
7
|
+
IconPassportCapture,
|
|
9
8
|
IconUser,
|
|
9
|
+
IconUserInformation,
|
|
10
10
|
IconWavingHand,
|
|
11
11
|
} from '@incodetech/prizma-core-icons';
|
|
12
|
-
import {
|
|
12
|
+
import { PrizmaList } from '@incodetech/prizma-core-web/list';
|
|
13
13
|
import { PrizmaListScreen, type PrizmaListScreenProps } from './ListScreen';
|
|
14
14
|
import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
|
|
15
15
|
import type {
|
|
@@ -27,9 +27,13 @@ import './ScreenLayout.css';
|
|
|
27
27
|
*/
|
|
28
28
|
const ROW_ICONS: Record<VideoSelfieTutorialRowKind, ReactNode> = {
|
|
29
29
|
recording: <IconMicrophone size="m" />,
|
|
30
|
-
|
|
30
|
+
// `user-information` and `passport-capture`, NOT `id-front` and `passport`:
|
|
31
|
+
// the tutorial promises the steps the Stepper then counts off, and the
|
|
32
|
+
// Stepper's own sequence uses these two. Corrected 2026-09-30 — the tutorial
|
|
33
|
+
// was drawing a different ID glyph from the one the journey shows.
|
|
34
|
+
idFront: <IconUserInformation size="m" />,
|
|
31
35
|
idBack: <IconBankCard size="m" />,
|
|
32
|
-
passport: <
|
|
36
|
+
passport: <IconPassportCapture size="m" />,
|
|
33
37
|
frontCamera: <IconUser size="m" />,
|
|
34
38
|
waveHand: <IconWavingHand size="m" />,
|
|
35
39
|
questions: <IconNumberedList size="m" />,
|
|
@@ -102,25 +106,32 @@ export const VideoSelfieTutorialScreen = forwardRef<
|
|
|
102
106
|
onAction={onAction}
|
|
103
107
|
{...shell}
|
|
104
108
|
>
|
|
105
|
-
{/* Core's
|
|
106
|
-
|
|
107
|
-
`
|
|
108
|
-
|
|
109
|
+
{/* Core's `PrizmaList`, `icons-chain` — the stepper-style list: a 40
|
|
110
|
+
squircle per step with a 2×8 connector running down to the next one,
|
|
111
|
+
which is exactly what `1.0` and `1.2` draw (`List` → `List item` →
|
|
112
|
+
`Icon / List item` → `stepIcon` + `bottomConnector`).
|
|
109
113
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
114
|
+
It used to be a hand-rolled <ul> of bare `PrizmaListItem`s, for two
|
|
115
|
+
reasons that are both gone: the icon LIST variants capped at six
|
|
116
|
+
slots and this tutorial draws seven, and no list variant paired with
|
|
117
|
+
the `icon-connector` ROW variant at all, so a stepper list could
|
|
118
|
+
only be numbered. `icons-chain` was added to Core on 2026-09-30 with
|
|
119
|
+
seven slots, and closes both (#VS-8, #VS-16). The list declares no
|
|
120
|
+
layout of its own here — the rhythm, the squircle and the connector
|
|
121
|
+
are the variant's. */}
|
|
122
|
+
<PrizmaList
|
|
123
|
+
variant="icons-chain"
|
|
124
|
+
showItem3
|
|
125
|
+
showItem4
|
|
126
|
+
showItem5
|
|
127
|
+
showItem6
|
|
128
|
+
showItem7
|
|
129
|
+
items={strings.tutorial.rows[documentType].map((row) => ({
|
|
130
|
+
variant: 'icon-connector' as const,
|
|
131
|
+
label: row.label,
|
|
132
|
+
icon: ROW_ICONS[row.kind],
|
|
133
|
+
}))}
|
|
134
|
+
/>
|
|
124
135
|
</PrizmaListScreen>
|
|
125
136
|
),
|
|
126
137
|
);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import React, { forwardRef, type ReactNode } from 'react';
|
|
2
|
+
import { PrizmaModalContent } from '@incodetech/prizma-core-web/modal';
|
|
3
|
+
import { PrizmaSelfieCapture } from '@incodetech/prizma-core-web/selfie-capture';
|
|
4
|
+
import { PrizmaStepper } from '@incodetech/prizma-core-web/stepper';
|
|
5
|
+
import { PrizmaScreenShell, type PrizmaScreenShellProps } from './ScreenShell';
|
|
6
|
+
import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
|
|
7
|
+
import type { VideoSelfieDocumentType, VideoSelfieStrings } from './VideoSelfieStrings';
|
|
8
|
+
import './ScreenLayout.css';
|
|
9
|
+
import './ViewfinderScreen.css';
|
|
10
|
+
import './VideoSelfieLayout.css';
|
|
11
|
+
|
|
12
|
+
/** The three beats the `👋 Wave` band draws. */
|
|
13
|
+
export type VideoSelfieWaveStage = 'waiting' | 'ready' | 'success';
|
|
14
|
+
|
|
15
|
+
export interface VideoSelfieWaveScreenProps
|
|
16
|
+
extends Omit<PrizmaScreenShellProps, 'dark' | 'title' | 'children'> {
|
|
17
|
+
/** Module copy — this screen reads `selfieCapture.wave`. */
|
|
18
|
+
strings: VideoSelfieStrings;
|
|
19
|
+
/** Which beat to render. Default `waiting`. */
|
|
20
|
+
stage?: VideoSelfieWaveStage;
|
|
21
|
+
/** Which document the journey collects — the Stepper's own `documentType`. */
|
|
22
|
+
documentType?: VideoSelfieDocumentType;
|
|
23
|
+
/** Live camera feed (<video>) or sample still (<img>), full-bleed behind
|
|
24
|
+
* the ring. */
|
|
25
|
+
camera?: ReactNode;
|
|
26
|
+
screenId?: string;
|
|
27
|
+
overrides?: PrizmaScreenOverrides;
|
|
28
|
+
onAction?: PrizmaScreenActionHandler;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Video Selfie — the hand-wave beat of the liveness sequence (Archetype D,
|
|
33
|
+
* GUIDELINES.md §6-D; always dark). NEW 2026-09-30, from the `👋 Wave` band of
|
|
34
|
+
* the full-flow frames; like the head turns, it had no screen at all.
|
|
35
|
+
*
|
|
36
|
+
* The composition is the liveness ring with the spoken steps' card under it:
|
|
37
|
+
* the feed runs full-bleed, the ring frames the face, and Core's Modal asks
|
|
38
|
+
* for the gesture with a CTA that only lights up once the stage has seen it
|
|
39
|
+
* (the frames' "5 second delay" note between the two). The verdict beat is the
|
|
40
|
+
* ring's own positive treatment plus the module's "Looking good!" — the SAME
|
|
41
|
+
* copy and the same mark `8.2` uses, because it is the same answer.
|
|
42
|
+
*
|
|
43
|
+
* The scrim/panel positioning is the recipe Document Capture's method sheet
|
|
44
|
+
* established (DEVIATIONS #DC-30): Core's safe presenter portals to
|
|
45
|
+
* `document.body`, which would escape the 390×844 frame and the host's
|
|
46
|
+
* `transform: scale()`, so the screen owns the panel and Core owns the card.
|
|
47
|
+
* The card is pinned back to LIGHT inside the dark shell for the reason
|
|
48
|
+
* `VideoSelfieSpeakScreen` documents (#VS-10).
|
|
49
|
+
*
|
|
50
|
+
* The hand that sweeps across the face is Core's, not this screen's:
|
|
51
|
+
* `PrizmaSelfieCapture` grew a `hint="wave"` axis on 2026-09-30 (owner: "we
|
|
52
|
+
* need the hand wave"), the face counterpart of `PrizmaIdCapture`'s animated
|
|
53
|
+
* hand hint, so the art and the choreography live where every other capture
|
|
54
|
+
* gesture already lives. The screen only says WHEN: the hand runs while the
|
|
55
|
+
* stage is waiting and stops the moment the wave has landed, which is the
|
|
56
|
+
* frames' own "5 second delay · Remove animation (Accessibility)" note.
|
|
57
|
+
* DEVIATIONS #VS-21.
|
|
58
|
+
*/
|
|
59
|
+
export const VideoSelfieWaveScreen = forwardRef<HTMLDivElement, VideoSelfieWaveScreenProps>(
|
|
60
|
+
(
|
|
61
|
+
{
|
|
62
|
+
strings,
|
|
63
|
+
stage = 'waiting',
|
|
64
|
+
documentType = 'id',
|
|
65
|
+
camera,
|
|
66
|
+
screenId = `video-selfie-wave-${stage}`,
|
|
67
|
+
overrides,
|
|
68
|
+
onAction,
|
|
69
|
+
className,
|
|
70
|
+
...shell
|
|
71
|
+
},
|
|
72
|
+
ref,
|
|
73
|
+
) => {
|
|
74
|
+
const slice = strings.selfieCapture.wave;
|
|
75
|
+
const seen = stage === 'success';
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<PrizmaScreenShell ref={ref} dark className={className} {...shell}>
|
|
79
|
+
<div className="prizma-viewfinder__surface">{camera}</div>
|
|
80
|
+
<div className="video-selfie-stepper">
|
|
81
|
+
<PrizmaStepper
|
|
82
|
+
steps={7}
|
|
83
|
+
currentState={5}
|
|
84
|
+
documentType={documentType}
|
|
85
|
+
completedValueText={strings.stepperCompletedLabel}
|
|
86
|
+
/>
|
|
87
|
+
</div>
|
|
88
|
+
<div
|
|
89
|
+
className={[
|
|
90
|
+
'prizma-viewfinder__content',
|
|
91
|
+
overrides?.align === 'left' ? 'prizma-viewfinder__content--left' : '',
|
|
92
|
+
]
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join(' ')}
|
|
95
|
+
>
|
|
96
|
+
<div className="video-selfie-band video-selfie-band--ring">
|
|
97
|
+
<PrizmaSelfieCapture
|
|
98
|
+
mode="overlay"
|
|
99
|
+
scrim
|
|
100
|
+
state={seen ? 'success' : 'filled'}
|
|
101
|
+
/* The hand demonstrates the gesture only while the stage is
|
|
102
|
+
still waiting for it. On `ready` the wave has landed and the
|
|
103
|
+
CTA lights up instead — the frames drop the animation at
|
|
104
|
+
exactly that beat. */
|
|
105
|
+
hint={stage === 'waiting' ? 'wave' : undefined}
|
|
106
|
+
showFeedback={false}
|
|
107
|
+
statusLabels={{ success: strings.selfieCapture.successLabel }}
|
|
108
|
+
/>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
{/* The card is gone on the verdict beat — the ring and the mark are the
|
|
112
|
+
whole answer there, exactly as the band's last frame draws it. */}
|
|
113
|
+
{seen ? null : (
|
|
114
|
+
<div data-prizma-theme="light" className="video-selfie-sheet-panel">
|
|
115
|
+
<PrizmaModalContent
|
|
116
|
+
title={slice.instruction}
|
|
117
|
+
showCloseIcon={false}
|
|
118
|
+
buttonGroupProps={{
|
|
119
|
+
orientation: 'vertical-v1',
|
|
120
|
+
showPrimary: true,
|
|
121
|
+
showSecondary: false,
|
|
122
|
+
primaryLabel: slice.doneLabel,
|
|
123
|
+
primaryDisabled: stage !== 'ready',
|
|
124
|
+
onPrimaryClick: () => onAction?.({ type: 'cta', screenId }),
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</PrizmaScreenShell>
|
|
130
|
+
);
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
VideoSelfieWaveScreen.displayName = 'VideoSelfieWaveScreen';
|
|
134
|
+
|
|
135
|
+
export default VideoSelfieWaveScreen;
|
|
@@ -41,8 +41,9 @@ export interface WatchlistBusinessFormScreenProps
|
|
|
41
41
|
* (GUIDELINES.md §6-E, `PrizmaFormScreen`).
|
|
42
42
|
*
|
|
43
43
|
* ONE screen for both purple form sections (`watchlist.default` and
|
|
44
|
-
* `watchlist.filled`)
|
|
45
|
-
*
|
|
44
|
+
* `watchlist.filled`), plus the `emptyError` state the GUIDELINES §6-E
|
|
45
|
+
* empty-submit rule adds (Elena 2026-09-30, see note 3): measured against the 390 frames they share an identical
|
|
46
|
+
* skeleton — nav → Title (H2 + the "fields marked with *" H5) → Spacing/32 → Country dropdown,
|
|
46
47
|
* Business Name and Tax ID at Spacing/16 → primary CTA → tag — and differ ONLY
|
|
47
48
|
* in field content and whether the CTA is live.
|
|
48
49
|
*
|
|
@@ -57,11 +58,14 @@ export interface WatchlistBusinessFormScreenProps
|
|
|
57
58
|
* and Core's `readOnly`, the other way to make a field inert, paints the
|
|
58
59
|
* DISABLED grey surface on states Figma draws white (DEVIATIONS #PI-6).
|
|
59
60
|
* Neither section greys anything, so `disabled` is set nowhere.
|
|
60
|
-
* 3. The CTA state comes from the SECTION, not from the values
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
61
|
+
* 3. The CTA state comes from the SECTION, not from the values. Under the
|
|
62
|
+
* GUIDELINES §6-E empty-submit rule (Elena 2026-09-30) "Continue" is live
|
|
63
|
+
* on the resting `default` form, so the user can press it, and greys ONLY
|
|
64
|
+
* on `emptyError`, the state that press leads to: Country and Business
|
|
65
|
+
* Name render empty (no placeholder) in Core's error state with
|
|
66
|
+
* `requiredError`. Tax ID never errors — `filled` leaves it empty with the
|
|
67
|
+
* CTA live, which is how we know it does not gate the lookup (DEVIATIONS
|
|
68
|
+
* #WB-2).
|
|
65
69
|
* 4. The country flag is Core's canonical raster (24×18, radius 2) in the
|
|
66
70
|
* dropdown's LEADING slot — `PrizmaCountryFlag` composed into
|
|
67
71
|
* `PrizmaDropdown.renderLeading`, which exists for exactly this case and
|
|
@@ -95,9 +99,11 @@ export const WatchlistBusinessFormScreen = forwardRef<
|
|
|
95
99
|
ref,
|
|
96
100
|
) => {
|
|
97
101
|
const copy = strings.form;
|
|
98
|
-
//
|
|
99
|
-
// spec, not the field
|
|
100
|
-
|
|
102
|
+
// The CTA greys only after an empty submit; the resting form keeps it live
|
|
103
|
+
// so that submit can happen. The section is the spec, not the field
|
|
104
|
+
// contents (see note 3 above).
|
|
105
|
+
const emptyError = state === 'emptyError';
|
|
106
|
+
const errorText = emptyError ? strings.requiredError : undefined;
|
|
101
107
|
|
|
102
108
|
// The label is the country NAME only. The flag is not part of it — it rides
|
|
103
109
|
// the dropdown's leading slot below, where Core paints the raster.
|
|
@@ -112,7 +118,7 @@ export const WatchlistBusinessFormScreen = forwardRef<
|
|
|
112
118
|
title={copy.title}
|
|
113
119
|
subtitle={copy.subtitle}
|
|
114
120
|
ctaLabel={copy.ctaLabel}
|
|
115
|
-
ctaDisabled={
|
|
121
|
+
ctaDisabled={emptyError}
|
|
116
122
|
ctaLoadingLabel={copy.ctaLoadingLabel}
|
|
117
123
|
/* Figma draws no verified tag on either form section, but GUIDELINES §1
|
|
118
124
|
and the standing owner rule (#F-30, #PI-8) keep header AND footer on
|
|
@@ -127,9 +133,17 @@ export const WatchlistBusinessFormScreen = forwardRef<
|
|
|
127
133
|
>
|
|
128
134
|
<PrizmaDropdown
|
|
129
135
|
label={copy.country.label}
|
|
130
|
-
|
|
136
|
+
/* Country and Business Name are required and marked (Elena,
|
|
137
|
+
2026-09-30); the H5 explains the asterisk. */
|
|
138
|
+
required
|
|
139
|
+
requiredHint={strings.requiredHint}
|
|
140
|
+
/* The empty-submit error draws the field with nothing in it, as the
|
|
141
|
+
`FormsFieldScreen` `emptyError` precedent does. */
|
|
142
|
+
placeholder={emptyError ? undefined : copy.country.placeholder}
|
|
131
143
|
options={options}
|
|
132
|
-
defaultValue={values?.country ?? null}
|
|
144
|
+
defaultValue={emptyError ? null : (values?.country ?? null)}
|
|
145
|
+
error={emptyError}
|
|
146
|
+
errorText={errorText}
|
|
133
147
|
/* A country catalogue is long enough that Core's own search bar is
|
|
134
148
|
the right behaviour; no purple section draws the menu open, so
|
|
135
149
|
nothing about its appearance is being invented here. */
|
|
@@ -155,9 +169,13 @@ export const WatchlistBusinessFormScreen = forwardRef<
|
|
|
155
169
|
/>
|
|
156
170
|
<PrizmaTextField
|
|
157
171
|
label={copy.businessName.label}
|
|
158
|
-
|
|
172
|
+
required
|
|
173
|
+
requiredHint={strings.requiredHint}
|
|
174
|
+
placeholder={emptyError ? undefined : copy.businessName.placeholder}
|
|
159
175
|
autoComplete="organization"
|
|
160
|
-
defaultValue={values?.businessName ?? ''}
|
|
176
|
+
defaultValue={emptyError ? '' : (values?.businessName ?? '')}
|
|
177
|
+
error={emptyError}
|
|
178
|
+
errorText={errorText}
|
|
161
179
|
/>
|
|
162
180
|
<PrizmaTextField
|
|
163
181
|
label={copy.taxId.label}
|
|
@@ -165,7 +183,8 @@ export const WatchlistBusinessFormScreen = forwardRef<
|
|
|
165
183
|
autoComplete="off"
|
|
166
184
|
autoCapitalize="characters"
|
|
167
185
|
spellCheck={false}
|
|
168
|
-
|
|
186
|
+
/* Optional (#WB-2): an empty submit leaves it clean. */
|
|
187
|
+
defaultValue={emptyError ? '' : (values?.taxId ?? '')}
|
|
169
188
|
/>
|
|
170
189
|
</PrizmaFormScreen>
|
|
171
190
|
);
|
|
@@ -23,15 +23,18 @@
|
|
|
23
23
|
* in field content and whether the CTA is live, which is exactly what
|
|
24
24
|
* GUIDELINES §6-E means by "states are separate screens sharing the identical
|
|
25
25
|
* layout":
|
|
26
|
-
* - `default`
|
|
27
|
-
* CTA
|
|
28
|
-
* - `filled`
|
|
26
|
+
* - `default` — every field empty (the country dropdown on its placeholder),
|
|
27
|
+
* CTA live so the user can press it (GUIDELINES §6-E empty-submit rule)
|
|
28
|
+
* - `filled` — country selected and business name typed, CTA live
|
|
29
|
+
* - `emptyError` — the user pressed the CTA with the form empty: Country and
|
|
30
|
+
* Business Name render empty in Core's error state with `requiredError`,
|
|
31
|
+
* Tax ID stays clean, CTA greyed (Elena 2026-09-30)
|
|
29
32
|
*
|
|
30
33
|
* Note what `filled` does NOT do: it leaves **Tax ID empty** and still enables
|
|
31
34
|
* the CTA, so the tax identifier is optional and does not gate the lookup
|
|
32
|
-
* (DEVIATIONS #WB-2).
|
|
35
|
+
* (DEVIATIONS #WB-2). That is also why `emptyError` leaves Tax ID alone.
|
|
33
36
|
*/
|
|
34
|
-
export type WatchlistBusinessFormState = 'default' | 'filled';
|
|
37
|
+
export type WatchlistBusinessFormState = 'default' | 'filled' | 'emptyError';
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
40
|
* One selectable country of registration. Runtime data, never copy — the host
|
|
@@ -81,12 +84,23 @@ export interface WatchlistBusinessStrings {
|
|
|
81
84
|
verifiedLabel: string;
|
|
82
85
|
/** Accessible name for the logo nav row. */
|
|
83
86
|
navLabel?: string;
|
|
87
|
+
/**
|
|
88
|
+
* The error row the REQUIRED fields (Country, Business Name) show on the
|
|
89
|
+
* `emptyError` form state ("This field is required"). Maps to the SDK's
|
|
90
|
+
* `dynamicForms.fieldRequired` key. GUIDELINES §6-E empty-submit rule.
|
|
91
|
+
*/
|
|
92
|
+
requiredError: string;
|
|
93
|
+
/**
|
|
94
|
+
* Required-field marker ("*"), passed to Core's own `requiredHint` prop on
|
|
95
|
+
* Country and Business Name (Elena, 2026-09-30).
|
|
96
|
+
*/
|
|
97
|
+
requiredHint?: string;
|
|
84
98
|
|
|
85
|
-
/** watchlist.{default,filled} */
|
|
99
|
+
/** watchlist.{default,filled,emptyError} */
|
|
86
100
|
form: {
|
|
87
101
|
/** H2 ("Watchlist for business"). */
|
|
88
102
|
title: string;
|
|
89
|
-
/** H5
|
|
103
|
+
/** H5 ("All fields marked with * are required."), explains the asterisk. */
|
|
90
104
|
subtitle?: string;
|
|
91
105
|
country: WatchlistBusinessFieldStrings & {
|
|
92
106
|
/** Search placeholder inside the open menu. */
|