@41devs/naya-id-angular 0.1.1 → 0.4.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/README.md +84 -193
- package/fesm2022/41devs-naya-id-angular.mjs +197 -1
- package/fesm2022/41devs-naya-id-angular.mjs.map +1 -1
- package/index.d.ts +88 -198
- package/package.json +5 -5
- package/fesm2022/41devs-naya-id-angular-41devs-naya-id-angular-B7t92Ogc.mjs +0 -292
- package/fesm2022/41devs-naya-id-angular-41devs-naya-id-angular-B7t92Ogc.mjs.map +0 -1
- package/fesm2022/41devs-naya-id-angular-naya-flow.launcher-xIwcsV6g.mjs +0 -2066
- package/fesm2022/41devs-naya-id-angular-naya-flow.launcher-xIwcsV6g.mjs.map +0 -1
|
@@ -1,2066 +0,0 @@
|
|
|
1
|
-
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, Injectable, input, ChangeDetectionStrategy, Component, output, inject, computed, viewChild, HostListener, HostBinding, createComponent } from '@angular/core';
|
|
3
|
-
import { N as NayaApiClient, d as documentNeedsVerso, c as countryLabel, a as NayaApiError, e as errorResult } from './41devs-naya-id-angular-41devs-naya-id-angular-B7t92Ogc.mjs';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Chaînes de l'interface, français et anglais. Reprise fidèle du fichier
|
|
7
|
-
* Flutter `naya_strings.dart` — mêmes intitulés, mêmes étapes.
|
|
8
|
-
*/
|
|
9
|
-
class NayaStrings {
|
|
10
|
-
locale;
|
|
11
|
-
constructor(locale) {
|
|
12
|
-
this.locale = locale;
|
|
13
|
-
}
|
|
14
|
-
t(fr, en) {
|
|
15
|
-
return this.locale === 'fr' ? fr : en;
|
|
16
|
-
}
|
|
17
|
-
// Commun
|
|
18
|
-
get poweredBy() { return this.t('Propulsé par Naya ID (41Devs)', 'Powered by Naya ID (41Devs)'); }
|
|
19
|
-
get continueLabel() { return this.t('Continuer', 'Continue'); }
|
|
20
|
-
get backToApp() { return this.t("Retourner à l'application", 'Back to the app'); }
|
|
21
|
-
// 1 · Bienvenue
|
|
22
|
-
get welcomeTitle() { return this.t('Vérifiez votre identité', 'Verify your identity'); }
|
|
23
|
-
get welcomeSubtitle() {
|
|
24
|
-
return this.t("Prêt en moins de 60 secondes.\nMunissez-vous de votre pièce d'identité.", 'Ready in under 60 seconds.\nHave your ID document at hand.');
|
|
25
|
-
}
|
|
26
|
-
get welcomeCta() { return this.t('Commencer', 'Get started'); }
|
|
27
|
-
get welcomeSecurity() {
|
|
28
|
-
return this.t('Vos données sont chiffrées et transmises de façon sécurisée', 'Your data is encrypted and transmitted securely');
|
|
29
|
-
}
|
|
30
|
-
get stepDocument() { return this.t('Document', 'Document'); }
|
|
31
|
-
get stepSelfie() { return this.t('Selfie', 'Selfie'); }
|
|
32
|
-
get stepResult() { return this.t('Résultat', 'Result'); }
|
|
33
|
-
// 2 · Choix du document
|
|
34
|
-
get docSelectTitle() { return this.t('Quel document\navez-vous ?', 'Which document\ndo you have?'); }
|
|
35
|
-
get docSelectSubtitle() {
|
|
36
|
-
return this.t("Choisissez votre pays et votre pièce d'identité.", 'Choose your country and your ID document.');
|
|
37
|
-
}
|
|
38
|
-
docLabel(doc) {
|
|
39
|
-
switch (doc) {
|
|
40
|
-
case 'cni': return this.t("Carte Nationale d'Identité", 'National Identity Card');
|
|
41
|
-
case 'passeport': return this.t('Passeport', 'Passport');
|
|
42
|
-
case 'permis': return this.t('Permis de conduire', 'Driving licence');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
// 3-4 · Capture
|
|
46
|
-
get captureRectoInstruction() {
|
|
47
|
-
return this.t('Cadrez le recto de votre document dans le rectangle', 'Frame the front of your document inside the rectangle');
|
|
48
|
-
}
|
|
49
|
-
get captureVersoInstruction() {
|
|
50
|
-
return this.t('Retournez la carte — la bande MRZ doit être lisible', 'Flip the card — the MRZ strip must be readable');
|
|
51
|
-
}
|
|
52
|
-
get captureHint() {
|
|
53
|
-
return this.t('Appuyez sur le déclencheur quand le cadre est vert', 'Tap the shutter when the frame turns green');
|
|
54
|
-
}
|
|
55
|
-
get captureReady() {
|
|
56
|
-
return this.t('Image nette — appuyez pour capturer', 'Sharp image — tap to capture');
|
|
57
|
-
}
|
|
58
|
-
get qualityLight() { return this.t('Luminosité', 'Lighting'); }
|
|
59
|
-
get qualitySharpness() { return this.t('Netteté', 'Sharpness'); }
|
|
60
|
-
// 5 · Liveness
|
|
61
|
-
get livenessTitle() { return this.t('Un selfie rapide', 'A quick selfie'); }
|
|
62
|
-
get livenessSubtitle() { return this.t("Placez votre visage dans l'ovale.", 'Place your face in the oval.'); }
|
|
63
|
-
get livenessLookStraight() { return this.t('Regardez droit vers la caméra', 'Look straight at the camera'); }
|
|
64
|
-
get livenessBlink() { return this.t('Clignez des yeux', 'Blink your eyes'); }
|
|
65
|
-
get livenessTurnHead() { return this.t('Tournez lentement la tête', 'Slowly turn your head'); }
|
|
66
|
-
get livenessHold() { return this.t('Ne bougez plus…', 'Hold still…'); }
|
|
67
|
-
get livenessStepFace() { return this.t('Visage', 'Face'); }
|
|
68
|
-
get livenessStepBlink() { return this.t('Clignement', 'Blink'); }
|
|
69
|
-
get livenessStepTurn() { return this.t('Rotation', 'Turn'); }
|
|
70
|
-
get livenessPrivacy() {
|
|
71
|
-
return this.t("L'analyse s'effectue sur votre appareil —\nla photo n'est envoyée qu'à la fin du défi", 'Analysis runs on your device —\nthe photo is only sent once the challenge is done');
|
|
72
|
-
}
|
|
73
|
-
// 6 · Traitement
|
|
74
|
-
get processingDocument() { return this.t('Document analysé', 'Document analyzed'); }
|
|
75
|
-
get processingFace() { return this.t('Visage comparé', 'Face compared'); }
|
|
76
|
-
get processingCompliance() { return this.t('Contrôles de conformité', 'Compliance checks'); }
|
|
77
|
-
get processingDecision() { return this.t('Décision', 'Decision'); }
|
|
78
|
-
get processingWait() { return this.t('Ne fermez pas la fenêtre', "Don't close the window"); }
|
|
79
|
-
// 7 · Résultats
|
|
80
|
-
get resultApprovedTitle() { return this.t('Identité vérifiée !', 'Identity verified!'); }
|
|
81
|
-
get resultApprovedSubtitle() {
|
|
82
|
-
return this.t('Votre vérification a été transmise avec succès.', 'Your verification was submitted successfully.');
|
|
83
|
-
}
|
|
84
|
-
get resultReference() { return this.t('Référence', 'Reference'); }
|
|
85
|
-
get resultName() { return this.t('Nom complet', 'Full name'); }
|
|
86
|
-
get resultReviewTitle() { return this.t("Vérification en cours d'examen", 'Verification under review'); }
|
|
87
|
-
get resultReviewSubtitle() {
|
|
88
|
-
return this.t('Un analyste examine votre dossier.', 'An analyst is reviewing your file.');
|
|
89
|
-
}
|
|
90
|
-
get resultReviewB1() { return this.t('Réponse généralement sous 24 h', 'Usually answered within 24 h'); }
|
|
91
|
-
get resultReviewB2() { return this.t("Vous serez notifié par l'application", 'The app will notify you'); }
|
|
92
|
-
get resultReviewB3() { return this.t('Aucune action requise de votre part', 'No action needed from you'); }
|
|
93
|
-
get resultRejectedTitle() { return this.t('Vérification non aboutie', 'Verification unsuccessful'); }
|
|
94
|
-
get resultRejectedReason() { return this.t('Motif', 'Reason'); }
|
|
95
|
-
get retry() { return this.t('Réessayer', 'Try again'); }
|
|
96
|
-
// Erreurs
|
|
97
|
-
get errorTitle() { return this.t('Une erreur est survenue', 'Something went wrong'); }
|
|
98
|
-
get errorNetwork() {
|
|
99
|
-
return this.t('Vérifiez votre connexion internet puis réessayez.', 'Check your internet connection and try again.');
|
|
100
|
-
}
|
|
101
|
-
get errorCamera() {
|
|
102
|
-
return this.t("Impossible d'accéder à la caméra. Vérifiez les autorisations du navigateur.", 'Cannot access the camera. Check the browser permissions.');
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* État partagé du parcours et logique de session — équivalent du
|
|
108
|
-
* `NayaFlowController` Flutter, l'état exposé en **signals** plutôt qu'en champs
|
|
109
|
-
* mutables. Un service, instancié par parcours (fourni par l'hôte), non
|
|
110
|
-
* singleton racine.
|
|
111
|
-
*
|
|
112
|
-
* Aucune dépendance à la vue : ce service est entièrement testable seul, ce qui
|
|
113
|
-
* est l'intérêt de le porter en premier.
|
|
114
|
-
*/
|
|
115
|
-
class NayaFlowService {
|
|
116
|
-
api;
|
|
117
|
-
config;
|
|
118
|
-
finish;
|
|
119
|
-
/** Chaînes localisées, dérivées de la config. */
|
|
120
|
-
strings;
|
|
121
|
-
// ── État réactif ───────────────────────────────────────────────────────────
|
|
122
|
-
step = signal('welcome', ...(ngDevMode ? [{ debugName: "step" }] : []));
|
|
123
|
-
session = signal(null, ...(ngDevMode ? [{ debugName: "session" }] : []));
|
|
124
|
-
country = signal(null, ...(ngDevMode ? [{ debugName: "country" }] : []));
|
|
125
|
-
document = signal(null, ...(ngDevMode ? [{ debugName: "document" }] : []));
|
|
126
|
-
rectoBase64 = signal(null, ...(ngDevMode ? [{ debugName: "rectoBase64" }] : []));
|
|
127
|
-
versoBase64 = signal(null, ...(ngDevMode ? [{ debugName: "versoBase64" }] : []));
|
|
128
|
-
livenessScore = signal(0, ...(ngDevMode ? [{ debugName: "livenessScore" }] : []));
|
|
129
|
-
selfieBase64 = signal(null, ...(ngDevMode ? [{ debugName: "selfieBase64" }] : []));
|
|
130
|
-
attemptsUsed = signal(0, ...(ngDevMode ? [{ debugName: "attemptsUsed" }] : []));
|
|
131
|
-
/** Réponse du submit, transmise à l'écran de résultat. */
|
|
132
|
-
submitResponse = signal(null, ...(ngDevMode ? [{ debugName: "submitResponse" }] : []));
|
|
133
|
-
/** Pile de navigation, pour le bouton retour. */
|
|
134
|
-
stepHistory = [];
|
|
135
|
-
documentUpload = null;
|
|
136
|
-
finished = false;
|
|
137
|
-
/** Initialise le service pour un parcours donné. Appelé par l'hôte. */
|
|
138
|
-
init(params) {
|
|
139
|
-
this.config = params.config;
|
|
140
|
-
this.finish = params.finish;
|
|
141
|
-
this.api = new NayaApiClient(params.auth, params.config.baseUrl);
|
|
142
|
-
this.strings = new NayaStrings(params.config.locale);
|
|
143
|
-
this.country.set(params.config.country ?? null);
|
|
144
|
-
}
|
|
145
|
-
get resolvedConfig() {
|
|
146
|
-
return this.config;
|
|
147
|
-
}
|
|
148
|
-
// ── Navigation ───────────────────────────────────────────────────────────
|
|
149
|
-
/** Empile l'étape courante et avance vers `next`. */
|
|
150
|
-
go(next) {
|
|
151
|
-
this.stepHistory.push(this.step());
|
|
152
|
-
this.step.set(next);
|
|
153
|
-
}
|
|
154
|
-
/** Remplace l'étape courante sans l'empiler (transitions sans retour). */
|
|
155
|
-
replace(next) {
|
|
156
|
-
this.step.set(next);
|
|
157
|
-
}
|
|
158
|
-
get canGoBack() {
|
|
159
|
-
return this.stepHistory.length > 0;
|
|
160
|
-
}
|
|
161
|
-
/** Revient à l'étape précédente si elle existe. */
|
|
162
|
-
back() {
|
|
163
|
-
const prev = this.stepHistory.pop();
|
|
164
|
-
if (prev)
|
|
165
|
-
this.step.set(prev);
|
|
166
|
-
}
|
|
167
|
-
get client() {
|
|
168
|
-
return this.api;
|
|
169
|
-
}
|
|
170
|
-
/** La CNI CEDEAO porte sa MRZ au verso ; les autres pièces n'en ont pas besoin. */
|
|
171
|
-
get needsVerso() {
|
|
172
|
-
return this.document() === 'cni';
|
|
173
|
-
}
|
|
174
|
-
get canRetry() {
|
|
175
|
-
return this.attemptsUsed() < this.config.maxAttempts;
|
|
176
|
-
}
|
|
177
|
-
/** Crée la session serveur si nécessaire (documentType + pays requis). */
|
|
178
|
-
async ensureSession() {
|
|
179
|
-
const existing = this.session();
|
|
180
|
-
if (existing)
|
|
181
|
-
return existing;
|
|
182
|
-
const created = await this.api.start({
|
|
183
|
-
documentType: this.document(),
|
|
184
|
-
country: this.country(),
|
|
185
|
-
callbackUrl: this.config.callbackUrl,
|
|
186
|
-
});
|
|
187
|
-
this.session.set(created);
|
|
188
|
-
return created;
|
|
189
|
-
}
|
|
190
|
-
/**
|
|
191
|
-
* Lance (une seule fois) l'envoi du document en arrière-plan : l'OCR serveur
|
|
192
|
-
* tourne pendant que l'utilisateur fait son selfie.
|
|
193
|
-
*/
|
|
194
|
-
startDocumentUpload() {
|
|
195
|
-
if (this.documentUpload)
|
|
196
|
-
return;
|
|
197
|
-
const upload = this.uploadDocument();
|
|
198
|
-
this.documentUpload = upload;
|
|
199
|
-
// Tant que personne n'attend cet envoi (il tourne pendant le selfie), un
|
|
200
|
-
// échec produirait un unhandled rejection. On le neutralise ici sans
|
|
201
|
-
// consommer la promesse stockée : `ensureDocumentUploaded` en relance une
|
|
202
|
-
// fraîche puisque `uploadDocument` a remis `documentUpload` à null.
|
|
203
|
-
upload.catch(() => { });
|
|
204
|
-
}
|
|
205
|
-
/**
|
|
206
|
-
* Attend la fin de l'envoi du document, en le (re)lançant si besoin. Après
|
|
207
|
-
* un échec, un nouvel appel retente l'envoi.
|
|
208
|
-
*/
|
|
209
|
-
ensureDocumentUploaded() {
|
|
210
|
-
return (this.documentUpload ??= this.uploadDocument());
|
|
211
|
-
}
|
|
212
|
-
async uploadDocument() {
|
|
213
|
-
try {
|
|
214
|
-
const s = await this.ensureSession();
|
|
215
|
-
await this.api.uploadDocument(s.verificationId, {
|
|
216
|
-
rectoBase64: this.rectoBase64(),
|
|
217
|
-
versoBase64: this.versoBase64() ?? undefined,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
catch (err) {
|
|
221
|
-
this.documentUpload = null; // permet le retry
|
|
222
|
-
throw err;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Prépare une nouvelle tentative après un rejet : nouvelle session, captures
|
|
227
|
-
* remises à zéro.
|
|
228
|
-
*/
|
|
229
|
-
resetForRetry() {
|
|
230
|
-
this.attemptsUsed.update((n) => n + 1);
|
|
231
|
-
this.session.set(null);
|
|
232
|
-
this.rectoBase64.set(null);
|
|
233
|
-
this.versoBase64.set(null);
|
|
234
|
-
this.livenessScore.set(0);
|
|
235
|
-
this.selfieBase64.set(null);
|
|
236
|
-
this.submitResponse.set(null);
|
|
237
|
-
this.documentUpload = null;
|
|
238
|
-
this.stepHistory = [];
|
|
239
|
-
}
|
|
240
|
-
/** Un document a-t-il besoin de son verso avant de passer au selfie ? */
|
|
241
|
-
documentTypeNeedsVerso(doc) {
|
|
242
|
-
return documentNeedsVerso(doc);
|
|
243
|
-
}
|
|
244
|
-
complete(result) {
|
|
245
|
-
if (this.finished)
|
|
246
|
-
return;
|
|
247
|
-
this.finished = true;
|
|
248
|
-
this.finish({ ...result, verificationId: result.verificationId ?? this.session()?.verificationId });
|
|
249
|
-
}
|
|
250
|
-
cancel() {
|
|
251
|
-
this.complete({ status: 'cancelled' });
|
|
252
|
-
}
|
|
253
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaFlowService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
254
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaFlowService });
|
|
255
|
-
}
|
|
256
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaFlowService, decorators: [{
|
|
257
|
-
type: Injectable
|
|
258
|
-
}] });
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Jetons de design Naya — charte produit (navy, ambre, rayons 12/8/24),
|
|
262
|
-
* exposés en variables CSS. Reprend `naya_theme.dart`.
|
|
263
|
-
*
|
|
264
|
-
* `primary` et `accent` sont surchargeables par le client ; le reste est fixe.
|
|
265
|
-
* Toutes les variables sont posées sur l'élément hôte de l'overlay, si bien que
|
|
266
|
-
* le thème du SDK reste isolé de celui de l'application.
|
|
267
|
-
*/
|
|
268
|
-
const NAYA_TOKENS = {
|
|
269
|
-
success: '#27AE60',
|
|
270
|
-
error: '#E74C3C',
|
|
271
|
-
warning: '#F39C12',
|
|
272
|
-
background: '#F7F8FA',
|
|
273
|
-
surface: '#FFFFFF',
|
|
274
|
-
textPrimary: '#1A1A2E',
|
|
275
|
-
textSecondary: '#6B7280',
|
|
276
|
-
line: '#E5E7EB',
|
|
277
|
-
dark: '#0E1C2C',
|
|
278
|
-
radiusCard: '12px',
|
|
279
|
-
radiusInput: '8px',
|
|
280
|
-
radiusButton: '24px',
|
|
281
|
-
};
|
|
282
|
-
/** Construit le dictionnaire de variables CSS à appliquer sur l'hôte. */
|
|
283
|
-
function buildThemeVars(config) {
|
|
284
|
-
return {
|
|
285
|
-
'--naya-primary': config.theme.primaryColor,
|
|
286
|
-
'--naya-accent': config.theme.accentColor,
|
|
287
|
-
'--naya-success': NAYA_TOKENS.success,
|
|
288
|
-
'--naya-error': NAYA_TOKENS.error,
|
|
289
|
-
'--naya-warning': NAYA_TOKENS.warning,
|
|
290
|
-
'--naya-bg': NAYA_TOKENS.background,
|
|
291
|
-
'--naya-surface': NAYA_TOKENS.surface,
|
|
292
|
-
'--naya-text': NAYA_TOKENS.textPrimary,
|
|
293
|
-
'--naya-text-2': NAYA_TOKENS.textSecondary,
|
|
294
|
-
'--naya-line': NAYA_TOKENS.line,
|
|
295
|
-
'--naya-dark': NAYA_TOKENS.dark,
|
|
296
|
-
'--naya-r-card': NAYA_TOKENS.radiusCard,
|
|
297
|
-
'--naya-r-input': NAYA_TOKENS.radiusInput,
|
|
298
|
-
'--naya-r-button': NAYA_TOKENS.radiusButton,
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
class NayaIconComponent {
|
|
303
|
-
name = input.required(...(ngDevMode ? [{ debugName: "name" }] : []));
|
|
304
|
-
size = input(20, ...(ngDevMode ? [{ debugName: "size" }] : []));
|
|
305
|
-
stroke = input(2, ...(ngDevMode ? [{ debugName: "stroke" }] : []));
|
|
306
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
307
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NayaIconComponent, isStandalone: true, selector: "naya-icon", inputs: { name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: true, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, stroke: { classPropertyName: "stroke", publicName: "stroke", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
308
|
-
<svg
|
|
309
|
-
[attr.width]="size()"
|
|
310
|
-
[attr.height]="size()"
|
|
311
|
-
viewBox="0 0 24 24"
|
|
312
|
-
fill="none"
|
|
313
|
-
stroke="currentColor"
|
|
314
|
-
[attr.stroke-width]="stroke()"
|
|
315
|
-
stroke-linecap="round"
|
|
316
|
-
stroke-linejoin="round"
|
|
317
|
-
aria-hidden="true"
|
|
318
|
-
>
|
|
319
|
-
@switch (name()) {
|
|
320
|
-
@case ('shield') {
|
|
321
|
-
<path d="M12 3l7 3v5c0 4.5-3 8.3-7 10-4-1.7-7-5.5-7-10V6l7-3z" />
|
|
322
|
-
<path d="M9 12l2 2 4-4" />
|
|
323
|
-
}
|
|
324
|
-
@case ('lock') {
|
|
325
|
-
<rect x="5" y="11" width="14" height="9" rx="2" />
|
|
326
|
-
<path d="M8 11V8a4 4 0 0 1 8 0v3" />
|
|
327
|
-
}
|
|
328
|
-
@case ('fingerprint') {
|
|
329
|
-
<path d="M12 11a2 2 0 0 1 2 2v1a5 5 0 0 1-.5 2.2" />
|
|
330
|
-
<path d="M8 13a4 4 0 0 1 8 0v1a8 8 0 0 1-.8 3.5" />
|
|
331
|
-
<path d="M5.5 15A7 7 0 0 1 5 12a7 7 0 0 1 12-5" />
|
|
332
|
-
<path d="M12 20v-7" />
|
|
333
|
-
}
|
|
334
|
-
@case ('close') {
|
|
335
|
-
<path d="M6 6l12 12M18 6L6 18" />
|
|
336
|
-
}
|
|
337
|
-
@case ('back') {
|
|
338
|
-
<path d="M15 5l-7 7 7 7" />
|
|
339
|
-
}
|
|
340
|
-
@case ('chevron') {
|
|
341
|
-
<path d="M9 6l6 6-6 6" />
|
|
342
|
-
}
|
|
343
|
-
@case ('check') {
|
|
344
|
-
<path d="M5 12l5 5L20 7" />
|
|
345
|
-
}
|
|
346
|
-
@case ('clock') {
|
|
347
|
-
<circle cx="12" cy="12" r="8" />
|
|
348
|
-
<path d="M12 8v4l3 2" />
|
|
349
|
-
}
|
|
350
|
-
@case ('wifi-off') {
|
|
351
|
-
<path d="M3 3l18 18" />
|
|
352
|
-
<path d="M5 12a11 11 0 0 1 4-2.5" />
|
|
353
|
-
<path d="M8.5 16.5A5 5 0 0 1 12 15a5 5 0 0 1 2 .4" />
|
|
354
|
-
<path d="M12 20h.01" />
|
|
355
|
-
}
|
|
356
|
-
@case ('refresh') {
|
|
357
|
-
<path d="M4 12a8 8 0 0 1 13.7-5.7L20 8" />
|
|
358
|
-
<path d="M20 4v4h-4" />
|
|
359
|
-
<path d="M20 12a8 8 0 0 1-13.7 5.7L4 16" />
|
|
360
|
-
<path d="M4 20v-4h4" />
|
|
361
|
-
}
|
|
362
|
-
@case ('id-card') {
|
|
363
|
-
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
364
|
-
<circle cx="8.5" cy="11" r="2" />
|
|
365
|
-
<path d="M13 10h5M13 14h5M5.5 15a3 3 0 0 1 6 0" />
|
|
366
|
-
}
|
|
367
|
-
@case ('passport') {
|
|
368
|
-
<rect x="5" y="3" width="14" height="18" rx="2" />
|
|
369
|
-
<circle cx="12" cy="10" r="3" />
|
|
370
|
-
<path d="M9 16h6" />
|
|
371
|
-
}
|
|
372
|
-
@case ('car') {
|
|
373
|
-
<path d="M5 16l1.5-5A2 2 0 0 1 8.4 10h7.2a2 2 0 0 1 1.9 1l1.5 5" />
|
|
374
|
-
<rect x="3" y="16" width="18" height="4" rx="1" />
|
|
375
|
-
<circle cx="7.5" cy="20" r="1" />
|
|
376
|
-
<circle cx="16.5" cy="20" r="1" />
|
|
377
|
-
}
|
|
378
|
-
@case ('sun') {
|
|
379
|
-
<circle cx="12" cy="12" r="4" />
|
|
380
|
-
<path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5L19 19M19 5l-1.5 1.5M6.5 17.5L5 19" />
|
|
381
|
-
}
|
|
382
|
-
@case ('focus') {
|
|
383
|
-
<path d="M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2" />
|
|
384
|
-
<circle cx="12" cy="12" r="2.5" />
|
|
385
|
-
}
|
|
386
|
-
@case ('eye') {
|
|
387
|
-
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" />
|
|
388
|
-
<circle cx="12" cy="12" r="3" />
|
|
389
|
-
}
|
|
390
|
-
@case ('rotate') {
|
|
391
|
-
<path d="M12 4a8 8 0 1 1-7.5 5.3" />
|
|
392
|
-
<path d="M4 4v5h5" />
|
|
393
|
-
}
|
|
394
|
-
@case ('hourglass') {
|
|
395
|
-
<path d="M7 3h10M7 21h10M8 3c0 4 8 5 8 9s-8 5-8 9M16 3c0 4-8 5-8 9s8 5 8 9" />
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
</svg>
|
|
399
|
-
`, isInline: true, styles: [":host{display:inline-flex;line-height:0}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
400
|
-
}
|
|
401
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaIconComponent, decorators: [{
|
|
402
|
-
type: Component,
|
|
403
|
-
args: [{ selector: 'naya-icon', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
404
|
-
<svg
|
|
405
|
-
[attr.width]="size()"
|
|
406
|
-
[attr.height]="size()"
|
|
407
|
-
viewBox="0 0 24 24"
|
|
408
|
-
fill="none"
|
|
409
|
-
stroke="currentColor"
|
|
410
|
-
[attr.stroke-width]="stroke()"
|
|
411
|
-
stroke-linecap="round"
|
|
412
|
-
stroke-linejoin="round"
|
|
413
|
-
aria-hidden="true"
|
|
414
|
-
>
|
|
415
|
-
@switch (name()) {
|
|
416
|
-
@case ('shield') {
|
|
417
|
-
<path d="M12 3l7 3v5c0 4.5-3 8.3-7 10-4-1.7-7-5.5-7-10V6l7-3z" />
|
|
418
|
-
<path d="M9 12l2 2 4-4" />
|
|
419
|
-
}
|
|
420
|
-
@case ('lock') {
|
|
421
|
-
<rect x="5" y="11" width="14" height="9" rx="2" />
|
|
422
|
-
<path d="M8 11V8a4 4 0 0 1 8 0v3" />
|
|
423
|
-
}
|
|
424
|
-
@case ('fingerprint') {
|
|
425
|
-
<path d="M12 11a2 2 0 0 1 2 2v1a5 5 0 0 1-.5 2.2" />
|
|
426
|
-
<path d="M8 13a4 4 0 0 1 8 0v1a8 8 0 0 1-.8 3.5" />
|
|
427
|
-
<path d="M5.5 15A7 7 0 0 1 5 12a7 7 0 0 1 12-5" />
|
|
428
|
-
<path d="M12 20v-7" />
|
|
429
|
-
}
|
|
430
|
-
@case ('close') {
|
|
431
|
-
<path d="M6 6l12 12M18 6L6 18" />
|
|
432
|
-
}
|
|
433
|
-
@case ('back') {
|
|
434
|
-
<path d="M15 5l-7 7 7 7" />
|
|
435
|
-
}
|
|
436
|
-
@case ('chevron') {
|
|
437
|
-
<path d="M9 6l6 6-6 6" />
|
|
438
|
-
}
|
|
439
|
-
@case ('check') {
|
|
440
|
-
<path d="M5 12l5 5L20 7" />
|
|
441
|
-
}
|
|
442
|
-
@case ('clock') {
|
|
443
|
-
<circle cx="12" cy="12" r="8" />
|
|
444
|
-
<path d="M12 8v4l3 2" />
|
|
445
|
-
}
|
|
446
|
-
@case ('wifi-off') {
|
|
447
|
-
<path d="M3 3l18 18" />
|
|
448
|
-
<path d="M5 12a11 11 0 0 1 4-2.5" />
|
|
449
|
-
<path d="M8.5 16.5A5 5 0 0 1 12 15a5 5 0 0 1 2 .4" />
|
|
450
|
-
<path d="M12 20h.01" />
|
|
451
|
-
}
|
|
452
|
-
@case ('refresh') {
|
|
453
|
-
<path d="M4 12a8 8 0 0 1 13.7-5.7L20 8" />
|
|
454
|
-
<path d="M20 4v4h-4" />
|
|
455
|
-
<path d="M20 12a8 8 0 0 1-13.7 5.7L4 16" />
|
|
456
|
-
<path d="M4 20v-4h4" />
|
|
457
|
-
}
|
|
458
|
-
@case ('id-card') {
|
|
459
|
-
<rect x="3" y="5" width="18" height="14" rx="2" />
|
|
460
|
-
<circle cx="8.5" cy="11" r="2" />
|
|
461
|
-
<path d="M13 10h5M13 14h5M5.5 15a3 3 0 0 1 6 0" />
|
|
462
|
-
}
|
|
463
|
-
@case ('passport') {
|
|
464
|
-
<rect x="5" y="3" width="14" height="18" rx="2" />
|
|
465
|
-
<circle cx="12" cy="10" r="3" />
|
|
466
|
-
<path d="M9 16h6" />
|
|
467
|
-
}
|
|
468
|
-
@case ('car') {
|
|
469
|
-
<path d="M5 16l1.5-5A2 2 0 0 1 8.4 10h7.2a2 2 0 0 1 1.9 1l1.5 5" />
|
|
470
|
-
<rect x="3" y="16" width="18" height="4" rx="1" />
|
|
471
|
-
<circle cx="7.5" cy="20" r="1" />
|
|
472
|
-
<circle cx="16.5" cy="20" r="1" />
|
|
473
|
-
}
|
|
474
|
-
@case ('sun') {
|
|
475
|
-
<circle cx="12" cy="12" r="4" />
|
|
476
|
-
<path d="M12 2v2M12 20v2M2 12h2M20 12h2M5 5l1.5 1.5M17.5 17.5L19 19M19 5l-1.5 1.5M6.5 17.5L5 19" />
|
|
477
|
-
}
|
|
478
|
-
@case ('focus') {
|
|
479
|
-
<path d="M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2" />
|
|
480
|
-
<circle cx="12" cy="12" r="2.5" />
|
|
481
|
-
}
|
|
482
|
-
@case ('eye') {
|
|
483
|
-
<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" />
|
|
484
|
-
<circle cx="12" cy="12" r="3" />
|
|
485
|
-
}
|
|
486
|
-
@case ('rotate') {
|
|
487
|
-
<path d="M12 4a8 8 0 1 1-7.5 5.3" />
|
|
488
|
-
<path d="M4 4v5h5" />
|
|
489
|
-
}
|
|
490
|
-
@case ('hourglass') {
|
|
491
|
-
<path d="M7 3h10M7 21h10M8 3c0 4 8 5 8 9s-8 5-8 9M16 3c0 4-8 5-8 9s8 5 8 9" />
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
</svg>
|
|
495
|
-
`, styles: [":host{display:inline-flex;line-height:0}\n"] }]
|
|
496
|
-
}], propDecorators: { name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: true }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], stroke: [{ type: i0.Input, args: [{ isSignal: true, alias: "stroke", required: false }] }] } });
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Gabarit d'écran du SDK : en-tête (retour / fermer), contenu projeté, pied
|
|
500
|
-
* « Propulsé par Naya ID ». Reprend `NayaScreen` (widgets/common.dart).
|
|
501
|
-
*
|
|
502
|
-
* `dark` bascule le fond sombre des écrans caméra (phases 3-4).
|
|
503
|
-
*/
|
|
504
|
-
class NayaScreenComponent {
|
|
505
|
-
poweredBy = input.required(...(ngDevMode ? [{ debugName: "poweredBy" }] : []));
|
|
506
|
-
canBack = input(false, ...(ngDevMode ? [{ debugName: "canBack" }] : []));
|
|
507
|
-
canClose = input(true, ...(ngDevMode ? [{ debugName: "canClose" }] : []));
|
|
508
|
-
dark = input(false, ...(ngDevMode ? [{ debugName: "dark" }] : []));
|
|
509
|
-
back = output();
|
|
510
|
-
close = output();
|
|
511
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaScreenComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
512
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NayaScreenComponent, isStandalone: true, selector: "naya-screen", inputs: { poweredBy: { classPropertyName: "poweredBy", publicName: "poweredBy", isSignal: true, isRequired: true, transformFunction: null }, canBack: { classPropertyName: "canBack", publicName: "canBack", isSignal: true, isRequired: false, transformFunction: null }, canClose: { classPropertyName: "canClose", publicName: "canClose", isSignal: true, isRequired: false, transformFunction: null }, dark: { classPropertyName: "dark", publicName: "dark", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { back: "back", close: "close" }, ngImport: i0, template: `
|
|
513
|
-
<div class="shell" [class.dark]="dark()">
|
|
514
|
-
<header class="bar">
|
|
515
|
-
@if (canBack()) {
|
|
516
|
-
<button type="button" class="round" (click)="back.emit()" aria-label="Retour">
|
|
517
|
-
<naya-icon name="back" [size]="20" />
|
|
518
|
-
</button>
|
|
519
|
-
} @else {
|
|
520
|
-
<span class="spacer"></span>
|
|
521
|
-
}
|
|
522
|
-
@if (canClose()) {
|
|
523
|
-
<button type="button" class="round" (click)="close.emit()" aria-label="Fermer">
|
|
524
|
-
<naya-icon name="close" [size]="20" />
|
|
525
|
-
</button>
|
|
526
|
-
} @else {
|
|
527
|
-
<span class="spacer"></span>
|
|
528
|
-
}
|
|
529
|
-
</header>
|
|
530
|
-
|
|
531
|
-
<main class="content">
|
|
532
|
-
<ng-content />
|
|
533
|
-
</main>
|
|
534
|
-
|
|
535
|
-
<footer class="powered">{{ poweredBy() }}</footer>
|
|
536
|
-
</div>
|
|
537
|
-
`, isInline: true, styles: [".shell{display:flex;flex-direction:column;height:100%;background:var(--naya-bg);color:var(--naya-text)}.shell.dark{background:var(--naya-dark);color:#fff}.bar{display:flex;justify-content:space-between;align-items:center;padding:8px 12px 0;flex:0 0 auto}.spacer{width:40px;height:40px}.round{width:40px;height:40px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:50%;cursor:pointer;color:var(--naya-primary);background:color-mix(in srgb,var(--naya-primary) 8%,transparent);transition:background .15s ease}.dark .round{color:#fff;background:#ffffff29}.round:hover{background:color-mix(in srgb,var(--naya-primary) 16%,transparent)}.round:focus-visible{outline:2px solid var(--naya-accent);outline-offset:2px}.content{flex:1 1 auto;min-height:0;overflow-y:auto;display:flex;flex-direction:column}.powered{flex:0 0 auto;text-align:center;padding:6px 0 12px;font-size:11px;font-weight:600;color:#b3bac4}.dark .powered{color:#ffffff61}\n"], dependencies: [{ kind: "component", type: NayaIconComponent, selector: "naya-icon", inputs: ["name", "size", "stroke"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
538
|
-
}
|
|
539
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaScreenComponent, decorators: [{
|
|
540
|
-
type: Component,
|
|
541
|
-
args: [{ selector: 'naya-screen', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NayaIconComponent], template: `
|
|
542
|
-
<div class="shell" [class.dark]="dark()">
|
|
543
|
-
<header class="bar">
|
|
544
|
-
@if (canBack()) {
|
|
545
|
-
<button type="button" class="round" (click)="back.emit()" aria-label="Retour">
|
|
546
|
-
<naya-icon name="back" [size]="20" />
|
|
547
|
-
</button>
|
|
548
|
-
} @else {
|
|
549
|
-
<span class="spacer"></span>
|
|
550
|
-
}
|
|
551
|
-
@if (canClose()) {
|
|
552
|
-
<button type="button" class="round" (click)="close.emit()" aria-label="Fermer">
|
|
553
|
-
<naya-icon name="close" [size]="20" />
|
|
554
|
-
</button>
|
|
555
|
-
} @else {
|
|
556
|
-
<span class="spacer"></span>
|
|
557
|
-
}
|
|
558
|
-
</header>
|
|
559
|
-
|
|
560
|
-
<main class="content">
|
|
561
|
-
<ng-content />
|
|
562
|
-
</main>
|
|
563
|
-
|
|
564
|
-
<footer class="powered">{{ poweredBy() }}</footer>
|
|
565
|
-
</div>
|
|
566
|
-
`, styles: [".shell{display:flex;flex-direction:column;height:100%;background:var(--naya-bg);color:var(--naya-text)}.shell.dark{background:var(--naya-dark);color:#fff}.bar{display:flex;justify-content:space-between;align-items:center;padding:8px 12px 0;flex:0 0 auto}.spacer{width:40px;height:40px}.round{width:40px;height:40px;display:inline-flex;align-items:center;justify-content:center;border:none;border-radius:50%;cursor:pointer;color:var(--naya-primary);background:color-mix(in srgb,var(--naya-primary) 8%,transparent);transition:background .15s ease}.dark .round{color:#fff;background:#ffffff29}.round:hover{background:color-mix(in srgb,var(--naya-primary) 16%,transparent)}.round:focus-visible{outline:2px solid var(--naya-accent);outline-offset:2px}.content{flex:1 1 auto;min-height:0;overflow-y:auto;display:flex;flex-direction:column}.powered{flex:0 0 auto;text-align:center;padding:6px 0 12px;font-size:11px;font-weight:600;color:#b3bac4}.dark .powered{color:#ffffff61}\n"] }]
|
|
567
|
-
}], propDecorators: { poweredBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "poweredBy", required: true }] }], canBack: [{ type: i0.Input, args: [{ isSignal: true, alias: "canBack", required: false }] }], canClose: [{ type: i0.Input, args: [{ isSignal: true, alias: "canClose", required: false }] }], dark: [{ type: i0.Input, args: [{ isSignal: true, alias: "dark", required: false }] }], back: [{ type: i0.Output, args: ["back"] }], close: [{ type: i0.Output, args: ["close"] }] } });
|
|
568
|
-
|
|
569
|
-
/** Marque Naya (pictogramme + nom). Écrans bienvenue et traitement. */
|
|
570
|
-
class NayaBrandComponent {
|
|
571
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaBrandComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
572
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.28", type: NayaBrandComponent, isStandalone: true, selector: "naya-brand", ngImport: i0, template: `
|
|
573
|
-
<span class="brand">
|
|
574
|
-
<span class="mark"><naya-icon name="fingerprint" [size]="17" /></span>
|
|
575
|
-
<span class="name">Naya ID</span>
|
|
576
|
-
</span>
|
|
577
|
-
`, isInline: true, styles: [".brand{display:inline-flex;align-items:center;gap:7px}.mark{width:26px;height:26px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;background:var(--naya-primary);color:var(--naya-accent)}.name{font-size:17px;font-weight:800;letter-spacing:-.2px;color:var(--naya-primary)}\n"], dependencies: [{ kind: "component", type: NayaIconComponent, selector: "naya-icon", inputs: ["name", "size", "stroke"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
578
|
-
}
|
|
579
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaBrandComponent, decorators: [{
|
|
580
|
-
type: Component,
|
|
581
|
-
args: [{ selector: 'naya-brand', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NayaIconComponent], template: `
|
|
582
|
-
<span class="brand">
|
|
583
|
-
<span class="mark"><naya-icon name="fingerprint" [size]="17" /></span>
|
|
584
|
-
<span class="name">Naya ID</span>
|
|
585
|
-
</span>
|
|
586
|
-
`, styles: [".brand{display:inline-flex;align-items:center;gap:7px}.mark{width:26px;height:26px;border-radius:8px;display:inline-flex;align-items:center;justify-content:center;background:var(--naya-primary);color:var(--naya-accent)}.name{font-size:17px;font-weight:800;letter-spacing:-.2px;color:var(--naya-primary)}\n"] }]
|
|
587
|
-
}] });
|
|
588
|
-
/** CTA principal (plein, ambre) avec état de chargement. */
|
|
589
|
-
class NayaPrimaryButtonComponent {
|
|
590
|
-
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
591
|
-
loading = input(false, ...(ngDevMode ? [{ debugName: "loading" }] : []));
|
|
592
|
-
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : []));
|
|
593
|
-
icon = input(null, ...(ngDevMode ? [{ debugName: "icon" }] : []));
|
|
594
|
-
pressed = output();
|
|
595
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaPrimaryButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
596
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NayaPrimaryButtonComponent, isStandalone: true, selector: "naya-primary-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { pressed: "pressed" }, ngImport: i0, template: `
|
|
597
|
-
<button
|
|
598
|
-
type="button"
|
|
599
|
-
class="btn"
|
|
600
|
-
[disabled]="loading() || disabled()"
|
|
601
|
-
(click)="pressed.emit()"
|
|
602
|
-
>
|
|
603
|
-
@if (loading()) {
|
|
604
|
-
<span class="spinner" aria-hidden="true"></span>
|
|
605
|
-
} @else {
|
|
606
|
-
@if (icon()) { <naya-icon [name]="icon()!" [size]="18" /> }
|
|
607
|
-
<span>{{ label() }}</span>
|
|
608
|
-
}
|
|
609
|
-
</button>
|
|
610
|
-
`, isInline: true, styles: [".btn{width:100%;min-height:52px;display:inline-flex;align-items:center;justify-content:center;gap:8px;border:none;border-radius:var(--naya-r-button);cursor:pointer;background:var(--naya-accent);color:var(--naya-primary);font-size:15px;font-weight:800;transition:filter .15s ease}.btn:hover:not(:disabled){filter:brightness(.96)}.btn:disabled{opacity:.5;cursor:not-allowed}.btn:focus-visible{outline:2px solid var(--naya-primary);outline-offset:2px}.spinner{width:22px;height:22px;border-radius:50%;border:2.5px solid color-mix(in srgb,var(--naya-primary) 30%,transparent);border-top-color:var(--naya-primary);animation:naya-spin .8s linear infinite}@keyframes naya-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinner{animation-duration:1.6s}}\n"], dependencies: [{ kind: "component", type: NayaIconComponent, selector: "naya-icon", inputs: ["name", "size", "stroke"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
611
|
-
}
|
|
612
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaPrimaryButtonComponent, decorators: [{
|
|
613
|
-
type: Component,
|
|
614
|
-
args: [{ selector: 'naya-primary-button', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NayaIconComponent], template: `
|
|
615
|
-
<button
|
|
616
|
-
type="button"
|
|
617
|
-
class="btn"
|
|
618
|
-
[disabled]="loading() || disabled()"
|
|
619
|
-
(click)="pressed.emit()"
|
|
620
|
-
>
|
|
621
|
-
@if (loading()) {
|
|
622
|
-
<span class="spinner" aria-hidden="true"></span>
|
|
623
|
-
} @else {
|
|
624
|
-
@if (icon()) { <naya-icon [name]="icon()!" [size]="18" /> }
|
|
625
|
-
<span>{{ label() }}</span>
|
|
626
|
-
}
|
|
627
|
-
</button>
|
|
628
|
-
`, styles: [".btn{width:100%;min-height:52px;display:inline-flex;align-items:center;justify-content:center;gap:8px;border:none;border-radius:var(--naya-r-button);cursor:pointer;background:var(--naya-accent);color:var(--naya-primary);font-size:15px;font-weight:800;transition:filter .15s ease}.btn:hover:not(:disabled){filter:brightness(.96)}.btn:disabled{opacity:.5;cursor:not-allowed}.btn:focus-visible{outline:2px solid var(--naya-primary);outline-offset:2px}.spinner{width:22px;height:22px;border-radius:50%;border:2.5px solid color-mix(in srgb,var(--naya-primary) 30%,transparent);border-top-color:var(--naya-primary);animation:naya-spin .8s linear infinite}@keyframes naya-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.spinner{animation-duration:1.6s}}\n"] }]
|
|
629
|
-
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], loading: [{ type: i0.Input, args: [{ isSignal: true, alias: "loading", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], pressed: [{ type: i0.Output, args: ["pressed"] }] } });
|
|
630
|
-
/** Bouton secondaire (contour). */
|
|
631
|
-
class NayaSecondaryButtonComponent {
|
|
632
|
-
label = input.required(...(ngDevMode ? [{ debugName: "label" }] : []));
|
|
633
|
-
pressed = output();
|
|
634
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaSecondaryButtonComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
635
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "20.3.28", type: NayaSecondaryButtonComponent, isStandalone: true, selector: "naya-secondary-button", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { pressed: "pressed" }, ngImport: i0, template: `
|
|
636
|
-
<button type="button" class="btn" (click)="pressed.emit()">{{ label() }}</button>
|
|
637
|
-
`, isInline: true, styles: [".btn{width:100%;min-height:52px;border:1.5px solid var(--naya-primary);border-radius:var(--naya-r-button);background:transparent;color:var(--naya-primary);cursor:pointer;font-size:15px;font-weight:800}.btn:hover{background:color-mix(in srgb,var(--naya-primary) 6%,transparent)}.btn:focus-visible{outline:2px solid var(--naya-accent);outline-offset:2px}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
638
|
-
}
|
|
639
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaSecondaryButtonComponent, decorators: [{
|
|
640
|
-
type: Component,
|
|
641
|
-
args: [{ selector: 'naya-secondary-button', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
642
|
-
<button type="button" class="btn" (click)="pressed.emit()">{{ label() }}</button>
|
|
643
|
-
`, styles: [".btn{width:100%;min-height:52px;border:1.5px solid var(--naya-primary);border-radius:var(--naya-r-button);background:transparent;color:var(--naya-primary);cursor:pointer;font-size:15px;font-weight:800}.btn:hover{background:color-mix(in srgb,var(--naya-primary) 6%,transparent)}.btn:focus-visible{outline:2px solid var(--naya-accent);outline-offset:2px}\n"] }]
|
|
644
|
-
}], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], pressed: [{ type: i0.Output, args: ["pressed"] }] } });
|
|
645
|
-
/** Indicateur des trois grandes étapes (Document · Selfie · Résultat). */
|
|
646
|
-
class NayaStepsDotsComponent {
|
|
647
|
-
labels = input.required(...(ngDevMode ? [{ debugName: "labels" }] : []));
|
|
648
|
-
activeIndex = input(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : []));
|
|
649
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaStepsDotsComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
650
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NayaStepsDotsComponent, isStandalone: true, selector: "naya-steps-dots", inputs: { labels: { classPropertyName: "labels", publicName: "labels", isSignal: true, isRequired: true, transformFunction: null }, activeIndex: { classPropertyName: "activeIndex", publicName: "activeIndex", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
|
|
651
|
-
<div class="dots">
|
|
652
|
-
@for (label of labels(); track label; let i = $index) {
|
|
653
|
-
<div class="step">
|
|
654
|
-
<span class="dot" [class.on]="i <= activeIndex()"></span>
|
|
655
|
-
<span class="label">{{ label }}</span>
|
|
656
|
-
</div>
|
|
657
|
-
}
|
|
658
|
-
</div>
|
|
659
|
-
`, isInline: true, styles: [".dots{display:flex;justify-content:center;gap:22px}.step{display:flex;flex-direction:column;align-items:center;gap:5px}.dot{width:9px;height:9px;border-radius:50%;background:#d6dce4}.dot.on{background:var(--naya-accent)}.label{font-size:11px;color:var(--naya-text-2)}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
660
|
-
}
|
|
661
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaStepsDotsComponent, decorators: [{
|
|
662
|
-
type: Component,
|
|
663
|
-
args: [{ selector: 'naya-steps-dots', changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
664
|
-
<div class="dots">
|
|
665
|
-
@for (label of labels(); track label; let i = $index) {
|
|
666
|
-
<div class="step">
|
|
667
|
-
<span class="dot" [class.on]="i <= activeIndex()"></span>
|
|
668
|
-
<span class="label">{{ label }}</span>
|
|
669
|
-
</div>
|
|
670
|
-
}
|
|
671
|
-
</div>
|
|
672
|
-
`, styles: [".dots{display:flex;justify-content:center;gap:22px}.step{display:flex;flex-direction:column;align-items:center;gap:5px}.dot{width:9px;height:9px;border-radius:50%;background:#d6dce4}.dot.on{background:var(--naya-accent)}.label{font-size:11px;color:var(--naya-text-2)}\n"] }]
|
|
673
|
-
}], propDecorators: { labels: [{ type: i0.Input, args: [{ isSignal: true, alias: "labels", required: true }] }], activeIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeIndex", required: false }] }] } });
|
|
674
|
-
|
|
675
|
-
/** Écran 1 — Bienvenue. */
|
|
676
|
-
class WelcomeScreen {
|
|
677
|
-
flow = inject(NayaFlowService);
|
|
678
|
-
s = this.flow.strings;
|
|
679
|
-
config = this.flow.resolvedConfig;
|
|
680
|
-
stepLabels = computed(() => [
|
|
681
|
-
this.s.stepDocument,
|
|
682
|
-
this.s.stepSelfie,
|
|
683
|
-
this.s.stepResult,
|
|
684
|
-
], ...(ngDevMode ? [{ debugName: "stepLabels" }] : []));
|
|
685
|
-
start() {
|
|
686
|
-
const cfg = this.config;
|
|
687
|
-
const single = !!cfg.country && cfg.documentTypes.length === 1;
|
|
688
|
-
if (single) {
|
|
689
|
-
this.flow.country.set(cfg.country);
|
|
690
|
-
this.flow.document.set(cfg.documentTypes[0]);
|
|
691
|
-
this.flow.go('capture-recto');
|
|
692
|
-
}
|
|
693
|
-
else {
|
|
694
|
-
this.flow.go('select');
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: WelcomeScreen, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
698
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: WelcomeScreen, isStandalone: true, selector: "naya-welcome-screen", ngImport: i0, template: `
|
|
699
|
-
<naya-screen [poweredBy]="s.poweredBy" (close)="flow.cancel()">
|
|
700
|
-
<div class="pad">
|
|
701
|
-
<div class="brandRow">
|
|
702
|
-
<naya-brand />
|
|
703
|
-
@if (config.clientLogoUrl) {
|
|
704
|
-
<span class="sep"></span>
|
|
705
|
-
<img class="clientLogo" [src]="config.clientLogoUrl" alt="" />
|
|
706
|
-
}
|
|
707
|
-
</div>
|
|
708
|
-
|
|
709
|
-
<div class="spacer"></div>
|
|
710
|
-
|
|
711
|
-
<div class="hero"><naya-icon name="shield" [size]="56" [stroke]="1.6" /></div>
|
|
712
|
-
<h1 class="title">{{ s.welcomeTitle }}</h1>
|
|
713
|
-
<p class="body">{{ s.welcomeSubtitle }}</p>
|
|
714
|
-
|
|
715
|
-
<div class="steps">
|
|
716
|
-
<naya-steps-dots [labels]="stepLabels()" [activeIndex]="0" />
|
|
717
|
-
</div>
|
|
718
|
-
|
|
719
|
-
<div class="spacer"></div>
|
|
720
|
-
|
|
721
|
-
<naya-primary-button [label]="s.welcomeCta" (pressed)="start()" />
|
|
722
|
-
<div class="secure">
|
|
723
|
-
<naya-icon name="lock" [size]="13" [stroke]="2" />
|
|
724
|
-
<span>{{ s.welcomeSecurity }}</span>
|
|
725
|
-
</div>
|
|
726
|
-
</div>
|
|
727
|
-
</naya-screen>
|
|
728
|
-
`, isInline: true, styles: [".pad{display:flex;flex-direction:column;flex:1;padding:4px 24px 8px}.brandRow{display:flex;align-items:center;justify-content:center;gap:14px}.sep{width:1px;height:22px;background:var(--naya-line)}.clientLogo{height:24px;width:auto;object-fit:contain}.spacer{flex:1 1 auto}.hero{align-self:center;width:116px;height:116px;border-radius:32px;display:flex;align-items:center;justify-content:center;background:var(--naya-primary);color:var(--naya-accent)}.title{margin:28px 0 10px;text-align:center;text-wrap:balance;font-size:23px;font-weight:800;line-height:1.25;letter-spacing:-.2px;color:var(--naya-primary)}.body{margin:0;text-align:center;white-space:pre-line;font-size:15px;line-height:1.5;color:var(--naya-text-2)}.steps{margin-top:26px}.secure{display:flex;align-items:center;justify-content:center;gap:5px;margin-top:12px;text-align:center;font-size:11px;color:var(--naya-text-2)}\n"], dependencies: [{ kind: "component", type: NayaScreenComponent, selector: "naya-screen", inputs: ["poweredBy", "canBack", "canClose", "dark"], outputs: ["back", "close"] }, { kind: "component", type: NayaIconComponent, selector: "naya-icon", inputs: ["name", "size", "stroke"] }, { kind: "component", type: NayaBrandComponent, selector: "naya-brand" }, { kind: "component", type: NayaPrimaryButtonComponent, selector: "naya-primary-button", inputs: ["label", "loading", "disabled", "icon"], outputs: ["pressed"] }, { kind: "component", type: NayaStepsDotsComponent, selector: "naya-steps-dots", inputs: ["labels", "activeIndex"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
729
|
-
}
|
|
730
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: WelcomeScreen, decorators: [{
|
|
731
|
-
type: Component,
|
|
732
|
-
args: [{ selector: 'naya-welcome-screen', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
733
|
-
NayaScreenComponent,
|
|
734
|
-
NayaIconComponent,
|
|
735
|
-
NayaBrandComponent,
|
|
736
|
-
NayaPrimaryButtonComponent,
|
|
737
|
-
NayaStepsDotsComponent,
|
|
738
|
-
], template: `
|
|
739
|
-
<naya-screen [poweredBy]="s.poweredBy" (close)="flow.cancel()">
|
|
740
|
-
<div class="pad">
|
|
741
|
-
<div class="brandRow">
|
|
742
|
-
<naya-brand />
|
|
743
|
-
@if (config.clientLogoUrl) {
|
|
744
|
-
<span class="sep"></span>
|
|
745
|
-
<img class="clientLogo" [src]="config.clientLogoUrl" alt="" />
|
|
746
|
-
}
|
|
747
|
-
</div>
|
|
748
|
-
|
|
749
|
-
<div class="spacer"></div>
|
|
750
|
-
|
|
751
|
-
<div class="hero"><naya-icon name="shield" [size]="56" [stroke]="1.6" /></div>
|
|
752
|
-
<h1 class="title">{{ s.welcomeTitle }}</h1>
|
|
753
|
-
<p class="body">{{ s.welcomeSubtitle }}</p>
|
|
754
|
-
|
|
755
|
-
<div class="steps">
|
|
756
|
-
<naya-steps-dots [labels]="stepLabels()" [activeIndex]="0" />
|
|
757
|
-
</div>
|
|
758
|
-
|
|
759
|
-
<div class="spacer"></div>
|
|
760
|
-
|
|
761
|
-
<naya-primary-button [label]="s.welcomeCta" (pressed)="start()" />
|
|
762
|
-
<div class="secure">
|
|
763
|
-
<naya-icon name="lock" [size]="13" [stroke]="2" />
|
|
764
|
-
<span>{{ s.welcomeSecurity }}</span>
|
|
765
|
-
</div>
|
|
766
|
-
</div>
|
|
767
|
-
</naya-screen>
|
|
768
|
-
`, styles: [".pad{display:flex;flex-direction:column;flex:1;padding:4px 24px 8px}.brandRow{display:flex;align-items:center;justify-content:center;gap:14px}.sep{width:1px;height:22px;background:var(--naya-line)}.clientLogo{height:24px;width:auto;object-fit:contain}.spacer{flex:1 1 auto}.hero{align-self:center;width:116px;height:116px;border-radius:32px;display:flex;align-items:center;justify-content:center;background:var(--naya-primary);color:var(--naya-accent)}.title{margin:28px 0 10px;text-align:center;text-wrap:balance;font-size:23px;font-weight:800;line-height:1.25;letter-spacing:-.2px;color:var(--naya-primary)}.body{margin:0;text-align:center;white-space:pre-line;font-size:15px;line-height:1.5;color:var(--naya-text-2)}.steps{margin-top:26px}.secure{display:flex;align-items:center;justify-content:center;gap:5px;margin-top:12px;text-align:center;font-size:11px;color:var(--naya-text-2)}\n"] }]
|
|
769
|
-
}] });
|
|
770
|
-
|
|
771
|
-
/** Écran 2 — Choix du pays et du type de document. */
|
|
772
|
-
class DocumentSelectionScreen {
|
|
773
|
-
flow = inject(NayaFlowService);
|
|
774
|
-
s = this.flow.strings;
|
|
775
|
-
config = this.flow.resolvedConfig;
|
|
776
|
-
lockedCountry = !!this.config.country;
|
|
777
|
-
country = signal(this.config.country ?? this.config.countries[0], ...(ngDevMode ? [{ debugName: "country" }] : []));
|
|
778
|
-
document = signal(this.config.documentTypes.length === 1 ? this.config.documentTypes[0] : null, ...(ngDevMode ? [{ debugName: "document" }] : []));
|
|
779
|
-
docIconMap = computed(() => this.document(), ...(ngDevMode ? [{ debugName: "docIconMap" }] : []));
|
|
780
|
-
label(code) {
|
|
781
|
-
return countryLabel(code);
|
|
782
|
-
}
|
|
783
|
-
docIcon(doc) {
|
|
784
|
-
switch (doc) {
|
|
785
|
-
case 'cni': return 'id-card';
|
|
786
|
-
case 'passeport': return 'passport';
|
|
787
|
-
case 'permis': return 'car';
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
next() {
|
|
791
|
-
const doc = this.document();
|
|
792
|
-
if (!doc)
|
|
793
|
-
return;
|
|
794
|
-
this.flow.country.set(this.country());
|
|
795
|
-
this.flow.document.set(doc);
|
|
796
|
-
this.flow.go('capture-recto');
|
|
797
|
-
}
|
|
798
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: DocumentSelectionScreen, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
799
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: DocumentSelectionScreen, isStandalone: true, selector: "naya-document-selection-screen", ngImport: i0, template: `
|
|
800
|
-
<naya-screen
|
|
801
|
-
[poweredBy]="s.poweredBy"
|
|
802
|
-
[canBack]="flow.canGoBack"
|
|
803
|
-
(back)="flow.back()"
|
|
804
|
-
(close)="flow.cancel()"
|
|
805
|
-
>
|
|
806
|
-
<div class="pad">
|
|
807
|
-
<h1 class="title">{{ s.docSelectTitle }}</h1>
|
|
808
|
-
<p class="body">{{ s.docSelectSubtitle }}</p>
|
|
809
|
-
|
|
810
|
-
@if (!lockedCountry) {
|
|
811
|
-
<div class="chips" role="listbox" aria-label="Pays">
|
|
812
|
-
@for (code of config.countries; track code) {
|
|
813
|
-
<button
|
|
814
|
-
type="button"
|
|
815
|
-
class="chip"
|
|
816
|
-
role="option"
|
|
817
|
-
[class.on]="code === country()"
|
|
818
|
-
[attr.aria-selected]="code === country()"
|
|
819
|
-
(click)="country.set(code)"
|
|
820
|
-
>{{ label(code) }}</button>
|
|
821
|
-
}
|
|
822
|
-
</div>
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
<div class="cards">
|
|
826
|
-
@for (doc of config.documentTypes; track doc) {
|
|
827
|
-
<button
|
|
828
|
-
type="button"
|
|
829
|
-
class="card"
|
|
830
|
-
[class.on]="doc === document()"
|
|
831
|
-
(click)="document.set(doc)"
|
|
832
|
-
>
|
|
833
|
-
<span class="ico"><naya-icon [name]="docIcon(doc)" [size]="20" /></span>
|
|
834
|
-
<span class="lbl">{{ s.docLabel(doc) }}</span>
|
|
835
|
-
<naya-icon name="chevron" [size]="20" [stroke]="2" />
|
|
836
|
-
</button>
|
|
837
|
-
}
|
|
838
|
-
</div>
|
|
839
|
-
|
|
840
|
-
<div class="spacer"></div>
|
|
841
|
-
<naya-primary-button
|
|
842
|
-
[label]="s.continueLabel"
|
|
843
|
-
[disabled]="!document()"
|
|
844
|
-
(pressed)="next()"
|
|
845
|
-
/>
|
|
846
|
-
</div>
|
|
847
|
-
</naya-screen>
|
|
848
|
-
`, isInline: true, styles: [".pad{display:flex;flex-direction:column;flex:1;padding:8px 24px 16px}.title{margin:0 0 8px;white-space:pre-line;text-wrap:balance;font-size:23px;font-weight:800;line-height:1.25;letter-spacing:-.2px;color:var(--naya-primary)}.body{margin:0 0 18px;font-size:15px;line-height:1.5;color:var(--naya-text-2)}.chips{display:flex;gap:8px;overflow-x:auto;padding-bottom:4px;margin-bottom:18px}.chip{flex:0 0 auto;padding:8px 14px;border-radius:999px;cursor:pointer;background:var(--naya-surface);border:1px solid var(--naya-line);font-size:13px;font-weight:500;color:var(--naya-text);white-space:nowrap}.chip.on{border:1.6px solid var(--naya-accent);font-weight:700;background:color-mix(in srgb,var(--naya-accent) 18%,transparent)}.chip:focus-visible{outline:2px solid var(--naya-accent);outline-offset:2px}.cards{display:flex;flex-direction:column;gap:10px}.card{display:flex;align-items:center;gap:12px;width:100%;padding:14px;cursor:pointer;text-align:left;background:var(--naya-surface);border:1px solid var(--naya-line);border-radius:var(--naya-r-card)}.card.on{border:1.8px solid var(--naya-accent);background:#fff9ee}.card:focus-visible{outline:2px solid var(--naya-accent);outline-offset:2px}.ico{width:36px;height:36px;border-radius:8px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;background:#edf3fa;color:var(--naya-primary)}.lbl{flex:1;font-size:14px;font-weight:700;color:var(--naya-text)}.card naya-icon:last-child{color:var(--naya-text-2)}.spacer{flex:1 1 auto;min-height:16px}\n"], dependencies: [{ kind: "component", type: NayaScreenComponent, selector: "naya-screen", inputs: ["poweredBy", "canBack", "canClose", "dark"], outputs: ["back", "close"] }, { kind: "component", type: NayaIconComponent, selector: "naya-icon", inputs: ["name", "size", "stroke"] }, { kind: "component", type: NayaPrimaryButtonComponent, selector: "naya-primary-button", inputs: ["label", "loading", "disabled", "icon"], outputs: ["pressed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
849
|
-
}
|
|
850
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: DocumentSelectionScreen, decorators: [{
|
|
851
|
-
type: Component,
|
|
852
|
-
args: [{ selector: 'naya-document-selection-screen', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NayaScreenComponent, NayaIconComponent, NayaPrimaryButtonComponent], template: `
|
|
853
|
-
<naya-screen
|
|
854
|
-
[poweredBy]="s.poweredBy"
|
|
855
|
-
[canBack]="flow.canGoBack"
|
|
856
|
-
(back)="flow.back()"
|
|
857
|
-
(close)="flow.cancel()"
|
|
858
|
-
>
|
|
859
|
-
<div class="pad">
|
|
860
|
-
<h1 class="title">{{ s.docSelectTitle }}</h1>
|
|
861
|
-
<p class="body">{{ s.docSelectSubtitle }}</p>
|
|
862
|
-
|
|
863
|
-
@if (!lockedCountry) {
|
|
864
|
-
<div class="chips" role="listbox" aria-label="Pays">
|
|
865
|
-
@for (code of config.countries; track code) {
|
|
866
|
-
<button
|
|
867
|
-
type="button"
|
|
868
|
-
class="chip"
|
|
869
|
-
role="option"
|
|
870
|
-
[class.on]="code === country()"
|
|
871
|
-
[attr.aria-selected]="code === country()"
|
|
872
|
-
(click)="country.set(code)"
|
|
873
|
-
>{{ label(code) }}</button>
|
|
874
|
-
}
|
|
875
|
-
</div>
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
<div class="cards">
|
|
879
|
-
@for (doc of config.documentTypes; track doc) {
|
|
880
|
-
<button
|
|
881
|
-
type="button"
|
|
882
|
-
class="card"
|
|
883
|
-
[class.on]="doc === document()"
|
|
884
|
-
(click)="document.set(doc)"
|
|
885
|
-
>
|
|
886
|
-
<span class="ico"><naya-icon [name]="docIcon(doc)" [size]="20" /></span>
|
|
887
|
-
<span class="lbl">{{ s.docLabel(doc) }}</span>
|
|
888
|
-
<naya-icon name="chevron" [size]="20" [stroke]="2" />
|
|
889
|
-
</button>
|
|
890
|
-
}
|
|
891
|
-
</div>
|
|
892
|
-
|
|
893
|
-
<div class="spacer"></div>
|
|
894
|
-
<naya-primary-button
|
|
895
|
-
[label]="s.continueLabel"
|
|
896
|
-
[disabled]="!document()"
|
|
897
|
-
(pressed)="next()"
|
|
898
|
-
/>
|
|
899
|
-
</div>
|
|
900
|
-
</naya-screen>
|
|
901
|
-
`, styles: [".pad{display:flex;flex-direction:column;flex:1;padding:8px 24px 16px}.title{margin:0 0 8px;white-space:pre-line;text-wrap:balance;font-size:23px;font-weight:800;line-height:1.25;letter-spacing:-.2px;color:var(--naya-primary)}.body{margin:0 0 18px;font-size:15px;line-height:1.5;color:var(--naya-text-2)}.chips{display:flex;gap:8px;overflow-x:auto;padding-bottom:4px;margin-bottom:18px}.chip{flex:0 0 auto;padding:8px 14px;border-radius:999px;cursor:pointer;background:var(--naya-surface);border:1px solid var(--naya-line);font-size:13px;font-weight:500;color:var(--naya-text);white-space:nowrap}.chip.on{border:1.6px solid var(--naya-accent);font-weight:700;background:color-mix(in srgb,var(--naya-accent) 18%,transparent)}.chip:focus-visible{outline:2px solid var(--naya-accent);outline-offset:2px}.cards{display:flex;flex-direction:column;gap:10px}.card{display:flex;align-items:center;gap:12px;width:100%;padding:14px;cursor:pointer;text-align:left;background:var(--naya-surface);border:1px solid var(--naya-line);border-radius:var(--naya-r-card)}.card.on{border:1.8px solid var(--naya-accent);background:#fff9ee}.card:focus-visible{outline:2px solid var(--naya-accent);outline-offset:2px}.ico{width:36px;height:36px;border-radius:8px;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;background:#edf3fa;color:var(--naya-primary)}.lbl{flex:1;font-size:14px;font-weight:700;color:var(--naya-text)}.card naya-icon:last-child{color:var(--naya-text-2)}.spacer{flex:1 1 auto;min-height:16px}\n"] }]
|
|
902
|
-
}] });
|
|
903
|
-
|
|
904
|
-
/**
|
|
905
|
-
* Traitement d'image pour le web — équivalent de `image_utils.dart`, adapté au
|
|
906
|
-
* pipeline canvas du navigateur (le SDK Flutter lisait les plans bruts de la
|
|
907
|
-
* caméra).
|
|
908
|
-
*
|
|
909
|
-
* Les seuils sont repris de Flutter à titre de point de départ. Ils devront
|
|
910
|
-
* être **recalibrés** : la mesure porte ici sur des pixels RGBA issus d'un
|
|
911
|
-
* canvas, pas sur les plans NV21/BGRA natifs, si bien que les valeurs absolues
|
|
912
|
-
* ne se transposent pas telles quelles.
|
|
913
|
-
*/
|
|
914
|
-
/**
|
|
915
|
-
* Seuil de netteté (variance du Laplacien) au-delà duquel le cadre passe au
|
|
916
|
-
* vert. Volontairement prudent — à ajuster après essais multi-navigateurs.
|
|
917
|
-
*/
|
|
918
|
-
const SHARPNESS_READY_THRESHOLD = 60;
|
|
919
|
-
/** En dessous, l'éclairage est jugé insuffisant (luminance moyenne 0-255). */
|
|
920
|
-
const LUMINANCE_MIN = 60;
|
|
921
|
-
/** Au-dessus, l'image est surexposée. */
|
|
922
|
-
const LUMINANCE_MAX = 235;
|
|
923
|
-
/**
|
|
924
|
-
* Compresse une image (élément vidéo ou canvas) en data-URL JPEG.
|
|
925
|
-
*
|
|
926
|
-
* L'API accepte un corps JSON de 5 Mo ; un JPEG ≤ 1600 px de côté à qualité
|
|
927
|
-
* 0,85 reste largement sous la limite tout en préservant la lisibilité OCR —
|
|
928
|
-
* important sur les réseaux mobiles d'Afrique de l'Ouest.
|
|
929
|
-
*/
|
|
930
|
-
function canvasToJpegDataUrl(source, maxDimension = 1600, quality = 0.85) {
|
|
931
|
-
const srcW = source instanceof HTMLVideoElement ? source.videoWidth : source.width;
|
|
932
|
-
const srcH = source instanceof HTMLVideoElement ? source.videoHeight : source.height;
|
|
933
|
-
const scale = Math.min(1, maxDimension / Math.max(srcW, srcH));
|
|
934
|
-
const w = Math.round(srcW * scale);
|
|
935
|
-
const h = Math.round(srcH * scale);
|
|
936
|
-
const canvas = document.createElement('canvas');
|
|
937
|
-
canvas.width = w;
|
|
938
|
-
canvas.height = h;
|
|
939
|
-
const ctx = canvas.getContext('2d');
|
|
940
|
-
if (!ctx)
|
|
941
|
-
return '';
|
|
942
|
-
ctx.drawImage(source, 0, 0, w, h);
|
|
943
|
-
return canvas.toDataURL('image/jpeg', quality);
|
|
944
|
-
}
|
|
945
|
-
function measureFrameQuality(video, scratch, region = { x: 0.25, y: 0.25, w: 0.5, h: 0.5 }, sampleWidth = 240) {
|
|
946
|
-
const vw = video.videoWidth;
|
|
947
|
-
const vh = video.videoHeight;
|
|
948
|
-
if (vw < 8 || vh < 8) {
|
|
949
|
-
return { luminance: 0, sharpness: 0, lightOk: false, sharpOk: false };
|
|
950
|
-
}
|
|
951
|
-
const sx = Math.floor(vw * region.x);
|
|
952
|
-
const sy = Math.floor(vh * region.y);
|
|
953
|
-
const sw = Math.floor(vw * region.w);
|
|
954
|
-
const sh = Math.floor(vh * region.h);
|
|
955
|
-
// Réduction : on ramène la région à ~sampleWidth de large.
|
|
956
|
-
const scale = Math.min(1, sampleWidth / sw);
|
|
957
|
-
const w = Math.max(8, Math.round(sw * scale));
|
|
958
|
-
const h = Math.max(8, Math.round(sh * scale));
|
|
959
|
-
scratch.width = w;
|
|
960
|
-
scratch.height = h;
|
|
961
|
-
const ctx = scratch.getContext('2d', { willReadFrequently: true });
|
|
962
|
-
if (!ctx)
|
|
963
|
-
return { luminance: 0, sharpness: 0, lightOk: false, sharpOk: false };
|
|
964
|
-
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, w, h);
|
|
965
|
-
const { data } = ctx.getImageData(0, 0, w, h);
|
|
966
|
-
// Luminance (Rec. 601) par pixel, puis variance du Laplacien 4-voisins.
|
|
967
|
-
const lum = new Float32Array(w * h);
|
|
968
|
-
let sumLum = 0;
|
|
969
|
-
for (let i = 0, p = 0; i < data.length; i += 4, p++) {
|
|
970
|
-
const l = 0.299 * data[i] + 0.587 * data[i + 1] + 0.114 * data[i + 2];
|
|
971
|
-
lum[p] = l;
|
|
972
|
-
sumLum += l;
|
|
973
|
-
}
|
|
974
|
-
const luminance = sumLum / (w * h);
|
|
975
|
-
let sum = 0;
|
|
976
|
-
let sumSq = 0;
|
|
977
|
-
let count = 0;
|
|
978
|
-
for (let y = 1; y < h - 1; y++) {
|
|
979
|
-
for (let x = 1; x < w - 1; x++) {
|
|
980
|
-
const idx = y * w + x;
|
|
981
|
-
const lap = 4 * lum[idx] - lum[idx - 1] - lum[idx + 1] - lum[idx - w] - lum[idx + w];
|
|
982
|
-
sum += lap;
|
|
983
|
-
sumSq += lap * lap;
|
|
984
|
-
count++;
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
const mean = count ? sum / count : 0;
|
|
988
|
-
const sharpness = count ? sumSq / count - mean * mean : 0;
|
|
989
|
-
return {
|
|
990
|
-
luminance,
|
|
991
|
-
sharpness,
|
|
992
|
-
lightOk: luminance >= LUMINANCE_MIN && luminance <= LUMINANCE_MAX,
|
|
993
|
-
sharpOk: sharpness >= SHARPNESS_READY_THRESHOLD,
|
|
994
|
-
};
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
/**
|
|
998
|
-
* Écrans 3-4 — Capture du document (recto puis verso pour la CNI).
|
|
999
|
-
*
|
|
1000
|
-
* Caméra arrière, cadre de guidage centré au ratio carte (1,586), contrôles
|
|
1001
|
-
* qualité en direct (luminosité + netteté). Quand l'image est nette et bien
|
|
1002
|
-
* éclairée, le cadre et le déclencheur passent au vert — **c'est l'utilisateur
|
|
1003
|
-
* qui valide**. Reprend `document_capture_screen.dart`.
|
|
1004
|
-
*
|
|
1005
|
-
* `side` est déduit de l'étape courante ('capture-recto' | 'capture-verso').
|
|
1006
|
-
*/
|
|
1007
|
-
class DocumentCaptureScreen {
|
|
1008
|
-
flow = inject(NayaFlowService);
|
|
1009
|
-
s = this.flow.strings;
|
|
1010
|
-
/** Face capturée par cet écran. */
|
|
1011
|
-
side = input('recto', ...(ngDevMode ? [{ debugName: "side" }] : []));
|
|
1012
|
-
videoRef = viewChild('video', ...(ngDevMode ? [{ debugName: "videoRef" }] : []));
|
|
1013
|
-
streaming = signal(false, ...(ngDevMode ? [{ debugName: "streaming" }] : []));
|
|
1014
|
-
capturing = signal(false, ...(ngDevMode ? [{ debugName: "capturing" }] : []));
|
|
1015
|
-
cameraError = signal(null, ...(ngDevMode ? [{ debugName: "cameraError" }] : []));
|
|
1016
|
-
lightOk = signal(false, ...(ngDevMode ? [{ debugName: "lightOk" }] : []));
|
|
1017
|
-
sharpOk = signal(false, ...(ngDevMode ? [{ debugName: "sharpOk" }] : []));
|
|
1018
|
-
ready = computed(() => this.lightOk() && this.sharpOk() && !this.capturing(), ...(ngDevMode ? [{ debugName: "ready" }] : []));
|
|
1019
|
-
stream = null;
|
|
1020
|
-
qualityTimer;
|
|
1021
|
-
scratch = typeof document !== 'undefined' ? document.createElement('canvas') : null;
|
|
1022
|
-
instruction = computed(() => this.side() === 'recto' ? this.s.captureRectoInstruction : this.s.captureVersoInstruction, ...(ngDevMode ? [{ debugName: "instruction" }] : []));
|
|
1023
|
-
ngOnInit() {
|
|
1024
|
-
void this.startCamera();
|
|
1025
|
-
}
|
|
1026
|
-
ngOnDestroy() {
|
|
1027
|
-
this.stopCamera();
|
|
1028
|
-
}
|
|
1029
|
-
async startCamera() {
|
|
1030
|
-
if (!navigator.mediaDevices?.getUserMedia) {
|
|
1031
|
-
this.cameraError.set(this.s.errorCamera);
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
try {
|
|
1035
|
-
// Caméra arrière privilégiée pour le document ; repli sur défaut.
|
|
1036
|
-
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
1037
|
-
video: { facingMode: { ideal: 'environment' }, width: { ideal: 1920 } },
|
|
1038
|
-
audio: false,
|
|
1039
|
-
});
|
|
1040
|
-
// L'écran a pu être quitté pendant l'attente d'autorisation.
|
|
1041
|
-
const video = this.videoRef()?.nativeElement;
|
|
1042
|
-
if (!video) {
|
|
1043
|
-
this.stopCamera();
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
video.srcObject = this.stream;
|
|
1047
|
-
await video.play().catch(() => { });
|
|
1048
|
-
this.streaming.set(true);
|
|
1049
|
-
this.startQualityLoop(video);
|
|
1050
|
-
}
|
|
1051
|
-
catch {
|
|
1052
|
-
this.cameraError.set(this.s.errorCamera);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
startQualityLoop(video) {
|
|
1056
|
-
if (!this.scratch)
|
|
1057
|
-
return;
|
|
1058
|
-
// Cadence alignée sur Flutter (~350 ms) : suffisant pour un retour fluide
|
|
1059
|
-
// sans saturer le thread principal avec des getImageData.
|
|
1060
|
-
this.qualityTimer = setInterval(() => {
|
|
1061
|
-
if (this.capturing() || video.videoWidth < 8)
|
|
1062
|
-
return;
|
|
1063
|
-
const q = measureFrameQuality(video, this.scratch);
|
|
1064
|
-
if (q.lightOk !== this.lightOk())
|
|
1065
|
-
this.lightOk.set(q.lightOk);
|
|
1066
|
-
if (q.sharpOk !== this.sharpOk())
|
|
1067
|
-
this.sharpOk.set(q.sharpOk);
|
|
1068
|
-
}, 350);
|
|
1069
|
-
}
|
|
1070
|
-
stopCamera() {
|
|
1071
|
-
if (this.qualityTimer)
|
|
1072
|
-
clearInterval(this.qualityTimer);
|
|
1073
|
-
this.qualityTimer = undefined;
|
|
1074
|
-
this.stream?.getTracks().forEach((t) => t.stop());
|
|
1075
|
-
this.stream = null;
|
|
1076
|
-
this.streaming.set(false);
|
|
1077
|
-
}
|
|
1078
|
-
async capture() {
|
|
1079
|
-
const video = this.videoRef()?.nativeElement;
|
|
1080
|
-
if (!video || this.capturing() || !this.streaming())
|
|
1081
|
-
return;
|
|
1082
|
-
this.capturing.set(true);
|
|
1083
|
-
try {
|
|
1084
|
-
const dataUrl = canvasToJpegDataUrl(video);
|
|
1085
|
-
this.stopCamera();
|
|
1086
|
-
if (this.side() === 'recto') {
|
|
1087
|
-
this.flow.rectoBase64.set(dataUrl);
|
|
1088
|
-
if (this.flow.needsVerso) {
|
|
1089
|
-
this.flow.go('capture-verso');
|
|
1090
|
-
return;
|
|
1091
|
-
}
|
|
1092
|
-
}
|
|
1093
|
-
else {
|
|
1094
|
-
this.flow.versoBase64.set(dataUrl);
|
|
1095
|
-
}
|
|
1096
|
-
// Dernière face : l'envoi (session + document) part en arrière-plan
|
|
1097
|
-
// pendant que l'utilisateur enchaîne sur le selfie.
|
|
1098
|
-
this.flow.startDocumentUpload();
|
|
1099
|
-
this.flow.go('liveness');
|
|
1100
|
-
}
|
|
1101
|
-
finally {
|
|
1102
|
-
this.capturing.set(false);
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: DocumentCaptureScreen, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1106
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: DocumentCaptureScreen, isStandalone: true, selector: "naya-document-capture-screen", inputs: { side: { classPropertyName: "side", publicName: "side", isSignal: true, isRequired: false, transformFunction: null } }, viewQueries: [{ propertyName: "videoRef", first: true, predicate: ["video"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1107
|
-
<naya-screen
|
|
1108
|
-
[poweredBy]="s.poweredBy"
|
|
1109
|
-
[dark]="true"
|
|
1110
|
-
[canBack]="flow.canGoBack"
|
|
1111
|
-
(back)="flow.back()"
|
|
1112
|
-
(close)="flow.cancel()"
|
|
1113
|
-
>
|
|
1114
|
-
<div class="pad">
|
|
1115
|
-
<p class="instruction">{{ instruction() }}</p>
|
|
1116
|
-
|
|
1117
|
-
<div class="cardWrap">
|
|
1118
|
-
<div class="card" [class.ready]="ready()">
|
|
1119
|
-
@if (cameraError()) {
|
|
1120
|
-
<div class="camMsg">{{ cameraError() }}</div>
|
|
1121
|
-
} @else {
|
|
1122
|
-
<video #video class="preview" playsinline muted autoplay></video>
|
|
1123
|
-
@if (!streaming()) {
|
|
1124
|
-
<div class="camMsg"><span class="loader"></span></div>
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
<!-- Coins de guidage, ambre puis verts quand l'image est bonne -->
|
|
1128
|
-
<svg class="corners" viewBox="0 0 100 63" preserveAspectRatio="none" aria-hidden="true">
|
|
1129
|
-
<path d="M2,14 L2,6 Q2,2 6,2 L16,2" />
|
|
1130
|
-
<path d="M84,2 L94,2 Q98,2 98,6 L98,14" />
|
|
1131
|
-
<path d="M98,49 L98,57 Q98,61 94,61 L84,61" />
|
|
1132
|
-
<path d="M16,61 L6,61 Q2,61 2,57 L2,49" />
|
|
1133
|
-
</svg>
|
|
1134
|
-
</div>
|
|
1135
|
-
</div>
|
|
1136
|
-
|
|
1137
|
-
<div class="pills">
|
|
1138
|
-
<span class="pill" [class.ok]="lightOk()">
|
|
1139
|
-
<naya-icon [name]="lightOk() ? 'check' : 'sun'" [size]="15" [stroke]="2.4" />
|
|
1140
|
-
{{ s.qualityLight }}
|
|
1141
|
-
</span>
|
|
1142
|
-
<span class="pill" [class.ok]="sharpOk()">
|
|
1143
|
-
<naya-icon [name]="sharpOk() ? 'check' : 'focus'" [size]="15" [stroke]="2.4" />
|
|
1144
|
-
{{ s.qualitySharpness }}
|
|
1145
|
-
</span>
|
|
1146
|
-
</div>
|
|
1147
|
-
|
|
1148
|
-
<button
|
|
1149
|
-
type="button"
|
|
1150
|
-
class="shutter"
|
|
1151
|
-
[class.ready]="ready()"
|
|
1152
|
-
[disabled]="!streaming() || capturing()"
|
|
1153
|
-
(click)="capture()"
|
|
1154
|
-
[attr.aria-label]="s.captureHint"
|
|
1155
|
-
>
|
|
1156
|
-
@if (capturing()) { <span class="loader sm"></span> }
|
|
1157
|
-
</button>
|
|
1158
|
-
|
|
1159
|
-
<p class="hint" [class.ready]="ready()">
|
|
1160
|
-
{{ ready() ? s.captureReady : s.captureHint }}
|
|
1161
|
-
</p>
|
|
1162
|
-
</div>
|
|
1163
|
-
</naya-screen>
|
|
1164
|
-
`, isInline: true, styles: [".pad{display:flex;flex-direction:column;flex:1;align-items:center;padding:0 16px 16px}.instruction{width:100%;margin:0 0 12px;padding:12px 16px;text-align:center;background:#0000008c;border-radius:12px;color:#fff;font-size:15.5px;line-height:1.35;font-weight:600}.cardWrap{flex:1 1 auto;display:flex;align-items:center;width:100%;min-height:0}.card{position:relative;width:100%;aspect-ratio:1.586;border-radius:10px;overflow:hidden;background:#ffffff0f}.preview{width:100%;height:100%;object-fit:cover;display:block}.camMsg{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:16px;text-align:center;color:#ffffffb3;font-size:13px}.corners{position:absolute;inset:0;width:100%;height:100%;fill:none;stroke:var(--naya-accent);stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;transition:stroke .2s ease;pointer-events:none}.card.ready .corners{stroke:var(--naya-success)}.pills{display:flex;gap:8px;justify-content:center;margin:14px 0}.pill{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:999px;background:color-mix(in srgb,var(--naya-warning) 92%,transparent);color:#fff;font-size:12.5px;font-weight:700}.pill.ok{background:color-mix(in srgb,var(--naya-success) 92%,transparent)}.shutter{width:72px;height:72px;border-radius:50%;cursor:pointer;border:4.5px solid #fff;background:#ffffff4d;display:inline-flex;align-items:center;justify-content:center;transition:border-color .2s ease,background .2s ease}.shutter.ready{border-color:var(--naya-success);background:color-mix(in srgb,var(--naya-success) 30%,transparent)}.shutter:disabled{cursor:default}.shutter:focus-visible{outline:2px solid #fff;outline-offset:3px}.hint{margin:12px 0 0;text-align:center;font-size:13px;font-weight:500;color:#9fb2c6}.hint.ready{color:var(--naya-success);font-weight:700}.loader{width:30px;height:30px;border-radius:50%;border:3px solid rgba(255,255,255,.25);border-top-color:#fff;animation:naya-spin .8s linear infinite}.loader.sm{width:24px;height:24px;border-width:2.5px}@keyframes naya-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.loader{animation-duration:1.6s}}\n"], dependencies: [{ kind: "component", type: NayaScreenComponent, selector: "naya-screen", inputs: ["poweredBy", "canBack", "canClose", "dark"], outputs: ["back", "close"] }, { kind: "component", type: NayaIconComponent, selector: "naya-icon", inputs: ["name", "size", "stroke"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1165
|
-
}
|
|
1166
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: DocumentCaptureScreen, decorators: [{
|
|
1167
|
-
type: Component,
|
|
1168
|
-
args: [{ selector: 'naya-document-capture-screen', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NayaScreenComponent, NayaIconComponent], template: `
|
|
1169
|
-
<naya-screen
|
|
1170
|
-
[poweredBy]="s.poweredBy"
|
|
1171
|
-
[dark]="true"
|
|
1172
|
-
[canBack]="flow.canGoBack"
|
|
1173
|
-
(back)="flow.back()"
|
|
1174
|
-
(close)="flow.cancel()"
|
|
1175
|
-
>
|
|
1176
|
-
<div class="pad">
|
|
1177
|
-
<p class="instruction">{{ instruction() }}</p>
|
|
1178
|
-
|
|
1179
|
-
<div class="cardWrap">
|
|
1180
|
-
<div class="card" [class.ready]="ready()">
|
|
1181
|
-
@if (cameraError()) {
|
|
1182
|
-
<div class="camMsg">{{ cameraError() }}</div>
|
|
1183
|
-
} @else {
|
|
1184
|
-
<video #video class="preview" playsinline muted autoplay></video>
|
|
1185
|
-
@if (!streaming()) {
|
|
1186
|
-
<div class="camMsg"><span class="loader"></span></div>
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
<!-- Coins de guidage, ambre puis verts quand l'image est bonne -->
|
|
1190
|
-
<svg class="corners" viewBox="0 0 100 63" preserveAspectRatio="none" aria-hidden="true">
|
|
1191
|
-
<path d="M2,14 L2,6 Q2,2 6,2 L16,2" />
|
|
1192
|
-
<path d="M84,2 L94,2 Q98,2 98,6 L98,14" />
|
|
1193
|
-
<path d="M98,49 L98,57 Q98,61 94,61 L84,61" />
|
|
1194
|
-
<path d="M16,61 L6,61 Q2,61 2,57 L2,49" />
|
|
1195
|
-
</svg>
|
|
1196
|
-
</div>
|
|
1197
|
-
</div>
|
|
1198
|
-
|
|
1199
|
-
<div class="pills">
|
|
1200
|
-
<span class="pill" [class.ok]="lightOk()">
|
|
1201
|
-
<naya-icon [name]="lightOk() ? 'check' : 'sun'" [size]="15" [stroke]="2.4" />
|
|
1202
|
-
{{ s.qualityLight }}
|
|
1203
|
-
</span>
|
|
1204
|
-
<span class="pill" [class.ok]="sharpOk()">
|
|
1205
|
-
<naya-icon [name]="sharpOk() ? 'check' : 'focus'" [size]="15" [stroke]="2.4" />
|
|
1206
|
-
{{ s.qualitySharpness }}
|
|
1207
|
-
</span>
|
|
1208
|
-
</div>
|
|
1209
|
-
|
|
1210
|
-
<button
|
|
1211
|
-
type="button"
|
|
1212
|
-
class="shutter"
|
|
1213
|
-
[class.ready]="ready()"
|
|
1214
|
-
[disabled]="!streaming() || capturing()"
|
|
1215
|
-
(click)="capture()"
|
|
1216
|
-
[attr.aria-label]="s.captureHint"
|
|
1217
|
-
>
|
|
1218
|
-
@if (capturing()) { <span class="loader sm"></span> }
|
|
1219
|
-
</button>
|
|
1220
|
-
|
|
1221
|
-
<p class="hint" [class.ready]="ready()">
|
|
1222
|
-
{{ ready() ? s.captureReady : s.captureHint }}
|
|
1223
|
-
</p>
|
|
1224
|
-
</div>
|
|
1225
|
-
</naya-screen>
|
|
1226
|
-
`, styles: [".pad{display:flex;flex-direction:column;flex:1;align-items:center;padding:0 16px 16px}.instruction{width:100%;margin:0 0 12px;padding:12px 16px;text-align:center;background:#0000008c;border-radius:12px;color:#fff;font-size:15.5px;line-height:1.35;font-weight:600}.cardWrap{flex:1 1 auto;display:flex;align-items:center;width:100%;min-height:0}.card{position:relative;width:100%;aspect-ratio:1.586;border-radius:10px;overflow:hidden;background:#ffffff0f}.preview{width:100%;height:100%;object-fit:cover;display:block}.camMsg{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;padding:16px;text-align:center;color:#ffffffb3;font-size:13px}.corners{position:absolute;inset:0;width:100%;height:100%;fill:none;stroke:var(--naya-accent);stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round;transition:stroke .2s ease;pointer-events:none}.card.ready .corners{stroke:var(--naya-success)}.pills{display:flex;gap:8px;justify-content:center;margin:14px 0}.pill{display:inline-flex;align-items:center;gap:6px;padding:7px 14px;border-radius:999px;background:color-mix(in srgb,var(--naya-warning) 92%,transparent);color:#fff;font-size:12.5px;font-weight:700}.pill.ok{background:color-mix(in srgb,var(--naya-success) 92%,transparent)}.shutter{width:72px;height:72px;border-radius:50%;cursor:pointer;border:4.5px solid #fff;background:#ffffff4d;display:inline-flex;align-items:center;justify-content:center;transition:border-color .2s ease,background .2s ease}.shutter.ready{border-color:var(--naya-success);background:color-mix(in srgb,var(--naya-success) 30%,transparent)}.shutter:disabled{cursor:default}.shutter:focus-visible{outline:2px solid #fff;outline-offset:3px}.hint{margin:12px 0 0;text-align:center;font-size:13px;font-weight:500;color:#9fb2c6}.hint.ready{color:var(--naya-success);font-weight:700}.loader{width:30px;height:30px;border-radius:50%;border:3px solid rgba(255,255,255,.25);border-top-color:#fff;animation:naya-spin .8s linear infinite}.loader.sm{width:24px;height:24px;border-width:2.5px}@keyframes naya-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.loader{animation-duration:1.6s}}\n"] }]
|
|
1227
|
-
}], propDecorators: { side: [{ type: i0.Input, args: [{ isSignal: true, alias: "side", required: false }] }], videoRef: [{ type: i0.ViewChild, args: ['video', { isSignal: true }] }] } });
|
|
1228
|
-
|
|
1229
|
-
/**
|
|
1230
|
-
* Machine à états du défi de vivacité, exécutée sur l'appareil.
|
|
1231
|
-
*
|
|
1232
|
-
* Port fidèle de `liveness_challenge.dart`. La logique est identique ; seule
|
|
1233
|
-
* l'entrée change : au lieu d'objets ML Kit, elle reçoit des signaux déjà
|
|
1234
|
-
* normalisés par l'adaptateur MediaPipe — probabilités d'yeux ouverts (0-1) et
|
|
1235
|
-
* lacet approximatif en degrés.
|
|
1236
|
-
*
|
|
1237
|
-
* Séquence : détection du visage → clignement → rotation de la tête. Chaque
|
|
1238
|
-
* trame fait avancer (ou reculer) l'état ; le score final reflète la complétion
|
|
1239
|
-
* du défi et la stabilité du suivi.
|
|
1240
|
-
*/
|
|
1241
|
-
// Seuils repris de Flutter. Valeurs de départ : la source des signaux diffère
|
|
1242
|
-
// (blendshapes MediaPipe vs classification ML Kit), un recalibrage sur visages
|
|
1243
|
-
// réels est attendu.
|
|
1244
|
-
const EYES_CLOSED_THRESHOLD = 0.25;
|
|
1245
|
-
const EYES_OPEN_THRESHOLD = 0.65;
|
|
1246
|
-
const HEAD_TURN_DEGREES = 16;
|
|
1247
|
-
const FRAMES_TO_LOCK_FACE = 6;
|
|
1248
|
-
const FRAMES_LOST_BEFORE_RESET = 12;
|
|
1249
|
-
class LivenessChallenge {
|
|
1250
|
-
step = 'searching-face';
|
|
1251
|
-
framesWithFace = 0;
|
|
1252
|
-
framesWithoutFace = 0;
|
|
1253
|
-
eyesClosedSeen = false;
|
|
1254
|
-
blinkDone = false;
|
|
1255
|
-
turnDone = false;
|
|
1256
|
-
/** Index numérique de l'étape (pour l'anneau de progression). */
|
|
1257
|
-
get stepIndex() {
|
|
1258
|
-
return { 'searching-face': 0, blink: 1, 'turn-head': 2, done: 3 }[this.step];
|
|
1259
|
-
}
|
|
1260
|
-
get isDone() {
|
|
1261
|
-
return this.step === 'done';
|
|
1262
|
-
}
|
|
1263
|
-
/**
|
|
1264
|
-
* Fait avancer la machine avec les signaux d'une trame. Retourne `true` si
|
|
1265
|
-
* l'étape courante a changé (pour rafraîchir l'interface).
|
|
1266
|
-
*/
|
|
1267
|
-
onFrame(sig) {
|
|
1268
|
-
const before = this.step;
|
|
1269
|
-
if (sig.faceCount !== 1) {
|
|
1270
|
-
this.framesWithoutFace++;
|
|
1271
|
-
// Visage perdu trop longtemps → retour à la détection, sans annuler les
|
|
1272
|
-
// défis déjà réussis.
|
|
1273
|
-
if (this.framesWithoutFace > FRAMES_LOST_BEFORE_RESET && this.step !== 'done') {
|
|
1274
|
-
this.step = 'searching-face';
|
|
1275
|
-
this.framesWithFace = 0;
|
|
1276
|
-
}
|
|
1277
|
-
return this.step !== before;
|
|
1278
|
-
}
|
|
1279
|
-
this.framesWithoutFace = 0;
|
|
1280
|
-
this.framesWithFace++;
|
|
1281
|
-
switch (this.step) {
|
|
1282
|
-
case 'searching-face':
|
|
1283
|
-
if (this.framesWithFace >= FRAMES_TO_LOCK_FACE) {
|
|
1284
|
-
this.step = this.blinkDone ? 'turn-head' : 'blink';
|
|
1285
|
-
}
|
|
1286
|
-
break;
|
|
1287
|
-
case 'blink': {
|
|
1288
|
-
const { leftEyeOpen: l, rightEyeOpen: r } = sig;
|
|
1289
|
-
if (l == null || r == null)
|
|
1290
|
-
break;
|
|
1291
|
-
if (l < EYES_CLOSED_THRESHOLD && r < EYES_CLOSED_THRESHOLD) {
|
|
1292
|
-
this.eyesClosedSeen = true;
|
|
1293
|
-
}
|
|
1294
|
-
else if (this.eyesClosedSeen && l > EYES_OPEN_THRESHOLD && r > EYES_OPEN_THRESHOLD) {
|
|
1295
|
-
this.blinkDone = true;
|
|
1296
|
-
this.step = this.turnDone ? 'done' : 'turn-head';
|
|
1297
|
-
}
|
|
1298
|
-
break;
|
|
1299
|
-
}
|
|
1300
|
-
case 'turn-head': {
|
|
1301
|
-
const yaw = sig.yawDegrees;
|
|
1302
|
-
if (yaw != null && Math.abs(yaw) > HEAD_TURN_DEGREES) {
|
|
1303
|
-
this.turnDone = true;
|
|
1304
|
-
this.step = 'done';
|
|
1305
|
-
}
|
|
1306
|
-
break;
|
|
1307
|
-
}
|
|
1308
|
-
case 'done':
|
|
1309
|
-
break;
|
|
1310
|
-
}
|
|
1311
|
-
return this.step !== before;
|
|
1312
|
-
}
|
|
1313
|
-
/**
|
|
1314
|
-
* Score 0-100 transmis à l'API. Défi complet → 90 + bonus de stabilité de
|
|
1315
|
-
* suivi (jusqu'à 8). Jamais de score élevé sans défi réussi.
|
|
1316
|
-
*/
|
|
1317
|
-
score() {
|
|
1318
|
-
if (!this.isDone) {
|
|
1319
|
-
let partial = 20;
|
|
1320
|
-
if (this.framesWithFace >= FRAMES_TO_LOCK_FACE)
|
|
1321
|
-
partial += 15;
|
|
1322
|
-
if (this.blinkDone)
|
|
1323
|
-
partial += 15;
|
|
1324
|
-
return partial;
|
|
1325
|
-
}
|
|
1326
|
-
const stabilityBonus = Math.min(8, Math.round(this.framesWithFace / 30));
|
|
1327
|
-
return 90 + stabilityBonus;
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
// La version du WASM doit correspondre à celle du paquet JS (@mediapipe/
|
|
1332
|
-
// tasks-vision) déclaré en dépendance, sous peine d'incompatibilité runtime.
|
|
1333
|
-
const DEFAULT_WASM_BASE = 'https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@1.0.1/wasm';
|
|
1334
|
-
const DEFAULT_MODEL = 'https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task';
|
|
1335
|
-
// Indices de landmarks (topologie 468 points de MediaPipe Face Mesh).
|
|
1336
|
-
const NOSE_TIP = 1;
|
|
1337
|
-
const FACE_LEFT_EDGE = 234;
|
|
1338
|
-
const FACE_RIGHT_EDGE = 454;
|
|
1339
|
-
class FaceTracker {
|
|
1340
|
-
landmarker = null;
|
|
1341
|
-
/** Charge le modèle. À appeler une fois, avant `detect`. */
|
|
1342
|
-
async init(assets = {}) {
|
|
1343
|
-
const vision = await import('@mediapipe/tasks-vision');
|
|
1344
|
-
const { FaceLandmarker, FilesetResolver } = vision;
|
|
1345
|
-
const fileset = await FilesetResolver.forVisionTasks(assets.wasmBaseUrl ?? DEFAULT_WASM_BASE);
|
|
1346
|
-
this.landmarker = await FaceLandmarker.createFromOptions(fileset, {
|
|
1347
|
-
baseOptions: {
|
|
1348
|
-
modelAssetPath: assets.modelUrl ?? DEFAULT_MODEL,
|
|
1349
|
-
delegate: 'GPU',
|
|
1350
|
-
},
|
|
1351
|
-
runningMode: 'VIDEO',
|
|
1352
|
-
numFaces: 1,
|
|
1353
|
-
outputFaceBlendshapes: true,
|
|
1354
|
-
});
|
|
1355
|
-
}
|
|
1356
|
-
get ready() {
|
|
1357
|
-
return !!this.landmarker;
|
|
1358
|
-
}
|
|
1359
|
-
/** Analyse une trame vidéo et renvoie les signaux normalisés du défi. */
|
|
1360
|
-
detect(video, timestampMs) {
|
|
1361
|
-
if (!this.landmarker || video.videoWidth < 8) {
|
|
1362
|
-
return { faceCount: 0, leftEyeOpen: null, rightEyeOpen: null, yawDegrees: null };
|
|
1363
|
-
}
|
|
1364
|
-
const result = this.landmarker.detectForVideo(video, timestampMs);
|
|
1365
|
-
const faceCount = result.faceLandmarks?.length ?? 0;
|
|
1366
|
-
if (faceCount !== 1) {
|
|
1367
|
-
return { faceCount, leftEyeOpen: null, rightEyeOpen: null, yawDegrees: null };
|
|
1368
|
-
}
|
|
1369
|
-
// ── Clignement via blendshapes ────────────────────────────────────────
|
|
1370
|
-
let leftEyeOpen = null;
|
|
1371
|
-
let rightEyeOpen = null;
|
|
1372
|
-
const shapes = result.faceBlendshapes?.[0]?.categories;
|
|
1373
|
-
if (shapes) {
|
|
1374
|
-
for (const c of shapes) {
|
|
1375
|
-
if (c.categoryName === 'eyeBlinkLeft')
|
|
1376
|
-
leftEyeOpen = 1 - c.score;
|
|
1377
|
-
else if (c.categoryName === 'eyeBlinkRight')
|
|
1378
|
-
rightEyeOpen = 1 - c.score;
|
|
1379
|
-
}
|
|
1380
|
-
}
|
|
1381
|
-
// ── Lacet via landmarks ───────────────────────────────────────────────
|
|
1382
|
-
let yawDegrees = null;
|
|
1383
|
-
const lm = result.faceLandmarks[0];
|
|
1384
|
-
const nose = lm[NOSE_TIP];
|
|
1385
|
-
const left = lm[FACE_LEFT_EDGE];
|
|
1386
|
-
const right = lm[FACE_RIGHT_EDGE];
|
|
1387
|
-
if (nose && left && right) {
|
|
1388
|
-
const span = right.x - left.x;
|
|
1389
|
-
if (Math.abs(span) > 1e-4) {
|
|
1390
|
-
// Position relative du nez entre les bords : 0,5 = centré.
|
|
1391
|
-
const ratio = (nose.x - left.x) / span;
|
|
1392
|
-
// Conversion en pseudo-degrés : ±0,1 d'écart ≈ ±16°, seuil Flutter.
|
|
1393
|
-
yawDegrees = (ratio - 0.5) * 160;
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
return { faceCount: 1, leftEyeOpen, rightEyeOpen, yawDegrees };
|
|
1397
|
-
}
|
|
1398
|
-
close() {
|
|
1399
|
-
this.landmarker?.close?.();
|
|
1400
|
-
this.landmarker = null;
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
/**
|
|
1405
|
-
* Écran 5 — Selfie avec défi de vivacité sur l'appareil (MediaPipe).
|
|
1406
|
-
*
|
|
1407
|
-
* Caméra frontale, chaque trame alimente le suivi facial puis la machine à
|
|
1408
|
-
* états (visage → clignement → rotation). Défi complet → photo capturée,
|
|
1409
|
-
* compressée, et passage au traitement. Reprend `liveness_screen.dart`.
|
|
1410
|
-
*
|
|
1411
|
-
* L'analyse est locale : la photo n'est transmise qu'à la fin du défi.
|
|
1412
|
-
*/
|
|
1413
|
-
class LivenessScreen {
|
|
1414
|
-
flow = inject(NayaFlowService);
|
|
1415
|
-
s = this.flow.strings;
|
|
1416
|
-
videoRef = viewChild('video', ...(ngDevMode ? [{ debugName: "videoRef" }] : []));
|
|
1417
|
-
streaming = signal(false, ...(ngDevMode ? [{ debugName: "streaming" }] : []));
|
|
1418
|
-
cameraError = signal(null, ...(ngDevMode ? [{ debugName: "cameraError" }] : []));
|
|
1419
|
-
/** Étape courante, exposée en signal pour piloter l'affichage. */
|
|
1420
|
-
step = signal('searching-face', ...(ngDevMode ? [{ debugName: "step" }] : []));
|
|
1421
|
-
/**
|
|
1422
|
-
* Ellipse tracée comme deux demi-arcs partant du sommet (105,4) dans le sens
|
|
1423
|
-
* horaire. `pathLength=100` normalise la longueur : l'offset s'exprime alors
|
|
1424
|
-
* en pourcentage, indépendamment de la taille rendue.
|
|
1425
|
-
*/
|
|
1426
|
-
ringPath = 'M105,4 A101,131 0 0 1 105,266 A101,131 0 0 1 105,4 Z';
|
|
1427
|
-
dashOffset = computed(() => {
|
|
1428
|
-
const progress = Math.min(1, this.stepIndex() / 3);
|
|
1429
|
-
return 100 * (1 - progress);
|
|
1430
|
-
}, ...(ngDevMode ? [{ debugName: "dashOffset" }] : []));
|
|
1431
|
-
stepIndex = computed(() => ({ 'searching-face': 0, blink: 1, 'turn-head': 2, done: 3 })[this.step()], ...(ngDevMode ? [{ debugName: "stepIndex" }] : []));
|
|
1432
|
-
challenge = new LivenessChallenge();
|
|
1433
|
-
tracker = new FaceTracker();
|
|
1434
|
-
stream = null;
|
|
1435
|
-
rafId = 0;
|
|
1436
|
-
finishing = false;
|
|
1437
|
-
instructionText = computed(() => {
|
|
1438
|
-
switch (this.step()) {
|
|
1439
|
-
case 'searching-face': return this.s.livenessLookStraight;
|
|
1440
|
-
case 'blink': return this.s.livenessBlink;
|
|
1441
|
-
case 'turn-head': return this.s.livenessTurnHead;
|
|
1442
|
-
case 'done': return this.s.livenessHold;
|
|
1443
|
-
}
|
|
1444
|
-
}, ...(ngDevMode ? [{ debugName: "instructionText" }] : []));
|
|
1445
|
-
instructionIcon = computed(() => {
|
|
1446
|
-
switch (this.step()) {
|
|
1447
|
-
case 'searching-face': return 'focus';
|
|
1448
|
-
case 'blink': return 'eye';
|
|
1449
|
-
case 'turn-head': return 'rotate';
|
|
1450
|
-
case 'done': return 'hourglass';
|
|
1451
|
-
}
|
|
1452
|
-
}, ...(ngDevMode ? [{ debugName: "instructionIcon" }] : []));
|
|
1453
|
-
challengeSteps = computed(() => {
|
|
1454
|
-
const cur = this.stepIndex();
|
|
1455
|
-
const state = (n) => (cur >= n ? 'done' : cur === n - 1 ? 'active' : 'pending');
|
|
1456
|
-
return [
|
|
1457
|
-
{ label: this.s.livenessStepFace, state: state(1) },
|
|
1458
|
-
{ label: this.s.livenessStepBlink, state: state(2) },
|
|
1459
|
-
{ label: this.s.livenessStepTurn, state: state(3) },
|
|
1460
|
-
];
|
|
1461
|
-
}, ...(ngDevMode ? [{ debugName: "challengeSteps" }] : []));
|
|
1462
|
-
async ngOnInit() {
|
|
1463
|
-
await this.start();
|
|
1464
|
-
}
|
|
1465
|
-
ngOnDestroy() {
|
|
1466
|
-
this.teardown();
|
|
1467
|
-
}
|
|
1468
|
-
async start() {
|
|
1469
|
-
if (!navigator.mediaDevices?.getUserMedia) {
|
|
1470
|
-
this.cameraError.set(this.s.errorCamera);
|
|
1471
|
-
return;
|
|
1472
|
-
}
|
|
1473
|
-
try {
|
|
1474
|
-
// Le modèle et son WASM (volumineux) ne sont chargés qu'ici.
|
|
1475
|
-
await this.tracker.init(this.flow.resolvedConfig.livenessAssets);
|
|
1476
|
-
}
|
|
1477
|
-
catch {
|
|
1478
|
-
this.cameraError.set(this.s.errorNetwork);
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
try {
|
|
1482
|
-
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
1483
|
-
video: { facingMode: { ideal: 'user' }, width: { ideal: 1280 } },
|
|
1484
|
-
audio: false,
|
|
1485
|
-
});
|
|
1486
|
-
const video = this.videoRef()?.nativeElement;
|
|
1487
|
-
if (!video) {
|
|
1488
|
-
this.teardown();
|
|
1489
|
-
return;
|
|
1490
|
-
}
|
|
1491
|
-
video.srcObject = this.stream;
|
|
1492
|
-
await video.play().catch(() => { });
|
|
1493
|
-
this.streaming.set(true);
|
|
1494
|
-
this.loop();
|
|
1495
|
-
}
|
|
1496
|
-
catch {
|
|
1497
|
-
this.cameraError.set(this.s.errorCamera);
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
loop = () => {
|
|
1501
|
-
const video = this.videoRef()?.nativeElement;
|
|
1502
|
-
if (!video || this.finishing)
|
|
1503
|
-
return;
|
|
1504
|
-
if (this.tracker.ready && video.videoWidth > 8) {
|
|
1505
|
-
try {
|
|
1506
|
-
const signals = this.tracker.detect(video, performance.now());
|
|
1507
|
-
this.challenge.onFrame(signals);
|
|
1508
|
-
if (this.challenge.step !== this.step())
|
|
1509
|
-
this.step.set(this.challenge.step);
|
|
1510
|
-
if (this.challenge.isDone) {
|
|
1511
|
-
void this.finish();
|
|
1512
|
-
return;
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
catch {
|
|
1516
|
-
// Trame illisible : ignorée, la suivante arrive au prochain rAF.
|
|
1517
|
-
}
|
|
1518
|
-
}
|
|
1519
|
-
this.rafId = requestAnimationFrame(this.loop);
|
|
1520
|
-
};
|
|
1521
|
-
async finish() {
|
|
1522
|
-
if (this.finishing)
|
|
1523
|
-
return;
|
|
1524
|
-
this.finishing = true;
|
|
1525
|
-
cancelAnimationFrame(this.rafId);
|
|
1526
|
-
// Libérer MediaPipe (contexte WebGL/GPU) AVANT la capture. Sur iOS Safari,
|
|
1527
|
-
// dessiner la vidéo sur un canvas 2D pendant que MediaPipe détient sa
|
|
1528
|
-
// texture GPU corrompt l'image : striures verticales et dérive magenta —
|
|
1529
|
-
// que la détection anti-fraude serveur interprète (à raison) comme une
|
|
1530
|
-
// photo d'écran. La libérer d'abord rend au <video> des trames propres,
|
|
1531
|
-
// et améliore du même coup la netteté utile à la comparaison faciale.
|
|
1532
|
-
this.tracker.close();
|
|
1533
|
-
const video = this.videoRef()?.nativeElement;
|
|
1534
|
-
try {
|
|
1535
|
-
// Laisser la caméra composer quelques trames propres, hors emprise GPU,
|
|
1536
|
-
// et le temps à l'utilisateur de revenir face caméra.
|
|
1537
|
-
await this.waitFrames(3);
|
|
1538
|
-
await new Promise((r) => setTimeout(r, 300));
|
|
1539
|
-
if (video && video.videoWidth > 8) {
|
|
1540
|
-
this.flow.livenessScore.set(this.challenge.score());
|
|
1541
|
-
this.flow.selfieBase64.set(canvasToJpegDataUrl(video, 1080));
|
|
1542
|
-
}
|
|
1543
|
-
this.stopStream();
|
|
1544
|
-
this.flow.replace('processing');
|
|
1545
|
-
}
|
|
1546
|
-
catch {
|
|
1547
|
-
this.finishing = false;
|
|
1548
|
-
this.stopStream();
|
|
1549
|
-
this.cameraError.set(this.s.errorCamera);
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
/** Attend `n` trames d'affichage — laisse le compositeur présenter du frais. */
|
|
1553
|
-
waitFrames(n) {
|
|
1554
|
-
return new Promise((resolve) => {
|
|
1555
|
-
let i = 0;
|
|
1556
|
-
const tick = () => (++i >= n ? resolve() : requestAnimationFrame(tick));
|
|
1557
|
-
requestAnimationFrame(tick);
|
|
1558
|
-
});
|
|
1559
|
-
}
|
|
1560
|
-
stopStream() {
|
|
1561
|
-
this.stream?.getTracks().forEach((t) => t.stop());
|
|
1562
|
-
this.stream = null;
|
|
1563
|
-
this.streaming.set(false);
|
|
1564
|
-
}
|
|
1565
|
-
teardown() {
|
|
1566
|
-
cancelAnimationFrame(this.rafId);
|
|
1567
|
-
this.tracker.close();
|
|
1568
|
-
this.stopStream();
|
|
1569
|
-
}
|
|
1570
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: LivenessScreen, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1571
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: LivenessScreen, isStandalone: true, selector: "naya-liveness-screen", viewQueries: [{ propertyName: "videoRef", first: true, predicate: ["video"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
1572
|
-
<naya-screen [poweredBy]="s.poweredBy" (close)="flow.cancel()">
|
|
1573
|
-
<div class="pad">
|
|
1574
|
-
<h1 class="title">{{ s.livenessTitle }}</h1>
|
|
1575
|
-
<p class="sub">{{ s.livenessSubtitle }}</p>
|
|
1576
|
-
|
|
1577
|
-
<div class="ovalWrap">
|
|
1578
|
-
<div class="oval" [class.error]="!!cameraError()">
|
|
1579
|
-
@if (cameraError()) {
|
|
1580
|
-
<div class="camMsg">{{ cameraError() }}</div>
|
|
1581
|
-
} @else {
|
|
1582
|
-
<video #video class="preview" playsinline muted autoplay></video>
|
|
1583
|
-
@if (!streaming()) { <div class="camMsg"><span class="loader"></span></div> }
|
|
1584
|
-
}
|
|
1585
|
-
</div>
|
|
1586
|
-
<svg class="ring" viewBox="0 0 210 270" aria-hidden="true">
|
|
1587
|
-
<path class="track" [attr.d]="ringPath" />
|
|
1588
|
-
<path
|
|
1589
|
-
class="prog" [attr.d]="ringPath"
|
|
1590
|
-
pathLength="100" stroke-dasharray="100"
|
|
1591
|
-
[attr.stroke-dashoffset]="dashOffset()"
|
|
1592
|
-
/>
|
|
1593
|
-
</svg>
|
|
1594
|
-
</div>
|
|
1595
|
-
|
|
1596
|
-
<div class="instruction">
|
|
1597
|
-
<naya-icon [name]="instructionIcon()" [size]="22" [stroke]="2" />
|
|
1598
|
-
<span>{{ instructionText() }}</span>
|
|
1599
|
-
</div>
|
|
1600
|
-
|
|
1601
|
-
<div class="steps">
|
|
1602
|
-
@for (st of challengeSteps(); track st.label) {
|
|
1603
|
-
<div class="cstep">
|
|
1604
|
-
<span class="cdot" [class.done]="st.state === 'done'" [class.active]="st.state === 'active'">
|
|
1605
|
-
@if (st.state === 'done') { <naya-icon name="check" [size]="13" [stroke]="3" /> }
|
|
1606
|
-
</span>
|
|
1607
|
-
<span class="clbl">{{ st.label }}</span>
|
|
1608
|
-
</div>
|
|
1609
|
-
}
|
|
1610
|
-
</div>
|
|
1611
|
-
|
|
1612
|
-
<div class="spacer"></div>
|
|
1613
|
-
<p class="privacy">{{ s.livenessPrivacy }}</p>
|
|
1614
|
-
</div>
|
|
1615
|
-
</naya-screen>
|
|
1616
|
-
`, isInline: true, styles: [".pad{display:flex;flex-direction:column;flex:1;align-items:center;padding:4px 24px 12px}.title{margin:0;font-size:23px;font-weight:800;color:var(--naya-primary);text-align:center}.sub{margin:6px 0 18px;font-size:15px;color:var(--naya-text-2);text-align:center}.ovalWrap{position:relative;width:min(230px,62vw);aspect-ratio:210 / 270;margin:4px 0}.oval{position:absolute;inset:7px;border-radius:50%;overflow:hidden;background:#0000000a;display:flex;align-items:center;justify-content:center}.preview{width:100%;height:100%;object-fit:cover;transform:scaleX(-1)}.camMsg{padding:20px;text-align:center;color:var(--naya-text-2);font-size:12px}.ring{position:absolute;inset:0;width:100%;height:100%;fill:none;overflow:visible}.ring .track{stroke:#d8dfe8;stroke-width:4}.ring .prog{stroke:var(--naya-accent);stroke-width:4;stroke-linecap:round;transition:stroke-dashoffset .35s ease}.instruction{display:inline-flex;align-items:center;gap:10px;margin:18px 0 14px;padding:12px 16px;background:var(--naya-surface);border:1px solid var(--naya-line);border-radius:var(--naya-r-card);color:var(--naya-primary)}.instruction span{font-size:16px;font-weight:700;color:var(--naya-text)}.steps{display:flex;gap:20px;justify-content:center}.cstep{display:flex;flex-direction:column;align-items:center;gap:5px}.cdot{width:22px;height:22px;border-radius:50%;background:#d6dce4;display:inline-flex;align-items:center;justify-content:center;color:#fff}.cdot.active{background:var(--naya-accent)}.cdot.done{background:var(--naya-success)}.clbl{font-size:11px;color:var(--naya-text-2)}.spacer{flex:1 1 auto;min-height:12px}.privacy{margin:0;text-align:center;white-space:pre-line;font-size:12.5px;color:var(--naya-text-2)}.loader{width:30px;height:30px;border-radius:50%;border:3px solid rgba(0,0,0,.12);border-top-color:var(--naya-accent);animation:naya-spin .8s linear infinite}@keyframes naya-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.loader{animation-duration:1.6s}.ring .prog{transition:none}}\n"], dependencies: [{ kind: "component", type: NayaScreenComponent, selector: "naya-screen", inputs: ["poweredBy", "canBack", "canClose", "dark"], outputs: ["back", "close"] }, { kind: "component", type: NayaIconComponent, selector: "naya-icon", inputs: ["name", "size", "stroke"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1617
|
-
}
|
|
1618
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: LivenessScreen, decorators: [{
|
|
1619
|
-
type: Component,
|
|
1620
|
-
args: [{ selector: 'naya-liveness-screen', changeDetection: ChangeDetectionStrategy.OnPush, imports: [NayaScreenComponent, NayaIconComponent], template: `
|
|
1621
|
-
<naya-screen [poweredBy]="s.poweredBy" (close)="flow.cancel()">
|
|
1622
|
-
<div class="pad">
|
|
1623
|
-
<h1 class="title">{{ s.livenessTitle }}</h1>
|
|
1624
|
-
<p class="sub">{{ s.livenessSubtitle }}</p>
|
|
1625
|
-
|
|
1626
|
-
<div class="ovalWrap">
|
|
1627
|
-
<div class="oval" [class.error]="!!cameraError()">
|
|
1628
|
-
@if (cameraError()) {
|
|
1629
|
-
<div class="camMsg">{{ cameraError() }}</div>
|
|
1630
|
-
} @else {
|
|
1631
|
-
<video #video class="preview" playsinline muted autoplay></video>
|
|
1632
|
-
@if (!streaming()) { <div class="camMsg"><span class="loader"></span></div> }
|
|
1633
|
-
}
|
|
1634
|
-
</div>
|
|
1635
|
-
<svg class="ring" viewBox="0 0 210 270" aria-hidden="true">
|
|
1636
|
-
<path class="track" [attr.d]="ringPath" />
|
|
1637
|
-
<path
|
|
1638
|
-
class="prog" [attr.d]="ringPath"
|
|
1639
|
-
pathLength="100" stroke-dasharray="100"
|
|
1640
|
-
[attr.stroke-dashoffset]="dashOffset()"
|
|
1641
|
-
/>
|
|
1642
|
-
</svg>
|
|
1643
|
-
</div>
|
|
1644
|
-
|
|
1645
|
-
<div class="instruction">
|
|
1646
|
-
<naya-icon [name]="instructionIcon()" [size]="22" [stroke]="2" />
|
|
1647
|
-
<span>{{ instructionText() }}</span>
|
|
1648
|
-
</div>
|
|
1649
|
-
|
|
1650
|
-
<div class="steps">
|
|
1651
|
-
@for (st of challengeSteps(); track st.label) {
|
|
1652
|
-
<div class="cstep">
|
|
1653
|
-
<span class="cdot" [class.done]="st.state === 'done'" [class.active]="st.state === 'active'">
|
|
1654
|
-
@if (st.state === 'done') { <naya-icon name="check" [size]="13" [stroke]="3" /> }
|
|
1655
|
-
</span>
|
|
1656
|
-
<span class="clbl">{{ st.label }}</span>
|
|
1657
|
-
</div>
|
|
1658
|
-
}
|
|
1659
|
-
</div>
|
|
1660
|
-
|
|
1661
|
-
<div class="spacer"></div>
|
|
1662
|
-
<p class="privacy">{{ s.livenessPrivacy }}</p>
|
|
1663
|
-
</div>
|
|
1664
|
-
</naya-screen>
|
|
1665
|
-
`, styles: [".pad{display:flex;flex-direction:column;flex:1;align-items:center;padding:4px 24px 12px}.title{margin:0;font-size:23px;font-weight:800;color:var(--naya-primary);text-align:center}.sub{margin:6px 0 18px;font-size:15px;color:var(--naya-text-2);text-align:center}.ovalWrap{position:relative;width:min(230px,62vw);aspect-ratio:210 / 270;margin:4px 0}.oval{position:absolute;inset:7px;border-radius:50%;overflow:hidden;background:#0000000a;display:flex;align-items:center;justify-content:center}.preview{width:100%;height:100%;object-fit:cover;transform:scaleX(-1)}.camMsg{padding:20px;text-align:center;color:var(--naya-text-2);font-size:12px}.ring{position:absolute;inset:0;width:100%;height:100%;fill:none;overflow:visible}.ring .track{stroke:#d8dfe8;stroke-width:4}.ring .prog{stroke:var(--naya-accent);stroke-width:4;stroke-linecap:round;transition:stroke-dashoffset .35s ease}.instruction{display:inline-flex;align-items:center;gap:10px;margin:18px 0 14px;padding:12px 16px;background:var(--naya-surface);border:1px solid var(--naya-line);border-radius:var(--naya-r-card);color:var(--naya-primary)}.instruction span{font-size:16px;font-weight:700;color:var(--naya-text)}.steps{display:flex;gap:20px;justify-content:center}.cstep{display:flex;flex-direction:column;align-items:center;gap:5px}.cdot{width:22px;height:22px;border-radius:50%;background:#d6dce4;display:inline-flex;align-items:center;justify-content:center;color:#fff}.cdot.active{background:var(--naya-accent)}.cdot.done{background:var(--naya-success)}.clbl{font-size:11px;color:var(--naya-text-2)}.spacer{flex:1 1 auto;min-height:12px}.privacy{margin:0;text-align:center;white-space:pre-line;font-size:12.5px;color:var(--naya-text-2)}.loader{width:30px;height:30px;border-radius:50%;border:3px solid rgba(0,0,0,.12);border-top-color:var(--naya-accent);animation:naya-spin .8s linear infinite}@keyframes naya-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.loader{animation-duration:1.6s}.ring .prog{transition:none}}\n"] }]
|
|
1666
|
-
}], propDecorators: { videoRef: [{ type: i0.ViewChild, args: ['video', { isSignal: true }] }] } });
|
|
1667
|
-
|
|
1668
|
-
/**
|
|
1669
|
-
* Écran 6 — Traitement : envoi du liveness puis `submit` (OCR différé,
|
|
1670
|
-
* biométrie, AML, décision — 30 à 90 s). Reprend `processing_screen.dart`.
|
|
1671
|
-
*/
|
|
1672
|
-
class ProcessingScreen {
|
|
1673
|
-
flow = inject(NayaFlowService);
|
|
1674
|
-
s = this.flow.strings;
|
|
1675
|
-
visualStep = signal(0, ...(ngDevMode ? [{ debugName: "visualStep" }] : []));
|
|
1676
|
-
error = signal(null, ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
1677
|
-
timer;
|
|
1678
|
-
steps = [
|
|
1679
|
-
{ label: this.s.processingDocument, index: 0 },
|
|
1680
|
-
{ label: this.s.processingFace, index: 1 },
|
|
1681
|
-
{ label: this.s.processingCompliance, index: 2 },
|
|
1682
|
-
{ label: this.s.processingDecision, index: 3 },
|
|
1683
|
-
];
|
|
1684
|
-
ngOnInit() {
|
|
1685
|
-
this.startVisualProgress();
|
|
1686
|
-
void this.run();
|
|
1687
|
-
}
|
|
1688
|
-
ngOnDestroy() {
|
|
1689
|
-
if (this.timer)
|
|
1690
|
-
clearInterval(this.timer);
|
|
1691
|
-
}
|
|
1692
|
-
startVisualProgress() {
|
|
1693
|
-
// Progression indicative pendant l'attente réelle du serveur.
|
|
1694
|
-
this.timer = setInterval(() => {
|
|
1695
|
-
if (this.visualStep() < 2)
|
|
1696
|
-
this.visualStep.update((n) => n + 1);
|
|
1697
|
-
}, 9000);
|
|
1698
|
-
}
|
|
1699
|
-
async run() {
|
|
1700
|
-
this.error.set(null);
|
|
1701
|
-
this.visualStep.set(0);
|
|
1702
|
-
try {
|
|
1703
|
-
// L'envoi du document est parti pendant le selfie — on s'assure qu'il
|
|
1704
|
-
// est terminé (et on le relance s'il a échoué).
|
|
1705
|
-
await this.flow.ensureDocumentUploaded();
|
|
1706
|
-
const session = this.flow.session();
|
|
1707
|
-
if (!session)
|
|
1708
|
-
throw new NayaApiError('Session introuvable.');
|
|
1709
|
-
await this.flow.client.submitLiveness(session.verificationId, {
|
|
1710
|
-
livenessScore: this.flow.livenessScore(),
|
|
1711
|
-
selfieBase64: this.flow.selfieBase64(),
|
|
1712
|
-
});
|
|
1713
|
-
const response = await this.flow.client.submit(session.verificationId);
|
|
1714
|
-
this.flow.submitResponse.set(response);
|
|
1715
|
-
this.flow.replace('result');
|
|
1716
|
-
}
|
|
1717
|
-
catch (err) {
|
|
1718
|
-
const message = err instanceof NayaApiError ? err.message : this.s.errorNetwork;
|
|
1719
|
-
this.error.set(message);
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: ProcessingScreen, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1723
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: ProcessingScreen, isStandalone: true, selector: "naya-processing-screen", ngImport: i0, template: `
|
|
1724
|
-
<naya-screen [poweredBy]="s.poweredBy" [canClose]="!!error()" (close)="flow.cancel()">
|
|
1725
|
-
@if (error(); as err) {
|
|
1726
|
-
<div class="pad center">
|
|
1727
|
-
<naya-icon name="wifi-off" [size]="48" [stroke]="1.8" />
|
|
1728
|
-
<h1 class="title">{{ s.errorTitle }}</h1>
|
|
1729
|
-
<p class="body">{{ err }}</p>
|
|
1730
|
-
<div class="cta"><naya-primary-button [label]="s.retry" (pressed)="run()" /></div>
|
|
1731
|
-
</div>
|
|
1732
|
-
} @else {
|
|
1733
|
-
<div class="pad">
|
|
1734
|
-
<naya-brand />
|
|
1735
|
-
<div class="spacer"></div>
|
|
1736
|
-
|
|
1737
|
-
<span class="ring" aria-hidden="true"></span>
|
|
1738
|
-
|
|
1739
|
-
<ul class="steps">
|
|
1740
|
-
@for (item of steps; track item.index) {
|
|
1741
|
-
<li class="row">
|
|
1742
|
-
@if (item.index <= visualStep()) {
|
|
1743
|
-
<span class="dot done"><naya-icon name="check" [size]="12" [stroke]="3" /></span>
|
|
1744
|
-
} @else if (item.index === visualStep() + 1) {
|
|
1745
|
-
<span class="dot active" aria-hidden="true"></span>
|
|
1746
|
-
} @else {
|
|
1747
|
-
<span class="dot idle"></span>
|
|
1748
|
-
}
|
|
1749
|
-
<span class="lbl" [class.dim]="item.index > visualStep() + 1">{{ item.label }}</span>
|
|
1750
|
-
</li>
|
|
1751
|
-
}
|
|
1752
|
-
</ul>
|
|
1753
|
-
|
|
1754
|
-
<div class="spacer"></div>
|
|
1755
|
-
<p class="wait">{{ s.processingWait }}</p>
|
|
1756
|
-
</div>
|
|
1757
|
-
}
|
|
1758
|
-
</naya-screen>
|
|
1759
|
-
`, isInline: true, styles: [".pad{display:flex;flex-direction:column;flex:1;align-items:center;padding:8px 32px 16px}.pad.center{justify-content:center;text-align:center}.spacer{flex:1 1 auto}.title{margin:16px 0 8px;font-size:23px;font-weight:800;color:var(--naya-primary);text-wrap:balance}.body{margin:0;font-size:15px;line-height:1.5;color:var(--naya-text-2)}.cta{width:100%;margin-top:24px}.ring{width:110px;height:110px;border-radius:50%;border:7px solid #E1E6EC;border-top-color:var(--naya-primary);animation:naya-spin 1s linear infinite}@keyframes naya-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.ring{animation-duration:2s}}.steps{list-style:none;margin:34px 0 0;padding:0;align-self:flex-start}.row{display:flex;align-items:center;gap:10px;padding:5px 0}.dot{width:20px;height:20px;border-radius:50%;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center}.dot.done{background:var(--naya-success);color:#fff}.dot.idle{background:#d6dce4}.dot.active{border:2.5px solid var(--naya-accent);border-top-color:transparent;animation:naya-spin .8s linear infinite}.lbl{font-size:15px;font-weight:600;color:var(--naya-text)}.lbl.dim{color:var(--naya-text-2);font-weight:500}.wait{font-size:13px;color:var(--naya-text-2);margin:0}\n"], dependencies: [{ kind: "component", type: NayaScreenComponent, selector: "naya-screen", inputs: ["poweredBy", "canBack", "canClose", "dark"], outputs: ["back", "close"] }, { kind: "component", type: NayaIconComponent, selector: "naya-icon", inputs: ["name", "size", "stroke"] }, { kind: "component", type: NayaBrandComponent, selector: "naya-brand" }, { kind: "component", type: NayaPrimaryButtonComponent, selector: "naya-primary-button", inputs: ["label", "loading", "disabled", "icon"], outputs: ["pressed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1760
|
-
}
|
|
1761
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: ProcessingScreen, decorators: [{
|
|
1762
|
-
type: Component,
|
|
1763
|
-
args: [{ selector: 'naya-processing-screen', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
1764
|
-
NayaScreenComponent,
|
|
1765
|
-
NayaIconComponent,
|
|
1766
|
-
NayaBrandComponent,
|
|
1767
|
-
NayaPrimaryButtonComponent,
|
|
1768
|
-
], template: `
|
|
1769
|
-
<naya-screen [poweredBy]="s.poweredBy" [canClose]="!!error()" (close)="flow.cancel()">
|
|
1770
|
-
@if (error(); as err) {
|
|
1771
|
-
<div class="pad center">
|
|
1772
|
-
<naya-icon name="wifi-off" [size]="48" [stroke]="1.8" />
|
|
1773
|
-
<h1 class="title">{{ s.errorTitle }}</h1>
|
|
1774
|
-
<p class="body">{{ err }}</p>
|
|
1775
|
-
<div class="cta"><naya-primary-button [label]="s.retry" (pressed)="run()" /></div>
|
|
1776
|
-
</div>
|
|
1777
|
-
} @else {
|
|
1778
|
-
<div class="pad">
|
|
1779
|
-
<naya-brand />
|
|
1780
|
-
<div class="spacer"></div>
|
|
1781
|
-
|
|
1782
|
-
<span class="ring" aria-hidden="true"></span>
|
|
1783
|
-
|
|
1784
|
-
<ul class="steps">
|
|
1785
|
-
@for (item of steps; track item.index) {
|
|
1786
|
-
<li class="row">
|
|
1787
|
-
@if (item.index <= visualStep()) {
|
|
1788
|
-
<span class="dot done"><naya-icon name="check" [size]="12" [stroke]="3" /></span>
|
|
1789
|
-
} @else if (item.index === visualStep() + 1) {
|
|
1790
|
-
<span class="dot active" aria-hidden="true"></span>
|
|
1791
|
-
} @else {
|
|
1792
|
-
<span class="dot idle"></span>
|
|
1793
|
-
}
|
|
1794
|
-
<span class="lbl" [class.dim]="item.index > visualStep() + 1">{{ item.label }}</span>
|
|
1795
|
-
</li>
|
|
1796
|
-
}
|
|
1797
|
-
</ul>
|
|
1798
|
-
|
|
1799
|
-
<div class="spacer"></div>
|
|
1800
|
-
<p class="wait">{{ s.processingWait }}</p>
|
|
1801
|
-
</div>
|
|
1802
|
-
}
|
|
1803
|
-
</naya-screen>
|
|
1804
|
-
`, styles: [".pad{display:flex;flex-direction:column;flex:1;align-items:center;padding:8px 32px 16px}.pad.center{justify-content:center;text-align:center}.spacer{flex:1 1 auto}.title{margin:16px 0 8px;font-size:23px;font-weight:800;color:var(--naya-primary);text-wrap:balance}.body{margin:0;font-size:15px;line-height:1.5;color:var(--naya-text-2)}.cta{width:100%;margin-top:24px}.ring{width:110px;height:110px;border-radius:50%;border:7px solid #E1E6EC;border-top-color:var(--naya-primary);animation:naya-spin 1s linear infinite}@keyframes naya-spin{to{transform:rotate(360deg)}}@media(prefers-reduced-motion:reduce){.ring{animation-duration:2s}}.steps{list-style:none;margin:34px 0 0;padding:0;align-self:flex-start}.row{display:flex;align-items:center;gap:10px;padding:5px 0}.dot{width:20px;height:20px;border-radius:50%;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center}.dot.done{background:var(--naya-success);color:#fff}.dot.idle{background:#d6dce4}.dot.active{border:2.5px solid var(--naya-accent);border-top-color:transparent;animation:naya-spin .8s linear infinite}.lbl{font-size:15px;font-weight:600;color:var(--naya-text)}.lbl.dim{color:var(--naya-text-2);font-weight:500}.wait{font-size:13px;color:var(--naya-text-2);margin:0}\n"] }]
|
|
1805
|
-
}] });
|
|
1806
|
-
|
|
1807
|
-
/** Écran 7 — Résultat (approuvé / en revue / rejeté). */
|
|
1808
|
-
class ResultScreen {
|
|
1809
|
-
flow = inject(NayaFlowService);
|
|
1810
|
-
s = this.flow.strings;
|
|
1811
|
-
response = computed(() => this.flow.submitResponse(), ...(ngDevMode ? [{ debugName: "response" }] : []));
|
|
1812
|
-
status = computed(() => {
|
|
1813
|
-
switch (this.response()?.status) {
|
|
1814
|
-
case 'approved': return 'approved';
|
|
1815
|
-
case 'rejected': return 'rejected';
|
|
1816
|
-
default: return 'review';
|
|
1817
|
-
}
|
|
1818
|
-
}, ...(ngDevMode ? [{ debugName: "status" }] : []));
|
|
1819
|
-
view = computed(() => {
|
|
1820
|
-
switch (this.status()) {
|
|
1821
|
-
case 'approved':
|
|
1822
|
-
return {
|
|
1823
|
-
icon: 'check', color: 'var(--naya-success)',
|
|
1824
|
-
title: this.s.resultApprovedTitle, subtitle: this.s.resultApprovedSubtitle,
|
|
1825
|
-
};
|
|
1826
|
-
case 'rejected': {
|
|
1827
|
-
const note = this.response()?.note;
|
|
1828
|
-
return {
|
|
1829
|
-
icon: 'close', color: 'var(--naya-error)',
|
|
1830
|
-
title: this.s.resultRejectedTitle,
|
|
1831
|
-
subtitle: note ? `${this.s.resultRejectedReason} : ${note}` : '',
|
|
1832
|
-
};
|
|
1833
|
-
}
|
|
1834
|
-
default:
|
|
1835
|
-
return {
|
|
1836
|
-
icon: 'clock', color: 'var(--naya-warning)',
|
|
1837
|
-
title: this.s.resultReviewTitle, subtitle: this.s.resultReviewSubtitle,
|
|
1838
|
-
};
|
|
1839
|
-
}
|
|
1840
|
-
}, ...(ngDevMode ? [{ debugName: "view" }] : []));
|
|
1841
|
-
fullName = computed(() => {
|
|
1842
|
-
const ocr = this.response()?.ocrData;
|
|
1843
|
-
return [ocr?.firstName, ocr?.lastName].filter((p) => p && p.length).join(' ');
|
|
1844
|
-
}, ...(ngDevMode ? [{ debugName: "fullName" }] : []));
|
|
1845
|
-
reference = computed(() => {
|
|
1846
|
-
const id = this.flow.session()?.verificationId;
|
|
1847
|
-
return id ? id.split('-')[0].toUpperCase() : '—';
|
|
1848
|
-
}, ...(ngDevMode ? [{ debugName: "reference" }] : []));
|
|
1849
|
-
reviewBullets = computed(() => [
|
|
1850
|
-
this.s.resultReviewB1,
|
|
1851
|
-
this.s.resultReviewB2,
|
|
1852
|
-
this.s.resultReviewB3,
|
|
1853
|
-
], ...(ngDevMode ? [{ debugName: "reviewBullets" }] : []));
|
|
1854
|
-
// Des données ont été extraites : l'app cliente prend le relais → « Continuer ».
|
|
1855
|
-
doneLabel = computed(() => this.response()?.ocrData ? this.s.continueLabel : this.s.backToApp, ...(ngDevMode ? [{ debugName: "doneLabel" }] : []));
|
|
1856
|
-
retry() {
|
|
1857
|
-
this.flow.resetForRetry();
|
|
1858
|
-
this.flow.replace('capture-recto');
|
|
1859
|
-
}
|
|
1860
|
-
done() {
|
|
1861
|
-
const r = this.response();
|
|
1862
|
-
this.flow.complete({
|
|
1863
|
-
status: this.status(),
|
|
1864
|
-
verificationId: this.flow.session()?.verificationId,
|
|
1865
|
-
riskScore: r?.riskScore,
|
|
1866
|
-
matchScore: r?.matchScore,
|
|
1867
|
-
authenticityScore: r?.authenticityScore,
|
|
1868
|
-
livenessScore: r?.livenessScore,
|
|
1869
|
-
ocrConfidence: r?.ocrConfidence,
|
|
1870
|
-
ocrData: r?.ocrData,
|
|
1871
|
-
note: r?.note,
|
|
1872
|
-
});
|
|
1873
|
-
}
|
|
1874
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: ResultScreen, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1875
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: ResultScreen, isStandalone: true, selector: "naya-result-screen", ngImport: i0, template: `
|
|
1876
|
-
<naya-screen [poweredBy]="s.poweredBy">
|
|
1877
|
-
<div class="pad">
|
|
1878
|
-
<div class="spacer"></div>
|
|
1879
|
-
|
|
1880
|
-
<span class="badge" [style.background]="view().color">
|
|
1881
|
-
<naya-icon [name]="view().icon" [size]="44" [stroke]="3" />
|
|
1882
|
-
</span>
|
|
1883
|
-
<h1 class="title">{{ view().title }}</h1>
|
|
1884
|
-
@if (view().subtitle) { <p class="body">{{ view().subtitle }}</p> }
|
|
1885
|
-
|
|
1886
|
-
@if (status() === 'approved') {
|
|
1887
|
-
<div class="summary">
|
|
1888
|
-
@if (fullName()) {
|
|
1889
|
-
<div class="srow"><span class="k">{{ s.resultName }}</span><span class="v">{{ fullName() }}</span></div>
|
|
1890
|
-
}
|
|
1891
|
-
<div class="srow"><span class="k">{{ s.resultReference }}</span><span class="v mono">{{ reference() }}</span></div>
|
|
1892
|
-
</div>
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
@if (status() === 'review') {
|
|
1896
|
-
<ul class="bullets">
|
|
1897
|
-
@for (b of reviewBullets(); track b) {
|
|
1898
|
-
<li><span class="pip"></span>{{ b }}</li>
|
|
1899
|
-
}
|
|
1900
|
-
</ul>
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
<div class="spacer"></div>
|
|
1904
|
-
|
|
1905
|
-
@if (status() === 'rejected' && flow.canRetry) {
|
|
1906
|
-
<naya-primary-button [label]="s.retry" icon="refresh" (pressed)="retry()" />
|
|
1907
|
-
<div class="gap"></div>
|
|
1908
|
-
<naya-secondary-button [label]="s.backToApp" (pressed)="done()" />
|
|
1909
|
-
} @else {
|
|
1910
|
-
<naya-primary-button [label]="doneLabel()" (pressed)="done()" />
|
|
1911
|
-
}
|
|
1912
|
-
</div>
|
|
1913
|
-
</naya-screen>
|
|
1914
|
-
`, isInline: true, styles: [".pad{display:flex;flex-direction:column;flex:1;align-items:center;padding:8px 24px 16px}.spacer{flex:1 1 auto}.badge{width:88px;height:88px;border-radius:50%;color:#fff;display:inline-flex;align-items:center;justify-content:center}.title{margin:18px 0 8px;text-align:center;text-wrap:balance;font-size:23px;font-weight:800;color:var(--naya-primary)}.body{margin:0;text-align:center;font-size:15px;line-height:1.5;color:var(--naya-text-2)}.summary{width:100%;margin-top:20px;padding:14px;background:var(--naya-surface);border:1px solid var(--naya-line);border-radius:var(--naya-r-card)}.srow{display:flex;justify-content:space-between;align-items:center;padding:4px 0;gap:12px}.k{font-size:13px;color:var(--naya-text-2)}.v{font-size:13px;font-weight:800;color:var(--naya-text);text-align:right}.v.mono{font-family:ui-monospace,SF Mono,Menlo,monospace}.bullets{list-style:none;margin:20px 0 0;padding:0;align-self:stretch}.bullets li{display:flex;align-items:center;gap:9px;padding:4px 0;font-size:13px;color:var(--naya-text)}.pip{width:6px;height:6px;border-radius:50%;background:var(--naya-warning);flex:0 0 auto}.gap{height:10px}\n"], dependencies: [{ kind: "component", type: NayaScreenComponent, selector: "naya-screen", inputs: ["poweredBy", "canBack", "canClose", "dark"], outputs: ["back", "close"] }, { kind: "component", type: NayaIconComponent, selector: "naya-icon", inputs: ["name", "size", "stroke"] }, { kind: "component", type: NayaPrimaryButtonComponent, selector: "naya-primary-button", inputs: ["label", "loading", "disabled", "icon"], outputs: ["pressed"] }, { kind: "component", type: NayaSecondaryButtonComponent, selector: "naya-secondary-button", inputs: ["label"], outputs: ["pressed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1915
|
-
}
|
|
1916
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: ResultScreen, decorators: [{
|
|
1917
|
-
type: Component,
|
|
1918
|
-
args: [{ selector: 'naya-result-screen', changeDetection: ChangeDetectionStrategy.OnPush, imports: [
|
|
1919
|
-
NayaScreenComponent,
|
|
1920
|
-
NayaIconComponent,
|
|
1921
|
-
NayaPrimaryButtonComponent,
|
|
1922
|
-
NayaSecondaryButtonComponent,
|
|
1923
|
-
], template: `
|
|
1924
|
-
<naya-screen [poweredBy]="s.poweredBy">
|
|
1925
|
-
<div class="pad">
|
|
1926
|
-
<div class="spacer"></div>
|
|
1927
|
-
|
|
1928
|
-
<span class="badge" [style.background]="view().color">
|
|
1929
|
-
<naya-icon [name]="view().icon" [size]="44" [stroke]="3" />
|
|
1930
|
-
</span>
|
|
1931
|
-
<h1 class="title">{{ view().title }}</h1>
|
|
1932
|
-
@if (view().subtitle) { <p class="body">{{ view().subtitle }}</p> }
|
|
1933
|
-
|
|
1934
|
-
@if (status() === 'approved') {
|
|
1935
|
-
<div class="summary">
|
|
1936
|
-
@if (fullName()) {
|
|
1937
|
-
<div class="srow"><span class="k">{{ s.resultName }}</span><span class="v">{{ fullName() }}</span></div>
|
|
1938
|
-
}
|
|
1939
|
-
<div class="srow"><span class="k">{{ s.resultReference }}</span><span class="v mono">{{ reference() }}</span></div>
|
|
1940
|
-
</div>
|
|
1941
|
-
}
|
|
1942
|
-
|
|
1943
|
-
@if (status() === 'review') {
|
|
1944
|
-
<ul class="bullets">
|
|
1945
|
-
@for (b of reviewBullets(); track b) {
|
|
1946
|
-
<li><span class="pip"></span>{{ b }}</li>
|
|
1947
|
-
}
|
|
1948
|
-
</ul>
|
|
1949
|
-
}
|
|
1950
|
-
|
|
1951
|
-
<div class="spacer"></div>
|
|
1952
|
-
|
|
1953
|
-
@if (status() === 'rejected' && flow.canRetry) {
|
|
1954
|
-
<naya-primary-button [label]="s.retry" icon="refresh" (pressed)="retry()" />
|
|
1955
|
-
<div class="gap"></div>
|
|
1956
|
-
<naya-secondary-button [label]="s.backToApp" (pressed)="done()" />
|
|
1957
|
-
} @else {
|
|
1958
|
-
<naya-primary-button [label]="doneLabel()" (pressed)="done()" />
|
|
1959
|
-
}
|
|
1960
|
-
</div>
|
|
1961
|
-
</naya-screen>
|
|
1962
|
-
`, styles: [".pad{display:flex;flex-direction:column;flex:1;align-items:center;padding:8px 24px 16px}.spacer{flex:1 1 auto}.badge{width:88px;height:88px;border-radius:50%;color:#fff;display:inline-flex;align-items:center;justify-content:center}.title{margin:18px 0 8px;text-align:center;text-wrap:balance;font-size:23px;font-weight:800;color:var(--naya-primary)}.body{margin:0;text-align:center;font-size:15px;line-height:1.5;color:var(--naya-text-2)}.summary{width:100%;margin-top:20px;padding:14px;background:var(--naya-surface);border:1px solid var(--naya-line);border-radius:var(--naya-r-card)}.srow{display:flex;justify-content:space-between;align-items:center;padding:4px 0;gap:12px}.k{font-size:13px;color:var(--naya-text-2)}.v{font-size:13px;font-weight:800;color:var(--naya-text);text-align:right}.v.mono{font-family:ui-monospace,SF Mono,Menlo,monospace}.bullets{list-style:none;margin:20px 0 0;padding:0;align-self:stretch}.bullets li{display:flex;align-items:center;gap:9px;padding:4px 0;font-size:13px;color:var(--naya-text)}.pip{width:6px;height:6px;border-radius:50%;background:var(--naya-warning);flex:0 0 auto}.gap{height:10px}\n"] }]
|
|
1963
|
-
}] });
|
|
1964
|
-
|
|
1965
|
-
/**
|
|
1966
|
-
* Hôte du parcours : conteneur plein écran monté au-dessus de l'application,
|
|
1967
|
-
* fournisseur du `NayaFlowService`, et routeur d'étapes. Équivalent de
|
|
1968
|
-
* `NayaFlowHost` (Flutter), le Navigator imbriqué étant remplacé par un
|
|
1969
|
-
* `@switch` piloté par le signal `step`.
|
|
1970
|
-
*
|
|
1971
|
-
* Le thème est appliqué en variables CSS sur l'élément hôte : les styles du
|
|
1972
|
-
* SDK n'affectent pas l'application, et inversement.
|
|
1973
|
-
*/
|
|
1974
|
-
class NayaFlowHostComponent {
|
|
1975
|
-
flow = inject(NayaFlowService);
|
|
1976
|
-
/** Variables de thème posées sur l'hôte (dictionnaire → style inline). */
|
|
1977
|
-
get style() {
|
|
1978
|
-
return buildThemeVars(this.flow.resolvedConfig);
|
|
1979
|
-
}
|
|
1980
|
-
/** Échap ferme le parcours (annulation), comme le bouton système Android. */
|
|
1981
|
-
onEscape() {
|
|
1982
|
-
this.flow.cancel();
|
|
1983
|
-
}
|
|
1984
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaFlowHostComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1985
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.28", type: NayaFlowHostComponent, isStandalone: true, selector: "naya-flow-host", host: { listeners: { "document:keydown.escape": "onEscape()" }, properties: { "style": "this.style" } }, providers: [NayaFlowService], ngImport: i0, template: `
|
|
1986
|
-
@switch (flow.step()) {
|
|
1987
|
-
@case ('welcome') { <naya-welcome-screen /> }
|
|
1988
|
-
@case ('select') { <naya-document-selection-screen /> }
|
|
1989
|
-
@case ('capture-recto') { <naya-document-capture-screen side="recto" /> }
|
|
1990
|
-
@case ('capture-verso') { <naya-document-capture-screen side="verso" /> }
|
|
1991
|
-
@case ('liveness') { <naya-liveness-screen /> }
|
|
1992
|
-
@case ('processing') { <naya-processing-screen /> }
|
|
1993
|
-
@case ('result') { <naya-result-screen /> }
|
|
1994
|
-
}
|
|
1995
|
-
`, isInline: true, styles: [":host{position:fixed;inset:0;z-index:2147483000;display:block;background:var(--naya-bg);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;-webkit-font-smoothing:antialiased}:host,:host *{box-sizing:border-box}\n"], dependencies: [{ kind: "component", type: WelcomeScreen, selector: "naya-welcome-screen" }, { kind: "component", type: DocumentSelectionScreen, selector: "naya-document-selection-screen" }, { kind: "component", type: DocumentCaptureScreen, selector: "naya-document-capture-screen", inputs: ["side"] }, { kind: "component", type: LivenessScreen, selector: "naya-liveness-screen" }, { kind: "component", type: ProcessingScreen, selector: "naya-processing-screen" }, { kind: "component", type: ResultScreen, selector: "naya-result-screen" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1996
|
-
}
|
|
1997
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.28", ngImport: i0, type: NayaFlowHostComponent, decorators: [{
|
|
1998
|
-
type: Component,
|
|
1999
|
-
args: [{ selector: 'naya-flow-host', changeDetection: ChangeDetectionStrategy.OnPush, providers: [NayaFlowService], imports: [
|
|
2000
|
-
WelcomeScreen,
|
|
2001
|
-
DocumentSelectionScreen,
|
|
2002
|
-
DocumentCaptureScreen,
|
|
2003
|
-
LivenessScreen,
|
|
2004
|
-
ProcessingScreen,
|
|
2005
|
-
ResultScreen,
|
|
2006
|
-
], template: `
|
|
2007
|
-
@switch (flow.step()) {
|
|
2008
|
-
@case ('welcome') { <naya-welcome-screen /> }
|
|
2009
|
-
@case ('select') { <naya-document-selection-screen /> }
|
|
2010
|
-
@case ('capture-recto') { <naya-document-capture-screen side="recto" /> }
|
|
2011
|
-
@case ('capture-verso') { <naya-document-capture-screen side="verso" /> }
|
|
2012
|
-
@case ('liveness') { <naya-liveness-screen /> }
|
|
2013
|
-
@case ('processing') { <naya-processing-screen /> }
|
|
2014
|
-
@case ('result') { <naya-result-screen /> }
|
|
2015
|
-
}
|
|
2016
|
-
`, styles: [":host{position:fixed;inset:0;z-index:2147483000;display:block;background:var(--naya-bg);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,sans-serif;-webkit-font-smoothing:antialiased}:host,:host *{box-sizing:border-box}\n"] }]
|
|
2017
|
-
}], propDecorators: { style: [{
|
|
2018
|
-
type: HostBinding,
|
|
2019
|
-
args: ['style']
|
|
2020
|
-
}], onEscape: [{
|
|
2021
|
-
type: HostListener,
|
|
2022
|
-
args: ['document:keydown.escape']
|
|
2023
|
-
}] } });
|
|
2024
|
-
|
|
2025
|
-
/**
|
|
2026
|
-
* Monte l'hôte du parcours en plein écran, au-dessus de l'application, et
|
|
2027
|
-
* résout le `NayaResult` à la fermeture.
|
|
2028
|
-
*
|
|
2029
|
-
* Aucune dépendance à `@angular/cdk` : l'overlay est un composant créé
|
|
2030
|
-
* dynamiquement (`createComponent`), attaché à `ApplicationRef` et inséré dans
|
|
2031
|
-
* `document.body`. Le composant et tout l'arbre d'écrans sont importés ici, si
|
|
2032
|
-
* bien qu'ils restent hors du bundle initial de l'application (import différé
|
|
2033
|
-
* depuis `NayaIdService`).
|
|
2034
|
-
*/
|
|
2035
|
-
async function mountNayaFlow(refs, auth, config) {
|
|
2036
|
-
if (typeof document === 'undefined') {
|
|
2037
|
-
return errorResult('Le SDK Naya nécessite un environnement navigateur.');
|
|
2038
|
-
}
|
|
2039
|
-
const componentRef = createComponent(NayaFlowHostComponent, {
|
|
2040
|
-
environmentInjector: refs.environmentInjector,
|
|
2041
|
-
});
|
|
2042
|
-
const flow = componentRef.injector.get(NayaFlowService);
|
|
2043
|
-
const previousOverflow = document.body.style.overflow;
|
|
2044
|
-
return new Promise((resolve) => {
|
|
2045
|
-
let settled = false;
|
|
2046
|
-
const finish = (result) => {
|
|
2047
|
-
if (settled)
|
|
2048
|
-
return;
|
|
2049
|
-
settled = true;
|
|
2050
|
-
// Restauration puis démontage : l'ordre évite un flash de l'application.
|
|
2051
|
-
document.body.style.overflow = previousOverflow;
|
|
2052
|
-
refs.appRef.detachView(componentRef.hostView);
|
|
2053
|
-
componentRef.destroy();
|
|
2054
|
-
resolve(result);
|
|
2055
|
-
};
|
|
2056
|
-
// `init` avant tout rendu : le thème et les écrans lisent la config résolue.
|
|
2057
|
-
flow.init({ auth, config, finish });
|
|
2058
|
-
refs.appRef.attachView(componentRef.hostView);
|
|
2059
|
-
document.body.appendChild(componentRef.location.nativeElement);
|
|
2060
|
-
document.body.style.overflow = 'hidden'; // fige le défilement de l'app
|
|
2061
|
-
componentRef.changeDetectorRef.detectChanges();
|
|
2062
|
-
});
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
export { mountNayaFlow };
|
|
2066
|
-
//# sourceMappingURL=41devs-naya-id-angular-naya-flow.launcher-xIwcsV6g.mjs.map
|