@simprints/simface-sdk 0.9.1 → 0.11.1
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/README.md +177 -89
- package/dist/components/simface-capture.d.ts +5 -0
- package/dist/components/simface-capture.js +116 -33
- package/dist/components/simface-capture.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +10 -10
- package/dist/index.js.map +1 -1
- package/dist/services/camera.d.ts +3 -3
- package/dist/services/camera.js +17 -30
- package/dist/services/camera.js.map +1 -1
- package/dist/shared/capture-flow.d.ts +13 -5
- package/dist/shared/capture-flow.js +16 -10
- package/dist/shared/capture-flow.js.map +1 -1
- package/dist/simface-sdk.js +1366 -1296
- package/dist/simface-sdk.umd.cjs +103 -47
- package/dist/types/index.d.ts +16 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# SimFace SDK
|
|
2
2
|
|
|
3
|
-
SimFace SDK provides facial recognition for web-based KYC workflows. It
|
|
3
|
+
SimFace SDK provides facial recognition for web-based KYC workflows. It exposes one primary JavaScript API for enrollment and verification, plus a lower-level Web Component for advanced UI control.
|
|
4
4
|
|
|
5
5
|
Works in: all modern browsers, WhatsApp in-app browser, and mobile WebViews.
|
|
6
6
|
|
|
@@ -8,7 +8,20 @@ This repository is the public frontend SDK and demo repo for SimFace. For fronte
|
|
|
8
8
|
|
|
9
9
|
The backend API, infrastructure, and TensorFlow Lite runtime live in the separate private backend repository.
|
|
10
10
|
|
|
11
|
-
The capture flow is planned explicitly as: auto camera -> manual camera -> media picker.
|
|
11
|
+
The capture flow is planned explicitly as: auto camera -> manual camera -> media picker. The primary API supports two UI modes:
|
|
12
|
+
- popup capture: the SDK opens and manages its own modal capture flow
|
|
13
|
+
- embedded capture: the SDK runs capture inside a host-provided `simface-capture` element
|
|
14
|
+
|
|
15
|
+
## Face Quality Checks
|
|
16
|
+
|
|
17
|
+
The SDK automatically performs these checks on captured images before submission:
|
|
18
|
+
|
|
19
|
+
1. Face presence - at least one face must be detected.
|
|
20
|
+
2. Single face - only one face should be in the frame.
|
|
21
|
+
3. Face size - face must not be too close or too far.
|
|
22
|
+
4. Centering - face must be approximately centered in the frame.
|
|
23
|
+
|
|
24
|
+
If a check fails, the user is prompted with specific guidance and asked to retake the photo.
|
|
12
25
|
|
|
13
26
|
## Quick Start
|
|
14
27
|
|
|
@@ -75,55 +88,138 @@ if (result.match) {
|
|
|
75
88
|
}
|
|
76
89
|
```
|
|
77
90
|
|
|
78
|
-
|
|
91
|
+
### 5. Choose the capture UI mode
|
|
79
92
|
|
|
80
|
-
|
|
93
|
+
`enroll()` and `verify()` are the main SDK entry points. They both use the same capture workflow and backend API. The only UI difference is where the capture UI is rendered.
|
|
81
94
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
Both functions accept:
|
|
96
|
+
- `workflowOptions`: optional capture behavior that applies to both popup and embedded flows
|
|
97
|
+
- `captureElement`: optional existing `simface-capture` element; if this argument is present, the SDK uses embedded mode
|
|
85
98
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
99
|
+
#### Popup capture
|
|
100
|
+
|
|
101
|
+
If you omit `captureElement`, the SDK opens its popup capture UI:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
const workflowOptions = {
|
|
105
|
+
capturePreference: 'auto-preferred',
|
|
106
|
+
allowMediaPickerFallback: true,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const enrollResult = await enroll(config, 'unique-user-id', workflowOptions);
|
|
110
|
+
const verifyResult = await verify(config, 'unique-user-id', workflowOptions);
|
|
89
111
|
```
|
|
90
112
|
|
|
91
|
-
|
|
113
|
+
#### Embedded capture
|
|
92
114
|
|
|
93
|
-
|
|
115
|
+
If you want capture inline in your page, create a `simface-capture` element and pass it as `captureElement`. The SDK still owns the capture lifecycle; it just renders the UI inline instead of in a popup.
|
|
94
116
|
|
|
95
|
-
|
|
117
|
+
```html
|
|
118
|
+
<simface-capture
|
|
119
|
+
embedded
|
|
120
|
+
capture-preference="auto-preferred"
|
|
121
|
+
label="Take a selfie for verification"
|
|
122
|
+
idle-feedback-label="Start verification to see camera guidance here."
|
|
123
|
+
capture-label="Snap photo"
|
|
124
|
+
retake-label="Take another"
|
|
125
|
+
confirm-label="Use this photo"
|
|
126
|
+
retry-label="Start over"
|
|
127
|
+
></simface-capture>
|
|
128
|
+
```
|
|
96
129
|
|
|
97
130
|
```javascript
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const result = await enroll(config, 'unique-user-id', {
|
|
101
|
-
presentation: 'embedded',
|
|
102
|
-
container: '#capture-slot',
|
|
131
|
+
const workflowOptions = {
|
|
103
132
|
capturePreference: 'auto-preferred',
|
|
104
133
|
allowMediaPickerFallback: true,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const captureElement = document.querySelector('simface-capture');
|
|
137
|
+
|
|
138
|
+
const enrollResult = await enroll(config, 'unique-user-id', workflowOptions, captureElement);
|
|
139
|
+
const verifyResult = await verify(config, 'unique-user-id', workflowOptions, captureElement);
|
|
108
140
|
```
|
|
109
141
|
|
|
110
|
-
|
|
142
|
+
| workflowOptions | Type | Default | Notes |
|
|
143
|
+
|--------|------|---------|-------|
|
|
144
|
+
| `capturePreference` | `'auto-preferred' \| 'manual-only'` | `'auto-preferred'` | Controls auto vs manual shutter |
|
|
145
|
+
| `allowMediaPickerFallback` | `boolean` | `true` | Falls back to file picker if camera is unavailable |
|
|
146
|
+
|
|
147
|
+
## API Reference
|
|
148
|
+
|
|
149
|
+
### Primary SDK API
|
|
150
|
+
|
|
151
|
+
The main integration surface is:
|
|
152
|
+
- `enroll(config, clientId, workflowOptions?, captureElement?)`
|
|
153
|
+
- `verify(config, clientId, workflowOptions?, captureElement?)`
|
|
154
|
+
|
|
155
|
+
These functions:
|
|
156
|
+
- run the camera capture workflow
|
|
157
|
+
- manage popup or embedded capture UI
|
|
158
|
+
- perform face quality validation
|
|
159
|
+
- call the backend API for enrollment or verification
|
|
160
|
+
|
|
161
|
+
### `enroll(config, clientId, workflowOptions?, captureElement?): Promise<EnrollResult>`
|
|
162
|
+
|
|
163
|
+
Opens the camera, captures a face image with quality validation, and enrolls the user.
|
|
164
|
+
|
|
165
|
+
Parameters:
|
|
166
|
+
|
|
167
|
+
| Parameter | Type | Description |
|
|
168
|
+
|-----------|------|-------------|
|
|
169
|
+
| `config` | `SimFaceConfig` | SDK configuration (`apiUrl`, `projectId`, `apiKey`) |
|
|
170
|
+
| `clientId` | `string` | Unique identifier for the user |
|
|
171
|
+
| `workflowOptions` | `SimFaceWorkflowOptions` | Optional popup/embedded-agnostic capture behavior |
|
|
172
|
+
| `captureElement` | `SimFaceCaptureElement` | Optional embedded `simface-capture` element |
|
|
173
|
+
|
|
174
|
+
Returns: `EnrollResult`
|
|
175
|
+
|
|
176
|
+
### `verify(config, clientId, workflowOptions?, captureElement?): Promise<VerifyResult>`
|
|
177
|
+
|
|
178
|
+
Opens the camera, captures a face image, and verifies against the enrolled face.
|
|
179
|
+
|
|
180
|
+
Parameters:
|
|
181
|
+
|
|
182
|
+
| Parameter | Type | Description |
|
|
183
|
+
|-----------|------|-------------|
|
|
184
|
+
| `config` | `SimFaceConfig` | SDK configuration (`apiUrl`, `projectId`, `apiKey`) |
|
|
185
|
+
| `clientId` | `string` | Unique identifier for the user |
|
|
186
|
+
| `workflowOptions` | `SimFaceWorkflowOptions` | Optional popup/embedded-agnostic capture behavior |
|
|
187
|
+
| `captureElement` | `SimFaceCaptureElement` | Optional embedded `simface-capture` element |
|
|
188
|
+
|
|
189
|
+
Returns: `VerifyResult`
|
|
190
|
+
|
|
191
|
+
### `SimFaceAPIClient` and the backend REST interface
|
|
192
|
+
|
|
193
|
+
`SimFaceAPIClient` is the lower-level HTTP client used internally by `enroll()` and `verify()`. Use it when you want direct control over when capture happens and when backend calls are made.
|
|
111
194
|
|
|
112
|
-
|
|
113
|
-
-
|
|
114
|
-
- `
|
|
115
|
-
-
|
|
116
|
-
- `label` / `confirmLabel`
|
|
195
|
+
Typical cases for using `SimFaceAPIClient` directly:
|
|
196
|
+
- advanced UI flows driven by your own application state
|
|
197
|
+
- direct use of the `simface-capture` component
|
|
198
|
+
- custom orchestration where capture and backend submission happen in separate steps
|
|
117
199
|
|
|
118
|
-
|
|
200
|
+
At a high level:
|
|
201
|
+
- `enroll()` and `verify()` = capture UI + quality checks + backend submission
|
|
202
|
+
- `SimFaceAPIClient` = backend submission only
|
|
119
203
|
|
|
120
|
-
|
|
204
|
+
`SimFaceAPIClient` maps directly to the backend REST interface:
|
|
205
|
+
- `validateAPIKey()` -> `POST /api/v1/auth/validate`
|
|
206
|
+
- `enroll(clientId, imageBlob)` -> `POST /api/v1/enroll`
|
|
207
|
+
- `verify(clientId, imageBlob)` -> `POST /api/v1/verify`
|
|
208
|
+
|
|
209
|
+
## Advanced: Direct `simface-capture` control
|
|
210
|
+
|
|
211
|
+
Use the `simface-capture` Web Component directly when you want the host application to manage capture state itself instead of letting `enroll()` or `verify()` orchestrate it. In this mode, the component is also the source of truth for embedded UI copy.
|
|
121
212
|
|
|
122
213
|
```html
|
|
123
214
|
<simface-capture
|
|
124
215
|
embedded
|
|
125
216
|
capture-preference="auto-preferred"
|
|
126
217
|
label="Take a selfie for verification"
|
|
218
|
+
idle-feedback-label="Start verification to see camera guidance here."
|
|
219
|
+
capture-label="Snap photo"
|
|
220
|
+
retake-label="Take another"
|
|
221
|
+
confirm-label="Use this photo"
|
|
222
|
+
retry-label="Start over"
|
|
127
223
|
></simface-capture>
|
|
128
224
|
|
|
129
225
|
<script type="module">
|
|
@@ -150,16 +246,30 @@ For more control over the UI, use the `<simface-capture>` Web Component directly
|
|
|
150
246
|
captureEl.addEventListener('simface-error', (e) => {
|
|
151
247
|
console.error('Capture error:', e.detail.error);
|
|
152
248
|
});
|
|
249
|
+
|
|
250
|
+
await captureEl.startCapture();
|
|
153
251
|
</script>
|
|
154
252
|
```
|
|
155
253
|
|
|
254
|
+
In this advanced flow:
|
|
255
|
+
1. the host renders the component
|
|
256
|
+
2. the host starts capture with `startCapture()` or by setting `active = true`
|
|
257
|
+
3. the component emits capture events
|
|
258
|
+
4. the host decides what backend call to make with `SimFaceAPIClient`
|
|
259
|
+
|
|
260
|
+
This is more flexible, but it also means the host owns more of the workflow.
|
|
261
|
+
|
|
156
262
|
### Component Attributes
|
|
157
263
|
|
|
158
264
|
| Attribute | Type | Default | Description |
|
|
159
265
|
|-----------|------|---------|-------------|
|
|
160
|
-
| `label` | String | `"
|
|
266
|
+
| `label` | String | `"Capturing Face"` | Primary instructional text shown by the component |
|
|
267
|
+
| `idle-feedback-label` | String | `"Start a capture to see camera guidance here."` | Idle guidance text shown in the feedback area before capture begins |
|
|
161
268
|
| `embedded` | Boolean | `false` | Runs the component inline instead of delegating to the popup capture service |
|
|
162
|
-
| `
|
|
269
|
+
| `capture-label` | String | `"Take photo"` | Manual capture button label |
|
|
270
|
+
| `retake-label` | String | `"Retake"` | Preview retake button label |
|
|
271
|
+
| `confirm-label` | String | `"Accept"` | Confirm button label used in preview state |
|
|
272
|
+
| `retry-label` | String | `"Try again"` | Error-state retry button label |
|
|
163
273
|
| `capture-preference` | `"auto-preferred" \| "manual-only"` | `"auto-preferred"` | Whether auto capture should be preferred or disabled |
|
|
164
274
|
| `allow-media-picker-fallback` | Boolean | `true` | Whether the component may fall back to the media picker if camera capture is unavailable |
|
|
165
275
|
|
|
@@ -171,23 +281,31 @@ For more control over the UI, use the `<simface-capture>` Web Component directly
|
|
|
171
281
|
| `simface-cancelled` | - | Fires when the user cancels the capture flow |
|
|
172
282
|
| `simface-error` | `{ error: string }` | Fires on capture/detection errors |
|
|
173
283
|
|
|
174
|
-
##
|
|
284
|
+
## Type Definitions
|
|
175
285
|
|
|
176
|
-
### `
|
|
286
|
+
### `SimFaceConfig`
|
|
177
287
|
|
|
178
|
-
|
|
288
|
+
```typescript
|
|
289
|
+
interface SimFaceConfig {
|
|
290
|
+
apiUrl: string;
|
|
291
|
+
projectId: string;
|
|
292
|
+
apiKey: string;
|
|
293
|
+
}
|
|
294
|
+
```
|
|
179
295
|
|
|
180
|
-
|
|
296
|
+
### `SimFaceWorkflowOptions`
|
|
181
297
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
298
|
+
```typescript
|
|
299
|
+
interface SimFaceWorkflowOptions {
|
|
300
|
+
capturePreference?: 'auto-preferred' | 'manual-only';
|
|
301
|
+
allowMediaPickerFallback?: boolean;
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### `EnrollResult`
|
|
187
306
|
|
|
188
|
-
Returns `EnrollResult`:
|
|
189
307
|
```typescript
|
|
190
|
-
{
|
|
308
|
+
interface EnrollResult {
|
|
191
309
|
success: boolean;
|
|
192
310
|
clientId: string;
|
|
193
311
|
message?: string;
|
|
@@ -195,21 +313,10 @@ Returns `EnrollResult`:
|
|
|
195
313
|
}
|
|
196
314
|
```
|
|
197
315
|
|
|
198
|
-
### `
|
|
199
|
-
|
|
200
|
-
Opens the camera, captures a face image, and verifies against the enrolled face.
|
|
201
|
-
|
|
202
|
-
Parameters:
|
|
203
|
-
|
|
204
|
-
| Parameter | Type | Description |
|
|
205
|
-
|-----------|------|-------------|
|
|
206
|
-
| `config` | `SimFaceConfig` | SDK configuration (`apiUrl`, `projectId`, `apiKey`) |
|
|
207
|
-
| `clientId` | `string` | Unique identifier for the user |
|
|
208
|
-
| `captureOptions` | `SimFaceCaptureOptions` | Optional capture presentation/fallback overrides |
|
|
316
|
+
### `VerifyResult`
|
|
209
317
|
|
|
210
|
-
Returns `VerifyResult`:
|
|
211
318
|
```typescript
|
|
212
|
-
{
|
|
319
|
+
interface VerifyResult {
|
|
213
320
|
match: boolean;
|
|
214
321
|
score: number;
|
|
215
322
|
threshold: number;
|
|
@@ -218,29 +325,6 @@ Returns `VerifyResult`:
|
|
|
218
325
|
}
|
|
219
326
|
```
|
|
220
327
|
|
|
221
|
-
### `SimFaceConfig`
|
|
222
|
-
|
|
223
|
-
```typescript
|
|
224
|
-
{
|
|
225
|
-
apiUrl: string;
|
|
226
|
-
projectId: string;
|
|
227
|
-
apiKey: string;
|
|
228
|
-
}
|
|
229
|
-
```
|
|
230
|
-
|
|
231
|
-
### `SimFaceCaptureOptions`
|
|
232
|
-
|
|
233
|
-
```typescript
|
|
234
|
-
{
|
|
235
|
-
presentation?: 'popup' | 'embedded';
|
|
236
|
-
capturePreference?: 'auto-preferred' | 'manual-only';
|
|
237
|
-
allowMediaPickerFallback?: boolean;
|
|
238
|
-
container?: HTMLElement | string;
|
|
239
|
-
label?: string;
|
|
240
|
-
confirmLabel?: string;
|
|
241
|
-
}
|
|
242
|
-
```
|
|
243
|
-
|
|
244
328
|
## Backend API Endpoints
|
|
245
329
|
|
|
246
330
|
For clients integrating directly with the REST API:
|
|
@@ -268,17 +352,6 @@ Health check endpoint.
|
|
|
268
352
|
|
|
269
353
|
Response: `{ "status": "ok" }`
|
|
270
354
|
|
|
271
|
-
## Face Quality Checks
|
|
272
|
-
|
|
273
|
-
The SDK automatically performs these checks on captured images before submission:
|
|
274
|
-
|
|
275
|
-
1. Face presence - at least one face must be detected.
|
|
276
|
-
2. Single face - only one face should be in the frame.
|
|
277
|
-
3. Face size - face must not be too close or too far.
|
|
278
|
-
4. Centering - face must be approximately centered in the frame.
|
|
279
|
-
|
|
280
|
-
If a check fails, the user is prompted with specific guidance and asked to retake the photo.
|
|
281
|
-
|
|
282
355
|
## Browser Compatibility
|
|
283
356
|
|
|
284
357
|
| Browser | Camera Capture | Face Detection |
|
|
@@ -290,3 +363,18 @@ If a check fails, the user is prompted with specific guidance and asked to retak
|
|
|
290
363
|
| Samsung Internet | Yes | Yes |
|
|
291
364
|
|
|
292
365
|
> Note: In WhatsApp's in-app browser, the camera opens via the device's native camera app rather than an in-browser preview. Face quality checks run after the photo is taken.
|
|
366
|
+
|
|
367
|
+
## Try the local demo
|
|
368
|
+
|
|
369
|
+
Build the SDK at the repository root, then run the demo:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
npm install
|
|
373
|
+
npm run build
|
|
374
|
+
|
|
375
|
+
cd demo
|
|
376
|
+
npm install
|
|
377
|
+
npm run dev
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
The demo runs at `http://localhost:4173` and consumes the built SDK artifact from `dist/`. To enable HTTPS (required for camera access from other devices on the local network), set `DEMO_USE_HTTPS=true` before starting the demo.
|
|
@@ -10,9 +10,13 @@ import type { CapturePreference } from '../types/index.js';
|
|
|
10
10
|
*/
|
|
11
11
|
export declare class SimFaceCapture extends LitElement {
|
|
12
12
|
label: string;
|
|
13
|
+
idleFeedbackLabel: string;
|
|
13
14
|
embedded: boolean;
|
|
14
15
|
active: boolean;
|
|
15
16
|
confirmLabel: string;
|
|
17
|
+
captureLabel: string;
|
|
18
|
+
retakeLabel: string;
|
|
19
|
+
retryLabel: string;
|
|
16
20
|
capturePreference: CapturePreference;
|
|
17
21
|
allowMediaPickerFallback: boolean;
|
|
18
22
|
private captureState;
|
|
@@ -31,6 +35,7 @@ export declare class SimFaceCapture extends LitElement {
|
|
|
31
35
|
private pendingActiveSync;
|
|
32
36
|
static styles: import("lit").CSSResult;
|
|
33
37
|
disconnectedCallback(): void;
|
|
38
|
+
protected willUpdate(changedProperties: Map<string, unknown>): void;
|
|
34
39
|
updated(changedProperties: Map<string, unknown>): void;
|
|
35
40
|
render(): import("lit-html").TemplateResult<1>;
|
|
36
41
|
startCapture(): Promise<void>;
|
|
@@ -8,7 +8,7 @@ import { LitElement, html, css } from 'lit';
|
|
|
8
8
|
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
9
9
|
import { assessFaceQuality } from '../services/face-detection.js';
|
|
10
10
|
import { CAPTURE_GUIDE_MASK_PATH, CAPTURE_GUIDE_PATH, } from '../shared/auto-capture.js';
|
|
11
|
-
import { buildCapturePlan, normalizeCaptureOptions, resolveCaptureCapabilities, } from '../shared/capture-flow.js';
|
|
11
|
+
import { buildCapturePlan, normalizeCaptureOptions, resolveCaptureCapabilities, DEFAULT_CAPTURE_LABEL, DEFAULT_IDLE_FEEDBACK_LABEL, DEFAULT_LABEL, DEFAULT_CONFIRM_LABEL, DEFAULT_RETAKE_LABEL, DEFAULT_RETRY_LABEL, } from '../shared/capture-flow.js';
|
|
12
12
|
import { CameraCaptureSessionController, } from '../shared/capture-session.js';
|
|
13
13
|
import { CameraAccessError, blobToImage, captureFromFileInput, openUserFacingCameraStream, } from '../shared/capture-runtime.js';
|
|
14
14
|
/**
|
|
@@ -22,15 +22,19 @@ import { CameraAccessError, blobToImage, captureFromFileInput, openUserFacingCam
|
|
|
22
22
|
let SimFaceCapture = class SimFaceCapture extends LitElement {
|
|
23
23
|
constructor() {
|
|
24
24
|
super(...arguments);
|
|
25
|
-
this.label =
|
|
25
|
+
this.label = DEFAULT_LABEL;
|
|
26
|
+
this.idleFeedbackLabel = DEFAULT_IDLE_FEEDBACK_LABEL;
|
|
26
27
|
this.embedded = false;
|
|
27
28
|
this.active = false;
|
|
28
|
-
this.confirmLabel =
|
|
29
|
+
this.confirmLabel = DEFAULT_CONFIRM_LABEL;
|
|
30
|
+
this.captureLabel = DEFAULT_CAPTURE_LABEL;
|
|
31
|
+
this.retakeLabel = DEFAULT_RETAKE_LABEL;
|
|
32
|
+
this.retryLabel = DEFAULT_RETRY_LABEL;
|
|
29
33
|
this.capturePreference = 'auto-preferred';
|
|
30
34
|
this.allowMediaPickerFallback = true;
|
|
31
35
|
this.captureState = 'idle';
|
|
32
36
|
this.errorMessage = '';
|
|
33
|
-
this.feedbackMessage =
|
|
37
|
+
this.feedbackMessage = DEFAULT_IDLE_FEEDBACK_LABEL;
|
|
34
38
|
this.feedbackTone = 'neutral';
|
|
35
39
|
this.previewUrl = '';
|
|
36
40
|
this.qualityResult = null;
|
|
@@ -46,6 +50,11 @@ let SimFaceCapture = class SimFaceCapture extends LitElement {
|
|
|
46
50
|
super.disconnectedCallback();
|
|
47
51
|
this.stopSession();
|
|
48
52
|
}
|
|
53
|
+
willUpdate(changedProperties) {
|
|
54
|
+
if (changedProperties.has('idleFeedbackLabel') && this.captureState === 'idle') {
|
|
55
|
+
this.feedbackMessage = this.idleFeedbackLabel;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
49
58
|
updated(changedProperties) {
|
|
50
59
|
if (!changedProperties.has('active') || this.pendingActiveSync) {
|
|
51
60
|
return;
|
|
@@ -78,11 +87,20 @@ let SimFaceCapture = class SimFaceCapture extends LitElement {
|
|
|
78
87
|
await this.beginCapture();
|
|
79
88
|
}
|
|
80
89
|
renderCaptureState() {
|
|
90
|
+
const showClose = this.captureState !== 'idle';
|
|
81
91
|
return html `
|
|
82
|
-
|
|
92
|
+
${showClose
|
|
93
|
+
? html `<button class="close-btn" data-simface-action="cancel" @click=${this.handleCancel} aria-label="Close">
|
|
94
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
95
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
96
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
97
|
+
</svg>
|
|
98
|
+
</button>`
|
|
99
|
+
: ''}
|
|
83
100
|
|
|
101
|
+
<p class="capture-copy">${this.label}</p>
|
|
84
102
|
${this.captureState === 'idle'
|
|
85
|
-
?
|
|
103
|
+
? ''
|
|
86
104
|
: html `
|
|
87
105
|
<div class="stage">
|
|
88
106
|
<video
|
|
@@ -117,24 +135,21 @@ let SimFaceCapture = class SimFaceCapture extends LitElement {
|
|
|
117
135
|
${this.captureState === 'live'
|
|
118
136
|
? html `
|
|
119
137
|
${this.captureMode === 'manual'
|
|
120
|
-
? html `<button class="btn btn-
|
|
138
|
+
? html `<button class="btn btn-primary" data-simface-action="capture" ?disabled=${!this.canTakePhoto} @click=${this.handleManualCapture}>${this.captureLabel}</button>`
|
|
121
139
|
: ''}
|
|
122
|
-
<button class="btn btn-ghost" data-simface-action="cancel" @click=${this.handleCancel}>Cancel</button>
|
|
123
140
|
`
|
|
124
141
|
: ''}
|
|
125
142
|
${this.captureState === 'preview'
|
|
126
143
|
? html `
|
|
127
|
-
<button class="btn btn-
|
|
144
|
+
<button class="btn btn-retake" data-simface-action="retake" @click=${this.handleRetake}>${this.retakeLabel}</button>
|
|
128
145
|
${this.qualityResult?.passesQualityChecks === false
|
|
129
146
|
? ''
|
|
130
|
-
: html `<button class="btn btn-
|
|
131
|
-
<button class="btn btn-ghost" data-simface-action="cancel" @click=${this.handleCancel}>Cancel</button>
|
|
147
|
+
: html `<button class="btn btn-confirm" data-simface-action="confirm" @click=${this.handleConfirm}>${this.confirmLabel}</button>`}
|
|
132
148
|
`
|
|
133
149
|
: ''}
|
|
134
150
|
${this.captureState === 'error'
|
|
135
151
|
? html `
|
|
136
|
-
<button class="btn btn-primary" data-simface-action="retry" @click=${this.beginCapture}
|
|
137
|
-
<button class="btn btn-ghost" data-simface-action="cancel" @click=${this.handleCancel}>Cancel</button>
|
|
152
|
+
<button class="btn btn-primary" data-simface-action="retry" @click=${this.beginCapture}>${this.retryLabel}</button>
|
|
138
153
|
`
|
|
139
154
|
: ''}
|
|
140
155
|
</div>
|
|
@@ -150,12 +165,15 @@ let SimFaceCapture = class SimFaceCapture extends LitElement {
|
|
|
150
165
|
this.feedbackMessage = 'Requesting camera access...';
|
|
151
166
|
this.feedbackTone = 'neutral';
|
|
152
167
|
const options = normalizeCaptureOptions({
|
|
153
|
-
presentation: 'embedded',
|
|
154
168
|
capturePreference: this.capturePreference,
|
|
155
169
|
allowMediaPickerFallback: this.allowMediaPickerFallback,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
}, this);
|
|
171
|
+
options.label = this.label;
|
|
172
|
+
options.idleFeedbackLabel = this.idleFeedbackLabel;
|
|
173
|
+
options.confirmLabel = this.confirmLabel;
|
|
174
|
+
options.captureLabel = this.captureLabel;
|
|
175
|
+
options.retakeLabel = this.retakeLabel;
|
|
176
|
+
options.retryLabel = this.retryLabel;
|
|
159
177
|
const capabilities = await resolveCaptureCapabilities({
|
|
160
178
|
capturePreference: options.capturePreference,
|
|
161
179
|
});
|
|
@@ -321,7 +339,7 @@ let SimFaceCapture = class SimFaceCapture extends LitElement {
|
|
|
321
339
|
this.clearPreviewUrl();
|
|
322
340
|
this.captureState = 'idle';
|
|
323
341
|
this.errorMessage = '';
|
|
324
|
-
this.feedbackMessage =
|
|
342
|
+
this.feedbackMessage = this.idleFeedbackLabel;
|
|
325
343
|
this.feedbackTone = 'neutral';
|
|
326
344
|
this.syncProgress(0);
|
|
327
345
|
this.qualityResult = null;
|
|
@@ -396,25 +414,56 @@ SimFaceCapture.styles = css `
|
|
|
396
414
|
:host([embedded]) {
|
|
397
415
|
max-width: none;
|
|
398
416
|
margin: 0;
|
|
399
|
-
text-align: left;
|
|
400
417
|
}
|
|
401
418
|
|
|
402
419
|
.container {
|
|
420
|
+
position: relative;
|
|
403
421
|
padding: 16px;
|
|
404
422
|
border: 1px solid #e0e0e0;
|
|
405
|
-
border-radius:
|
|
423
|
+
border-radius: 16px;
|
|
406
424
|
background: #fafafa;
|
|
407
425
|
}
|
|
408
426
|
|
|
427
|
+
.close-btn {
|
|
428
|
+
position: absolute;
|
|
429
|
+
top: 12px;
|
|
430
|
+
left: 12px;
|
|
431
|
+
z-index: 10;
|
|
432
|
+
display: inline-flex;
|
|
433
|
+
align-items: center;
|
|
434
|
+
justify-content: center;
|
|
435
|
+
width: 40px;
|
|
436
|
+
height: 40px;
|
|
437
|
+
padding: 0;
|
|
438
|
+
border: none;
|
|
439
|
+
border-radius: 50%;
|
|
440
|
+
background: rgba(0, 0, 0, 0.06);
|
|
441
|
+
color: #49454f;
|
|
442
|
+
cursor: pointer;
|
|
443
|
+
transition: background-color 0.15s;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.close-btn:hover {
|
|
447
|
+
background: rgba(0, 0, 0, 0.12);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.close-btn svg {
|
|
451
|
+
width: 20px;
|
|
452
|
+
height: 20px;
|
|
453
|
+
}
|
|
454
|
+
|
|
409
455
|
.capture-shell {
|
|
410
456
|
display: flex;
|
|
411
457
|
flex-direction: column;
|
|
458
|
+
align-items: center;
|
|
412
459
|
gap: 16px;
|
|
413
460
|
}
|
|
414
461
|
|
|
415
462
|
.capture-copy {
|
|
416
463
|
margin: 0;
|
|
417
464
|
color: #334155;
|
|
465
|
+
text-align: center;
|
|
466
|
+
width: 100%;
|
|
418
467
|
}
|
|
419
468
|
|
|
420
469
|
.stage {
|
|
@@ -427,7 +476,6 @@ SimFaceCapture.styles = css `
|
|
|
427
476
|
radial-gradient(circle at top, rgba(56, 189, 248, 0.16), transparent 30%),
|
|
428
477
|
linear-gradient(180deg, #0f172a, #020617);
|
|
429
478
|
box-shadow: inset 0 0 0 1px rgba(148, 163, 184, 0.2);
|
|
430
|
-
align-self: center;
|
|
431
479
|
}
|
|
432
480
|
|
|
433
481
|
.video,
|
|
@@ -483,22 +531,23 @@ SimFaceCapture.styles = css `
|
|
|
483
531
|
|
|
484
532
|
.btn-row {
|
|
485
533
|
display: flex;
|
|
486
|
-
flex-wrap: wrap;
|
|
487
534
|
gap: 12px;
|
|
535
|
+
width: min(100%, 420px);
|
|
488
536
|
}
|
|
489
537
|
|
|
490
538
|
.btn {
|
|
491
539
|
display: inline-flex;
|
|
492
540
|
align-items: center;
|
|
493
541
|
justify-content: center;
|
|
494
|
-
|
|
495
|
-
|
|
542
|
+
flex: 1;
|
|
543
|
+
padding: 14px 24px;
|
|
496
544
|
border: none;
|
|
497
|
-
border-radius:
|
|
498
|
-
font-size:
|
|
545
|
+
border-radius: 100px;
|
|
546
|
+
font-size: 15px;
|
|
499
547
|
font-weight: 600;
|
|
548
|
+
letter-spacing: 0.02em;
|
|
500
549
|
cursor: pointer;
|
|
501
|
-
transition: background-color 0.
|
|
550
|
+
transition: background-color 0.15s, box-shadow 0.15s;
|
|
502
551
|
}
|
|
503
552
|
|
|
504
553
|
.btn-primary {
|
|
@@ -508,11 +557,33 @@ SimFaceCapture.styles = css `
|
|
|
508
557
|
|
|
509
558
|
.btn-primary:hover {
|
|
510
559
|
background: #1d4ed8;
|
|
560
|
+
box-shadow: 0 1px 3px rgba(37, 99, 235, 0.3);
|
|
511
561
|
}
|
|
512
562
|
|
|
513
563
|
.btn-primary:disabled {
|
|
514
564
|
background: #93c5fd;
|
|
515
565
|
cursor: not-allowed;
|
|
566
|
+
box-shadow: none;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.btn-confirm {
|
|
570
|
+
background: #16a34a;
|
|
571
|
+
color: white;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.btn-confirm:hover {
|
|
575
|
+
background: #15803d;
|
|
576
|
+
box-shadow: 0 1px 3px rgba(22, 163, 74, 0.3);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
.btn-retake {
|
|
580
|
+
background: #dc2626;
|
|
581
|
+
color: white;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.btn-retake:hover {
|
|
585
|
+
background: #b91c1c;
|
|
586
|
+
box-shadow: 0 1px 3px rgba(220, 38, 38, 0.3);
|
|
516
587
|
}
|
|
517
588
|
|
|
518
589
|
.btn-secondary {
|
|
@@ -524,17 +595,17 @@ SimFaceCapture.styles = css `
|
|
|
524
595
|
background: #d1d5db;
|
|
525
596
|
}
|
|
526
597
|
|
|
527
|
-
.btn-ghost {
|
|
528
|
-
background: #e2e8f0;
|
|
529
|
-
color: #0f172a;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
598
|
.quality-msg {
|
|
533
599
|
padding: 10px 14px;
|
|
534
600
|
border-radius: 14px;
|
|
535
|
-
margin: 8px 0 0;
|
|
536
601
|
font-size: 14px;
|
|
537
602
|
font-weight: 600;
|
|
603
|
+
width: min(100%, 420px);
|
|
604
|
+
min-height: 44px;
|
|
605
|
+
display: flex;
|
|
606
|
+
align-items: center;
|
|
607
|
+
justify-content: center;
|
|
608
|
+
text-align: center;
|
|
538
609
|
}
|
|
539
610
|
|
|
540
611
|
.quality-good {
|
|
@@ -581,6 +652,9 @@ SimFaceCapture.styles = css `
|
|
|
581
652
|
__decorate([
|
|
582
653
|
property({ type: String })
|
|
583
654
|
], SimFaceCapture.prototype, "label", void 0);
|
|
655
|
+
__decorate([
|
|
656
|
+
property({ type: String, attribute: 'idle-feedback-label' })
|
|
657
|
+
], SimFaceCapture.prototype, "idleFeedbackLabel", void 0);
|
|
584
658
|
__decorate([
|
|
585
659
|
property({ type: Boolean, reflect: true })
|
|
586
660
|
], SimFaceCapture.prototype, "embedded", void 0);
|
|
@@ -590,6 +664,15 @@ __decorate([
|
|
|
590
664
|
__decorate([
|
|
591
665
|
property({ type: String, attribute: 'confirm-label' })
|
|
592
666
|
], SimFaceCapture.prototype, "confirmLabel", void 0);
|
|
667
|
+
__decorate([
|
|
668
|
+
property({ type: String, attribute: 'capture-label' })
|
|
669
|
+
], SimFaceCapture.prototype, "captureLabel", void 0);
|
|
670
|
+
__decorate([
|
|
671
|
+
property({ type: String, attribute: 'retake-label' })
|
|
672
|
+
], SimFaceCapture.prototype, "retakeLabel", void 0);
|
|
673
|
+
__decorate([
|
|
674
|
+
property({ type: String, attribute: 'retry-label' })
|
|
675
|
+
], SimFaceCapture.prototype, "retryLabel", void 0);
|
|
593
676
|
__decorate([
|
|
594
677
|
property({ type: String, attribute: 'capture-preference' })
|
|
595
678
|
], SimFaceCapture.prototype, "capturePreference", void 0);
|