@smileid/web-components 11.4.5 → 11.5.0
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/esm/{DocumentCaptureScreens-D2G0NOQr.js → DocumentCaptureScreens-ucJDu5nH.js} +555 -2470
- package/dist/esm/DocumentCaptureScreens-ucJDu5nH.js.map +1 -0
- package/dist/esm/{EndUserConsent-uHfA3txP.js → EndUserConsent-CsiwoThZ.js} +3 -3
- package/dist/esm/{EndUserConsent-uHfA3txP.js.map → EndUserConsent-CsiwoThZ.js.map} +1 -1
- package/dist/esm/{Navigation-Bb7MPLE8.js → Navigation-Xg565kcu.js} +28 -22
- package/dist/esm/Navigation-Xg565kcu.js.map +1 -0
- package/dist/esm/SelfieCaptureScreens-D3KuMzZA.js +11471 -0
- package/dist/esm/SelfieCaptureScreens-D3KuMzZA.js.map +1 -0
- package/dist/esm/{TotpConsent-Depzg0ti.js → TotpConsent-CRtmtudl.js} +2 -2
- package/dist/esm/{TotpConsent-Depzg0ti.js.map → TotpConsent-CRtmtudl.js.map} +1 -1
- package/dist/esm/combobox.js +1 -1
- package/dist/esm/document.js +1 -1
- package/dist/esm/end-user-consent.js +1 -1
- package/dist/esm/index-CUwa6MPI.js +1363 -0
- package/dist/esm/{index-C4RTMbgw.js.map → index-CUwa6MPI.js.map} +1 -1
- package/dist/esm/localisation.js +1 -1
- package/dist/esm/main.js +6 -6
- package/dist/esm/navigation.js +1 -1
- package/dist/esm/package-BmVbDNny.js +2535 -0
- package/dist/esm/package-BmVbDNny.js.map +1 -0
- package/dist/esm/selfie.js +1 -1
- package/dist/esm/smart-camera-web.js +67 -37
- package/dist/esm/smart-camera-web.js.map +1 -1
- package/dist/esm/totp-consent.js +1 -1
- package/dist/smart-camera-web.js +877 -122
- package/dist/smart-camera-web.js.map +1 -1
- package/dist/types/main.d.ts +11 -0
- package/lib/components/navigation/src/Navigation.js +27 -8
- package/lib/components/selfie/src/SelfieCaptureScreens.js +56 -8
- package/lib/components/selfie/src/enhanced-smartselfie-capture/EnhancedSmartSelfieCapture.tsx +684 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/EnhancedSmartSelfieConsent.tsx +71 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/EnhancedSmartSelfieSubmission.tsx +181 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/OvalProgress.tsx +87 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/Icon.svg +8 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/accessories.svg +77 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/active_liveness_animation.lottie +0 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/device.svg +12 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/device_orientation.lottie +0 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/good.svg +52 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/id-card.svg +9 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/illustrations.tsx +852 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/instructions-img.svg +3 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/multiple-faces.svg +69 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/person.svg +6 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/phone.svg +8 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/poor-lighting.svg +53 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/assets/too_dark_animation.lottie +0 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/ActiveLivenessOverlay.tsx +226 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/AlertDisplay.tsx +38 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/BackNavigation.tsx +45 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/CameraPreview.tsx +96 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/CaptureControls.tsx +97 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/CaptureGuidelines.tsx +374 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/ConsentView.tsx +460 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/SubmissionView.tsx +426 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/components/index.ts +3 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/constants.ts +23 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/hooks/index.ts +2 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/hooks/useCamera.ts +238 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/hooks/useFaceCapture.ts +1075 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/index.ts +1 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/alertMessages.ts +20 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/canvas.ts +108 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/faceDetection.ts +545 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/imageCapture.ts +66 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/imageQuality.ts +151 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/index.ts +5 -0
- package/lib/components/selfie/src/enhanced-smartselfie-capture/utils/mediapipeManager.ts +215 -0
- package/lib/components/selfie/src/selfie-capture-wrapper/SelfieCaptureWrapper.tsx +24 -1
- package/lib/components/selfie/src/smartselfie-capture/SmartSelfieCapture.tsx +2 -2
- package/lib/components/selfie/src/smartselfie-capture/hooks/useFaceCapture.ts +15 -7
- package/lib/components/selfie/src/smartselfie-capture/utils/canvas.ts +4 -6
- package/lib/components/signature-pad/package.json +1 -1
- package/lib/components/smart-camera-web/src/SmartCameraWeb.js +64 -7
- package/lib/domain/localisation/index.js +2 -2
- package/package.json +2 -2
- package/dist/esm/DocumentCaptureScreens-D2G0NOQr.js.map +0 -1
- package/dist/esm/Navigation-Bb7MPLE8.js.map +0 -1
- package/dist/esm/SelfieCaptureScreens-Dr7VzON7.js +0 -7651
- package/dist/esm/SelfieCaptureScreens-Dr7VzON7.js.map +0 -1
- package/dist/esm/index-C4RTMbgw.js +0 -1360
- package/dist/esm/package-D6YrpMcO.js +0 -565
- package/dist/esm/package-D6YrpMcO.js.map +0 -1
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import type { FunctionComponent } from 'preact';
|
|
2
|
+
import { t } from '../../../../../domain/localisation';
|
|
3
|
+
|
|
4
|
+
export type SubmissionViewMode = 'review' | 'submitting' | 'success' | 'error';
|
|
5
|
+
|
|
6
|
+
interface SubmissionViewProps {
|
|
7
|
+
imageSrc: string;
|
|
8
|
+
mirror: boolean;
|
|
9
|
+
themeColor: string;
|
|
10
|
+
hideAttribution: boolean;
|
|
11
|
+
mode: SubmissionViewMode;
|
|
12
|
+
/** Optional supporting copy under the title (e.g. failure reason). */
|
|
13
|
+
message?: string;
|
|
14
|
+
onConfirm?: () => void;
|
|
15
|
+
onRetake?: () => void;
|
|
16
|
+
onContinue?: () => void;
|
|
17
|
+
onExit?: () => void;
|
|
18
|
+
onBack?: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const TickIcon: FunctionComponent = () => (
|
|
22
|
+
<svg
|
|
23
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
24
|
+
width="64"
|
|
25
|
+
height="64"
|
|
26
|
+
viewBox="0 0 64 64"
|
|
27
|
+
fill="none"
|
|
28
|
+
aria-hidden="true"
|
|
29
|
+
>
|
|
30
|
+
<rect width="64" height="64" rx="32" fill="#2CC05C" />
|
|
31
|
+
<path
|
|
32
|
+
d="M27.1566 42.6663C26.4724 42.6663 25.7882 42.4088 25.2481 41.8568L19.4503 35.9324C18.9481 35.4131 18.6664 34.7123 18.6664 33.9821C18.6664 33.252 18.9481 32.5512 19.4503 32.0319C20.4946 30.9647 22.2232 30.9647 23.2675 32.0319L27.1566 36.006L40.7327 22.1334C41.777 21.0662 43.5055 21.0662 44.5498 22.1334C45.5941 23.2005 45.5941 24.9668 44.5498 26.0339L29.0652 41.8568C28.525 42.4088 27.8408 42.6663 27.1566 42.6663Z"
|
|
33
|
+
fill="white"
|
|
34
|
+
/>
|
|
35
|
+
</svg>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const CrossIcon: FunctionComponent = () => (
|
|
39
|
+
<svg
|
|
40
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
41
|
+
width="64"
|
|
42
|
+
height="64"
|
|
43
|
+
viewBox="0 0 64 64"
|
|
44
|
+
fill="none"
|
|
45
|
+
aria-hidden="true"
|
|
46
|
+
>
|
|
47
|
+
<rect width="64" height="64" rx="32" fill="#EC221F" />
|
|
48
|
+
<path
|
|
49
|
+
d="M36.1146 31.9953L45.0425 22.6828C46.1682 21.5086 46.1682 19.5651 45.0425 18.3909C44.4947 17.8261 43.7555 17.5094 42.9852 17.5094C42.215 17.5094 41.4758 17.8261 40.9279 18.3909L32 27.7035L23.0721 18.3909C22.5242 17.8261 21.785 17.5094 21.0148 17.5094C20.2445 17.5094 19.5053 17.8261 18.9575 18.3909C17.8318 19.5651 17.8318 21.5086 18.9575 22.6828L27.8854 31.9953L18.9575 41.3079C17.8318 42.4821 17.8318 44.4256 18.9575 45.5998C19.5397 46.2071 20.2773 46.4906 21.0148 46.4906C21.7523 46.4906 22.4898 46.2071 23.0721 45.5998L32 36.2872L40.9279 45.5998C41.5102 46.2071 42.2477 46.4906 42.9852 46.4906C43.7227 46.4906 44.4603 46.2071 45.0425 45.5998C46.1682 44.4256 46.1682 42.4821 45.0425 41.3079L36.1146 31.9953Z"
|
|
50
|
+
fill="white"
|
|
51
|
+
/>
|
|
52
|
+
</svg>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
export const SubmissionView: FunctionComponent<SubmissionViewProps> = ({
|
|
56
|
+
imageSrc,
|
|
57
|
+
mirror,
|
|
58
|
+
themeColor,
|
|
59
|
+
hideAttribution,
|
|
60
|
+
mode,
|
|
61
|
+
message,
|
|
62
|
+
onConfirm,
|
|
63
|
+
onRetake,
|
|
64
|
+
onContinue,
|
|
65
|
+
onExit,
|
|
66
|
+
}) => {
|
|
67
|
+
const isReview = mode === 'review';
|
|
68
|
+
const isSubmitting = mode === 'submitting';
|
|
69
|
+
const isSuccess = mode === 'success';
|
|
70
|
+
const isError = mode === 'error';
|
|
71
|
+
|
|
72
|
+
let title = '';
|
|
73
|
+
let body = '';
|
|
74
|
+
if (isReview) {
|
|
75
|
+
title = t('selfie.ess.submission.review.title');
|
|
76
|
+
body = t('selfie.ess.submission.review.body');
|
|
77
|
+
} else if (isSubmitting) {
|
|
78
|
+
title = t('selfie.ess.submission.submitting.title');
|
|
79
|
+
} else if (isSuccess) {
|
|
80
|
+
title = t('selfie.ess.submission.success.title');
|
|
81
|
+
body = message || t('selfie.ess.submission.success.body');
|
|
82
|
+
} else {
|
|
83
|
+
title = t('selfie.ess.submission.error.title');
|
|
84
|
+
body = message || '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="enhanced-submission">
|
|
89
|
+
<div className="submission-oval">
|
|
90
|
+
<img
|
|
91
|
+
src={imageSrc}
|
|
92
|
+
alt={t('selfie.ess.submission.imageAlt')}
|
|
93
|
+
style={mirror ? { transform: 'scaleX(-1)' } : undefined}
|
|
94
|
+
/>
|
|
95
|
+
{isSubmitting && (
|
|
96
|
+
<div className="spinner" aria-hidden="true">
|
|
97
|
+
<svg
|
|
98
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
99
|
+
width="64"
|
|
100
|
+
height="64"
|
|
101
|
+
viewBox="0 0 64 64"
|
|
102
|
+
fill="none"
|
|
103
|
+
>
|
|
104
|
+
<g clip-path="url(#ess-submit-loader-clip)">
|
|
105
|
+
<foreignObject
|
|
106
|
+
x="-1031.25"
|
|
107
|
+
y="-1031.25"
|
|
108
|
+
width="2062.5"
|
|
109
|
+
height="2062.5"
|
|
110
|
+
transform="matrix(0.032 0 0 0.032 32 32)"
|
|
111
|
+
>
|
|
112
|
+
<div
|
|
113
|
+
{...{ xmlns: 'http://www.w3.org/1999/xhtml' }}
|
|
114
|
+
style="background:conic-gradient(from 90deg,rgba(39,174,96,0) 0deg,rgba(58,225,128,0) 0.036deg,rgba(58,225,128,1) 360deg);height:100%;width:100%;opacity:1"
|
|
115
|
+
/>
|
|
116
|
+
</foreignObject>
|
|
117
|
+
</g>
|
|
118
|
+
<path
|
|
119
|
+
fill-rule="evenodd"
|
|
120
|
+
clip-rule="evenodd"
|
|
121
|
+
d="M60.751 25.6018C62.2117 25.4134 63.5486 26.4447 63.737 27.9053C63.9122 29.2632 64 30.6309 64 31.9999C64 33.4727 62.8061 34.6666 61.3334 34.6666C59.8606 34.6666 58.6667 33.4727 58.6667 31.9999C58.6667 30.859 58.5935 29.7193 58.4475 28.5878C58.2591 27.1271 59.2904 25.7903 60.751 25.6018Z"
|
|
122
|
+
fill="#2CC05C"
|
|
123
|
+
/>
|
|
124
|
+
<defs>
|
|
125
|
+
<clipPath id="ess-submit-loader-clip">
|
|
126
|
+
<path
|
|
127
|
+
fill-rule="evenodd"
|
|
128
|
+
clip-rule="evenodd"
|
|
129
|
+
d="M32 64C49.6731 64 64 49.6731 64 32C64 14.3269 49.6731 0 32 0C14.3269 0 0 14.3269 0 32C0 49.6731 14.3269 64 32 64ZM32 58.6667C46.7276 58.6667 58.6667 46.7276 58.6667 32C58.6667 17.2724 46.7276 5.33333 32 5.33333C17.2724 5.33333 5.33333 17.2724 5.33333 32C5.33333 46.7276 17.2724 58.6667 32 58.6667Z"
|
|
130
|
+
/>
|
|
131
|
+
</clipPath>
|
|
132
|
+
</defs>
|
|
133
|
+
</svg>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
{(isSuccess || isError) && (
|
|
137
|
+
<div
|
|
138
|
+
className={`badge ${isSuccess ? 'badge-success' : 'badge-error'}`}
|
|
139
|
+
>
|
|
140
|
+
{isSuccess ? <TickIcon /> : <CrossIcon />}
|
|
141
|
+
</div>
|
|
142
|
+
)}
|
|
143
|
+
</div>
|
|
144
|
+
|
|
145
|
+
<div className="submission-card">
|
|
146
|
+
<h2 style={{ color: themeColor }}>{title}</h2>
|
|
147
|
+
{isSubmitting ? (
|
|
148
|
+
<p>
|
|
149
|
+
{t('selfie.ess.submission.submitting.body')
|
|
150
|
+
.split('\n')
|
|
151
|
+
.map((line, i, arr) => (
|
|
152
|
+
<>
|
|
153
|
+
{line}
|
|
154
|
+
{i < arr.length - 1 && <br />}
|
|
155
|
+
</>
|
|
156
|
+
))}
|
|
157
|
+
</p>
|
|
158
|
+
) : (
|
|
159
|
+
body && <p>{body}</p>
|
|
160
|
+
)}
|
|
161
|
+
|
|
162
|
+
{isReview && (
|
|
163
|
+
<div className="actions">
|
|
164
|
+
<button
|
|
165
|
+
type="button"
|
|
166
|
+
className="retake"
|
|
167
|
+
onClick={onRetake}
|
|
168
|
+
aria-label={t('selfie.ess.submission.review.retake')}
|
|
169
|
+
>
|
|
170
|
+
<span className="icon">
|
|
171
|
+
<svg
|
|
172
|
+
width="26"
|
|
173
|
+
height="26"
|
|
174
|
+
viewBox="0 0 24 24"
|
|
175
|
+
fill="none"
|
|
176
|
+
stroke="#90A1B9"
|
|
177
|
+
stroke-width="2"
|
|
178
|
+
stroke-linecap="round"
|
|
179
|
+
stroke-linejoin="round"
|
|
180
|
+
>
|
|
181
|
+
<polyline points="1 4 1 10 7 10" />
|
|
182
|
+
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10" />
|
|
183
|
+
</svg>
|
|
184
|
+
</span>
|
|
185
|
+
<span className="label">
|
|
186
|
+
{t('selfie.ess.submission.review.retake')}
|
|
187
|
+
</span>
|
|
188
|
+
</button>
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
className="confirm"
|
|
192
|
+
onClick={onConfirm}
|
|
193
|
+
aria-label={t('selfie.ess.submission.review.confirm')}
|
|
194
|
+
>
|
|
195
|
+
<span className="icon">
|
|
196
|
+
<svg
|
|
197
|
+
width="28"
|
|
198
|
+
height="28"
|
|
199
|
+
viewBox="0 0 24 24"
|
|
200
|
+
fill="none"
|
|
201
|
+
stroke="#12B76A"
|
|
202
|
+
stroke-width="2.5"
|
|
203
|
+
stroke-linecap="round"
|
|
204
|
+
stroke-linejoin="round"
|
|
205
|
+
>
|
|
206
|
+
<polyline points="20 6 9 17 4 12" />
|
|
207
|
+
</svg>
|
|
208
|
+
</span>
|
|
209
|
+
<span className="label confirm-label">
|
|
210
|
+
{t('selfie.ess.submission.review.confirm')}
|
|
211
|
+
</span>
|
|
212
|
+
</button>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
{(isSuccess || isError) && (
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
className="primary"
|
|
220
|
+
style={{ background: themeColor }}
|
|
221
|
+
onClick={onContinue}
|
|
222
|
+
>
|
|
223
|
+
{t('selfie.ess.submission.continue')}
|
|
224
|
+
</button>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{isError && onExit && (
|
|
228
|
+
<button
|
|
229
|
+
type="button"
|
|
230
|
+
className="secondary"
|
|
231
|
+
style={{ color: themeColor, borderColor: themeColor }}
|
|
232
|
+
onClick={onExit}
|
|
233
|
+
>
|
|
234
|
+
{t('selfie.ess.submission.exit')}
|
|
235
|
+
</button>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
{!hideAttribution && (
|
|
240
|
+
// @ts-expect-error preact-custom-element types
|
|
241
|
+
<powered-by-smile-id />
|
|
242
|
+
)}
|
|
243
|
+
|
|
244
|
+
<style>{`
|
|
245
|
+
:host { display: block; height: 100%; }
|
|
246
|
+
.enhanced-submission {
|
|
247
|
+
display: flex;
|
|
248
|
+
flex-direction: column;
|
|
249
|
+
align-items: center;
|
|
250
|
+
height: 100%;
|
|
251
|
+
overflow: hidden;
|
|
252
|
+
padding: 1rem clamp(1rem, 4vw, 1.5rem) clamp(1rem, 3dvh, 1.5rem);
|
|
253
|
+
background: #F8FAFC;
|
|
254
|
+
font-family: "DM Sans", system-ui, sans-serif;
|
|
255
|
+
box-sizing: border-box;
|
|
256
|
+
}
|
|
257
|
+
.enhanced-submission > powered-by-smile-id {
|
|
258
|
+
display: block;
|
|
259
|
+
width: 100%;
|
|
260
|
+
margin-top: auto;
|
|
261
|
+
padding: 0;
|
|
262
|
+
background: #F8FAFC;
|
|
263
|
+
flex-shrink: 0;
|
|
264
|
+
}
|
|
265
|
+
.submission-oval {
|
|
266
|
+
position: relative;
|
|
267
|
+
width: clamp(180px, 36dvh, 260px);
|
|
268
|
+
max-width: 80%;
|
|
269
|
+
aspect-ratio: 3 / 4;
|
|
270
|
+
overflow: hidden;
|
|
271
|
+
border-radius: 50%;
|
|
272
|
+
margin: clamp(0.5rem, 1.5dvh, 1rem) auto;
|
|
273
|
+
background: #ddd;
|
|
274
|
+
flex-shrink: 0;
|
|
275
|
+
}
|
|
276
|
+
.submission-oval img {
|
|
277
|
+
width: 100%;
|
|
278
|
+
height: 100%;
|
|
279
|
+
object-fit: cover;
|
|
280
|
+
}
|
|
281
|
+
.submission-oval .spinner {
|
|
282
|
+
position: absolute;
|
|
283
|
+
top: 50%;
|
|
284
|
+
left: 50%;
|
|
285
|
+
transform: translate(-50%, -50%);
|
|
286
|
+
animation: ess-submit-spin 1.1s linear infinite;
|
|
287
|
+
}
|
|
288
|
+
@keyframes ess-submit-spin {
|
|
289
|
+
to { transform: translate(-50%, -50%) rotate(360deg); }
|
|
290
|
+
}
|
|
291
|
+
.badge {
|
|
292
|
+
position: absolute;
|
|
293
|
+
top: 50%;
|
|
294
|
+
left: 50%;
|
|
295
|
+
transform: translate(-50%, -50%);
|
|
296
|
+
width: 64px;
|
|
297
|
+
height: 64px;
|
|
298
|
+
border-radius: 50%;
|
|
299
|
+
display: inline-flex;
|
|
300
|
+
align-items: center;
|
|
301
|
+
justify-content: center;
|
|
302
|
+
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
|
|
303
|
+
}
|
|
304
|
+
.badge-success { background: #12B76A; }
|
|
305
|
+
.badge-error { background: #E5484D; }
|
|
306
|
+
|
|
307
|
+
.submission-card {
|
|
308
|
+
width: 327px;
|
|
309
|
+
max-width: 100%;
|
|
310
|
+
min-height: 204px;
|
|
311
|
+
margin-top: clamp(0.75rem, 2.5dvh, 1.25rem);
|
|
312
|
+
padding: 16px 16px 20px;
|
|
313
|
+
border-radius: 16px;
|
|
314
|
+
border: 1px solid #F1F5F9;
|
|
315
|
+
background: #FFF;
|
|
316
|
+
box-shadow: 0 -12px 48px 0 rgba(0, 0, 0, 0.06);
|
|
317
|
+
text-align: center;
|
|
318
|
+
box-sizing: border-box;
|
|
319
|
+
display: flex;
|
|
320
|
+
flex-direction: column;
|
|
321
|
+
align-items: stretch;
|
|
322
|
+
justify-content: center;
|
|
323
|
+
gap: 8px;
|
|
324
|
+
}
|
|
325
|
+
.submission-card h2 {
|
|
326
|
+
margin: 0;
|
|
327
|
+
font-family: "DM Sans", system-ui, sans-serif;
|
|
328
|
+
font-size: 20px;
|
|
329
|
+
font-style: normal;
|
|
330
|
+
font-weight: 800;
|
|
331
|
+
line-height: 25.5px;
|
|
332
|
+
letter-spacing: -0.425px;
|
|
333
|
+
text-align: center;
|
|
334
|
+
}
|
|
335
|
+
.submission-card p {
|
|
336
|
+
margin: 0;
|
|
337
|
+
font-family: "DM Sans", system-ui, sans-serif;
|
|
338
|
+
font-size: 14px;
|
|
339
|
+
font-style: normal;
|
|
340
|
+
font-weight: 500;
|
|
341
|
+
line-height: 1.4;
|
|
342
|
+
text-align: center;
|
|
343
|
+
color: #62748E;
|
|
344
|
+
}
|
|
345
|
+
.submission-card h2 + p {
|
|
346
|
+
margin-top: -4px;
|
|
347
|
+
}
|
|
348
|
+
.actions {
|
|
349
|
+
display: flex;
|
|
350
|
+
gap: 40px;
|
|
351
|
+
justify-content: center;
|
|
352
|
+
margin-top: 4px;
|
|
353
|
+
}
|
|
354
|
+
.actions button {
|
|
355
|
+
width: 80px;
|
|
356
|
+
display: flex;
|
|
357
|
+
flex-direction: column;
|
|
358
|
+
align-items: center;
|
|
359
|
+
gap: 0.5rem;
|
|
360
|
+
background: transparent;
|
|
361
|
+
border: none;
|
|
362
|
+
cursor: pointer;
|
|
363
|
+
padding: 0;
|
|
364
|
+
font-family: inherit;
|
|
365
|
+
}
|
|
366
|
+
.actions .icon {
|
|
367
|
+
display: flex;
|
|
368
|
+
width: 80px;
|
|
369
|
+
height: 80px;
|
|
370
|
+
justify-content: center;
|
|
371
|
+
align-items: center;
|
|
372
|
+
flex-shrink: 0;
|
|
373
|
+
border-radius: 24px;
|
|
374
|
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.10), 0 1px 2px -1px rgba(0, 0, 0, 0.10);
|
|
375
|
+
transition: transform 120ms ease, background 120ms ease;
|
|
376
|
+
}
|
|
377
|
+
.actions button:active .icon { transform: scale(0.96); }
|
|
378
|
+
.actions .retake .icon {
|
|
379
|
+
background: #F8FAFC;
|
|
380
|
+
border: 1px solid #E2E8F0;
|
|
381
|
+
}
|
|
382
|
+
.actions .confirm .icon {
|
|
383
|
+
background: rgba(4, 176, 74, 0.10);
|
|
384
|
+
border: 1px solid rgba(4, 176, 74, 0.30);
|
|
385
|
+
}
|
|
386
|
+
.actions .label {
|
|
387
|
+
font-family: "DM Sans", system-ui, sans-serif;
|
|
388
|
+
font-size: 10px;
|
|
389
|
+
font-style: normal;
|
|
390
|
+
font-weight: 700;
|
|
391
|
+
line-height: 15px;
|
|
392
|
+
text-transform: capitalize;
|
|
393
|
+
text-align: center;
|
|
394
|
+
color: #90A1B9;
|
|
395
|
+
}
|
|
396
|
+
.actions .confirm-label {
|
|
397
|
+
color: #12B76A;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.primary,
|
|
401
|
+
.secondary {
|
|
402
|
+
margin: 8px 0 0;
|
|
403
|
+
height: 48px;
|
|
404
|
+
padding: 10px 16px;
|
|
405
|
+
border-radius: 2.5rem;
|
|
406
|
+
font-size: 1.05rem;
|
|
407
|
+
font-weight: 600;
|
|
408
|
+
font-family: inherit;
|
|
409
|
+
cursor: pointer;
|
|
410
|
+
box-sizing: border-box;
|
|
411
|
+
width: 100%;
|
|
412
|
+
}
|
|
413
|
+
.primary {
|
|
414
|
+
color: white;
|
|
415
|
+
border: none;
|
|
416
|
+
}
|
|
417
|
+
.secondary {
|
|
418
|
+
background: transparent;
|
|
419
|
+
border: 1.5px solid;
|
|
420
|
+
}
|
|
421
|
+
`}</style>
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
export default SubmissionView;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The type of image submitted in the job request
|
|
3
|
+
* @readonly
|
|
4
|
+
* @enum {number}
|
|
5
|
+
*/
|
|
6
|
+
export const ImageType = {
|
|
7
|
+
/** ID_CARD_BACK_IMAGE_BASE64 Base64 encoded back of ID card image (.jpg or .png) */
|
|
8
|
+
ID_CARD_BACK_IMAGE_BASE64: 7,
|
|
9
|
+
/** ID_CARD_BACK_IMAGE_FILE Back of ID card image in .png or .jpg file format */
|
|
10
|
+
ID_CARD_BACK_IMAGE_FILE: 5,
|
|
11
|
+
/** ID_CARD_IMAGE_BASE64 Base64 encoded ID card image (.png or .jpg) */
|
|
12
|
+
ID_CARD_IMAGE_BASE64: 3,
|
|
13
|
+
/** ID_CARD_IMAGE_FILE ID card image in .png or .jpg file format */
|
|
14
|
+
ID_CARD_IMAGE_FILE: 1,
|
|
15
|
+
/** LIVENESS_IMAGE_BASE64 Base64 encoded liveness image (.jpg or .png) */
|
|
16
|
+
LIVENESS_IMAGE_BASE64: 6,
|
|
17
|
+
/** LIVENESS_IMAGE_FILE Liveness image in .png or .jpg file format */
|
|
18
|
+
LIVENESS_IMAGE_FILE: 4,
|
|
19
|
+
/** SELFIE_IMAGE_BASE64 Base64 encoded selfie image (.png or .jpg) */
|
|
20
|
+
SELFIE_IMAGE_BASE64: 2,
|
|
21
|
+
/** SELFIE_IMAGE_FILE Selfie image in .png or .jpg file format */
|
|
22
|
+
SELFIE_IMAGE_FILE: 0,
|
|
23
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { useRef, useState, useEffect } from 'preact/hooks';
|
|
2
|
+
|
|
3
|
+
export const useCamera = (initialFacingMode: CameraFacingMode = 'user') => {
|
|
4
|
+
const videoRef = useRef<HTMLVideoElement>(null);
|
|
5
|
+
const streamRef = useRef<MediaStream | null>(null);
|
|
6
|
+
const [facingMode, setFacingMode] = useState(initialFacingMode);
|
|
7
|
+
const [agentSupported, setAgentSupported] = useState(false);
|
|
8
|
+
const onCameraSwitchCallbackRef = useRef<(() => void) | null>(null);
|
|
9
|
+
const isSwitchingCameraRef = useRef(false);
|
|
10
|
+
const timeoutIdsRef = useRef<Set<NodeJS.Timeout>>(new Set());
|
|
11
|
+
|
|
12
|
+
const registerCameraSwitchCallback = (callback: () => void) => {
|
|
13
|
+
onCameraSwitchCallbackRef.current = callback;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const video = videoRef.current;
|
|
18
|
+
if (!video) return undefined;
|
|
19
|
+
|
|
20
|
+
const handleVideoReady = () => {
|
|
21
|
+
if (isSwitchingCameraRef.current && onCameraSwitchCallbackRef.current) {
|
|
22
|
+
const timeoutId = setTimeout(() => {
|
|
23
|
+
onCameraSwitchCallbackRef.current?.();
|
|
24
|
+
isSwitchingCameraRef.current = false;
|
|
25
|
+
timeoutIdsRef.current.delete(timeoutId);
|
|
26
|
+
}, 100);
|
|
27
|
+
timeoutIdsRef.current.add(timeoutId);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
video.addEventListener('loadedmetadata', handleVideoReady);
|
|
32
|
+
|
|
33
|
+
return () => {
|
|
34
|
+
video.removeEventListener('loadedmetadata', handleVideoReady);
|
|
35
|
+
timeoutIdsRef.current.forEach((timeoutId) => clearTimeout(timeoutId));
|
|
36
|
+
timeoutIdsRef.current.clear();
|
|
37
|
+
};
|
|
38
|
+
}, [videoRef.current?.src]);
|
|
39
|
+
|
|
40
|
+
useEffect(
|
|
41
|
+
() => () => {
|
|
42
|
+
timeoutIdsRef.current.forEach((timeoutId) => clearTimeout(timeoutId));
|
|
43
|
+
timeoutIdsRef.current.clear();
|
|
44
|
+
},
|
|
45
|
+
[],
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const startCamera = async (
|
|
49
|
+
targetFacingMode?: CameraFacingMode,
|
|
50
|
+
callback?: (cameraName?: string) => void,
|
|
51
|
+
) => {
|
|
52
|
+
try {
|
|
53
|
+
if (streamRef.current) {
|
|
54
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
55
|
+
streamRef.current = null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (videoRef.current) {
|
|
59
|
+
videoRef.current.srcObject = null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const stream = await navigator.mediaDevices.getUserMedia({
|
|
63
|
+
video: { facingMode: targetFacingMode || facingMode },
|
|
64
|
+
});
|
|
65
|
+
streamRef.current = stream;
|
|
66
|
+
|
|
67
|
+
const track = stream.getVideoTracks()[0];
|
|
68
|
+
const settings = track.getSettings();
|
|
69
|
+
const actualFacingMode = settings.facingMode as
|
|
70
|
+
| CameraFacingMode
|
|
71
|
+
| undefined;
|
|
72
|
+
|
|
73
|
+
const requestedFacingMode = targetFacingMode || facingMode;
|
|
74
|
+
|
|
75
|
+
if (actualFacingMode && actualFacingMode !== requestedFacingMode) {
|
|
76
|
+
setFacingMode(actualFacingMode);
|
|
77
|
+
} else if (actualFacingMode && actualFacingMode !== facingMode) {
|
|
78
|
+
setFacingMode(actualFacingMode);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
82
|
+
const videoDevice = devices.find(
|
|
83
|
+
(device) =>
|
|
84
|
+
device.kind === 'videoinput' &&
|
|
85
|
+
stream.getVideoTracks()[0].getSettings().deviceId === device.deviceId,
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
callback?.(videoDevice?.label);
|
|
89
|
+
|
|
90
|
+
if (videoRef.current) {
|
|
91
|
+
videoRef.current.srcObject = stream;
|
|
92
|
+
await videoRef.current.play();
|
|
93
|
+
// Video ready callback will be handled by useEffect
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('Failed to start camera:', error);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const switchCamera = async () => {
|
|
101
|
+
const newFacingMode = facingMode === 'user' ? 'environment' : 'user';
|
|
102
|
+
isSwitchingCameraRef.current = true;
|
|
103
|
+
|
|
104
|
+
const previousFacingMode = facingMode;
|
|
105
|
+
try {
|
|
106
|
+
setFacingMode(newFacingMode);
|
|
107
|
+
if (streamRef.current) {
|
|
108
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
109
|
+
streamRef.current = null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await startCamera(newFacingMode);
|
|
113
|
+
} catch {
|
|
114
|
+
setFacingMode(previousFacingMode);
|
|
115
|
+
isSwitchingCameraRef.current = false;
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
await startCamera(previousFacingMode);
|
|
119
|
+
} catch (restoreError) {
|
|
120
|
+
console.error('Failed to restore previous camera:', restoreError);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const detectBrowserEngine = () => {
|
|
126
|
+
const userAgent = navigator.userAgent.toLowerCase();
|
|
127
|
+
|
|
128
|
+
const isGecko =
|
|
129
|
+
userAgent.includes('firefox') ||
|
|
130
|
+
(userAgent.includes('gecko') &&
|
|
131
|
+
!userAgent.includes('chrome') &&
|
|
132
|
+
!userAgent.includes('edge'));
|
|
133
|
+
|
|
134
|
+
const hasFirefoxFeatures =
|
|
135
|
+
'mozInnerScreenX' in window ||
|
|
136
|
+
'mozInputSource' in window ||
|
|
137
|
+
'mozPaintCount' in window ||
|
|
138
|
+
typeof (window as any).InstallTrigger !== 'undefined';
|
|
139
|
+
|
|
140
|
+
const supportsMozCSS =
|
|
141
|
+
CSS.supports &&
|
|
142
|
+
(CSS.supports('-moz-appearance', 'none') ||
|
|
143
|
+
CSS.supports('-moz-user-select', 'none'));
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
isGecko: isGecko || hasFirefoxFeatures || supportsMozCSS,
|
|
147
|
+
isChromium:
|
|
148
|
+
userAgent.includes('chrome') ||
|
|
149
|
+
userAgent.includes('chromium') ||
|
|
150
|
+
userAgent.includes('edge'),
|
|
151
|
+
isWebKit: userAgent.includes('webkit') && !userAgent.includes('chrome'),
|
|
152
|
+
};
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const checkAgentSupport = async () => {
|
|
156
|
+
try {
|
|
157
|
+
const isMobile =
|
|
158
|
+
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
|
159
|
+
navigator.userAgent,
|
|
160
|
+
) ||
|
|
161
|
+
(navigator.maxTouchPoints && navigator.maxTouchPoints > 1);
|
|
162
|
+
|
|
163
|
+
// mobile devices generally support both cameras
|
|
164
|
+
// also, ios crashes if we try to check for cameras
|
|
165
|
+
if (isMobile) {
|
|
166
|
+
setAgentSupported(true);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const { isGecko } = detectBrowserEngine();
|
|
171
|
+
|
|
172
|
+
let userCameraId: string | null = null;
|
|
173
|
+
let environmentCameraId: string | null = null;
|
|
174
|
+
|
|
175
|
+
// test if we can get a user-facing camera
|
|
176
|
+
try {
|
|
177
|
+
const userStream = await navigator.mediaDevices.getUserMedia({
|
|
178
|
+
video: { facingMode: 'user' },
|
|
179
|
+
});
|
|
180
|
+
userCameraId =
|
|
181
|
+
userStream.getVideoTracks()[0].getSettings().deviceId ?? null;
|
|
182
|
+
userStream.getTracks().forEach((track) => track.stop());
|
|
183
|
+
} catch {
|
|
184
|
+
// no user-facing camera available
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// test if we can get an environment-facing camera
|
|
188
|
+
try {
|
|
189
|
+
const envStream = await navigator.mediaDevices.getUserMedia({
|
|
190
|
+
video: { facingMode: 'environment' },
|
|
191
|
+
});
|
|
192
|
+
environmentCameraId =
|
|
193
|
+
envStream.getVideoTracks()[0].getSettings().deviceId ?? null;
|
|
194
|
+
envStream.getTracks().forEach((track) => track.stop());
|
|
195
|
+
} catch {
|
|
196
|
+
// no environment-facing camera available
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const hasBothCameras =
|
|
200
|
+
userCameraId &&
|
|
201
|
+
environmentCameraId &&
|
|
202
|
+
userCameraId !== environmentCameraId;
|
|
203
|
+
|
|
204
|
+
if (!hasBothCameras) {
|
|
205
|
+
setAgentSupported(false);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setAgentSupported(!isGecko);
|
|
210
|
+
} catch {
|
|
211
|
+
setAgentSupported(false);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const stopCamera = () => {
|
|
216
|
+
if (streamRef.current) {
|
|
217
|
+
streamRef.current.getTracks().forEach((track) => track.stop());
|
|
218
|
+
streamRef.current = null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (videoRef.current) {
|
|
222
|
+
videoRef.current.srcObject = null;
|
|
223
|
+
videoRef.current.load();
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
videoRef,
|
|
229
|
+
streamRef,
|
|
230
|
+
facingMode,
|
|
231
|
+
agentSupported,
|
|
232
|
+
startCamera,
|
|
233
|
+
switchCamera,
|
|
234
|
+
checkAgentSupport,
|
|
235
|
+
stopCamera,
|
|
236
|
+
registerCameraSwitchCallback,
|
|
237
|
+
};
|
|
238
|
+
};
|