@gerege-systems/eid-mongolia-sdk 0.15.1 → 0.15.3

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 CHANGED
@@ -35,7 +35,7 @@ Smart-ID-тэй RP-үүдийн хэв маяг: хөтөчид **нэг тов
35
35
 
36
36
  | Хаана | Юу болно |
37
37
  |---|---|
38
- | Desktop хөтөч | Цонх: QR (секунд тутам шинэчлэгдэнэ) + «регистрийн дугаараар утас руу мэдэгдэл» (push), үлдсэн хугацаа, шууд төлөв |
38
+ | Desktop хөтөч | Цонх: QR (секунд тутам шинэчлэгдэнэ) + баталгаажуулах код + «Хүсэлтийн хугацаа m:ss» (сүүлийн 30с анхааруулах өнгө) + «Регистрийн дугаараар нэвтрэх» товч (ижил цонхонд доош дэлгэгдэх push хэсэг), шууд төлөв |
39
39
  | Утасны хөтөч | Товч → `start?mode=web2app` → сервер 302 → eID апп (Web2App). Апп баталгаажуулаад `/auth/eid/callback` руу буцаана |
40
40
  | Backend | session эхлүүлэх, QR угсрах (`sessionSecret` серверт), poll, `validateAuth` (гинж + ACSP_V2 + `flowType` + Web2App-д `userChallengeVerifier`) → таны `onSuccess` |
41
41
 
@@ -74,6 +74,8 @@ export const eidLogin = createEidLogin({
74
74
  // store: redisStore, // олон instance / serverless бол (анхдагч — процессын санах ой)
75
75
  // rateLimit: { start: (key) => redisLimiter.allow(key) }, // анхдагчтай нэгтгэгдэнэ (push 3/мин хэвээр) — доор «Хязгаарлалт»
76
76
  trustProxy: 1, // handle(Request)-д клиент IP: nginx/LB нэг hop бол 1 (доор «Клиент IP»)
77
+ onEvent: ({ kind, sessionId, meta }) => audit.record(`auth.eid_${kind}`, sessionId, meta), // таны аудит
78
+ // auditKey: process.env.EID_AUDIT_KEY, // pushKey-ийг instance/restart хооронд тогтвортой болгох
77
79
  });
78
80
  ```
79
81
 
@@ -197,7 +199,31 @@ RP талд нэмэлт шалгалт хэрэггүй. Эх сурвалж: e
197
199
  - `OK` нь зөвхөн `validateAuth` амжилттай үед: QR → `flowType=QR`, push → `Notification`, утас → `Web2App` + callback-ийн
198
200
  `userChallengeVerifier`. Web2App session-ийг poll дуусгахгүй (`WAIT_CALLBACK`). `onSuccess` нэг session-д нэг л удаа
199
201
  (store-ийн `delete` атомар байх ёстой — Redis `DEL`).
200
- - `onSuccess`-ийн `data` хөтөчид очно — нууц бүү оруул. Иргэний бүртгэлтэй эсэхийг push алдаагаар задлахгүй (`eid_unavailable`).
202
+ - `onSuccess`-ийн `data` хөтөчид очно — нууц бүү оруул.
203
+ - **Регистрийн oracle хаалт (decoy + доод хугацаа, анхдагчаар асаалттай).** eID push-ийг дугаарын шалтгаанаар татгалзвал
204
+ (eID-ийн өөрийн JSON 4xx — танихгүй / ашиглах боломжгүй дугаар) бодит push-тэй **ижил** хариу: `{mode, vc (санамсаргүй
205
+ 5 оронтой — eID-тэй ижил хэлбэр), expiresIn}`, ижил cookie; `status` нь eID-ээс асуулгүй `pollMs`-ийн хугацаанд барьж
206
+ `PENDING`, TTL-ийн дараа `EXPIRED` — утсаа аваагүй иргэнтэй ялгаагүй (бодит push-ийн CA `TIMEOUT` ч `EXPIRED` болно). Push start-ийн хариу бүр (бодит, decoy, 502)
207
+ `pushFloorMs`-оос (анхдагч 1000) хурдан гарахгүй — decoy-г цагаар ялгахгүйн тулд (`handle(Request)`-д хүсэлт тасарвал
208
+ хүлээхгүй; `handleRequest`-д `signal` өг). Хязгаар (`push:<idCode>`, `start:<ip>`) decoy-д ч үйлчилнэ; 429 ба 400
209
+ (`invalid_id` — хэлбэрийн алдаа, бүртгэлээс хамааралгүй) нь floor-гүй. eID-ийн 5xx, холболтын алдаа, JSON бус 4xx (eID-ийн
210
+ өмнөх proxy, ж: mTLS татгалзал), 401/403 (RP-ийн нууц/эрх) ба 429 нь **decoy биш** — `502 eid_unavailable` / `429` (eID
211
+ порталын зан: тасалдлыг decoy-оор нуувал иргэн дуусахгүй spinner харна; эдгээр нь дугаараас хамаардаггүй тул oracle
212
+ биш). Унтраах: `decoy: false`, `pushFloorMs: 0` — **⚠️** тэгвэл `/start` нь «энэ регистр eID-д бүртгэлтэй эсэх»-ийг
213
+ нэвтрээгүй хэнд ч хэлнэ; зөвхөн дотоод/тестийн орчинд.
214
+ - **Гэрчилгээний цуцлалт (OCSP, анхдагчаар асаалттай).** `validateAuth`-ийн дараа, `onSuccess`-ийн өмнө иргэний гэрчилгээг
215
+ түүний AIA-д бичигдсэн OCSP responder-оос асууна (`revocation`, анхдагч `ocspRevocationChecker()`; хариу олгогч эсвэл
216
+ түүний OCSPSigning responder-ийн гарын үсэгтэй, энэ serial-ийн, thisUpdate ≤ 48 цаг, good-ийг 5 мин кэшлэнэ). `revoked`,
217
+ мөн `unknown` (олгогч энэ гэрчилгээг олгоогүй) → `FAILED` / `VALIDATION` (500 биш), `onSuccess` дуудагдахгүй. Хариугүй
218
+ (timeout 2 с, 5xx, AIA алга, шалгалт унасан хариу): `revocationMode` анхдагч `"soft-fail"` — зөвшөөрч `onError`-оор
219
+ анхааруулна, `identity.revocation = "unchecked"` (eID порталын `EID_OCSP=soft`: CA-ийн responder унавал бүх иргэн
220
+ түгжигдэхгүй; утас session-ийг батлахад CA секундын өмнө хүрэх боломжтой байсан); `"hard-fail"` → `FAILED` /
221
+ `VALIDATION`. `revocation: false` — **⚠️** цуцалсан (хулгайлагдсан утасны) гэрчилгээгээр нэвтэрнэ.
222
+ - **Аудит — `onEvent({ kind, sessionId, meta })`** (await хийгдэнэ, алдаа нь `onError` руу): `push_sent` (meta `decoy` —
223
+ зөвхөн таны аудитад, хөтөч рүү хэзээ ч), `push_limited`, `refused` (иргэн татгалзсан), `expired` (TIMEOUT / eID-д
224
+ олдохгүй), `unverified` (`reason`: `validation` | `revoked` | `revocation_unavailable`), `flow_refused` (Web2App callback
225
+ өөр хөтчид), `success` (`flow`, `revocation`), `failed` (бусад `endResult`). meta-д `mode`; токен/нууц, регистрийн дугаар
226
+ орохгүй — push-д `pushKey` = HMAC-SHA256(`auditKey`, дугаар)-ын 32 hex (`auditKey` алга бол процесс бүрт санамсаргүй).
201
227
  - Олон instance / serverless (Vercel г.м.) дээр анхдагч санах ойн store ажиллахгүй — `EidLoginStore` (`get`/`set`/`delete`)-ийг Redis-ээр.
202
228
  - `onSuccess` дуудагдахаас ӨМНӨ tx store-оос атомар устгагдана (нэг удаагийн баталгаа) — `onSuccess` дотор store-оос бүү
203
229
  унш; хэрэгтэй бүхэн `identity`-д (`sessionId`, `documentNumber`, `certificateDerB64`, …).
@@ -482,6 +508,11 @@ const reg = await eid.registerSignature(registrationFromCompleted(req.body, {
482
508
  reply({ verifyUrl: reg.verifyUrl });
483
509
  ```
