@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.
Files changed (243) hide show
  1. package/dist/BiometricConsentScreen.d.ts +95 -0
  2. package/dist/BiometricConsentScreen.js +91 -0
  3. package/dist/BiometricConsentStrings.d.ts +84 -0
  4. package/dist/BiometricConsentStrings.js +31 -0
  5. package/dist/CaptureScreen.d.ts +13 -0
  6. package/dist/CaptureScreen.js +2 -2
  7. package/dist/CurpEnterScreen.d.ts +7 -0
  8. package/dist/CurpEnterScreen.js +17 -3
  9. package/dist/CurpGenerateScreen.d.ts +8 -0
  10. package/dist/CurpGenerateScreen.js +28 -13
  11. package/dist/CurpLayout.css +8 -34
  12. package/dist/CurpStrings.d.ts +16 -2
  13. package/dist/DemoScreensLayout.css +16 -0
  14. package/dist/DemoScreensRedirectScreen.d.ts +6 -1
  15. package/dist/DemoScreensRedirectScreen.js +3 -2
  16. package/dist/DemoScreensStrings.d.ts +5 -0
  17. package/dist/DigitalIdUploadErrorScreen.d.ts +31 -0
  18. package/dist/DigitalIdUploadErrorScreen.js +26 -0
  19. package/dist/DigitalIdUploadFileTooLargeScreen.d.ts +27 -0
  20. package/dist/DigitalIdUploadFileTooLargeScreen.js +21 -0
  21. package/dist/DigitalIdUploadReviewScreen.d.ts +57 -0
  22. package/dist/DigitalIdUploadReviewScreen.js +44 -0
  23. package/dist/DigitalIdUploadStrings.d.ts +64 -0
  24. package/dist/DigitalIdUploadStrings.js +19 -0
  25. package/dist/DigitalIdUploadTutorialScreen.d.ts +28 -0
  26. package/dist/DigitalIdUploadTutorialScreen.js +23 -0
  27. package/dist/DocumentCaptureIllustration.d.ts +1 -1
  28. package/dist/EkybAddressDetailsScreen.d.ts +7 -0
  29. package/dist/EkybAddressDetailsScreen.js +25 -7
  30. package/dist/EkybBusinessDetailsScreen.d.ts +6 -0
  31. package/dist/EkybBusinessDetailsScreen.js +23 -6
  32. package/dist/EkybCountryScreen.d.ts +13 -5
  33. package/dist/EkybCountryScreen.js +41 -10
  34. package/dist/EkybOwnersScreen.d.ts +8 -0
  35. package/dist/EkybOwnersScreen.js +26 -3
  36. package/dist/EkybStrings.d.ts +32 -10
  37. package/dist/EkycCountryCatalog.d.ts +4 -1
  38. package/dist/EkycCountryCatalog.js +5 -3
  39. package/dist/EkycCountryFormScreen.d.ts +12 -3
  40. package/dist/EkycCountryFormScreen.js +28 -10
  41. package/dist/EkycDetailsScreen.d.ts +7 -0
  42. package/dist/EkycDetailsScreen.js +21 -4
  43. package/dist/EkycStrings.d.ts +14 -2
  44. package/dist/ElectronicSignatureLayout.css +6 -0
  45. package/dist/EmailInputStrings.d.ts +11 -1
  46. package/dist/EmailInputVerifyScreen.d.ts +7 -0
  47. package/dist/EmailInputVerifyScreen.js +17 -3
  48. package/dist/ErrorScreensScreen.d.ts +5 -2
  49. package/dist/ErrorScreensScreen.js +15 -4
  50. package/dist/ErrorScreensStrings.d.ts +17 -2
  51. package/dist/ErrorScreensStrings.js +18 -10
  52. package/dist/FaceCaptureCapturingScreen.d.ts +63 -0
  53. package/dist/FaceCaptureCapturingScreen.js +62 -2
  54. package/dist/FaceCaptureLayout.css +69 -4
  55. package/dist/FaceCaptureStrings.d.ts +23 -0
  56. package/dist/FaceMatchComparisonScreen.d.ts +3 -3
  57. package/dist/FaceMatchLayout.css +21 -12
  58. package/dist/FormScreen.css +30 -0
  59. package/dist/FormScreen.d.ts +28 -0
  60. package/dist/FormScreen.js +3 -2
  61. package/dist/FormsLongFormScreen.d.ts +8 -0
  62. package/dist/FormsLongFormScreen.js +34 -13
  63. package/dist/FormsStrings.d.ts +11 -1
  64. package/dist/GeolocationBrowserStepsScreen.d.ts +6 -2
  65. package/dist/GeolocationBrowserStepsScreen.js +7 -3
  66. package/dist/GeolocationStrings.d.ts +3 -0
  67. package/dist/IdCaptureLayout.css +205 -2
  68. package/dist/IdCaptureStageScreen.d.ts +47 -0
  69. package/dist/IdCaptureStageScreen.js +39 -3
  70. package/dist/IdCaptureStrings.d.ts +19 -0
  71. package/dist/ListScreen.d.ts +21 -0
  72. package/dist/ListScreen.js +7 -2
  73. package/dist/LoaderScreen.d.ts +7 -5
  74. package/dist/LoaderScreen.js +7 -5
  75. package/dist/LoadersStrings.d.ts +18 -5
  76. package/dist/OcrReviewErrorScreen.d.ts +30 -0
  77. package/dist/OcrReviewErrorScreen.js +24 -0
  78. package/dist/OcrReviewLayout.css +24 -0
  79. package/dist/OcrReviewLoadingScreen.d.ts +25 -0
  80. package/dist/OcrReviewLoadingScreen.js +19 -0
  81. package/dist/OcrReviewScreen.d.ts +76 -0
  82. package/dist/OcrReviewScreen.js +65 -0
  83. package/dist/OcrReviewStrings.d.ts +47 -0
  84. package/dist/OcrReviewStrings.js +17 -0
  85. package/dist/OcrReviewSuccessScreen.d.ts +30 -0
  86. package/dist/OcrReviewSuccessScreen.js +24 -0
  87. package/dist/PermissionsAllowScreen.d.ts +8 -11
  88. package/dist/PermissionsAllowScreen.js +8 -11
  89. package/dist/PermissionsAskScreen.d.ts +41 -0
  90. package/dist/PermissionsAskScreen.js +43 -0
  91. package/dist/PermissionsBrowserStepsScreen.d.ts +9 -2
  92. package/dist/PermissionsBrowserStepsScreen.js +12 -3
  93. package/dist/PermissionsLayout.css +47 -0
  94. package/dist/PermissionsStrings.d.ts +20 -9
  95. package/dist/PermissionsStrings.js +10 -4
  96. package/dist/PhoneInputLayout.css +10 -9
  97. package/dist/PhoneInputOtpScreen.d.ts +8 -0
  98. package/dist/PhoneInputOtpScreen.js +26 -19
  99. package/dist/PhoneInputStrings.d.ts +22 -4
  100. package/dist/PhoneInputVerifyScreen.d.ts +8 -0
  101. package/dist/PhoneInputVerifyScreen.js +36 -7
  102. package/dist/QualifiedSignatureAcceptScreen.js +6 -1
  103. package/dist/QualifiedSignatureDocumentScreen.d.ts +17 -10
  104. package/dist/QualifiedSignatureDocumentScreen.js +22 -17
  105. package/dist/QualifiedSignatureLayout.css +76 -0
  106. package/dist/QualifiedSignatureResultScreen.d.ts +16 -13
  107. package/dist/QualifiedSignatureResultScreen.js +17 -18
  108. package/dist/QualifiedSignatureStrings.d.ts +19 -2
  109. package/dist/ResultScreen.d.ts +8 -0
  110. package/dist/ResultScreen.js +14 -3
  111. package/dist/ReusabilityCredentialSelectionScreen.d.ts +95 -0
  112. package/dist/ReusabilityCredentialSelectionScreen.js +94 -0
  113. package/dist/ReusabilityLayout.css +241 -0
  114. package/dist/ReusabilityPrivacyPolicyScreen.d.ts +55 -0
  115. package/dist/ReusabilityPrivacyPolicyScreen.js +44 -0
  116. package/dist/ReusabilityStrings.d.ts +85 -0
  117. package/dist/ReusabilityStrings.js +37 -0
  118. package/dist/ReviewScreen.d.ts +38 -12
  119. package/dist/ReviewScreen.js +41 -8
  120. package/dist/ScreenLayout.css +382 -6
  121. package/dist/ScreenNav.css +8 -0
  122. package/dist/ScreenNav.d.ts +15 -2
  123. package/dist/ScreenNav.js +10 -3
  124. package/dist/VideoSelfieErrorScreen.js +1 -1
  125. package/dist/VideoSelfieIdCaptureScreen.d.ts +10 -3
  126. package/dist/VideoSelfieIdCaptureScreen.js +45 -14
  127. package/dist/VideoSelfieLayout.css +467 -34
  128. package/dist/VideoSelfieSelfieCaptureScreen.d.ts +44 -27
  129. package/dist/VideoSelfieSelfieCaptureScreen.js +65 -39
  130. package/dist/VideoSelfieSpeakScreen.js +19 -2
  131. package/dist/VideoSelfieStrings.d.ts +82 -13
  132. package/dist/VideoSelfieTurnHeadScreen.d.ts +51 -0
  133. package/dist/VideoSelfieTurnHeadScreen.js +66 -0
  134. package/dist/VideoSelfieTutorialScreen.d.ts +9 -0
  135. package/dist/VideoSelfieTutorialScreen.js +22 -5
  136. package/dist/VideoSelfieWaveScreen.d.ts +53 -0
  137. package/dist/VideoSelfieWaveScreen.js +62 -0
  138. package/dist/ViewfinderScreen.css +36 -0
  139. package/dist/WatchlistBusinessFormScreen.d.ts +11 -7
  140. package/dist/WatchlistBusinessFormScreen.js +27 -13
  141. package/dist/WatchlistBusinessStrings.d.ts +21 -7
  142. package/dist/flows.d.ts +8 -0
  143. package/dist/flows.js +17 -0
  144. package/dist/index.d.ts +19 -1
  145. package/dist/index.js +14 -0
  146. package/dist/styles.css +3 -0
  147. package/dist/types.d.ts +3 -1
  148. package/package.json +79 -3
  149. package/samples/video-selfie-camera-wide.jpg +0 -0
  150. package/samples/video-selfie-room-wide.jpg +0 -0
  151. package/samples/video-selfie-room.png +0 -0
  152. package/src/BiometricConsentScreen.tsx +192 -0
  153. package/src/BiometricConsentStrings.ts +119 -0
  154. package/src/CaptureScreen.tsx +17 -0
  155. package/src/CurpEnterScreen.tsx +21 -4
  156. package/src/CurpGenerateScreen.tsx +46 -22
  157. package/src/CurpLayout.css +8 -34
  158. package/src/CurpStrings.ts +22 -2
  159. package/src/DemoScreensLayout.css +16 -0
  160. package/src/DemoScreensRedirectScreen.tsx +14 -6
  161. package/src/DemoScreensStrings.ts +5 -0
  162. package/src/DigitalIdUploadErrorScreen.tsx +83 -0
  163. package/src/DigitalIdUploadFileTooLargeScreen.tsx +67 -0
  164. package/src/DigitalIdUploadReviewScreen.tsx +119 -0
  165. package/src/DigitalIdUploadStrings.ts +88 -0
  166. package/src/DigitalIdUploadTutorialScreen.tsx +59 -0
  167. package/src/DocumentCaptureIllustration.tsx +1 -1
  168. package/src/EkybAddressDetailsScreen.tsx +32 -4
  169. package/src/EkybBusinessDetailsScreen.tsx +23 -5
  170. package/src/EkybCountryScreen.tsx +45 -7
  171. package/src/EkybOwnersScreen.tsx +34 -3
  172. package/src/EkybStrings.ts +34 -10
  173. package/src/EkycCountryCatalog.ts +9 -4
  174. package/src/EkycCountryFormScreen.tsx +51 -12
  175. package/src/EkycDetailsScreen.tsx +22 -1
  176. package/src/EkycStrings.ts +14 -2
  177. package/src/ElectronicSignatureLayout.css +6 -0
  178. package/src/EmailInputStrings.ts +12 -1
  179. package/src/EmailInputVerifyScreen.tsx +21 -4
  180. package/src/ErrorScreensScreen.tsx +15 -4
  181. package/src/ErrorScreensStrings.ts +37 -12
  182. package/src/FaceCaptureCapturingScreen.tsx +102 -13
  183. package/src/FaceCaptureLayout.css +69 -4
  184. package/src/FaceCaptureStrings.ts +24 -0
  185. package/src/FaceMatchComparisonScreen.tsx +3 -3
  186. package/src/FaceMatchLayout.css +21 -12
  187. package/src/FormScreen.css +30 -0
  188. package/src/FormScreen.tsx +34 -0
  189. package/src/FormsLongFormScreen.tsx +41 -13
  190. package/src/FormsStrings.ts +12 -1
  191. package/src/GeolocationBrowserStepsScreen.tsx +6 -3
  192. package/src/GeolocationStrings.ts +3 -0
  193. package/src/IdCaptureLayout.css +205 -2
  194. package/src/IdCaptureStageScreen.tsx +75 -0
  195. package/src/IdCaptureStrings.ts +19 -0
  196. package/src/ListScreen.tsx +58 -10
  197. package/src/LoaderScreen.tsx +7 -5
  198. package/src/LoadersStrings.ts +18 -5
  199. package/src/OcrReviewErrorScreen.tsx +53 -0
  200. package/src/OcrReviewLayout.css +24 -0
  201. package/src/OcrReviewLoadingScreen.tsx +48 -0
  202. package/src/OcrReviewScreen.tsx +190 -0
  203. package/src/OcrReviewStrings.ts +69 -0
  204. package/src/OcrReviewSuccessScreen.tsx +52 -0
  205. package/src/PermissionsAllowScreen.tsx +9 -10
  206. package/src/PermissionsAskScreen.tsx +105 -0
  207. package/src/PermissionsBrowserStepsScreen.tsx +11 -3
  208. package/src/PermissionsLayout.css +47 -0
  209. package/src/PermissionsStrings.ts +42 -12
  210. package/src/PhoneInputLayout.css +10 -9
  211. package/src/PhoneInputOtpScreen.tsx +23 -3
  212. package/src/PhoneInputStrings.ts +23 -4
  213. package/src/PhoneInputVerifyScreen.tsx +41 -5
  214. package/src/QualifiedSignatureAcceptScreen.tsx +12 -1
  215. package/src/QualifiedSignatureDocumentScreen.tsx +31 -19
  216. package/src/QualifiedSignatureLayout.css +76 -0
  217. package/src/QualifiedSignatureResultScreen.tsx +28 -26
  218. package/src/QualifiedSignatureStrings.ts +19 -2
  219. package/src/ResultScreen.tsx +33 -2
  220. package/src/ReusabilityCredentialSelectionScreen.tsx +246 -0
  221. package/src/ReusabilityLayout.css +241 -0
  222. package/src/ReusabilityPrivacyPolicyScreen.tsx +109 -0
  223. package/src/ReusabilityStrings.ts +127 -0
  224. package/src/ReviewScreen.tsx +96 -27
  225. package/src/ScreenLayout.css +382 -6
  226. package/src/ScreenNav.css +8 -0
  227. package/src/ScreenNav.tsx +35 -5
  228. package/src/VideoSelfieErrorScreen.tsx +1 -1
  229. package/src/VideoSelfieIdCaptureScreen.tsx +125 -31
  230. package/src/VideoSelfieLayout.css +467 -34
  231. package/src/VideoSelfieSelfieCaptureScreen.tsx +110 -57
  232. package/src/VideoSelfieSpeakScreen.tsx +34 -15
  233. package/src/VideoSelfieStrings.ts +89 -14
  234. package/src/VideoSelfieTurnHeadScreen.tsx +162 -0
  235. package/src/VideoSelfieTutorialScreen.tsx +45 -23
  236. package/src/VideoSelfieWaveScreen.tsx +143 -0
  237. package/src/ViewfinderScreen.css +36 -0
  238. package/src/WatchlistBusinessFormScreen.tsx +35 -16
  239. package/src/WatchlistBusinessStrings.ts +21 -7
  240. package/src/flows.ts +25 -0
  241. package/src/index.ts +88 -3
  242. package/src/styles.css +3 -0
  243. package/src/types.ts +3 -1
