@labyrinthe/otp-widget 0.0.0-stage → 1.2.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/CHANGELOG.md +80 -0
- package/LICENSE +21 -0
- package/README.fr.md +584 -0
- package/README.md +574 -2
- package/dist/index.cjs +887 -0
- package/dist/index.d.cts +215 -0
- package/dist/index.d.ts +215 -0
- package/dist/index.js +875 -0
- package/dist/labyrinthe-otp.min.js +70 -0
- package/dist/react.cjs +81 -0
- package/dist/react.d.cts +43 -0
- package/dist/react.d.ts +43 -0
- package/dist/react.js +66 -0
- package/dist/server.cjs +613 -0
- package/dist/server.d.cts +264 -0
- package/dist/server.d.ts +264 -0
- package/dist/server.js +610 -0
- package/package.json +131 -4
package/dist/index.js
ADDED
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
// src/i18n.ts
|
|
2
|
+
var fr = {
|
|
3
|
+
title: "V\xE9rification du num\xE9ro",
|
|
4
|
+
phoneLabel: "Num\xE9ro de t\xE9l\xE9phone",
|
|
5
|
+
countryCodeLabel: "Indicatif pays",
|
|
6
|
+
phonePlaceholder: "81 234 5678",
|
|
7
|
+
phoneHint: "Vous allez recevoir un code par SMS.",
|
|
8
|
+
send: "Recevoir le code",
|
|
9
|
+
sending: "Envoi\u2026",
|
|
10
|
+
codeLabel: "Code re\xE7u par SMS",
|
|
11
|
+
codeSentTo: "Code envoy\xE9 au {phone}.",
|
|
12
|
+
cellLabel: "Caract\xE8re {index} sur {total}",
|
|
13
|
+
verify: "V\xE9rifier",
|
|
14
|
+
verifying: "V\xE9rification\u2026",
|
|
15
|
+
changePhone: "Modifier le num\xE9ro",
|
|
16
|
+
resend: "Renvoyer le code",
|
|
17
|
+
resendIn: "Renvoyer le code dans {time}",
|
|
18
|
+
resendAvailable: "Vous pouvez demander un nouveau code.",
|
|
19
|
+
attemptsLeftOne: "Il vous reste 1 tentative.",
|
|
20
|
+
attemptsLeftOther: "Il vous reste {count} tentatives.",
|
|
21
|
+
success: "Num\xE9ro v\xE9rifi\xE9.",
|
|
22
|
+
testMode: "Mode test : le code est {code}",
|
|
23
|
+
retryIn: "R\xE9essayez dans {time}.",
|
|
24
|
+
errors: {
|
|
25
|
+
invalid_phone: "Ce num\xE9ro de t\xE9l\xE9phone n\u2019est pas valide.",
|
|
26
|
+
invalid_code: "Code incorrect.",
|
|
27
|
+
expired: "Ce code a expir\xE9. Demandez-en un nouveau.",
|
|
28
|
+
already_used: "Ce code a d\xE9j\xE0 \xE9t\xE9 utilis\xE9. Demandez-en un nouveau.",
|
|
29
|
+
max_attempts_reached: "Trop d\u2019essais incorrects. Demandez un nouveau code.",
|
|
30
|
+
resend_too_soon: "Patientez {time} avant de demander un nouveau code.",
|
|
31
|
+
rate_limited: "Trop de demandes. R\xE9essayez dans quelques minutes.",
|
|
32
|
+
no_pending_verification: "Votre demande a expir\xE9. Demandez un nouveau code.",
|
|
33
|
+
forbidden: "Demande refus\xE9e.",
|
|
34
|
+
invalid_request: "Demande invalide.",
|
|
35
|
+
service_unavailable: "Le service de v\xE9rification est momentan\xE9ment indisponible.",
|
|
36
|
+
account_suspended: "La v\xE9rification par SMS n\u2019est pas disponible sur ce site pour le moment.",
|
|
37
|
+
send_failed: "L\u2019envoi du code n\u2019a pas pu \xEAtre confirm\xE9. Si vous ne le recevez pas d\u2019ici une minute, demandez un nouveau code.",
|
|
38
|
+
server_error: "Une erreur est survenue. R\xE9essayez.",
|
|
39
|
+
network_error: "Connexion impossible. V\xE9rifiez votre r\xE9seau et r\xE9essayez.",
|
|
40
|
+
incomplete_code: "Saisissez les {count} caract\xE8res du code."
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
var en = {
|
|
44
|
+
title: "Phone verification",
|
|
45
|
+
phoneLabel: "Phone number",
|
|
46
|
+
countryCodeLabel: "Country code",
|
|
47
|
+
phonePlaceholder: "81 234 5678",
|
|
48
|
+
phoneHint: "We will send you a code by SMS.",
|
|
49
|
+
send: "Send code",
|
|
50
|
+
sending: "Sending\u2026",
|
|
51
|
+
codeLabel: "Code received by SMS",
|
|
52
|
+
codeSentTo: "Code sent to {phone}.",
|
|
53
|
+
cellLabel: "Character {index} of {total}",
|
|
54
|
+
verify: "Verify",
|
|
55
|
+
verifying: "Verifying\u2026",
|
|
56
|
+
changePhone: "Change number",
|
|
57
|
+
resend: "Resend code",
|
|
58
|
+
resendIn: "Resend code in {time}",
|
|
59
|
+
resendAvailable: "You can request a new code.",
|
|
60
|
+
attemptsLeftOne: "1 attempt left.",
|
|
61
|
+
attemptsLeftOther: "{count} attempts left.",
|
|
62
|
+
success: "Phone number verified.",
|
|
63
|
+
testMode: "Test mode: the code is {code}",
|
|
64
|
+
retryIn: "Try again in {time}.",
|
|
65
|
+
errors: {
|
|
66
|
+
invalid_phone: "This phone number is not valid.",
|
|
67
|
+
invalid_code: "Wrong code.",
|
|
68
|
+
expired: "This code has expired. Request a new one.",
|
|
69
|
+
already_used: "This code was already used. Request a new one.",
|
|
70
|
+
max_attempts_reached: "Too many wrong attempts. Request a new code.",
|
|
71
|
+
resend_too_soon: "Please wait {time} before requesting a new code.",
|
|
72
|
+
rate_limited: "Too many requests. Try again in a few minutes.",
|
|
73
|
+
no_pending_verification: "Your request has expired. Request a new code.",
|
|
74
|
+
forbidden: "Request refused.",
|
|
75
|
+
invalid_request: "Invalid request.",
|
|
76
|
+
service_unavailable: "The verification service is temporarily unavailable.",
|
|
77
|
+
account_suspended: "SMS verification is not available on this site at the moment.",
|
|
78
|
+
send_failed: "We could not confirm that the code was sent. If it does not arrive within a minute, ask for a new code.",
|
|
79
|
+
server_error: "Something went wrong. Please try again.",
|
|
80
|
+
network_error: "Cannot connect. Check your network and try again.",
|
|
81
|
+
incomplete_code: "Enter the {count} characters of the code."
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
var LOCALES = { fr, en };
|
|
85
|
+
function resolveTexts(locale, override) {
|
|
86
|
+
const base = locale?.toLowerCase().startsWith("en") === true ? en : fr;
|
|
87
|
+
if (override === void 0 || override === null) {
|
|
88
|
+
return base;
|
|
89
|
+
}
|
|
90
|
+
const { errors, ...rest } = override;
|
|
91
|
+
return { ...base, ...stripUndefined(rest), errors: { ...base.errors, ...stripUndefined(errors ?? {}) } };
|
|
92
|
+
}
|
|
93
|
+
function format(template, values) {
|
|
94
|
+
return template.replace(/\{(\w+)\}/g, (match, key) => {
|
|
95
|
+
const value = values[key];
|
|
96
|
+
return value === void 0 ? match : String(value);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function formatDuration(seconds) {
|
|
100
|
+
const s = Math.max(0, Math.ceil(seconds));
|
|
101
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
102
|
+
return s >= 3600 ? `${String(Math.floor(s / 3600))}:${pad(Math.floor(s / 60) % 60)}:${pad(s % 60)}` : `${String(Math.floor(s / 60))}:${pad(s % 60)}`;
|
|
103
|
+
}
|
|
104
|
+
function stripUndefined(value) {
|
|
105
|
+
return Object.fromEntries(Object.entries(value).filter(([, v]) => typeof v === "string"));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/phone.ts
|
|
109
|
+
var DEFAULT_COUNTRY_CODE = "243";
|
|
110
|
+
function normalizePhone(raw, defaultCountryCode = DEFAULT_COUNTRY_CODE) {
|
|
111
|
+
if (!isCountryCode(defaultCountryCode)) {
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
const input = raw.trim();
|
|
115
|
+
if (input === "") {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const explicitInternational = input.startsWith("+") || input.startsWith("00");
|
|
119
|
+
let digits = input.replace(/\D/g, "");
|
|
120
|
+
if (explicitInternational) {
|
|
121
|
+
digits = input.startsWith("00") ? digits.slice(2) : digits;
|
|
122
|
+
} else if (digits.startsWith(defaultCountryCode) && digits.length > 10) ; else if (digits.length >= 11 && !digits.startsWith("0")) ; else {
|
|
123
|
+
digits = defaultCountryCode + digits.replace(/^0+/, "");
|
|
124
|
+
}
|
|
125
|
+
return digits.length >= 8 && digits.length <= 15 ? digits : null;
|
|
126
|
+
}
|
|
127
|
+
function isCountryCode(value) {
|
|
128
|
+
return /^[1-9]\d{0,2}$/.test(value);
|
|
129
|
+
}
|
|
130
|
+
function maskPhone(phone) {
|
|
131
|
+
const digits = phone.replace(/\D/g, "");
|
|
132
|
+
if (digits.length <= 6) {
|
|
133
|
+
return "\u2022".repeat(digits.length);
|
|
134
|
+
}
|
|
135
|
+
return `+${digits.slice(0, 3)} ${digits.slice(3, 5)}${"\u2022".repeat(digits.length - 7)}${digits.slice(-2)}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// src/protocol.ts
|
|
139
|
+
var FAILURE_REASONS = ["invalid_code", "expired", "already_used", "max_attempts_reached"];
|
|
140
|
+
|
|
141
|
+
// src/styles.ts
|
|
142
|
+
var STYLES = `
|
|
143
|
+
:host{--lab-otp-primary:#008374;--lab-otp-primary-contrast:#fff;--lab-otp-accent:#F85A40;--lab-otp-error:#c2381f;
|
|
144
|
+
--lab-otp-success:#008374;--lab-otp-bg:#fff;--lab-otp-fg:#19201e;--lab-otp-muted:#5b6763;--lab-otp-border:#d5dcd9;
|
|
145
|
+
--lab-otp-field:#f8faf9;--lab-otp-radius:10px;--lab-otp-font:Inter,ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;
|
|
146
|
+
--lab-otp-cell-size:44px;--lab-otp-gap:8px;
|
|
147
|
+
display:block;max-width:420px;color:var(--lab-otp-fg);font:14px/1.45 var(--lab-otp-font);color-scheme:light}
|
|
148
|
+
:host([hidden]){display:none}
|
|
149
|
+
:host([theme=dark]){--lab-otp-primary:#2bb3a3;--lab-otp-primary-contrast:#06201c;--lab-otp-accent:#ff7a63;--lab-otp-error:#ff8a75;
|
|
150
|
+
--lab-otp-success:#2bb3a3;--lab-otp-bg:#19201e;--lab-otp-fg:#eef2f0;--lab-otp-muted:#a3b0ab;--lab-otp-border:#38433f;--lab-otp-field:#222b28;color-scheme:dark}
|
|
151
|
+
@media (prefers-color-scheme:dark){:host(:not([theme=light])){--lab-otp-primary:#2bb3a3;--lab-otp-primary-contrast:#06201c;
|
|
152
|
+
--lab-otp-accent:#ff7a63;--lab-otp-error:#ff8a75;--lab-otp-success:#2bb3a3;--lab-otp-bg:#19201e;--lab-otp-fg:#eef2f0;
|
|
153
|
+
--lab-otp-muted:#a3b0ab;--lab-otp-border:#38433f;--lab-otp-field:#222b28;color-scheme:dark}}
|
|
154
|
+
*{box-sizing:border-box}
|
|
155
|
+
.root{background:var(--lab-otp-bg);border:1px solid var(--lab-otp-border);border-radius:var(--lab-otp-radius);padding:20px}
|
|
156
|
+
h2{margin:0 0 12px;font-size:16px;font-weight:600}
|
|
157
|
+
form{display:flex;flex-direction:column;gap:10px;margin:0}
|
|
158
|
+
[hidden]{display:none!important}
|
|
159
|
+
label,.label{font-weight:500}
|
|
160
|
+
.hint,.muted{margin:0;color:var(--lab-otp-muted);font-size:13px}
|
|
161
|
+
.phone{display:flex;gap:var(--lab-otp-gap)}
|
|
162
|
+
input{font:inherit;color:inherit;background:var(--lab-otp-field);border:1px solid var(--lab-otp-border);
|
|
163
|
+
border-radius:calc(var(--lab-otp-radius) - 2px);padding:10px 12px;min-width:0;width:100%}
|
|
164
|
+
input:focus-visible,button:focus-visible{outline:2px solid var(--lab-otp-primary);outline-offset:2px}
|
|
165
|
+
input[aria-invalid=true]{border-color:var(--lab-otp-error)}
|
|
166
|
+
.cc{flex:0 0 5.5em;display:flex;align-items:center;gap:2px;background:var(--lab-otp-field);border:1px solid var(--lab-otp-border);
|
|
167
|
+
border-radius:calc(var(--lab-otp-radius) - 2px);padding-left:10px}
|
|
168
|
+
.cc input{border:0;padding-left:0;background:transparent}
|
|
169
|
+
.cc input:focus-visible{outline-offset:-2px}
|
|
170
|
+
.cells{display:flex;gap:var(--lab-otp-gap);flex-wrap:nowrap}
|
|
171
|
+
.cells input{width:var(--lab-otp-cell-size);height:calc(var(--lab-otp-cell-size) + 8px);padding:0;text-align:center;
|
|
172
|
+
font-size:20px;font-weight:600;font-variant-numeric:tabular-nums;flex:0 1 var(--lab-otp-cell-size)}
|
|
173
|
+
button{font:inherit;font-weight:600;border-radius:calc(var(--lab-otp-radius) - 2px);cursor:pointer;padding:10px 14px;
|
|
174
|
+
border:1px solid transparent}
|
|
175
|
+
button[disabled]{cursor:not-allowed;opacity:.6}
|
|
176
|
+
.primary{background:var(--lab-otp-primary);color:var(--lab-otp-primary-contrast)}
|
|
177
|
+
.primary:hover:not([disabled]){filter:brightness(1.08)}
|
|
178
|
+
.link{background:none;border:0;padding:4px 0;color:var(--lab-otp-primary);text-decoration:underline;text-underline-offset:2px;
|
|
179
|
+
align-self:flex-start;font-weight:500}
|
|
180
|
+
.row{display:flex;flex-wrap:wrap;justify-content:space-between;gap:4px 12px;align-items:center}
|
|
181
|
+
.error{margin:0;color:var(--lab-otp-error);font-weight:500}
|
|
182
|
+
.error:empty{display:none}
|
|
183
|
+
.test{margin:0;padding:6px 10px;border-radius:6px;border:1px dashed var(--lab-otp-accent);font-size:13px}
|
|
184
|
+
.success{display:flex;gap:10px;align-items:center;color:var(--lab-otp-success);font-weight:600}
|
|
185
|
+
.success svg{flex:none}
|
|
186
|
+
.sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
|
187
|
+
@media (max-width:360px){.root{padding:14px}:host{--lab-otp-cell-size:36px;--lab-otp-gap:6px}}
|
|
188
|
+
@media (prefers-reduced-motion:no-preference){button,input{transition:border-color .15s,filter .15s}}
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
// src/element.ts
|
|
192
|
+
var TAG_NAME = "labyrinthe-otp";
|
|
193
|
+
var EVENT_SENT = "labyrinthe-otp:sent";
|
|
194
|
+
var EVENT_VERIFIED = "labyrinthe-otp:verified";
|
|
195
|
+
var EVENT_ERROR = "labyrinthe-otp:error";
|
|
196
|
+
var SERVER_CODES = [
|
|
197
|
+
"invalid_phone",
|
|
198
|
+
"resend_too_soon",
|
|
199
|
+
"rate_limited",
|
|
200
|
+
"no_pending_verification",
|
|
201
|
+
"forbidden",
|
|
202
|
+
"invalid_request",
|
|
203
|
+
"service_unavailable",
|
|
204
|
+
"account_suspended",
|
|
205
|
+
"send_failed",
|
|
206
|
+
"server_error"
|
|
207
|
+
];
|
|
208
|
+
var NEEDS_NEW_CODE = ["expired", "already_used", "max_attempts_reached", "no_pending_verification"];
|
|
209
|
+
var ZERO_WIDTH_SPACE = String.fromCharCode(8203);
|
|
210
|
+
var CHECK_ICON = '<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="11" fill="none" stroke="currentColor" stroke-width="2"/><path d="M7 12.5l3.2 3.2L17 9" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
|
|
211
|
+
var TEMPLATE = `<style>${STYLES}</style>
|
|
212
|
+
<div class="root" part="root">
|
|
213
|
+
<h2 id="t" part="title"></h2>
|
|
214
|
+
<form id="pf" part="phone-form" novalidate>
|
|
215
|
+
<label for="pn" id="pl"></label>
|
|
216
|
+
<div class="phone"><span class="cc" part="country-code"><span aria-hidden="true">+</span><input id="cc" inputmode="numeric" autocomplete="tel-country-code" maxlength="3" spellcheck="false"></span>
|
|
217
|
+
<input id="pn" type="tel" inputmode="tel" autocomplete="tel-national" aria-describedby="ph er" part="phone-input"></div>
|
|
218
|
+
<p class="hint" id="ph"></p>
|
|
219
|
+
<button class="primary" type="submit" id="sb" part="button send-button"></button>
|
|
220
|
+
</form>
|
|
221
|
+
<form id="cf" part="code-form" novalidate hidden>
|
|
222
|
+
<div class="row"><p class="muted" id="st"></p><button type="button" class="link" id="chg" part="link change-button"></button></div>
|
|
223
|
+
<span class="label" id="cl"></span>
|
|
224
|
+
<div class="cells" role="group" id="cells" aria-labelledby="cl" aria-describedby="at er" part="cells"></div>
|
|
225
|
+
<p class="muted" id="at"></p>
|
|
226
|
+
<p class="test" id="tc" hidden part="test-code"></p>
|
|
227
|
+
<button class="primary" type="submit" id="vb" part="button verify-button"></button>
|
|
228
|
+
<button type="button" class="link" id="rs" part="link resend-button"></button>
|
|
229
|
+
</form>
|
|
230
|
+
<div class="success" id="ok" hidden tabindex="-1" part="success">${CHECK_ICON}<span id="okt"></span></div>
|
|
231
|
+
<p class="error" id="er" role="alert" part="error"></p>
|
|
232
|
+
<p class="sr" id="lv" aria-live="polite"></p>
|
|
233
|
+
</div>`;
|
|
234
|
+
function createElementClass() {
|
|
235
|
+
class LabyrintheOtp extends HTMLElement {
|
|
236
|
+
static observedAttributes = ["locale", "country-code", "phone", "code-length", "code-type", "heading"];
|
|
237
|
+
#root;
|
|
238
|
+
#el;
|
|
239
|
+
#textsOverride = null;
|
|
240
|
+
#texts = resolveTexts("fr");
|
|
241
|
+
#requestHeaders = {};
|
|
242
|
+
#step = "phone";
|
|
243
|
+
#phoneNumber = "";
|
|
244
|
+
#phoneMasked = "";
|
|
245
|
+
#busy = false;
|
|
246
|
+
#codeLength = 6;
|
|
247
|
+
#codeType = "numeric";
|
|
248
|
+
#attemptsLeft = null;
|
|
249
|
+
#needsNewCode = false;
|
|
250
|
+
#resendAt = 0;
|
|
251
|
+
#timer;
|
|
252
|
+
#webOtp;
|
|
253
|
+
#lastAutoSubmitted = "";
|
|
254
|
+
#errorCode = null;
|
|
255
|
+
#errorValues = {};
|
|
256
|
+
#maintenance = {};
|
|
257
|
+
/** Jeton du dernier envoi sans réponse exploitable : réutilisé pour ce numéro (Idempotency-Key côté serveur). */
|
|
258
|
+
#pendingSend = null;
|
|
259
|
+
constructor() {
|
|
260
|
+
super();
|
|
261
|
+
this.#root = this.attachShadow({ mode: "open", delegatesFocus: true });
|
|
262
|
+
this.#root.innerHTML = TEMPLATE;
|
|
263
|
+
const $ = (id) => this.#root.getElementById(id);
|
|
264
|
+
this.#el = {
|
|
265
|
+
title: $("t"),
|
|
266
|
+
phoneForm: $("pf"),
|
|
267
|
+
phoneLabel: $("pl"),
|
|
268
|
+
countryCode: $("cc"),
|
|
269
|
+
phone: $("pn"),
|
|
270
|
+
phoneHint: $("ph"),
|
|
271
|
+
sendButton: $("sb"),
|
|
272
|
+
codeForm: $("cf"),
|
|
273
|
+
sentTo: $("st"),
|
|
274
|
+
changeButton: $("chg"),
|
|
275
|
+
codeLabel: $("cl"),
|
|
276
|
+
cells: $("cells"),
|
|
277
|
+
attempts: $("at"),
|
|
278
|
+
testCode: $("tc"),
|
|
279
|
+
verifyButton: $("vb"),
|
|
280
|
+
resendButton: $("rs"),
|
|
281
|
+
success: $("ok"),
|
|
282
|
+
successText: $("okt"),
|
|
283
|
+
error: $("er"),
|
|
284
|
+
live: $("lv")
|
|
285
|
+
};
|
|
286
|
+
this.#el.phoneForm.addEventListener("submit", (event) => {
|
|
287
|
+
event.preventDefault();
|
|
288
|
+
void this.#submitPhone();
|
|
289
|
+
});
|
|
290
|
+
this.#el.codeForm.addEventListener("submit", (event) => {
|
|
291
|
+
event.preventDefault();
|
|
292
|
+
void this.#submitCode();
|
|
293
|
+
});
|
|
294
|
+
this.#el.changeButton.addEventListener("click", () => {
|
|
295
|
+
this.#goToPhoneStep();
|
|
296
|
+
});
|
|
297
|
+
this.#el.resendButton.addEventListener("click", () => {
|
|
298
|
+
if (!this.#busy && this.#secondsBeforeResend() === 0) {
|
|
299
|
+
void this.#send(this.#phoneNumber);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
this.#el.phone.addEventListener("input", () => {
|
|
303
|
+
this.#el.phone.removeAttribute("aria-invalid");
|
|
304
|
+
});
|
|
305
|
+
this.#el.cells.addEventListener("input", (event) => {
|
|
306
|
+
this.#onCellInput(event);
|
|
307
|
+
});
|
|
308
|
+
this.#el.cells.addEventListener("keydown", (event) => {
|
|
309
|
+
this.#onCellKeydown(event);
|
|
310
|
+
});
|
|
311
|
+
this.#el.cells.addEventListener("paste", (event) => {
|
|
312
|
+
this.#onCellPaste(event);
|
|
313
|
+
});
|
|
314
|
+
this.#el.cells.addEventListener("focusin", (event) => {
|
|
315
|
+
const cell = event.target;
|
|
316
|
+
if (cell instanceof HTMLInputElement) {
|
|
317
|
+
cell.select();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
connectedCallback() {
|
|
322
|
+
if (this.#el.countryCode.value === "") {
|
|
323
|
+
this.#el.countryCode.value = this.#defaultCountryCode();
|
|
324
|
+
}
|
|
325
|
+
const phone = this.getAttribute("phone");
|
|
326
|
+
if (phone !== null && this.#el.phone.value === "") {
|
|
327
|
+
this.#el.phone.value = phone;
|
|
328
|
+
}
|
|
329
|
+
this.#readCodeAttributes();
|
|
330
|
+
this.#applyTexts();
|
|
331
|
+
if (this.#step === "code") {
|
|
332
|
+
this.#startTimer();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
disconnectedCallback() {
|
|
336
|
+
this.#stopTimer();
|
|
337
|
+
this.#stopWebOtp();
|
|
338
|
+
}
|
|
339
|
+
attributeChangedCallback(name, oldValue, value) {
|
|
340
|
+
if (oldValue === value) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (name === "country-code" && value !== null && isCountryCode(value.replace(/^\+/, ""))) {
|
|
344
|
+
this.#el.countryCode.value = value.replace(/^\+/, "");
|
|
345
|
+
} else if (name === "phone" && value !== null && this.#step === "phone") {
|
|
346
|
+
this.#el.phone.value = value;
|
|
347
|
+
} else if ((name === "code-length" || name === "code-type") && this.#step === "phone") {
|
|
348
|
+
this.#readCodeAttributes();
|
|
349
|
+
}
|
|
350
|
+
this.#applyTexts();
|
|
351
|
+
}
|
|
352
|
+
get step() {
|
|
353
|
+
return this.#step;
|
|
354
|
+
}
|
|
355
|
+
get texts() {
|
|
356
|
+
return this.#textsOverride;
|
|
357
|
+
}
|
|
358
|
+
set texts(value) {
|
|
359
|
+
this.#textsOverride = value;
|
|
360
|
+
this.#applyTexts();
|
|
361
|
+
}
|
|
362
|
+
get requestHeaders() {
|
|
363
|
+
return { ...this.#requestHeaders };
|
|
364
|
+
}
|
|
365
|
+
set requestHeaders(value) {
|
|
366
|
+
this.#requestHeaders = { ...value ?? {} };
|
|
367
|
+
}
|
|
368
|
+
reset() {
|
|
369
|
+
this.#needsNewCode = false;
|
|
370
|
+
this.#attemptsLeft = null;
|
|
371
|
+
this.#clearCells();
|
|
372
|
+
this.#goToPhoneStep();
|
|
373
|
+
this.#el.phone.value = this.getAttribute("phone") ?? "";
|
|
374
|
+
}
|
|
375
|
+
focus(options) {
|
|
376
|
+
const target = this.#step === "phone" ? this.#el.phone : this.#step === "code" ? this.#firstEmptyCell() ?? this.#el.verifyButton : this.#el.success;
|
|
377
|
+
target.focus(options);
|
|
378
|
+
}
|
|
379
|
+
// ----- Étape 1 : numéro -------------------------------------------------
|
|
380
|
+
async #submitPhone() {
|
|
381
|
+
if (this.#busy) {
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
const countryCode = this.#el.countryCode.value.replace(/\D/g, "") || this.#defaultCountryCode();
|
|
385
|
+
const phone = normalizePhone(this.#el.phone.value, countryCode);
|
|
386
|
+
if (phone === null) {
|
|
387
|
+
this.#el.phone.setAttribute("aria-invalid", "true");
|
|
388
|
+
this.#fail("invalid_phone", null);
|
|
389
|
+
this.#el.phone.focus();
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
await this.#send(phone);
|
|
393
|
+
}
|
|
394
|
+
async #send(phone) {
|
|
395
|
+
this.#setBusy(true);
|
|
396
|
+
this.#clearError();
|
|
397
|
+
const locale = this.getAttribute("locale")?.toLowerCase().startsWith("en") === true ? "en" : "fr";
|
|
398
|
+
if (this.#pendingSend?.phone !== phone) {
|
|
399
|
+
this.#pendingSend = { phone, id: sendId() };
|
|
400
|
+
}
|
|
401
|
+
const result = await this.#post(this.getAttribute("send-url") ?? "/api/otp/send", {
|
|
402
|
+
phone: `+${phone}`,
|
|
403
|
+
locale,
|
|
404
|
+
send_id: this.#pendingSend.id
|
|
405
|
+
});
|
|
406
|
+
this.#setBusy(false);
|
|
407
|
+
if (result === null) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const body = result.body;
|
|
411
|
+
if (result.status >= 200 && result.status < 300 && body?.sent === true) {
|
|
412
|
+
this.#pendingSend = null;
|
|
413
|
+
this.#onSent(phone, body);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
const { code, retryAfterSeconds, maintenance } = readError(result);
|
|
417
|
+
if (code === "send_failed") {
|
|
418
|
+
this.#pendingSend = null;
|
|
419
|
+
}
|
|
420
|
+
if (code === "resend_too_soon" && retryAfterSeconds !== void 0) {
|
|
421
|
+
this.#resendAt = Date.now() + retryAfterSeconds * 1e3;
|
|
422
|
+
this.#startTimer();
|
|
423
|
+
}
|
|
424
|
+
if (code === "invalid_phone" && this.#step === "phone") {
|
|
425
|
+
this.#el.phone.setAttribute("aria-invalid", "true");
|
|
426
|
+
}
|
|
427
|
+
this.#fail(code, result.status, retryAfterSeconds, void 0, maintenance);
|
|
428
|
+
}
|
|
429
|
+
#onSent(phone, body) {
|
|
430
|
+
this.#phoneNumber = phone;
|
|
431
|
+
this.#phoneMasked = typeof body.phone_masked === "string" ? body.phone_masked : `+${phone}`;
|
|
432
|
+
const length = body.code_length;
|
|
433
|
+
if (typeof length === "number" && Number.isInteger(length) && length >= 1 && length <= 20) {
|
|
434
|
+
this.#codeLength = length;
|
|
435
|
+
}
|
|
436
|
+
if (body.code_type === "numeric" || body.code_type === "alphanumeric") {
|
|
437
|
+
this.#codeType = body.code_type;
|
|
438
|
+
}
|
|
439
|
+
const resendAfter = typeof body.resend_after_seconds === "number" ? Math.max(0, body.resend_after_seconds) : 60;
|
|
440
|
+
const maxAttempts = typeof body.max_attempts === "number" ? body.max_attempts : null;
|
|
441
|
+
this.#resendAt = Date.now() + resendAfter * 1e3;
|
|
442
|
+
this.#attemptsLeft = maxAttempts;
|
|
443
|
+
this.#needsNewCode = false;
|
|
444
|
+
this.#lastAutoSubmitted = "";
|
|
445
|
+
const testCode = typeof body.test_code === "string" && body.test_code !== "" ? body.test_code : void 0;
|
|
446
|
+
this.#el.testCode.hidden = testCode === void 0;
|
|
447
|
+
this.#el.testCode.textContent = testCode === void 0 ? "" : format(this.#texts.testMode, { code: testCode });
|
|
448
|
+
this.#step = "code";
|
|
449
|
+
this.#buildCells();
|
|
450
|
+
this.#showStep();
|
|
451
|
+
this.#applyTexts();
|
|
452
|
+
this.#startTimer();
|
|
453
|
+
this.#startWebOtp();
|
|
454
|
+
this.#announce(format(this.#texts.codeSentTo, { phone: this.#phoneMasked }));
|
|
455
|
+
this.#firstEmptyCell()?.focus();
|
|
456
|
+
const detail = {
|
|
457
|
+
phone,
|
|
458
|
+
phoneMasked: this.#phoneMasked,
|
|
459
|
+
expiresAt: new Date(typeof body.expires_at === "string" ? body.expires_at : Date.now()),
|
|
460
|
+
maxAttempts: maxAttempts ?? 0,
|
|
461
|
+
resendAfterSeconds: resendAfter,
|
|
462
|
+
...testCode === void 0 ? {} : { testCode }
|
|
463
|
+
};
|
|
464
|
+
this.#emit(EVENT_SENT, detail);
|
|
465
|
+
}
|
|
466
|
+
// ----- Étape 2 : code ---------------------------------------------------
|
|
467
|
+
async #submitCode() {
|
|
468
|
+
if (this.#busy || this.#needsNewCode) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
const code = this.#code();
|
|
472
|
+
if (code.length < this.#codeLength) {
|
|
473
|
+
this.#fail("incomplete_code", null);
|
|
474
|
+
this.#firstEmptyCell()?.focus();
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
this.#setBusy(true);
|
|
478
|
+
this.#clearError();
|
|
479
|
+
const result = await this.#post(this.getAttribute("verify-url") ?? "/api/otp/verify", { code });
|
|
480
|
+
this.#setBusy(false);
|
|
481
|
+
if (result === null) {
|
|
482
|
+
this.#lastAutoSubmitted = "";
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const body = result.body;
|
|
486
|
+
if (result.status >= 200 && result.status < 300 && body?.verified === true) {
|
|
487
|
+
this.#onVerified(body.data);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (result.status >= 200 && result.status < 300 && body?.verified === false) {
|
|
491
|
+
const reason = FAILURE_REASONS.includes(body.reason) ? body.reason : "invalid_code";
|
|
492
|
+
this.#attemptsLeft = typeof body.attempts_left === "number" ? body.attempts_left : null;
|
|
493
|
+
this.#onRefused(reason, result.status);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const { code: errorCode, retryAfterSeconds, maintenance } = readError(result);
|
|
497
|
+
if (NEEDS_NEW_CODE.includes(errorCode)) {
|
|
498
|
+
this.#onRefused(errorCode, result.status);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this.#lastAutoSubmitted = "";
|
|
502
|
+
this.#fail(errorCode, result.status, retryAfterSeconds, void 0, maintenance);
|
|
503
|
+
}
|
|
504
|
+
#onRefused(reason, status) {
|
|
505
|
+
this.#clearCells();
|
|
506
|
+
this.#lastAutoSubmitted = "";
|
|
507
|
+
if (NEEDS_NEW_CODE.includes(reason)) {
|
|
508
|
+
this.#needsNewCode = true;
|
|
509
|
+
this.#attemptsLeft = null;
|
|
510
|
+
}
|
|
511
|
+
this.#applyTexts();
|
|
512
|
+
this.#fail(reason, status, void 0, this.#attemptsLeft);
|
|
513
|
+
if (this.#needsNewCode) {
|
|
514
|
+
(this.#el.resendButton.disabled ? this.#el.changeButton : this.#el.resendButton).focus();
|
|
515
|
+
} else {
|
|
516
|
+
this.#firstEmptyCell()?.focus();
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
#onVerified(data) {
|
|
520
|
+
this.#step = "success";
|
|
521
|
+
this.#stopTimer();
|
|
522
|
+
this.#stopWebOtp();
|
|
523
|
+
this.#clearError();
|
|
524
|
+
this.#showStep();
|
|
525
|
+
this.#applyTexts();
|
|
526
|
+
this.#el.success.focus();
|
|
527
|
+
this.#announce(this.#texts.success);
|
|
528
|
+
const detail = { phone: this.#phoneNumber, data: data ?? null };
|
|
529
|
+
this.#emit(EVENT_VERIFIED, detail);
|
|
530
|
+
}
|
|
531
|
+
#goToPhoneStep() {
|
|
532
|
+
this.#step = "phone";
|
|
533
|
+
this.#stopTimer();
|
|
534
|
+
this.#stopWebOtp();
|
|
535
|
+
this.#clearError();
|
|
536
|
+
this.#readCodeAttributes();
|
|
537
|
+
this.#showStep();
|
|
538
|
+
this.#applyTexts();
|
|
539
|
+
this.#el.phone.focus();
|
|
540
|
+
}
|
|
541
|
+
// ----- Cases du code ----------------------------------------------------
|
|
542
|
+
#buildCells() {
|
|
543
|
+
const cells = this.#el.cells;
|
|
544
|
+
cells.textContent = "";
|
|
545
|
+
for (let i = 0; i < this.#codeLength; i++) {
|
|
546
|
+
const input = document.createElement("input");
|
|
547
|
+
input.type = "text";
|
|
548
|
+
input.inputMode = this.#codeType === "numeric" ? "numeric" : "text";
|
|
549
|
+
input.autocomplete = i === 0 ? "one-time-code" : "off";
|
|
550
|
+
input.spellcheck = false;
|
|
551
|
+
input.setAttribute("autocapitalize", this.#codeType === "numeric" ? "off" : "characters");
|
|
552
|
+
input.setAttribute("enterkeyhint", "done");
|
|
553
|
+
input.setAttribute("part", "cell");
|
|
554
|
+
input.dataset["index"] = String(i);
|
|
555
|
+
cells.append(input);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
#cells() {
|
|
559
|
+
return Array.from(this.#el.cells.querySelectorAll("input"));
|
|
560
|
+
}
|
|
561
|
+
#code() {
|
|
562
|
+
return this.#cells().map((cell) => cell.value).join("");
|
|
563
|
+
}
|
|
564
|
+
#firstEmptyCell() {
|
|
565
|
+
const cells = this.#cells();
|
|
566
|
+
return cells.find((cell) => cell.value === "") ?? cells[cells.length - 1];
|
|
567
|
+
}
|
|
568
|
+
#clearCells() {
|
|
569
|
+
for (const cell of this.#cells()) {
|
|
570
|
+
cell.value = "";
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
#sanitize(value) {
|
|
574
|
+
return this.#codeType === "numeric" ? value.replace(/\D/g, "") : value.toUpperCase().replace(/[^A-Z0-9]/g, "");
|
|
575
|
+
}
|
|
576
|
+
/** Écrit `text` à partir de la case `start` (collage, saisie automatique, Web OTP). */
|
|
577
|
+
#fillFrom(start, text) {
|
|
578
|
+
const cells = this.#cells();
|
|
579
|
+
const chars = this.#sanitize(text);
|
|
580
|
+
const from = chars.length >= cells.length ? 0 : start;
|
|
581
|
+
for (let i = 0; i < chars.length && from + i < cells.length; i++) {
|
|
582
|
+
const cell = cells[from + i];
|
|
583
|
+
if (cell !== void 0) {
|
|
584
|
+
cell.value = chars.charAt(i);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
const next = cells.find((cell, index) => index >= from && cell.value === "") ?? cells[Math.min(from + chars.length, cells.length - 1)];
|
|
588
|
+
next?.focus();
|
|
589
|
+
this.#afterCodeChange();
|
|
590
|
+
}
|
|
591
|
+
#onCellInput(event) {
|
|
592
|
+
const cell = event.target;
|
|
593
|
+
if (!(cell instanceof HTMLInputElement)) {
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
const index = Number(cell.dataset["index"]);
|
|
597
|
+
const value = this.#sanitize(cell.value);
|
|
598
|
+
if (value.length <= 1) {
|
|
599
|
+
cell.value = value;
|
|
600
|
+
if (value !== "") {
|
|
601
|
+
this.#cells()[index + 1]?.focus();
|
|
602
|
+
}
|
|
603
|
+
this.#afterCodeChange();
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
cell.value = "";
|
|
607
|
+
this.#fillFrom(index, value);
|
|
608
|
+
}
|
|
609
|
+
#onCellKeydown(event) {
|
|
610
|
+
const cell = event.target;
|
|
611
|
+
if (!(cell instanceof HTMLInputElement)) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
const cells = this.#cells();
|
|
615
|
+
const index = Number(cell.dataset["index"]);
|
|
616
|
+
let target;
|
|
617
|
+
if (event.key === "Backspace" && cell.value === "" && index > 0) {
|
|
618
|
+
target = cells[index - 1];
|
|
619
|
+
if (target !== void 0) {
|
|
620
|
+
target.value = "";
|
|
621
|
+
}
|
|
622
|
+
} else if (event.key === "ArrowLeft") {
|
|
623
|
+
target = cells[index - 1];
|
|
624
|
+
} else if (event.key === "ArrowRight") {
|
|
625
|
+
target = cells[index + 1];
|
|
626
|
+
} else if (event.key === "Home") {
|
|
627
|
+
target = cells[0];
|
|
628
|
+
} else if (event.key === "End") {
|
|
629
|
+
target = cells[cells.length - 1];
|
|
630
|
+
}
|
|
631
|
+
if (target !== void 0) {
|
|
632
|
+
event.preventDefault();
|
|
633
|
+
target.focus();
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
#onCellPaste(event) {
|
|
637
|
+
const cell = event.target;
|
|
638
|
+
const text = event.clipboardData?.getData("text") ?? "";
|
|
639
|
+
if (!(cell instanceof HTMLInputElement) || text === "") {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
event.preventDefault();
|
|
643
|
+
this.#fillFrom(Number(cell.dataset["index"]), text);
|
|
644
|
+
}
|
|
645
|
+
#afterCodeChange() {
|
|
646
|
+
if (this.#errorCode === "incomplete_code" || this.#errorCode === "invalid_code") {
|
|
647
|
+
this.#clearError();
|
|
648
|
+
}
|
|
649
|
+
const code = this.#code();
|
|
650
|
+
if (code.length === this.#codeLength && !this.hasAttribute("no-auto-submit") && code !== this.#lastAutoSubmitted && !this.#busy && !this.#needsNewCode) {
|
|
651
|
+
this.#lastAutoSubmitted = code;
|
|
652
|
+
void this.#submitCode();
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
// ----- Renvoi et Web OTP ------------------------------------------------
|
|
656
|
+
#secondsBeforeResend() {
|
|
657
|
+
return Math.max(0, Math.ceil((this.#resendAt - Date.now()) / 1e3));
|
|
658
|
+
}
|
|
659
|
+
#startTimer() {
|
|
660
|
+
this.#stopTimer();
|
|
661
|
+
this.#renderResend();
|
|
662
|
+
if (this.#secondsBeforeResend() > 0) {
|
|
663
|
+
this.#timer = setInterval(() => {
|
|
664
|
+
this.#renderResend();
|
|
665
|
+
if (this.#secondsBeforeResend() === 0) {
|
|
666
|
+
this.#stopTimer();
|
|
667
|
+
if (this.#step === "code") {
|
|
668
|
+
this.#announce(this.#texts.resendAvailable);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}, 1e3);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
#stopTimer() {
|
|
675
|
+
if (this.#timer !== void 0) {
|
|
676
|
+
clearInterval(this.#timer);
|
|
677
|
+
this.#timer = void 0;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
#renderResend() {
|
|
681
|
+
const seconds = this.#secondsBeforeResend();
|
|
682
|
+
const button = this.#el.resendButton;
|
|
683
|
+
button.disabled = seconds > 0 || this.#busy;
|
|
684
|
+
button.textContent = seconds > 0 ? format(this.#texts.resendIn, { time: formatDuration(seconds) }) : this.#texts.resend;
|
|
685
|
+
if (this.#step === "phone") {
|
|
686
|
+
this.#el.sendButton.disabled = this.#busy || this.#errorCode === "resend_too_soon" && seconds > 0;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
#startWebOtp() {
|
|
690
|
+
this.#stopWebOtp();
|
|
691
|
+
const credentials = navigator.credentials;
|
|
692
|
+
if (this.hasAttribute("no-web-otp") || !("OTPCredential" in window) || credentials === void 0) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const controller = new AbortController();
|
|
696
|
+
this.#webOtp = controller;
|
|
697
|
+
credentials.get({ otp: { transport: ["sms"] }, signal: controller.signal }).then((credential) => {
|
|
698
|
+
const code = credential?.code;
|
|
699
|
+
if (typeof code === "string" && !controller.signal.aborted && this.#step === "code") {
|
|
700
|
+
this.#fillFrom(0, code);
|
|
701
|
+
}
|
|
702
|
+
}).catch(() => {
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
#stopWebOtp() {
|
|
706
|
+
this.#webOtp?.abort();
|
|
707
|
+
this.#webOtp = void 0;
|
|
708
|
+
}
|
|
709
|
+
// ----- Rendu ------------------------------------------------------------
|
|
710
|
+
#showStep() {
|
|
711
|
+
this.#el.phoneForm.hidden = this.#step !== "phone";
|
|
712
|
+
this.#el.codeForm.hidden = this.#step !== "code";
|
|
713
|
+
this.#el.success.hidden = this.#step !== "success";
|
|
714
|
+
}
|
|
715
|
+
#applyTexts() {
|
|
716
|
+
const t = this.#texts = resolveTexts(this.getAttribute("locale"), this.#textsOverride);
|
|
717
|
+
const el = this.#el;
|
|
718
|
+
el.title.textContent = this.getAttribute("heading") ?? t.title;
|
|
719
|
+
el.title.hidden = this.getAttribute("heading") === "";
|
|
720
|
+
el.phoneLabel.textContent = t.phoneLabel;
|
|
721
|
+
el.countryCode.setAttribute("aria-label", t.countryCodeLabel);
|
|
722
|
+
el.phone.placeholder = t.phonePlaceholder;
|
|
723
|
+
el.phoneHint.textContent = t.phoneHint;
|
|
724
|
+
el.sendButton.textContent = this.#busy && this.#step === "phone" ? t.sending : t.send;
|
|
725
|
+
el.sentTo.textContent = format(t.codeSentTo, { phone: this.#phoneMasked });
|
|
726
|
+
el.changeButton.textContent = t.changePhone;
|
|
727
|
+
el.codeLabel.textContent = t.codeLabel;
|
|
728
|
+
el.verifyButton.textContent = this.#busy && this.#step === "code" ? t.verifying : t.verify;
|
|
729
|
+
el.successText.textContent = t.success;
|
|
730
|
+
const cells = this.#cells();
|
|
731
|
+
cells.forEach((cell, index) => {
|
|
732
|
+
cell.setAttribute("aria-label", format(t.cellLabel, { index: index + 1, total: cells.length }));
|
|
733
|
+
cell.disabled = this.#needsNewCode;
|
|
734
|
+
});
|
|
735
|
+
el.verifyButton.disabled = this.#busy || this.#needsNewCode;
|
|
736
|
+
const left = this.#attemptsLeft;
|
|
737
|
+
el.attempts.textContent = left === null || this.#errorCode !== "invalid_code" ? "" : format(left === 1 ? t.attemptsLeftOne : t.attemptsLeftOther, { count: left });
|
|
738
|
+
if (this.#errorCode !== null) {
|
|
739
|
+
const message = this.#errorMessage(this.#errorCode);
|
|
740
|
+
if (el.error.textContent.replace(ZERO_WIDTH_SPACE, "") !== message) {
|
|
741
|
+
el.error.textContent = message;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
this.#renderResend();
|
|
745
|
+
}
|
|
746
|
+
#setBusy(busy) {
|
|
747
|
+
this.#busy = busy;
|
|
748
|
+
const form = this.#step === "phone" ? this.#el.phoneForm : this.#el.codeForm;
|
|
749
|
+
form.setAttribute("aria-busy", String(busy));
|
|
750
|
+
this.#el.sendButton.disabled = busy;
|
|
751
|
+
this.#applyTexts();
|
|
752
|
+
}
|
|
753
|
+
#errorMessage(code) {
|
|
754
|
+
const t = this.#texts;
|
|
755
|
+
const values = { count: this.#codeLength, ...this.#errorValues };
|
|
756
|
+
if (code !== "service_unavailable") {
|
|
757
|
+
return format(t.errors[code], values);
|
|
758
|
+
}
|
|
759
|
+
const retry = this.#errorValues["time"] === void 0 ? "" : ` ${format(t.retryIn, values)}`;
|
|
760
|
+
return (this.#maintenance.adminMessage ?? format(t.errors[code], values)) + retry;
|
|
761
|
+
}
|
|
762
|
+
#fail(code, status, retryAfterSeconds, attemptsLeft, maintenance = {}) {
|
|
763
|
+
this.#errorCode = code;
|
|
764
|
+
this.#errorValues = retryAfterSeconds === void 0 ? {} : { time: formatDuration(retryAfterSeconds) };
|
|
765
|
+
this.#maintenance = maintenance;
|
|
766
|
+
const message = this.#errorMessage(code);
|
|
767
|
+
this.#el.error.textContent = this.#el.error.textContent === message ? message + ZERO_WIDTH_SPACE : message;
|
|
768
|
+
this.#applyTexts();
|
|
769
|
+
const detail = {
|
|
770
|
+
code,
|
|
771
|
+
message,
|
|
772
|
+
status,
|
|
773
|
+
...retryAfterSeconds === void 0 ? {} : { retryAfterSeconds },
|
|
774
|
+
...attemptsLeft === void 0 ? {} : { attemptsLeft },
|
|
775
|
+
...maintenance
|
|
776
|
+
};
|
|
777
|
+
this.#emit(EVENT_ERROR, detail);
|
|
778
|
+
}
|
|
779
|
+
#clearError() {
|
|
780
|
+
this.#errorCode = null;
|
|
781
|
+
this.#errorValues = {};
|
|
782
|
+
this.#maintenance = {};
|
|
783
|
+
this.#el.error.textContent = "";
|
|
784
|
+
this.#el.attempts.textContent = "";
|
|
785
|
+
}
|
|
786
|
+
#announce(message) {
|
|
787
|
+
this.#el.live.textContent = message;
|
|
788
|
+
}
|
|
789
|
+
#emit(type, detail) {
|
|
790
|
+
this.dispatchEvent(new CustomEvent(type, { detail, bubbles: true, composed: true }));
|
|
791
|
+
}
|
|
792
|
+
#defaultCountryCode() {
|
|
793
|
+
const value = (this.getAttribute("country-code") ?? "").replace(/^\+/, "");
|
|
794
|
+
return isCountryCode(value) ? value : DEFAULT_COUNTRY_CODE;
|
|
795
|
+
}
|
|
796
|
+
#readCodeAttributes() {
|
|
797
|
+
const length = Number(this.getAttribute("code-length") ?? "6");
|
|
798
|
+
this.#codeLength = Number.isInteger(length) && length >= 1 && length <= 20 ? length : 6;
|
|
799
|
+
this.#codeType = this.getAttribute("code-type") === "alphanumeric" ? "alphanumeric" : "numeric";
|
|
800
|
+
}
|
|
801
|
+
/** POST JSON vers le serveur de l'intégrateur ; null après une erreur réseau (déjà signalée). */
|
|
802
|
+
async #post(url, body) {
|
|
803
|
+
try {
|
|
804
|
+
const response = await fetch(url, {
|
|
805
|
+
method: "POST",
|
|
806
|
+
headers: { ...this.#requestHeaders, "Content-Type": "application/json", Accept: "application/json" },
|
|
807
|
+
body: JSON.stringify(body),
|
|
808
|
+
credentials: this.getAttribute("credentials") === "include" ? "include" : "same-origin"
|
|
809
|
+
});
|
|
810
|
+
let json = null;
|
|
811
|
+
try {
|
|
812
|
+
json = await response.json();
|
|
813
|
+
} catch {
|
|
814
|
+
json = null;
|
|
815
|
+
}
|
|
816
|
+
return { status: response.status, body: json };
|
|
817
|
+
} catch {
|
|
818
|
+
this.#fail("network_error", null);
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return LabyrintheOtp;
|
|
824
|
+
}
|
|
825
|
+
function sendId() {
|
|
826
|
+
return Array.from(crypto.getRandomValues(new Uint8Array(16)), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
827
|
+
}
|
|
828
|
+
function readError(result) {
|
|
829
|
+
const error = result.body?.error;
|
|
830
|
+
const retry = error?.retry_after_seconds;
|
|
831
|
+
const retryAfterSeconds = typeof retry === "number" && retry >= 0 ? Math.ceil(retry) : void 0;
|
|
832
|
+
let code;
|
|
833
|
+
if (typeof error?.code === "string" && SERVER_CODES.includes(error.code)) {
|
|
834
|
+
code = error.code;
|
|
835
|
+
} else if (result.status === 429) {
|
|
836
|
+
code = "rate_limited";
|
|
837
|
+
} else if (result.status === 403) {
|
|
838
|
+
code = "forbidden";
|
|
839
|
+
} else if (result.status === 503) {
|
|
840
|
+
code = "service_unavailable";
|
|
841
|
+
} else {
|
|
842
|
+
code = "server_error";
|
|
843
|
+
}
|
|
844
|
+
const maintenance = {};
|
|
845
|
+
if (code === "service_unavailable") {
|
|
846
|
+
if (typeof error?.admin_message === "string" && error.admin_message.trim() !== "") {
|
|
847
|
+
maintenance.adminMessage = error.admin_message.trim();
|
|
848
|
+
}
|
|
849
|
+
const until = typeof error?.until === "string" ? new Date(error.until) : null;
|
|
850
|
+
if (until !== null && !Number.isNaN(until.getTime())) {
|
|
851
|
+
maintenance.until = until;
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
return retryAfterSeconds === void 0 ? { code, maintenance } : { code, retryAfterSeconds, maintenance };
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
// src/version.ts
|
|
858
|
+
var VERSION = "1.2.0";
|
|
859
|
+
|
|
860
|
+
// src/index.ts
|
|
861
|
+
function defineLabyrintheOtp(tagName = TAG_NAME) {
|
|
862
|
+
if (typeof customElements === "undefined" || typeof HTMLElement === "undefined") {
|
|
863
|
+
return void 0;
|
|
864
|
+
}
|
|
865
|
+
const existing = customElements.get(tagName);
|
|
866
|
+
if (existing !== void 0) {
|
|
867
|
+
return existing;
|
|
868
|
+
}
|
|
869
|
+
const ElementClass = createElementClass();
|
|
870
|
+
customElements.define(tagName, ElementClass);
|
|
871
|
+
return ElementClass;
|
|
872
|
+
}
|
|
873
|
+
defineLabyrintheOtp();
|
|
874
|
+
|
|
875
|
+
export { DEFAULT_COUNTRY_CODE, EVENT_ERROR, EVENT_SENT, EVENT_VERIFIED, LOCALES, TAG_NAME, VERSION, defineLabyrintheOtp, maskPhone, normalizePhone, resolveTexts };
|