484
510
 
511
+ - `reg.created` — `true` 201 (шинэ), `false` 200 (ижил биеийн давталт). Өмнөх гарын үсэг CA-ийн угсарсан (горим A) бол
512
+ `complete().priorRevisionSha256`-ийг (`sha256(doc[0 : ByteRange[2]+ByteRange[3]])`) `registrationFromCompleted`
513
+ `priorSignatures` болгож дамжуулна; `registerSignature` 409 `PRIOR_MISMATCH`-ийн дараа өөрөө revision-оор тааруулна
514
+ (`priorRevisionSha256`-г гараар өгвөл оролдохгүй). Хөтчөөс JSON-оор дамжуулахдаа `priorRevisionSha256`-ийг ч явуул.
515
+
485
516
  ### Зөвхөн Node (backend бүгдийг хийнэ)
486
517
 
487
518
  ```ts
@@ -626,7 +657,8 @@ OK ирж болзошгүй. Иймд `ResponseValidator`:
626
657
  хүссэн session-д `ecdsa-with-SHA256` хариу ирвэл хуурамч гэж татгалзана.
627
658
 
628
659
  5. Цуцлалт — `trust.revocation` (OCSP/CRL hook) → `checkRevocation`, эсвэл `checkRevocationStatus` (`"good"` /
629
- `"unchecked"` (soft-fail) / `"skipped"` (hook алга)).
660
+ `"unchecked"` (soft-fail) / `"skipped"` (hook алга)). Бэлэн hook: `ocspRevocationChecker()` — гэрчилгээний AIA OCSP
661
+ (revoked/unknown → `"revoked"`; хариугүй → `"unknown"`, горим шийднэ). Өөр checker/горимоор нэг удаа: `checkRevocationWith`.
630
662
  6. Нэмэлт (сонголттой): `allowedIssuerSkis: [CITIZEN_ISSUING_CA_SKI]` — зөвхөн иргэний Issuing CA (04)-ийн гэрчилгээ;
631
663
  `checkKeyUsage: true` — auth → clientAuth, sign → nonRepudiation (CA-ийн профайл); `schemeName` — CA-ийн
632
664
  `acsp_scheme_name` (анхдагч `smart-id`).
package/dist/button.cjs CHANGED
@@ -397,8 +397,9 @@ var T = {
397
397
  loading: "\u0410\u0447\u0430\u0430\u043B\u0436 \u0431\u0430\u0439\u043D\u0430",
398
398
  qrAlt: "\u041D\u044D\u0432\u0442\u0440\u044D\u0445 QR \u043A\u043E\u0434 \u2014 \u0443\u0442\u0441\u0430\u043D \u0434\u044D\u044D\u0440\u0445 eID Mongolia \u0430\u043F\u043F\u0430\u0430\u0440 \u0443\u043D\u0448\u0443\u0443\u043B\u043D\u0430",
399
399
  steps: ["\u0423\u0442\u0441\u0430\u043D \u0434\u044D\u044D\u0440\u0445 eID Mongolia \u0430\u043F\u043F\u044B\u0433 \u043D\u044D\u044D\u043D\u044D", "QR \u043A\u043E\u0434\u044B\u0433 \u0443\u043D\u0448\u0443\u0443\u043B\u043D\u0430", "PIN1 \u043A\u043E\u0434\u043E\u043E \u043E\u0440\u0443\u0443\u043B\u043D\u0430"],
400
- code: "\u0423\u0442\u0441\u0430\u043D \u0434\u044D\u044D\u0440\u0445 \u043A\u043E\u0434:",
400
+ vcLabel: "\u0411\u0430\u0442\u0430\u043B\u0433\u0430\u0430\u0436\u0443\u0443\u043B\u0430\u0445 \u043A\u043E\u0434",
401
401
  or: "\u044D\u0441\u0432\u044D\u043B",
402
+ usePush: "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0439\u043D \u0434\u0443\u0433\u0430\u0430\u0440\u0430\u0430\u0440 \u043D\u044D\u0432\u0442\u0440\u044D\u0445",
402
403
  idLabel: "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0439\u043D \u0434\u0443\u0433\u0430\u0430\u0440",
403
404
  idHint: "\u0423\u0442\u0441\u0430\u043D\u0434 \u0442\u0430\u043D\u044C \u043C\u044D\u0434\u044D\u0433\u0434\u044D\u043B \u043E\u0447\u043D\u043E. \u0418\u0440\u0433\u044D\u043D\u0438\u0439 12 \u043E\u0440\u043E\u043D\u0442\u043E\u0439 \u0434\u0443\u0433\u0430\u0430\u0440\u0430\u0430\u0440 \u0447 \u0431\u043E\u043B\u043D\u043E.",
404
405
  idEmpty: "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0439\u043D \u0434\u0443\u0433\u0430\u0430\u0440 \u043E\u0440\u0443\u0443\u043B\u043D\u0430 \u0443\u0443",
@@ -411,7 +412,7 @@ var T = {
411
412
  waitText: "\u0423\u0442\u0441\u0430\u043D \u0434\u044D\u044D\u0440\u0445 \u043A\u043E\u0434 \u0438\u0436\u0438\u043B \u0431\u043E\u043B PIN1 \u043E\u0440\u0443\u0443\u043B\u043D\u0430 \u0443\u0443. \u0418\u0436\u0438\u043B \u0431\u0438\u0448 \u0431\u043E\u043B \u0442\u0430\u0442\u0433\u0430\u043B\u0437\u0430\u043D\u0430 \u0443\u0443.",
412
413
  waiting: "\u0425\u04AF\u043B\u044D\u044D\u0436 \u0431\u0430\u0439\u043D\u0430",
413
414
  back: "\u0411\u0443\u0446\u0430\u0445",
414
- left: "\u04AE\u043B\u0434\u0441\u044D\u043D \u0445\u0443\u0433\u0430\u0446\u0430\u0430",
415
+ left: "\u0425\u04AF\u0441\u044D\u043B\u0442\u0438\u0439\u043D \u0445\u0443\u0433\u0430\u0446\u0430\u0430:",
415
416
  stale: "QR \u043A\u043E\u0434 \u0445\u0443\u0443\u0447\u0438\u0440\u0441\u0430\u043D",
416
417
  refresh: "\u0428\u0438\u043D\u044D\u0447\u043B\u044D\u0445",
417
418
  offline: "\u0425\u043E\u043B\u0431\u043E\u043B\u0442 \u0442\u0430\u0441\u0430\u0440\u0441\u0430\u043D. \u0418\u043D\u0442\u0435\u0440\u043D\u044D\u0442\u044D\u044D \u0448\u0430\u043B\u0433\u0430\u043D\u0430 \u0443\u0443 \u2014 \u0434\u0430\u0445\u0438\u043D \u0445\u043E\u043B\u0431\u043E\u0433\u0434\u043E\u0445\u044B\u0433 \u043E\u0440\u043E\u043B\u0434\u043E\u0436 \u0431\u0430\u0439\u043D\u0430.",
@@ -425,7 +426,7 @@ var T = {
425
426
  retry: "\u0414\u0430\u0445\u0438\u043D \u043E\u0440\u043E\u043B\u0434\u043E\u0445",
426
427
  success: "\u0410\u043C\u0436\u0438\u043B\u0442\u0442\u0430\u0439 \u043D\u044D\u0432\u0442\u044D\u0440\u043B\u044D\u044D",
427
428
  sent: (vc) => `\u041C\u044D\u0434\u044D\u0433\u0434\u044D\u043B \u0438\u043B\u0433\u044D\u044D\u043B\u044D\u044D. \u0423\u0442\u0441\u0430\u043D \u0434\u044D\u044D\u0440\u0445 \u043A\u043E\u0434 ${vc.split("").join(" ")}`,
428
- minute: "1 \u043C\u0438\u043D\u0443\u0442 \u04AF\u043B\u0434\u043B\u044D\u044D"
429
+ minutes: (n) => `${n} \u043C\u0438\u043D\u0443\u0442 \u04AF\u043B\u0434\u043B\u044D\u044D`
429
430
  },
430
431
  en: {
431
432
  button: "Log in with eID",
@@ -434,8 +435,9 @@ var T = {
434
435
  loading: "Loading",
435
436
  qrAlt: "Login QR code \u2014 scan it with the eID Mongolia app on your phone",
436
437
  steps: ["Open the eID Mongolia app on your phone", "Scan the QR code", "Enter your PIN1"],
437
- code: "Code on your phone:",
438
+ vcLabel: "Verification code",
438
439
  or: "or",
440
+ usePush: "Log in with registration number",
439
441
  idLabel: "Registration number",
440
442
  idHint: "A notification will be sent to your phone. The 12-digit civil ID also works.",
441
443
  idEmpty: "Enter your registration number",
@@ -448,7 +450,7 @@ var T = {
448
450
  waitText: "If the code on your phone matches, enter PIN1. Otherwise decline.",
449
451
  waiting: "Waiting",
450
452
  back: "Back",
451
- left: "Time left",
453
+ left: "Request expires in:",
452
454
  stale: "QR code expired",
453
455
  refresh: "Refresh",
454
456
  offline: "Connection lost. Check your internet \u2014 reconnecting.",
@@ -462,7 +464,7 @@ var T = {
462
464
  retry: "Try again",
463
465
  success: "Logged in",
464
466
  sent: (vc) => `Notification sent. Code on your phone ${vc.split("").join(" ")}`,
465
- minute: "1 minute left"
467
+ minutes: (n) => `${n} minute${n === 1 ? "" : "s"} left`
466
468
  }
467
469
  };
468
470
  var EID_COLOR_TOKENS = [
@@ -528,7 +530,8 @@ h2, p, ol { margin: 0; }
528
530
  .qr.stale svg { opacity: 0.12; }
529
531
  .qr-stale { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
530
532
  color: hsl(222 47% 11%); font-weight: 500; }
531
- .qr-vc { margin-top: 8px; text-align: center; font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
533
+ .qr-vc { margin-top: 12px; text-align: center; }
534
+ .vc-label { font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
532
535
  .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-variant-numeric: tabular-nums; letter-spacing: 0.08em; color: var(--_eid-fg-muted); }
533
536
  .steps { margin-top: 16px; padding: 0; list-style: none; counter-reset: st; display: grid; gap: 8px; }
534
537
  .steps li { counter-increment: st; display: flex; align-items: center; gap: 12px; }
@@ -536,6 +539,13 @@ h2, p, ol { margin: 0; }
536
539
  color: var(--_eid-accent-subtle-fg); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 600; }
537
540
  .or { display: flex; align-items: center; gap: 12px; margin: 24px 0; color: var(--_eid-fg-subtle); font-size: 0.8125rem; }
538
541
  .or::before, .or::after { content: ""; flex: 1; height: 1px; background: var(--_eid-border); }
542
+ .disc-btn .chev { width: 16px; height: 16px; flex: none; transition: transform 200ms cubic-bezier(0,0,0.2,1); }
543
+ .disc-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }
544
+ .disc { margin: 0 -4px; display: grid; grid-template-rows: 0fr; transition: grid-template-rows 200ms cubic-bezier(0,0,0.2,1); }
545
+ .disc.open { grid-template-rows: 1fr; }
546
+ .disc > div { min-height: 0; overflow: hidden; visibility: hidden; transition: visibility 0s linear 200ms; }
547
+ .disc.open > div { visibility: visible; transition-delay: 0s; }
548
+ .disc form { padding: 16px 4px 4px; }
539
549
  .field { display: grid; gap: 8px; }
540
550
  .field label { font-weight: 500; }
541
551
  .field input { width: 100%; height: 44px; padding: 0 12px; border: 1px solid var(--_eid-border-strong); border-radius: var(--_eid-radius);
@@ -546,7 +556,8 @@ h2, p, ol { margin: 0; }
546
556
  .err { font-size: 0.8125rem; color: var(--_eid-danger-fg); font-weight: 500; }
547
557
  .push { margin-top: 16px; }
548
558
  .center { text-align: center; }
549
- .vc { font: 600 2.25rem/2.5rem ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.16em; padding-left: 0.16em; }
559
+ .vc { font: 700 2rem/2.5rem ui-monospace, SFMono-Regular, Menlo, monospace; font-variant-numeric: tabular-nums; letter-spacing: 0.16em;
560
+ padding-left: 0.16em; color: var(--_eid-fg); }
550
561
  .vc:focus, h2:focus { outline: none; }
551
562
  .text { margin-top: 12px; color: var(--_eid-fg-muted); }
552
563
  .pending { margin-top: 16px; display: inline-flex; align-items: center; gap: 8px; color: var(--_eid-fg-muted); }
@@ -555,7 +566,8 @@ h2, p, ol { margin: 0; }
555
566
  @keyframes spin { to { transform: rotate(360deg); } }
556
567
  .netwarn { margin-top: 16px; padding: 8px 12px; border: 1px solid var(--_eid-warning-border); border-radius: var(--_eid-radius); background: var(--_eid-warning-bg);
557
568
  color: var(--_eid-warning-fg); font-size: 0.8125rem; }
558
- .meta { margin-top: 16px; font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
569
+ .meta { margin-top: 4px; text-align: center; font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
570
+ .meta.low, .meta.low .mono { color: var(--_eid-warning-fg); }
559
571
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; }
560
572
  .spinner { animation: none; border-right-color: currentColor; opacity: 0.6; } }
561
573
  `;