@@ -12,20 +12,26 @@
12
12
  // is the ID Capture-flavoured copy of the same skeleton; it stays where it is so
13
13
  // nothing that imports it breaks (DEVIATIONS #IDC-26).
14
14
  //
15
- // The section's seven frames collapse onto THREE built screens:
15
+ // The section's seven frames collapse onto FOUR built screens:
16
16
  //
17
+ // 1-3-1-id-permission-modal → PermissionsAskScreen (the sheet)
17
18
  // 1-3-2-id → PermissionsRequiredScreen (reason rows)
18
19
  // 1-3-2-id-allow-camera → PermissionsAllowScreen (subtitle)
19
20
  // 1-4-2-id (+ 4 Android siblings) → PermissionsBrowserStepsScreen
20
21
  //
21
- // The other four are OS chrome and are NOT built:
22
+ // The other three are OS chrome and are NOT built:
22
23
  //
23
- // 1-3-1-id-permission-modal the browser's own "Allow camera access" sheet
24
24
  // 1-4-1-id the iOS "…Would Like to Access the Camera" alert
25
25
  // 1-3-2-id-quit-process an iOS confirm alert layered over 1-3-2-id
26
26
  // 1-3-2-id-allow-camera-quit the same alert layered over the allow screen
27
27
  //