@@ -578,6 +590,24 @@ function el(tag, attrs = {}, text) {
578
590
  if (text != null) e.textContent = text;
579
591
  return e;
580
592
  }
593
+ function chevron() {
594
+ const NS = "http://www.w3.org/2000/svg";
595
+ const svg = document.createElementNS(NS, "svg");
596
+ for (const [k, v] of Object.entries({
597
+ class: "chev",
598
+ viewBox: "0 0 24 24",
599
+ fill: "none",
600
+ stroke: "currentColor",
601
+ "stroke-width": "2",
602
+ "stroke-linecap": "round",
603
+ "stroke-linejoin": "round",
604
+ "aria-hidden": "true"
605
+ })) svg.setAttribute(k, v);
606
+ const path = document.createElementNS(NS, "path");
607
+ path.setAttribute("d", "m6 9 6 6 6-6");
608
+ svg.append(path);
609
+ return svg;
610
+ }
581
611
  var Base = typeof HTMLElement === "undefined" ? class {
582
612
  } : HTMLElement;
583
613
  var EidLoginButton = class extends Base {
@@ -710,7 +740,7 @@ var EidLoginButton = class extends Base {
710
740
  return;
711
741
  }
712
742
  if (e.key !== "Tab" || !this.backdrop) return;
713
- const items = Array.from(this.backdrop.querySelectorAll(FOCUSABLE)).filter((n) => !n.closest("[hidden]"));
743
+ const items = Array.from(this.backdrop.querySelectorAll(FOCUSABLE)).filter((n) => !n.closest("[hidden], [inert]"));
714
744
  if (items.length === 0) return;
715
745
  const first = items[0];
716
746
  const last = items[items.length - 1];
@@ -792,21 +822,22 @@ var EidLoginButton = class extends Base {
792
822
  if (s.qr) this.drawQr(s.qr);
793
823
  const nodes = [this.qrBox];
794
824
  if (s.vc) {
795
- const vc = el("p", { class: "qr-vc" }, t.code + " ");
796
- vc.append(el("span", { class: "mono" }, s.vc));
825
+ const vc = el("div", { class: "qr-vc" });
826
+ vc.append(el("p", { class: "vc-label", id: "eid-vc-label" }, t.vcLabel), el("p", { class: "vc" }, s.vc));
797
827
  nodes.push(vc);
798
828
  }
829
+ nodes.push(this.meta());
799
830
  const steps = el("ol", { class: "steps" });
800
831
  for (const st of t.steps) steps.append(el("li", {}, st));
801
- nodes.push(steps, el("div", { class: "or", role: "separator", "aria-label": t.or }, t.or), this.pushForm());
832
+ nodes.push(steps, el("div", { class: "or", role: "separator", "aria-label": t.or }, t.or), ...this.pushToggle());
802
833
  this.netwarn = el("p", { class: "netwarn", role: "status", hidden: "" }, t.offline);
803
- nodes.push(this.netwarn, this.meta());
834
+ nodes.push(this.netwarn);
804
835
  this.view(...nodes);
805
836
  }
806
837
  leftEl = null;
807
838
  meta() {
808
839
  const p = el("p", { class: "meta" }, this.t.left + " ");
809
- this.leftEl = el("span", { class: "mono", role: "timer", "aria-live": "off" }, "--:--");
840
+ this.leftEl = el("span", { class: "mono", role: "timer", "aria-live": "off" }, "-:--");
810
841
  p.append(this.leftEl);
811
842
  if (this.left < 0) p.hidden = true;
812
843
  return p;
@@ -819,8 +850,9 @@ var EidLoginButton = class extends Base {
819
850
  this.leftEl.parentElement.hidden = false;
820
851
  const m = Math.floor(this.left / 60);
821
852
  const s = this.left % 60;
822
- this.leftEl.textContent = `${m < 10 ? "0" : ""}${m}:${s < 10 ? "0" : ""}${s}`;
823
- if (this.left === 60) this.say(this.t.minute);
853
+ this.leftEl.textContent = `${m}:${s < 10 ? "0" : ""}${s}`;
854
+ this.leftEl.parentElement.classList.toggle("low", this.left <= 30);
855
+ if (this.left > 0 && s === 0 && this.left !== seconds) this.say(this.t.minutes(m));
824
856
  };
825
857
  render();
826
858
  if (seconds < 0) return;
@@ -830,6 +862,24 @@ var EidLoginButton = class extends Base {
830
862
  if (this.left <= 0) this.end("expired");
831
863
  }, 1e3);
832
864
  }
865
+ /** «Регистрийн дугаараар нэвтрэх» товч + ижил цонхонд доошоо дэлгэгдэх push хэсэг (QR, код харагдсаар; QR poll үргэлжилнэ). */
866
+ pushToggle() {
867
+ const toggle = el("button", { type: "button", class: "btn btn-outline disc-btn", "aria-expanded": "false", "aria-controls": "eid-push" });
868
+ toggle.append(el("span", {}, this.t.usePush), chevron());
869
+ const disc = el("div", { class: "disc", id: "eid-push" });
870
+ const inner = el("div", { inert: "" });
871
+ const form = this.pushForm();
872
+ inner.append(form);
873
+ disc.append(inner);
874
+ toggle.addEventListener("click", () => {
875
+ const open = toggle.getAttribute("aria-expanded") !== "true";
876
+ toggle.setAttribute("aria-expanded", String(open));
877
+ disc.classList.toggle("open", open);
878
+ inner.toggleAttribute("inert", !open);
879
+ if (open) form.querySelector("input")?.focus();
880
+ });
881
+ return [toggle, disc];
882
+ }
833
883
  pushForm() {
834
884
  const t = this.t;
835
885
  const form = el("form", { novalidate: "" });
@@ -898,14 +948,14 @@ var EidLoginButton = class extends Base {
898
948
  showWait(s) {
899
949
  const t = this.t;
900
950
  const c = el("div", { class: "center" });
901
- c.append(el("h2", { class: "sr-only" }, t.waiting));
902
- const vc = el("p", { class: "vc", tabindex: "-1", "aria-describedby": "eid-wait" }, s.vc ?? "");
951
+ c.append(el("h2", { class: "sr-only" }, t.waiting), el("p", { class: "vc-label", id: "eid-vc-label" }, t.vcLabel));
952
+ const vc = el("p", { class: "vc", tabindex: "-1", "aria-labelledby": "eid-vc-label", "aria-describedby": "eid-wait" }, s.vc ?? "");
903
953
  const pending = el("p", { class: "pending" });
904
954
  pending.append(el("span", { class: "spinner", "aria-hidden": "true" }), el("span", {}, t.waiting));
905
955
  const back = el("button", { type: "button", class: "btn btn-outline" }, t.back);
906
956
  back.addEventListener("click", () => this.open());
907
957
  this.netwarn = el("p", { class: "netwarn", role: "status", hidden: "" }, t.offline);
908
- c.append(vc, el("p", { class: "text", id: "eid-wait" }, t.waitText), pending, back, this.netwarn, this.meta());
958
+ c.append(vc, this.meta(), el("p", { class: "text", id: "eid-wait" }, t.waitText), pending, back, this.netwarn);
909
959
  this.view(c);
910
960
  this.startTimer(s.expiresIn ?? -1);
911
961
  vc.focus();
package/dist/button.d.cts CHANGED
@@ -78,6 +78,8 @@ declare class EidLoginButton extends Base {
78
78
  private leftEl;
79
79
  private meta;
80
80
  private startTimer;
81
+ /** «Регистрийн дугаараар нэвтрэх» товч + ижил цонхонд доошоо дэлгэгдэх push хэсэг (QR, код харагдсаар; QR poll үргэлжилнэ). */
82
+ private pushToggle;
81
83
  private pushForm;
82
84
  private showWait;
83
85
  private schedule;
package/dist/button.d.ts CHANGED
@@ -78,6 +78,8 @@ declare class EidLoginButton extends Base {
78
78
  private leftEl;
79
79
  private meta;
80
80
  private startTimer;
81
+ /** «Регистрийн дугаараар нэвтрэх» товч + ижил цонхонд доошоо дэлгэгдэх push хэсэг (QR, код харагдсаар; QR poll үргэлжилнэ). */
82
+ private pushToggle;
81
83
  private pushForm;
82
84
  private showWait;
83
85
  private schedule;
package/dist/button.js CHANGED
@@ -368,8 +368,9 @@ var T = {
368
368
  loading: "\u0410\u0447\u0430\u0430\u043B\u0436 \u0431\u0430\u0439\u043D\u0430",
369
369
  qrAlt: "\u041D\u044D\u0432\u0442\u0440\u044D\u0445 QR \u043A\u043E\u0434 \u2014 \u0443\u0442\u0441\u0430\u043D \u0434\u044D\u044D\u0440\u0445 eID Mongolia \u0430\u043F\u043F\u0430\u0430\u0440 \u0443\u043D\u0448\u0443\u0443\u043B\u043D\u0430",
370
370
  steps: ["\u0423\u0442\u0441\u0430\u043D \u0434\u044D\u044D\u0440\u0445 eID Mongolia \u0430\u043F\u043F\u044B\u0433 \u043D\u044D\u044D\u043D\u044D", "QR \u043A\u043E\u0434\u044B\u0433 \u0443\u043D\u0448\u0443\u0443\u043B\u043D\u0430", "PIN1 \u043A\u043E\u0434\u043E\u043E \u043E\u0440\u0443\u0443\u043B\u043D\u0430"],
371
- code: "\u0423\u0442\u0441\u0430\u043D \u0434\u044D\u044D\u0440\u0445 \u043A\u043E\u0434:",
371
+ vcLabel: "\u0411\u0430\u0442\u0430\u043B\u0433\u0430\u0430\u0436\u0443\u0443\u043B\u0430\u0445 \u043A\u043E\u0434",
372
372
  or: "\u044D\u0441\u0432\u044D\u043B",
373
+ usePush: "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0439\u043D \u0434\u0443\u0433\u0430\u0430\u0440\u0430\u0430\u0440 \u043D\u044D\u0432\u0442\u0440\u044D\u0445",
373
374
  idLabel: "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0439\u043D \u0434\u0443\u0433\u0430\u0430\u0440",
374
375
  idHint: "\u0423\u0442\u0441\u0430\u043D\u0434 \u0442\u0430\u043D\u044C \u043C\u044D\u0434\u044D\u0433\u0434\u044D\u043B \u043E\u0447\u043D\u043E. \u0418\u0440\u0433\u044D\u043D\u0438\u0439 12 \u043E\u0440\u043E\u043D\u0442\u043E\u0439 \u0434\u0443\u0433\u0430\u0430\u0440\u0430\u0430\u0440 \u0447 \u0431\u043E\u043B\u043D\u043E.",
375
376
  idEmpty: "\u0420\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0439\u043D \u0434\u0443\u0433\u0430\u0430\u0440 \u043E\u0440\u0443\u0443\u043B\u043D\u0430 \u0443\u0443",
@@ -382,7 +383,7 @@ var T = {
382
383
  waitText: "\u0423\u0442\u0441\u0430\u043D \u0434\u044D\u044D\u0440\u0445 \u043A\u043E\u0434 \u0438\u0436\u0438\u043B \u0431\u043E\u043B PIN1 \u043E\u0440\u0443\u0443\u043B\u043D\u0430 \u0443\u0443. \u0418\u0436\u0438\u043B \u0431\u0438\u0448 \u0431\u043E\u043B \u0442\u0430\u0442\u0433\u0430\u043B\u0437\u0430\u043D\u0430 \u0443\u0443.",
383
384
  waiting: "\u0425\u04AF\u043B\u044D\u044D\u0436 \u0431\u0430\u0439\u043D\u0430",
384
385
  back: "\u0411\u0443\u0446\u0430\u0445",
385
- left: "\u04AE\u043B\u0434\u0441\u044D\u043D \u0445\u0443\u0433\u0430\u0446\u0430\u0430",
386
+ left: "\u0425\u04AF\u0441\u044D\u043B\u0442\u0438\u0439\u043D \u0445\u0443\u0433\u0430\u0446\u0430\u0430:",
386
387
  stale: "QR \u043A\u043E\u0434 \u0445\u0443\u0443\u0447\u0438\u0440\u0441\u0430\u043D",
387
388
  refresh: "\u0428\u0438\u043D\u044D\u0447\u043B\u044D\u0445",
388
389
  offline: "\u0425\u043E\u043B\u0431\u043E\u043B\u0442 \u0442\u0430\u0441\u0430\u0440\u0441\u0430\u043D. \u0418\u043D\u0442\u0435\u0440\u043D\u044D\u0442\u044D\u044D \u0448\u0430\u043B\u0433\u0430\u043D\u0430 \u0443\u0443 \u2014 \u0434\u0430\u0445\u0438\u043D \u0445\u043E\u043B\u0431\u043E\u0433\u0434\u043E\u0445\u044B\u0433 \u043E\u0440\u043E\u043B\u0434\u043E\u0436 \u0431\u0430\u0439\u043D\u0430.",
@@ -396,7 +397,7 @@ var T = {
396
397
  retry: "\u0414\u0430\u0445\u0438\u043D \u043E\u0440\u043E\u043B\u0434\u043E\u0445",
397
398
  success: "\u0410\u043C\u0436\u0438\u043B\u0442\u0442\u0430\u0439 \u043D\u044D\u0432\u0442\u044D\u0440\u043B\u044D\u044D",
398
399
  sent: (vc) => `\u041C\u044D\u0434\u044D\u0433\u0434\u044D\u043B \u0438\u043B\u0433\u044D\u044D\u043B\u044D\u044D. \u0423\u0442\u0441\u0430\u043D \u0434\u044D\u044D\u0440\u0445 \u043A\u043E\u0434 ${vc.split("").join(" ")}`,
399
- minute: "1 \u043C\u0438\u043D\u0443\u0442 \u04AF\u043B\u0434\u043B\u044D\u044D"
400
+ minutes: (n) => `${n} \u043C\u0438\u043D\u0443\u0442 \u04AF\u043B\u0434\u043B\u044D\u044D`
400
401
  },
401
402
  en: {
402
403
  button: "Log in with eID",
@@ -405,8 +406,9 @@ var T = {
405
406
  loading: "Loading",
406
407
  qrAlt: "Login QR code \u2014 scan it with the eID Mongolia app on your phone",
407
408
  steps: ["Open the eID Mongolia app on your phone", "Scan the QR code", "Enter your PIN1"],
408
- code: "Code on your phone:",
409
+ vcLabel: "Verification code",
409
410
  or: "or",
411
+ usePush: "Log in with registration number",
410
412
  idLabel: "Registration number",
411
413
  idHint: "A notification will be sent to your phone. The 12-digit civil ID also works.",
412
414
  idEmpty: "Enter your registration number",
@@ -419,7 +421,7 @@ var T = {
419
421
  waitText: "If the code on your phone matches, enter PIN1. Otherwise decline.",
420
422
  waiting: "Waiting",
421
423
  back: "Back",
422
- left: "Time left",
424
+ left: "Request expires in:",
423
425
  stale: "QR code expired",
424
426
  refresh: "Refresh",
425
427
  offline: "Connection lost. Check your internet \u2014 reconnecting.",
@@ -433,7 +435,7 @@ var T = {
433
435
  retry: "Try again",
434
436
  success: "Logged in",
435
437
  sent: (vc) => `Notification sent. Code on your phone ${vc.split("").join(" ")}`,
436
- minute: "1 minute left"
438
+ minutes: (n) => `${n} minute${n === 1 ? "" : "s"} left`
437
439
  }
438
440
  };
439
441
  var EID_COLOR_TOKENS = [
@@ -499,7 +501,8 @@ h2, p, ol { margin: 0; }
499
501
  .qr.stale svg { opacity: 0.12; }
500
502
  .qr-stale { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px;
501
503
  color: hsl(222 47% 11%); font-weight: 500; }
502
- .qr-vc { margin-top: 8px; text-align: center; font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
504
+ .qr-vc { margin-top: 12px; text-align: center; }
505
+ .vc-label { font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
503
506
  .mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-variant-numeric: tabular-nums; letter-spacing: 0.08em; color: var(--_eid-fg-muted); }
504
507
  .steps { margin-top: 16px; padding: 0; list-style: none; counter-reset: st; display: grid; gap: 8px; }
505
508
  .steps li { counter-increment: st; display: flex; align-items: center; gap: 12px; }
@@ -507,6 +510,13 @@ h2, p, ol { margin: 0; }
507
510
  color: var(--_eid-accent-subtle-fg); display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 600; }
508
511
  .or { display: flex; align-items: center; gap: 12px; margin: 24px 0; color: var(--_eid-fg-subtle); font-size: 0.8125rem; }
509
512
  .or::before, .or::after { content: ""; flex: 1; height: 1px; background: var(--_eid-border); }
513
+ .disc-btn .chev { width: 16px; height: 16px; flex: none; transition: transform 200ms cubic-bezier(0,0,0.2,1); }
514
+ .disc-btn[aria-expanded="true"] .chev { transform: rotate(180deg); }
515
+ .disc { margin: 0 -4px; display: grid; grid-template-rows: 0fr; transition: grid-template-rows 200ms cubic-bezier(0,0,0.2,1); }
516
+ .disc.open { grid-template-rows: 1fr; }
517
+ .disc > div { min-height: 0; overflow: hidden; visibility: hidden; transition: visibility 0s linear 200ms; }
518
+ .disc.open > div { visibility: visible; transition-delay: 0s; }
519
+ .disc form { padding: 16px 4px 4px; }
510
520
  .field { display: grid; gap: 8px; }
511
521
  .field label { font-weight: 500; }
512
522
  .field input { width: 100%; height: 44px; padding: 0 12px; border: 1px solid var(--_eid-border-strong); border-radius: var(--_eid-radius);
@@ -517,7 +527,8 @@ h2, p, ol { margin: 0; }
517
527
  .err { font-size: 0.8125rem; color: var(--_eid-danger-fg); font-weight: 500; }
518
528
  .push { margin-top: 16px; }
519
529
  .center { text-align: center; }
520
- .vc { font: 600 2.25rem/2.5rem ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.16em; padding-left: 0.16em; }
530
+ .vc { font: 700 2rem/2.5rem ui-monospace, SFMono-Regular, Menlo, monospace; font-variant-numeric: tabular-nums; letter-spacing: 0.16em;
531
+ padding-left: 0.16em; color: var(--_eid-fg); }
521
532
  .vc:focus, h2:focus { outline: none; }
522
533
  .text { margin-top: 12px; color: var(--_eid-fg-muted); }
523
534
  .pending { margin-top: 16px; display: inline-flex; align-items: center; gap: 8px; color: var(--_eid-fg-muted); }
@@ -526,7 +537,8 @@ h2, p, ol { margin: 0; }
526
537
  @keyframes spin { to { transform: rotate(360deg); } }
527
538
  .netwarn { margin-top: 16px; padding: 8px 12px; border: 1px solid var(--_eid-warning-border); border-radius: var(--_eid-radius); background: var(--_eid-warning-bg);
528
539
  color: var(--_eid-warning-fg); font-size: 0.8125rem; }
529
- .meta { margin-top: 16px; font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
540
+ .meta { margin-top: 4px; text-align: center; font-size: 0.8125rem; color: var(--_eid-fg-subtle); }
541
+ .meta.low, .meta.low .mono { color: var(--_eid-warning-fg); }
530
542
  @media (prefers-reduced-motion: reduce) { *, *::before, *::after { transition: none !important; }
531
543
  .spinner { animation: none; border-right-color: currentColor; opacity: 0.6; } }
532
544
  `;
@@ -549,6 +561,24 @@ function el(tag, attrs = {}, text) {
549
561
  if (text != null) e.textContent = text;
550
562
  return e;
551
563
  }
564
+ function chevron() {
565
+ const NS = "http://www.w3.org/2000/svg";
566
+ const svg = document.createElementNS(NS, "svg");
567
+ for (const [k, v] of Object.entries({
568
+ class: "chev",
569
+ viewBox: "0 0 24 24",
570
+ fill: "none",
571
+ stroke: "currentColor",
572
+ "stroke-width": "2",
573
+ "stroke-linecap": "round",
574
+ "stroke-linejoin": "round",
575
+ "aria-hidden": "true"
576
+ })) svg.setAttribute(k, v);
577
+ const path = document.createElementNS(NS, "path");
578
+ path.setAttribute("d", "m6 9 6 6 6-6");
579
+ svg.append(path);
580
+ return svg;
581
+ }
552
582
  var Base = typeof HTMLElement === "undefined" ? class {
553
583
  } : HTMLElement;
554
584
  var EidLoginButton = class extends Base {
@@ -681,7 +711,7 @@ var EidLoginButton = class extends Base {
681
711
  return;
682
712
  }
683
713
  if (e.key !== "Tab" || !this.backdrop) return;
684
- const items = Array.from(this.backdrop.querySelectorAll(FOCUSABLE)).filter((n) => !n.closest("[hidden]"));
714
+ const items = Array.from(this.backdrop.querySelectorAll(FOCUSABLE)).filter((n) => !n.closest("[hidden], [inert]"));
685
715
  if (items.length === 0) return;
686
716
  const first = items[0];
687
717
  const last = items[items.length - 1];
@@ -763,21 +793,22 @@ var EidLoginButton = class extends Base {
763
793
  if (s.qr) this.drawQr(s.qr);
764
794
  const nodes = [this.qrBox];
765
795
  if (s.vc) {
766
- const vc = el("p", { class: "qr-vc" }, t.code + " ");
767
- vc.append(el("span", { class: "mono" }, s.vc));
796
+ const vc = el("div", { class: "qr-vc" });
797
+ vc.append(el("p", { class: "vc-label", id: "eid-vc-label" }, t.vcLabel), el("p", { class: "vc" }, s.vc));
768
798
  nodes.push(vc);
769
799
  }
800
+ nodes.push(this.meta());
770
801
  const steps = el("ol", { class: "steps" });
771
802
  for (const st of t.steps) steps.append(el("li", {}, st));
772
- nodes.push(steps, el("div", { class: "or", role: "separator", "aria-label": t.or }, t.or), this.pushForm());
803
+ nodes.push(steps, el("div", { class: "or", role: "separator", "aria-label": t.or }, t.or), ...this.pushToggle());
773
804
  this.netwarn = el("p", { class: "netwarn", role: "status", hidden: "" }, t.offline);
774
- nodes.push(this.netwarn, this.meta());
805
+ nodes.push(this.netwarn);
775
806
  this.view(...nodes);
776
807
  }
777
808
  leftEl = null;
778
809
  meta() {
779
810
  const p = el("p", { class: "meta" }, this.t.left + " ");
780
- this.leftEl = el("span", { class: "mono", role: "timer", "aria-live": "off" }, "--:--");
811
+ this.leftEl = el("span", { class: "mono", role: "timer", "aria-live": "off" }, "-:--");
781
812
  p.append(this.leftEl);
782
813
  if (this.left < 0) p.hidden = true;
783
814
  return p;
@@ -790,8 +821,9 @@ var EidLoginButton = class extends Base {
790
821
  this.leftEl.parentElement.hidden = false;
791
822
  const m = Math.floor(this.left / 60);
792
823
  const s = this.left % 60;
793
- this.leftEl.textContent = `${m < 10 ? "0" : ""}${m}:${s < 10 ? "0" : ""}${s}`;
794
- if (this.left === 60) this.say(this.t.minute);
824
+ this.leftEl.textContent = `${m}:${s < 10 ? "0" : ""}${s}`;
825
+ this.leftEl.parentElement.classList.toggle("low", this.left <= 30);
826
+ if (this.left > 0 && s === 0 && this.left !== seconds) this.say(this.t.minutes(m));
795
827
  };
796
828
  render();
797
829
  if (seconds < 0) return;
@@ -801,6 +833,24 @@ var EidLoginButton = class extends Base {
801
833
  if (this.left <= 0) this.end("expired");
802
834
  }, 1e3);
803
835
  }
836
+ /** «Регистрийн дугаараар нэвтрэх» товч + ижил цонхонд доошоо дэлгэгдэх push хэсэг (QR, код харагдсаар; QR poll үргэлжилнэ). */
837
+ pushToggle() {
838
+ const toggle = el("button", { type: "button", class: "btn btn-outline disc-btn", "aria-expanded": "false", "aria-controls": "eid-push" });
839
+ toggle.append(el("span", {}, this.t.usePush), chevron());
840
+ const disc = el("div", { class: "disc", id: "eid-push" });
841
+ const inner = el("div", { inert: "" });
842
+ const form = this.pushForm();
843
+ inner.append(form);
844
+ disc.append(inner);
845
+ toggle.addEventListener("click", () => {
846
+ const open = toggle.getAttribute("aria-expanded") !== "true";
847
+ toggle.setAttribute("aria-expanded", String(open));
848
+ disc.classList.toggle("open", open);
849
+ inner.toggleAttribute("inert", !open);
850
+ if (open) form.querySelector("input")?.focus();
851
+ });
852
+ return [toggle, disc];
853
+ }
804
854
  pushForm() {
805
855
  const t = this.t;
806
856
  const form = el("form", { novalidate: "" });
@@ -869,14 +919,14 @@ var EidLoginButton = class extends Base {
869
919
  showWait(s) {
870
920
  const t = this.t;
871
921
  const c = el("div", { class: "center" });
872
- c.append(el("h2", { class: "sr-only" }, t.waiting));
873
- const vc = el("p", { class: "vc", tabindex: "-1", "aria-describedby": "eid-wait" }, s.vc ?? "");
922
+ c.append(el("h2", { class: "sr-only" }, t.waiting), el("p", { class: "vc-label", id: "eid-vc-label" }, t.vcLabel));
923
+ const vc = el("p", { class: "vc", tabindex: "-1", "aria-labelledby": "eid-vc-label", "aria-describedby": "eid-wait" }, s.vc ?? "");
874
924
  const pending = el("p", { class: "pending" });
875
925
  pending.append(el("span", { class: "spinner", "aria-hidden": "true" }), el("span", {}, t.waiting));
876
926
  const back = el("button", { type: "button", class: "btn btn-outline" }, t.back);
877
927
  back.addEventListener("click", () => this.open());
878
928
  this.netwarn = el("p", { class: "netwarn", role: "status", hidden: "" }, t.offline);
879
- c.append(vc, el("p", { class: "text", id: "eid-wait" }, t.waitText), pending, back, this.netwarn, this.meta());
929
+ c.append(vc, this.meta(), el("p", { class: "text", id: "eid-wait" }, t.waitText), pending, back, this.netwarn);
880
930
  this.view(c);
881
931
  this.startTimer(s.expiresIn ?? -1);
882
932
  vc.focus();