28
- // None of them is a Prizma surface: their chrome does not exist in Core and
28
+ // `1-3-1-id-permission-modal` was on this list until 2026-10-07. It is not the
29
+ // browser's prompt: it is the SDK's own camera pre-permission sheet (the SDK
30
+ // release notes call it "the camera permission sheet", shown by every module
31
+ // that asks for the camera), drawn with Core's Bottom Sheet dialog and Button
32
+ // Group. So it is built, from those two components (DEVIATIONS #GEN-1).
33
+ //
34
+ // None of the remaining three is a Prizma surface: their chrome does not exist in Core and
29
35
  // rebuilding it would mean inventing primitives and shipping platform rasters,
30
36
  // both forbidden (rule 1, GUIDELINES §10). Same call as Document Capture's iOS
31
37
  // action sheets (#DC-20) and Geolocation's system alert. Core Lab draws them as
@@ -49,18 +55,30 @@ export interface PermissionsReasonRow {
49
55
  }
50
56
 
51
57
  /**
52
- * Which browser's "turn it back on" journey to show. `safariIos` is the base
53
- * `1-4-2-id` frame; the other four are the `Android browsers` section of the
54
- * Web flow (4:8159). Re-enabling a permission is a different journey in every
55
- * browser, which is why one screen carries five step lists rather than five
56
- * screens carrying one skeleton.
58
+ * Which browser's "turn it back on" journey to show. The SAME nine browsers,
59
+ * keys and order as Geolocation's (`GeolocationBrowser`), because a refused
60
+ * camera and a refused location are the same journey through the same browser
61
+ * settings; only the permission's name changes (Camila, 2026-10-07). The
62
+ * Permissions Figma draws five of them (`1-4-2-id` for Safari plus the four
63
+ * `Android browsers` frames, 4:8159); the other four follow Geolocation.
57
64
  */
58
65
  export type PermissionsBrowser =
59
66
  | 'safariIos'
67
+ | 'chromeIos'
68
+ | 'firefoxIos'
69
+ | 'operaIos'
60
70
  | 'chromeAndroid'
61
71
  | 'firefoxAndroid'
72
+ | 'operaAndroid'
62
73
  | 'samsungAndroid'
63
- | 'operaAndroid';
74
+ | 'unrecognizedAndroid'
75
+ // Desktop browsers. Not in Geolocation's set: a phone's lists send the user
76
+ // to the Settings app on an iPhone, which means nothing on a computer, so the
77
+ // desktop flow carries its own four (2026-10-07).
78
+ | 'chromeDesktop'
79
+ | 'edgeDesktop'
80
+ | 'firefoxDesktop'
81
+ | 'safariMac';
64
82
 
65
83
  /**
66
84
  * Step kind → canonical bank glyph, resolved in
@@ -73,6 +91,7 @@ export type PermissionsBrowser =
73
91
  * than inventing a second one.
74
92
  */
75
93
  export type PermissionsStepKind =
94
+ | 'addressBar'
76
95
  | 'settings'
77
96
  | 'browser'
78
97
  | 'menu'
@@ -90,7 +109,7 @@ export interface PermissionsStep {
90
109
 
91
110
  export interface PermissionsBrowserSteps {
92
111
  /** Names the browser under the headline. Figma labels the section, not the
93
- * screen; the label keeps five otherwise near-identical cards apart. */
112
+ * screen; the label keeps nine otherwise near-identical cards apart. */
94
113
  label: string;
95
114
  steps: PermissionsStep[];
96
115
  }
@@ -101,6 +120,17 @@ export interface PermissionsStrings {
101
120
  /** "verified by" attribution copy for the footer tag. */
102
121
  verifiedLabel: string;
103
122
 
123
+ /** `1-3-1-id-permission-modal` — the pre-permission sheet, the very first
124
+ * ask. Its "Allow" is what raises the real OS prompt. */
125
+ ask: {
126
+ title: string;
127
+ subtitle?: string;
128
+ /** Primary — raises the OS prompt. */
129
+ allowLabel: string;
130
+ /** Secondary — declines; the flow moves on to `required`. */
131
+ denyLabel: string;
132
+ };
133
+
104
134
  /** `1-3-2-id` — the permission was refused and the flow explains why it needs it. */
105
135
  required: {
106
136
  title: string;
@@ -112,7 +142,7 @@ export interface PermissionsStrings {
112
142
  ctaLabel: string;
113
143
  };
114
144
 
115
- /** `1-3-2-id-allow-camera` — the first ask, before any refusal. */
145
+ /** `1-3-2-id-allow-camera` — the softer ask after the REAL OS prompt was refused. */
116
146
  allow: {
117
147
  title: string;
118
148
  subtitle?: string;
@@ -21,6 +21,7 @@
21
21
  align-items: flex-start;
22
22
  gap: var(--prizma-spacing-8);
23
23
  width: 100%;
24
+ position: relative; /* the country menu's containing block — see below */
24
25
  }
25
26
 
26
27
  /* The dial-code dropdown hugs its content ("🇷🇸 +381"), as Figma draws it.
@@ -47,15 +48,15 @@
47
48
  min-width: 0;
48
49
  }
49
50
 
50
- /* The open menu must NOT inherit the hugged anchor's width: Core anchors the
51
- * menu to the trigger, so once the trigger hugs "🇷🇸 +381" the option rows
52
- * truncate ("+…"). The menu keeps its own readable width — it lists country
53
- * names and dial codes, which the closed field never shows. Sized to the
54
- * content and capped at the 342 column so it never escapes the screen.
51
+ /* The open menu spans the WHOLE row (dial code + number field), as Figma's
52
+ * `Input / Combined` draws it: Core anchors the menu overlay to the trigger's
53
+ * `__anchor` (`position: relative`, Dropdown.css), so once the trigger hugs
54
+ * "🇷🇸 +381" the menu came out as narrow as the dial code. Releasing the
55
+ * anchor's positioning hands the overlay's `left: 0; right: 0` to the row
56
+ * above (`position: relative`), so the menu matches the row's width exactly.
57
+ * Core's own overlay offset (`top: 100% + Spacing/8`) is left untouched.
55
58
  * DEVIATIONS #PI-1 (same root cause as the hug).
56
59
  */
57
- .phone-input-combined__country .prizma-dropdown__menu {
58
- width: max-content;
59
- min-width: 100%;
60
- max-width: 342px;
60
+ .phone-input-combined__country .prizma-dropdown__anchor {
61
+ position: static;
61
62
  }
@@ -19,6 +19,14 @@ export interface PhoneInputOtpScreenProps
19
19
  value?: string;
20
20
  /** How many digits the code carries. Default 6. */
21
21
  length?: number;
22
+ /**
23
+ * Seconds left before the code can be resent. While it is above 0 (and the
24
+ * copy carries `otp.resendCountdownLabel`), the resend line becomes one
25
+ * disabled link, "Resend code in 30s", exactly as Figma draws
26
+ * `phoneNumber.OTP.typing`. The host owns the timer; the screen only draws
27
+ * the number it is given.
28
+ */
29
+ resendIn?: number;
22
30
  screenId?: string;
23
31
  overrides?: PrizmaScreenOverrides;
24
32
  onAction?: PrizmaScreenActionHandler;
@@ -47,13 +55,16 @@ export const PhoneInputOtpScreen = forwardRef<HTMLDivElement, PhoneInputOtpScree
47
55
  state = 'typing',
48
56
  value,
49
57
  length = 6,
58
+ resendIn,
50
59
  screenId = `phone-input-otp-${state}`,
51
60
  overrides,
52
61
  onAction,
53
62
  ...shell
54
63
  },
55
64
  ref,
56
- ) => (
65
+ ) => {
66
+ const counting = (resendIn ?? 0) > 0 && Boolean(strings.otp.resendCountdownLabel);
67
+ return (
57
68
  <PrizmaFormScreen
58
69
  ref={ref}
59
70
  title={strings.otp.title}
@@ -70,8 +81,16 @@ export const PhoneInputOtpScreen = forwardRef<HTMLDivElement, PhoneInputOtpScree
70
81
  than `linkLabel` (a full-width 56px button cannot sit inside a line of
71
82
  body text, and one button could not tell the two actions apart —
72
83
  DEVIATIONS #PI-10). */
84
+ /* While the resend timer runs, the line is ONE disabled link (Figma
85
+ `OTP.typing`: Tertiary / Link · Disabled), not the composed sentence. */
86
+ linkLabel={
87
+ counting
88
+ ? strings.otp.resendCountdownLabel!.replace('{{time}}', String(resendIn))
89
+ : undefined
90
+ }
91
+ linkDisabled={counting}
73
92
  linkContent={
74
- strings.otp.resendPrompt ? (
93
+ !counting && strings.otp.resendPrompt ? (
75
94
  <p className="prizma-screen-layout__inline-prompt">
76
95
  {strings.otp.resendPrompt}{' '}
77
96
  {strings.otp.resendLabel ? (
@@ -119,7 +138,8 @@ export const PhoneInputOtpScreen = forwardRef<HTMLDivElement, PhoneInputOtpScree
119
138
  errorText={state === 'error' ? strings.otp.errorText : undefined}
120
139
  />
121
140
  </PrizmaFormScreen>
122
- ),
141
+ );
142
+ },
123
143
  );
124
144
  PhoneInputOtpScreen.displayName = 'PhoneInputOtpScreen';
125
145
 
@@ -20,13 +20,18 @@
20
20
  * - `typing` — partial value, focused field
21
21
  * - `filled` — complete value
22
22
  * - `loading` — CTA in its loading treatment while the code is requested
23
+ * - `emptyError` — "Continue" pressed with no number typed: the phone field
24
+ * stays empty (no placeholder) in Core's error state with `requiredError`,
25
+ * and the CTA is disabled. The dial code always carries a selection, so it
26
+ * takes no error. GUIDELINES §6-E empty-submit rule (Elena, 2026-09-30).
23
27
  */
24
28
  export type PhoneInputVerifyState =
25
29
  | 'default'
26
30
  | 'countryCode'
27
31
  | 'typing'
28
32
  | 'filled'
29
- | 'loading';
33
+ | 'loading'
34
+ | 'emptyError';
30
35
 
31
36
  /**
32
37
  * The `phoneNumber.OTP.*` sections — same layout, different field state:
@@ -45,8 +50,9 @@ export type PhoneInputOtpState = 'typing' | 'filled' | 'processing' | 'error';
45
50
  export type PhoneInputErrorVariant = 'tryAgain' | 'unknown' | 'noNetwork';
46
51
 
47
52
  /** One selectable country in the dial-code dropdown. Runtime data, not copy —
48
- * the host supplies its own list; `flag` is the emoji Figma draws (Core has no
49
- * country-flag primitive — DEVIATIONS #PI-3). */
53
+ * the host supplies its own list. The screen draws the flag from Core's
54
+ * raster bank by `value` (`PrizmaCountryFlag`, DEVIATIONS #PI-3); `flag` is
55
+ * only the emoji fallback for a country outside the bank. */
50
56
  export interface PhoneInputCountry {
51
57
  /** Stable value, e.g. an ISO 3166-1 alpha-2 code. */
52
58
  value: string;
@@ -54,7 +60,7 @@ export interface PhoneInputCountry {
54
60
  dialCode: string;
55
61
  /** Country name for the open menu row. */
56
62
  label: string;
57
- /** Emoji flag, e.g. `🇷🇸`. */
63
+ /** Emoji fallback, e.g. `🇷🇸`, drawn only when the bank has no raster. */
58
64
  flag?: string;
59
65
  }
60
66
 
@@ -63,6 +69,12 @@ export interface PhoneInputStrings {
63
69
  verifiedLabel: string;
64
70
  /** Accessible name for the logo nav row. */
65
71
  navLabel?: string;
72
+ /**
73
+ * Error row under the required field when the primary CTA is pressed with it
74
+ * still empty (the verify screen's `emptyError` state): "This field is
75
+ * required". SDK key `dynamicForms.fieldRequired`.
76
+ */
77
+ requiredError: string;
66
78
 
67
79
  /** phoneNumber.verify.* */
68
80
  verify: {
@@ -116,6 +128,13 @@ export interface PhoneInputStrings {
116
128
  resendSeparator?: string;
117
129
  /** Second link — returns to phone entry. Emits `tertiary`. */
118
130
  changeNumberLabel?: string;
131
+ /**
132
+ * While the resend timer runs, the resend line is replaced by ONE disabled
133
+ * link: "Resend code in {{time}}s" (Figma `phoneNumber.OTP.typing`). The
134
+ * `{{time}}` placeholder is the SDK's own (`otp.resendCountdown`) and is
135
+ * filled with the screen's `resendIn` seconds.
136
+ */
137
+ resendCountdownLabel?: string;
119
138
  /** Primary CTA. */
120
139
  ctaLabel: string;
121
140
  /** Accessible / visible busy copy while the code is checked (`processing`). */
@@ -1,6 +1,7 @@
1
1
  import React, { forwardRef } from 'react';
2
2
  import { PrizmaTextField } from '@incodetech/prizma-core-web/text-field';
3
3
  import { PrizmaDropdown } from '@incodetech/prizma-core-web/dropdown';
4
+ import { PrizmaCountryFlag } from '@incodetech/prizma-core-web/combined';
4
5
  import { PrizmaFormScreen, type PrizmaFormScreenProps } from './FormScreen';
5
6
  import type { PrizmaScreenActionHandler, PrizmaScreenOverrides } from './types';
6
7
  import type {
@@ -55,6 +56,14 @@ const DEFAULT_COUNTRIES: PhoneInputCountry[] = [
55
56
  * primitives (`PrizmaDropdown`, `PrizmaTextField`) at the designed 56px min
56
57
  * height and `Border/Radius/small`, so the module layer contributes only the
57
58
  * row that pairs them (`PhoneInputLayout.css`) — no primitive is re-implemented.
59
+ *
60
+ * `emptyError` is the empty-submit state (rule from Elena, 2026-09-30,
61
+ * GUIDELINES §6-E empty-submit rule): "Continue" was pressed with no number
62
+ * typed. The phone field renders empty with no placeholder, in Core's error
63
+ * state with `requiredError`, and the primary CTA is disabled, following the
64
+ * drawn precedent in `FormsFieldScreen`. The dial code always carries a
65
+ * selection, so it takes no error. The resting `default` keeps the CTA enabled
66
+ * so the user can reach this state.
58
67
  */
59
68
  export const PhoneInputVerifyScreen = forwardRef<HTMLDivElement, PhoneInputVerifyScreenProps>(
60
69
  (
@@ -74,9 +83,14 @@ export const PhoneInputVerifyScreen = forwardRef<HTMLDivElement, PhoneInputVerif
74
83
  const selected = country ?? countries[0]?.value;
75
84
  const options = countries.map((c) => ({
76
85
  value: c.value,
77
- // The closed field shows flag + dial code exactly as Figma draws it
78
- // ("🇷🇸 +381"); the open menu row adds the country name.
79
- label: [c.flag, c.dialCode].filter(Boolean).join(' '),
86
+ // The open menu row reads flag + dial code + country name
87
+ // ("+381 Serbia" after the flag); the closed field keeps only flag +
88
+ // dial code ("+381") through Core's `fieldLabel`, as Figma draws it.
89
+ // The flag is not text: it rides the leading slot below (DEVIATIONS
90
+ // #PI-3), so it is Core's raster on every OS instead of an emoji that
91
+ // Windows draws as the letters "RS".
92
+ label: [c.dialCode, c.label].join(' '),
93
+ fieldLabel: c.dialCode,
80
94
  }));
81
95
 
82
96
  // Figma treats the states as three distinct field treatments, not one:
@@ -88,6 +102,9 @@ export const PhoneInputVerifyScreen = forwardRef<HTMLDivElement, PhoneInputVerif
88
102
  // for none of them — it would grey the resting states too (DEVIATIONS #PI-6).
89
103
  const busy = state === 'loading';
90
104
  const focused = state === 'typing';
105
+ // Empty submit: the number field drops its placeholder and takes Core's
106
+ // error state with the shared "required" row; the CTA greys.
107
+ const emptyError = state === 'emptyError';
91
108
 
92
109
  return (
93
110
  <PrizmaFormScreen
@@ -97,6 +114,7 @@ export const PhoneInputVerifyScreen = forwardRef<HTMLDivElement, PhoneInputVerif
97
114
  ctaLabel={strings.verify.ctaLabel}
98
115
  /* Figma's `loading` section swaps the CTA label for Core's spinner. */
99
116
  ctaLoading={busy}
117
+ ctaDisabled={emptyError}
100
118
  ctaLoadingLabel={strings.verify.ctaLoadingLabel}
101
119
  verifiedLabel={strings.verifiedLabel}
102
120
  navLabel={strings.navLabel}
@@ -119,6 +137,22 @@ export const PhoneInputVerifyScreen = forwardRef<HTMLDivElement, PhoneInputVerif
119
137
  searchPlaceholder={strings.verify.countrySearchPlaceholder}
120
138
  /* The `countryCode` section IS the open-menu spec state. */
121
139
  forceOpen={state === 'countryCode'}
140
+ /* Core's canonical flag raster in the closed field and every menu
141
+ row, the WatchlistBusinessFormScreen precedent. A country
142
+ outside the bank falls back to its `flag` emoji. */
143
+ renderLeading={(option) => {
144
+ const match = countries.find((c) => c.value === option.value);
145
+ return match ? (
146
+ <PrizmaCountryFlag
147
+ country={{
148
+ code: match.value,
149
+ name: match.label,
150
+ dialCode: match.dialCode,
151
+ flag: match.flag ?? '',
152
+ }}
153
+ />
154
+ ) : null;
155
+ }}
122
156
  disabled={busy}
123
157
  />
124
158
  <PrizmaTextField
@@ -134,12 +168,14 @@ export const PhoneInputVerifyScreen = forwardRef<HTMLDivElement, PhoneInputVerif
134
168
  type="tel"
135
169
  inputMode="tel"
136
170
  aria-label={strings.verify.fieldLabel}
137
- placeholder={strings.verify.placeholder}
171
+ placeholder={emptyError ? undefined : strings.verify.placeholder}
138
172
  /* Uncontrolled: a screen is one static spec state, and `value`
139
173
  without `onChange` would either warn or force `readOnly`, whose
140
174
  disabled surface is exactly the grey Figma does NOT draw here. */
141
- defaultValue={value ?? ''}
175
+ defaultValue={emptyError ? '' : value ?? ''}
142
176
  disabled={busy}
177
+ error={emptyError}
178
+ errorText={emptyError ? strings.requiredError : undefined}
143
179
  /* Figma draws the clear ✕ only while the field is focused. */
144
180
  clearable={focused}
145
181
  />
@@ -118,6 +118,7 @@ export const QualifiedSignatureAcceptScreen = forwardRef<
118
118
  // `loading` makes the whole form inert.
119
119
  const busy = state === 'loading';
120
120
  const ticked = state !== 'default';
121
+ const requiredHint = strings.accept.requiredHint;
121
122
 
122
123
  return (
123
124
  <PrizmaFormScreen
@@ -177,6 +178,12 @@ export const QualifiedSignatureAcceptScreen = forwardRef<
177
178
  role="group"
178
179
  aria-label={strings.accept.termsLabel}
179
180
  >
181
+ {/* The legend LEADS the group (Elena, 2026-09-30, picked from four
182
+ placements): it sits where the markers are and, on the five-clause
183
+ reading that scrolls, is still on screen on arrival. */}
184
+ {requiredHint && strings.accept.requiredLegend ? (
185
+ <p className="qualified-signature-terms__legend">{strings.accept.requiredLegend}</p>
186
+ ) : null}
180
187
  {terms.map((term) => (
181
188
  <PrizmaCheckbox
182
189
  key={term.id}
@@ -185,7 +192,11 @@ export const QualifiedSignatureAcceptScreen = forwardRef<
185
192
  value={term.id}
186
193
  checked={ticked}
187
194
  disabled={busy}
188
- segments={term.segments}
195
+ required={Boolean(requiredHint)}
196
+ /* The `*` rides the END of the clause copy, the Data Sharing
197
+ Consent idiom — the L card paints no marker of its own beside
198
+ the box (owner, 2026-09-02). */
199
+ segments={requiredHint ? [...term.segments, ` ${requiredHint}`] : term.segments}
189
200
  />
190
201
  ))}
191
202
  </div>
@@ -55,16 +55,21 @@ export interface QualifiedSignatureDocumentScreenProps
55
55
  *
56
56
  * Archetype G is the fit: its `__list` middle is "content filling the space
57
57
  * between the title block and the CTA band", which is exactly what a document
58
- * page is, and both sections carry no CTA at all so the band never renders. The
58
+ * page is, and the band carries only the "Back" secondary (delta 1). The
59
59
  * body is allowed to scroll inside that middle — one module rule RELEASING an
60
60
  * overflow constraint, declaring no spacing or size, the same idiom as CURP's
61
61
  * taller-than-844 form (DEVIATIONS #CURP-1).
62
62
  *
63
- * THREE deltas from the frames, all reported rather than worked around:
64
- * 1. Both sections draw a back chevron at the nav's left (and `terms` replaces
65
- * the logo row with a chevron + heading bar). GUIDELINES §3 stands: SDK
66
- * screens carry no dismiss or back affordance — the standing ruling of
67
- * DEVIATIONS #PI-9, which is not re-litigated per module.
63
+ * THREE deltas from the frames:
64
+ * 1. Both sections draw a back chevron at the nav's left. These are viewers
65
+ * the user opened from the accept screen, so they need a way back (owner
66
+ * call 2026-09-29). It ships as a full-width SECONDARY "Back" button in the
67
+ * CTA band instead, emitting `secondary` (owner call 2026-10-08): the person
68
+ * reads and returns to sign, and a real button at the thumb is easier to
69
+ * find than a chevron. "Back" over "Done" because nothing is finished here;
70
+ * the signature still waits on the accept screen. The nav stays logo-only.
71
+ * `terms` replacing the logo row with a chevron + heading bar is still not
72
+ * followed — the title stays the archetype's H2 below the nav.
68
73
  * 2. `viewdoc` draws NO heading. GUIDELINES §4 is explicit that every screen has
69
74
  * exactly one H2, and archetype G has no title-less path, so the screen
70
75
  * carries one and the Figma frame is the side to change. (An archetype
@@ -73,10 +78,12 @@ export interface QualifiedSignatureDocumentScreenProps
73
78
  * for attribution chrome (#PI-8, #F-30, #F-34), so both variants keep the
74
79
  * header and the footer and nothing shifts between states.
75
80
  *
76
- * `terms` is LEFT-aligned, as Figma draws it: a centred heading over a
77
- * left-aligned wall of legal text reads as a mistake. The alignment rides the
78
- * sanctioned `overrides.align` path (types.ts) — nothing is re-styled here — and
79
- * an explicit host override still wins. The #GEO-2 precedent.
81
+ * Both variants frame the page in the box Electronic Signature's review screen
82
+ * uses, so the title is CENTRED on both, like that screen (owner call
83
+ * 2026-09-29, "take reference from other screens"). Figma's `QSign.T&C` draws
84
+ * the title left over an unframed wall of text; once the text sits in its own
85
+ * left-aligned frame the centred title no longer reads as a mistake. An
86
+ * explicit `overrides.align` from the host still wins.
80
87
  */
81
88
  export const QualifiedSignatureDocumentScreen = forwardRef<
82
89
  HTMLDivElement,
@@ -96,25 +103,30 @@ export const QualifiedSignatureDocumentScreen = forwardRef<
96
103
  ) => {
97
104
  const copy = variant === 'terms' ? strings.document.terms : strings.document.document;
98
105
 
99
- // The legal wall is left-aligned in Figma; the contract page keeps the
100
- // archetype's centred default. An explicit `overrides.align` still wins.
101
- const alignedOverrides: PrizmaScreenOverrides = {
102
- ...overrides,
103
- align: overrides?.align ?? (variant === 'terms' ? 'left' : 'centered'),
104
- };
105
-
106
106
  return (
107
107
  <PrizmaListScreen
108
108
  ref={ref}
109
109
  title={copy.title}
110
110
  verifiedLabel={strings.verifiedLabel}
111
111
  navLabel={strings.navLabel}
112
+ secondaryCtaLabel={strings.document.backLabel}
112
113
  screenId={screenId}
113
- overrides={alignedOverrides}
114
+ overrides={overrides}
114
115
  onAction={onAction}
115
116
  {...shell}
116
117
  >
117
- <div className="qualified-signature-document">{body}</div>
118
+ <div
119
+ className={[
120
+ 'qualified-signature-document',
121
+ 'qualified-signature-document--framed',
122
+ `qualified-signature-document--${variant}`,
123
+ ].join(' ')}
124
+ role="region"
125
+ aria-label={copy.title}
126
+ tabIndex={0}
127
+ >
128
+ {body}
129
+ </div>
118
130
  </PrizmaListScreen>
119
131
  );
120
132
  },
@@ -57,6 +57,26 @@
57
57
  display: flex;
58
58
  flex-direction: column;
59
59
  gap: var(--prizma-spacing-4);
60
+ /* The clauses sit at the BOTTOM of the column, on the primary button, not
61
+ * under the document row (Elena, 2026-09-30). The fields are a flex column,
62
+ * so the auto margin takes the free space; when the clauses outgrow the frame
63
+ * (the five-clause reading) there is none and the stack scrolls as before. */
64
+ margin-top: auto;
65
+ }
66
+
67
+ /* The required legend leading the group. Core ships no caption block outside a
68
+ * field, so the line binds the clause copy's own Size-S tokens in the secondary
69
+ * ink — no new size, no new colour. Spacing/8 more below it than the clauses
70
+ * keep between themselves, so it reads as the group's header. */
71
+ .qualified-signature-terms__legend {
72
+ margin: 0 0 var(--prizma-spacing-8);
73
+ font-family: var(--prizma-checkbox-radio-family);
74
+ font-weight: var(--prizma-checkbox-radio-s-font-weight);
75
+ font-size: var(--prizma-checkbox-radio-s-font-size);
76
+ line-height: var(--prizma-checkbox-radio-s-line-height);
77
+ letter-spacing: var(--prizma-checkbox-radio-s-letter-spacing);
78
+ color: var(--prizma-text-body-500-secondary);
79
+ text-align: left;
60
80
  }
61
81
 
62
82
  /* ── (c) Uploaded-file preview card ──────────────────────────────────────────
@@ -181,6 +201,62 @@
181
201
  overscroll-behavior: contain;
182
202
  }
183
203
 
204
+ /* The document FRAME — the two full-surface document screens draw the page
205
+ * inside the same box Electronic Signature's review screen uses
206
+ * (`.electronic-signature-document`): `Border/Neutral/100` hairline,
207
+ * `Border/Radius/small`, `Surface/Neutral/0`, clipped so the page scrolls INSIDE
208
+ * it. Without it the contract image floated loose on the screen and the legal
209
+ * text ran edge to edge as one grey wall (owner-reported 2026-09-29: "take
210
+ * reference from other screens"). A modifier, not the base class, because the
211
+ * gated-form exploration reuses `.qualified-signature-document` inside its own
212
+ * popup, which already carries a frame.
213
+ *
214
+ * Spacing/16 bottom margin: the gap Electronic Signature's frame keeps above the
215
+ * CTA band, here kept above the anchored tag so the frame never touches it.
216
+ */
217
+ .qualified-signature-document--framed {
218
+ box-sizing: border-box;
219
+ background-color: var(--prizma-surface-neutral-0);
220
+ border: var(--prizma-border-width-regular) solid var(--prizma-border-neutral-100);
221
+ border-radius: var(--prizma-border-radius-small);
222
+ margin-bottom: var(--prizma-spacing-16);
223
+ }
224
+
225
+ /* `terms` — a TEXT agreement needs a gutter inside the frame (Spacing/16) and
226
+ * the paragraph rhythm Electronic Signature uses (Spacing/12 between blocks). */
227
+ .qualified-signature-document--terms {
228
+ padding: var(--prizma-spacing-16);
229
+ text-align: start;
230
+ }
231
+
232
+ .qualified-signature-document--terms > * {
233
+ margin: 0;
234
+ }
235
+
236
+ .qualified-signature-document.qualified-signature-document--terms > * + * {
237
+ /* Doubled class: `__prose` resets `margin: 0` later in this file at the
238
+ same specificity, which would otherwise swallow the rhythm. */
239
+ margin-top: var(--prizma-spacing-12);
240
+ }
241
+
242
+ /* Section headings inside the form — Headline/H5 on Text/Body/800, the same
243
+ * pair as `.electronic-signature-document h4`. */
244
+ .qualified-signature-document--terms h4 {
245
+ font-family: var(--prizma-headline-family);
246
+ font-weight: var(--prizma-headline-h5-font-weight);
247
+ font-size: var(--prizma-headline-h5-font-size);
248
+ line-height: var(--prizma-headline-h5-line-height);
249
+ letter-spacing: var(--prizma-headline-h5-letter-spacing);
250
+ color: var(--prizma-text-body-800-primary);
251
+ }
252
+
253
+ /* Inside the frame the prose drops to Text/Body/500, the secondary ink every
254
+ * block of Core prose uses (`CardList.css`, `Card.css`) — the heading carries
255
+ * the hierarchy, as in Electronic Signature. */
256
+ .qualified-signature-document--terms .qualified-signature-document__prose {
257
+ color: var(--prizma-text-body-500-secondary);
258
+ }
259
+
184
260
  /* The contract page is host-supplied media (an <img> / <canvas> of the rendered
185
261
  * PDF, GUIDELINES §10's `cameraSurface` idiom) — it fills the column width and
186
262
  * keeps its own aspect. */