@arcblock/did-connect-core 4.1.52 → 4.1.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ui/design-tokens.js +4 -4
- package/dist/ui/i18n.d.ts +3 -1
- package/dist/ui/i18n.d.ts.map +1 -1
- package/dist/ui/i18n.js +120 -32
- package/dist/ui/i18n.js.map +1 -1
- package/dist/ui/index.bundle.js +1051 -191
- package/dist/ui/index.d.ts +2 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/login-dialog.d.ts +26 -0
- package/dist/ui/login-dialog.d.ts.map +1 -0
- package/dist/ui/login-dialog.js +210 -0
- package/dist/ui/login-dialog.js.map +1 -0
- package/dist/ui/login-page.d.ts +77 -2
- package/dist/ui/login-page.d.ts.map +1 -1
- package/dist/ui/login-page.js +572 -146
- package/dist/ui/login-page.js.map +1 -1
- package/dist/ui/qr-renderer.d.ts.map +1 -1
- package/dist/ui/qr-renderer.js +19 -3
- package/dist/ui/qr-renderer.js.map +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/styles.js +113 -22
- package/dist/ui/styles.js.map +1 -1
- package/dist/ui/webauthn-browser.d.ts +1 -0
- package/dist/ui/webauthn-browser.d.ts.map +1 -1
- package/dist/ui/webauthn-browser.js +19 -9
- package/dist/ui/webauthn-browser.js.map +1 -1
- package/package.json +1 -1
package/dist/ui/index.bundle.js
CHANGED
|
@@ -100,7 +100,7 @@ var lightTokens = {
|
|
|
100
100
|
var DESIGN_TOKENS_CSS = (
|
|
101
101
|
/* css */
|
|
102
102
|
`
|
|
103
|
-
:root {
|
|
103
|
+
:root, :host {
|
|
104
104
|
/* Backgrounds \u2014 layered depth */
|
|
105
105
|
--bg-root: ${tokens.bg.root};
|
|
106
106
|
--bg-surface: ${tokens.bg.surface};
|
|
@@ -159,7 +159,7 @@ var DESIGN_TOKENS_CSS = (
|
|
|
159
159
|
var DESIGN_TOKENS_LIGHT_CSS = (
|
|
160
160
|
/* css */
|
|
161
161
|
`
|
|
162
|
-
[data-theme="light"] {
|
|
162
|
+
[data-theme="light"], :host([data-theme="light"]) {
|
|
163
163
|
--bg-root: ${lightTokens.bg.root};
|
|
164
164
|
--bg-surface: ${lightTokens.bg.surface};
|
|
165
165
|
--bg-card: ${lightTokens.bg.card};
|
|
@@ -190,8 +190,8 @@ var DESIGN_RESET_CSS = (
|
|
|
190
190
|
/* css */
|
|
191
191
|
`
|
|
192
192
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
193
|
-
html { color-scheme: dark; }
|
|
194
|
-
html[data-theme="light"] { color-scheme: light; }
|
|
193
|
+
html, :host { color-scheme: dark; }
|
|
194
|
+
html[data-theme="light"], :host([data-theme="light"]) { color-scheme: light; }
|
|
195
195
|
body {
|
|
196
196
|
font-family: ${tokens.font.family};
|
|
197
197
|
background: var(--bg-root);
|
|
@@ -378,18 +378,39 @@ if (typeof customElements !== "undefined" && !customElements.get("did-address"))
|
|
|
378
378
|
// dist/ui/i18n.js
|
|
379
379
|
var messages = {
|
|
380
380
|
en: {
|
|
381
|
-
|
|
382
|
-
|
|
381
|
+
emailExplanation: "We will send you a verification code. No password needed.",
|
|
382
|
+
otherMethods: "Other ways to sign in",
|
|
383
|
+
passkeyCreateAccount: "Create an account with Passkey",
|
|
384
|
+
lastUsed: "Last used",
|
|
385
|
+
passkeyExplanation: "Create a new account using your fingerprint, face, or device unlock. Your device will ask you to save a Passkey. Already have an account? Go back to sign in.",
|
|
386
|
+
continueWith: "Continue with {provider}",
|
|
387
|
+
oauthWaiting: "Continue in the {provider} window",
|
|
388
|
+
oauthIncomplete: "Sign-in is not complete. Reopen the window or choose another method.",
|
|
389
|
+
oauthReopen: "Reopen window",
|
|
390
|
+
oauthUnconfirmed: "We could not confirm sign-in. Reload this page to check your session before trying again.",
|
|
391
|
+
oauthCheckSession: "Reload and check sign-in",
|
|
392
|
+
oauthCancelled: "Stopped waiting for sign-in. If the provider window is still open, close it before retrying.",
|
|
393
|
+
oauthDenied: "Authorization was declined. Try again or choose another sign-in method.",
|
|
394
|
+
oauthBlocked: "Your browser blocked the sign-in window. Reopen it or continue on this page.",
|
|
395
|
+
oauthTimedOut: "Sign-in timed out. Reopen the window to start again.",
|
|
396
|
+
oauthFailed: "We could not complete sign-in. Try again or choose another method.",
|
|
397
|
+
oauthFullPage: "Continue on this page",
|
|
398
|
+
emailEdit: "Change email",
|
|
399
|
+
emailCooldown: "Resend in {seconds}s",
|
|
400
|
+
walletRetry: "Wallet did not open? Try again or scan the QR code.",
|
|
401
|
+
tryAgain: "Try again",
|
|
402
|
+
signIn: "Sign in or create an account",
|
|
403
|
+
signInTo: "Sign in or join {appName}",
|
|
383
404
|
passkeyButton: "Continue with Passkey",
|
|
384
405
|
passkeySignIn: "Sign in with Passkey",
|
|
385
406
|
passkeyCreate: "Create Passkey",
|
|
386
|
-
passkeyFirstTime: "First time? Create
|
|
407
|
+
passkeyFirstTime: "First time? Create an account",
|
|
387
408
|
passkeyBackToSignIn: "Back to sign in",
|
|
388
409
|
passkeyWaiting: "Waiting for passkey\u2026",
|
|
389
410
|
passkeyCreating: "Creating your passkey\u2026",
|
|
390
|
-
passkeyNoLocal: "
|
|
411
|
+
passkeyNoLocal: "Passkey verification was not completed. Retry, use another device, or choose another method.",
|
|
391
412
|
passkeyUseAnotherDevice: "Use another device",
|
|
392
|
-
passkeyCreateInstead: "
|
|
413
|
+
passkeyCreateInstead: "First time? Create an account",
|
|
393
414
|
didWalletButton: "Continue with DID Wallet",
|
|
394
415
|
didWalletButtonShort: "DID Wallet",
|
|
395
416
|
scanWithWallet: "Scan with your DID Wallet",
|
|
@@ -422,26 +443,47 @@ var messages = {
|
|
|
422
443
|
emailTitle: "Continue with Email",
|
|
423
444
|
emailSubmit: "Send Code",
|
|
424
445
|
emailSending: "Sending code...",
|
|
425
|
-
emailSent: "We sent
|
|
426
|
-
emailCodePlaceholder: "
|
|
446
|
+
emailSent: "We sent an 8-digit code to {email}",
|
|
447
|
+
emailCodePlaceholder: "8-digit code",
|
|
427
448
|
emailVerify: "Verify",
|
|
428
449
|
emailResend: "Resend code",
|
|
429
450
|
emailInvalid: "Please enter a valid email address.",
|
|
430
|
-
emailCodeInvalid: "Please enter the
|
|
451
|
+
emailCodeInvalid: "Please enter the 8-digit code."
|
|
431
452
|
},
|
|
432
453
|
zh: {
|
|
433
|
-
|
|
434
|
-
|
|
454
|
+
emailExplanation: "\u6211\u4EEC\u4F1A\u53D1\u9001\u9A8C\u8BC1\u7801\uFF0C\u65E0\u9700\u5BC6\u7801\u3002",
|
|
455
|
+
otherMethods: "\u5176\u4ED6\u767B\u5F55\u65B9\u5F0F",
|
|
456
|
+
passkeyCreateAccount: "\u4F7F\u7528 Passkey \u521B\u5EFA\u8D26\u6237",
|
|
457
|
+
lastUsed: "\u4E0A\u6B21\u4F7F\u7528",
|
|
458
|
+
passkeyExplanation: "\u4F7F\u7528\u6307\u7EB9\u3001\u9762\u5BB9\u6216\u8BBE\u5907\u89E3\u9501\u65B9\u5F0F\u521B\u5EFA\u65B0\u8D26\u6237\uFF0C\u65E0\u9700\u5BC6\u7801\u3002\u8BBE\u5907\u4F1A\u63D0\u793A\u4FDD\u5B58 Passkey\u3002\u5DF2\u6709\u8D26\u6237\uFF1F\u8BF7\u8FD4\u56DE\u767B\u5F55\u3002",
|
|
459
|
+
continueWith: "\u4F7F\u7528 {provider} \u7EE7\u7EED",
|
|
460
|
+
oauthWaiting: "\u8BF7\u5728 {provider} \u7A97\u53E3\u4E2D\u7EE7\u7EED",
|
|
461
|
+
oauthIncomplete: "\u767B\u5F55\u5C1A\u672A\u5B8C\u6210\u3002\u60A8\u53EF\u4EE5\u91CD\u65B0\u6253\u5F00\u7A97\u53E3\u6216\u9009\u62E9\u5176\u4ED6\u65B9\u5F0F\u3002",
|
|
462
|
+
oauthReopen: "\u91CD\u65B0\u6253\u5F00\u7A97\u53E3",
|
|
463
|
+
oauthUnconfirmed: "\u6682\u65F6\u65E0\u6CD5\u786E\u8BA4\u767B\u5F55\u7ED3\u679C\u3002\u8BF7\u5148\u91CD\u65B0\u52A0\u8F7D\u9875\u9762\u68C0\u67E5\u767B\u5F55\u72B6\u6001\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u91CD\u8BD5\u3002",
|
|
464
|
+
oauthCheckSession: "\u91CD\u65B0\u52A0\u8F7D\u5E76\u68C0\u67E5\u767B\u5F55\u72B6\u6001",
|
|
465
|
+
oauthCancelled: "\u5DF2\u505C\u6B62\u7B49\u5F85\u767B\u5F55\u3002\u82E5\u6388\u6743\u7A97\u53E3\u4ECD\u7136\u6253\u5F00\uFF0C\u8BF7\u5173\u95ED\u540E\u518D\u91CD\u8BD5\u3002",
|
|
466
|
+
oauthDenied: "\u60A8\u672A\u540C\u610F\u6388\u6743\u3002\u53EF\u4EE5\u91CD\u8BD5\u6216\u9009\u62E9\u5176\u4ED6\u767B\u5F55\u65B9\u5F0F\u3002",
|
|
467
|
+
oauthBlocked: "\u6D4F\u89C8\u5668\u62E6\u622A\u4E86\u767B\u5F55\u7A97\u53E3\u3002\u8BF7\u91CD\u65B0\u6253\u5F00\uFF0C\u6216\u5728\u5F53\u524D\u9875\u9762\u7EE7\u7EED\u3002",
|
|
468
|
+
oauthTimedOut: "\u767B\u5F55\u7B49\u5F85\u5DF2\u8D85\u65F6\u3002\u8BF7\u91CD\u65B0\u6253\u5F00\u7A97\u53E3\u518D\u8BD5\u3002",
|
|
469
|
+
oauthFailed: "\u672A\u80FD\u5B8C\u6210\u767B\u5F55\u3002\u8BF7\u91CD\u8BD5\u6216\u9009\u62E9\u5176\u4ED6\u65B9\u5F0F\u3002",
|
|
470
|
+
oauthFullPage: "\u5728\u5F53\u524D\u9875\u9762\u7EE7\u7EED",
|
|
471
|
+
emailEdit: "\u4FEE\u6539\u90AE\u7BB1",
|
|
472
|
+
emailCooldown: "{seconds} \u79D2\u540E\u91CD\u53D1",
|
|
473
|
+
walletRetry: "\u94B1\u5305\u672A\u6253\u5F00\uFF1F\u8BF7\u91CD\u8BD5\u6216\u626B\u63CF\u4E8C\u7EF4\u7801\u3002",
|
|
474
|
+
tryAgain: "\u91CD\u8BD5",
|
|
475
|
+
signIn: "\u767B\u5F55\u6216\u521B\u5EFA\u8D26\u6237",
|
|
476
|
+
signInTo: "\u767B\u5F55\u6216\u52A0\u5165 {appName}",
|
|
435
477
|
passkeyButton: "\u4F7F\u7528 Passkey \u7EE7\u7EED",
|
|
436
478
|
passkeySignIn: "\u4F7F\u7528 Passkey \u767B\u5F55",
|
|
437
479
|
passkeyCreate: "\u521B\u5EFA Passkey",
|
|
438
|
-
passkeyFirstTime: "\
|
|
480
|
+
passkeyFirstTime: "\u9996\u6B21\u4F7F\u7528\uFF1F\u521B\u5EFA\u8D26\u6237",
|
|
439
481
|
passkeyBackToSignIn: "\u8FD4\u56DE\u767B\u5F55",
|
|
440
482
|
passkeyWaiting: "\u6B63\u5728\u7B49\u5F85 Passkey\u2026",
|
|
441
483
|
passkeyCreating: "\u6B63\u5728\u521B\u5EFA Passkey\u2026",
|
|
442
|
-
passkeyNoLocal: "\
|
|
484
|
+
passkeyNoLocal: "\u8FD9\u6B21\u672A\u5B8C\u6210 Passkey \u9A8C\u8BC1\u3002\u8BF7\u91CD\u8BD5\u3001\u4F7F\u7528\u5176\u4ED6\u8BBE\u5907\uFF0C\u6216\u9009\u62E9\u5176\u4ED6\u767B\u5F55\u65B9\u5F0F\u3002",
|
|
443
485
|
passkeyUseAnotherDevice: "\u7528\u5176\u4ED6\u8BBE\u5907\u767B\u5F55",
|
|
444
|
-
passkeyCreateInstead: "\
|
|
486
|
+
passkeyCreateInstead: "\u9996\u6B21\u4F7F\u7528\uFF1F\u521B\u5EFA\u8D26\u6237",
|
|
445
487
|
didWalletButton: "\u4F7F\u7528 DID Wallet \u7EE7\u7EED",
|
|
446
488
|
didWalletButtonShort: "DID Wallet",
|
|
447
489
|
scanWithWallet: "\u8BF7\u4F7F\u7528 DID Wallet \u626B\u7801",
|
|
@@ -475,25 +517,46 @@ var messages = {
|
|
|
475
517
|
emailSubmit: "\u53D1\u9001\u9A8C\u8BC1\u7801",
|
|
476
518
|
emailSending: "\u6B63\u5728\u53D1\u9001\u9A8C\u8BC1\u7801...",
|
|
477
519
|
emailSent: "\u9A8C\u8BC1\u7801\u5DF2\u53D1\u9001\u81F3 {email}",
|
|
478
|
-
emailCodePlaceholder: "
|
|
520
|
+
emailCodePlaceholder: "8 \u4F4D\u9A8C\u8BC1\u7801",
|
|
479
521
|
emailVerify: "\u9A8C\u8BC1",
|
|
480
522
|
emailResend: "\u91CD\u65B0\u53D1\u9001",
|
|
481
523
|
emailInvalid: "\u8BF7\u8F93\u5165\u6709\u6548\u7684\u90AE\u7BB1\u5730\u5740\u3002",
|
|
482
|
-
emailCodeInvalid: "\u8BF7\u8F93\u5165
|
|
524
|
+
emailCodeInvalid: "\u8BF7\u8F93\u5165 8 \u4F4D\u9A8C\u8BC1\u7801\u3002"
|
|
483
525
|
},
|
|
484
526
|
"zh-TW": {
|
|
485
|
-
|
|
486
|
-
|
|
527
|
+
emailExplanation: "\u6211\u5011\u6703\u50B3\u9001\u9A57\u8B49\u78BC\uFF0C\u7121\u9700\u5BC6\u78BC\u3002",
|
|
528
|
+
otherMethods: "\u5176\u4ED6\u767B\u5165\u65B9\u5F0F",
|
|
529
|
+
passkeyCreateAccount: "\u4F7F\u7528 Passkey \u5EFA\u7ACB\u5E33\u6236",
|
|
530
|
+
lastUsed: "\u4E0A\u6B21\u4F7F\u7528",
|
|
531
|
+
passkeyExplanation: "\u4F7F\u7528\u6307\u7D0B\u3001\u9762\u5BB9\u6216\u88DD\u7F6E\u89E3\u9396\u65B9\u5F0F\u5EFA\u7ACB\u65B0\u5E33\u6236\uFF0C\u7121\u9700\u5BC6\u78BC\u3002\u88DD\u7F6E\u6703\u63D0\u793A\u5132\u5B58 Passkey\u3002\u5DF2\u6709\u5E33\u6236\uFF1F\u8ACB\u8FD4\u56DE\u767B\u5165\u3002",
|
|
532
|
+
continueWith: "\u4F7F\u7528 {provider} \u7E7C\u7E8C",
|
|
533
|
+
oauthWaiting: "\u8ACB\u5728 {provider} \u8996\u7A97\u4E2D\u7E7C\u7E8C",
|
|
534
|
+
oauthIncomplete: "\u767B\u5165\u5C1A\u672A\u5B8C\u6210\u3002\u60A8\u53EF\u4EE5\u91CD\u65B0\u958B\u555F\u8996\u7A97\u6216\u9078\u64C7\u5176\u4ED6\u65B9\u5F0F\u3002",
|
|
535
|
+
oauthReopen: "\u91CD\u65B0\u958B\u555F\u8996\u7A97",
|
|
536
|
+
oauthUnconfirmed: "\u66AB\u6642\u7121\u6CD5\u78BA\u8A8D\u767B\u5165\u7D50\u679C\u3002\u8ACB\u5148\u91CD\u65B0\u8F09\u5165\u9801\u9762\u6AA2\u67E5\u767B\u5165\u72C0\u614B\uFF0C\u518D\u6C7A\u5B9A\u662F\u5426\u91CD\u8A66\u3002",
|
|
537
|
+
oauthCheckSession: "\u91CD\u65B0\u8F09\u5165\u4E26\u6AA2\u67E5\u767B\u5165\u72C0\u614B",
|
|
538
|
+
oauthCancelled: "\u5DF2\u505C\u6B62\u7B49\u5F85\u767B\u5165\u3002\u82E5\u6388\u6B0A\u8996\u7A97\u4ECD\u7136\u958B\u555F\uFF0C\u8ACB\u95DC\u9589\u5F8C\u518D\u91CD\u8A66\u3002",
|
|
539
|
+
oauthDenied: "\u60A8\u672A\u540C\u610F\u6388\u6B0A\u3002\u53EF\u4EE5\u91CD\u8A66\u6216\u9078\u64C7\u5176\u4ED6\u767B\u5165\u65B9\u5F0F\u3002",
|
|
540
|
+
oauthBlocked: "\u700F\u89BD\u5668\u5C01\u9396\u4E86\u767B\u5165\u8996\u7A97\u3002\u8ACB\u91CD\u65B0\u958B\u555F\uFF0C\u6216\u5728\u76EE\u524D\u9801\u9762\u7E7C\u7E8C\u3002",
|
|
541
|
+
oauthTimedOut: "\u767B\u5165\u7B49\u5F85\u5DF2\u903E\u6642\u3002\u8ACB\u91CD\u65B0\u958B\u555F\u8996\u7A97\u518D\u8A66\u3002",
|
|
542
|
+
oauthFailed: "\u672A\u80FD\u5B8C\u6210\u767B\u5165\u3002\u8ACB\u91CD\u8A66\u6216\u9078\u64C7\u5176\u4ED6\u65B9\u5F0F\u3002",
|
|
543
|
+
oauthFullPage: "\u5728\u76EE\u524D\u9801\u9762\u7E7C\u7E8C",
|
|
544
|
+
emailEdit: "\u4FEE\u6539\u96FB\u5B50\u90F5\u4EF6",
|
|
545
|
+
emailCooldown: "{seconds} \u79D2\u5F8C\u91CD\u65B0\u50B3\u9001",
|
|
546
|
+
walletRetry: "\u9322\u5305\u672A\u958B\u555F\uFF1F\u8ACB\u91CD\u8A66\u6216\u6383\u78BC\u3002",
|
|
547
|
+
tryAgain: "\u91CD\u8A66",
|
|
548
|
+
signIn: "\u767B\u5165\u6216\u5EFA\u7ACB\u5E33\u6236",
|
|
549
|
+
signInTo: "\u767B\u5165\u6216\u52A0\u5165 {appName}",
|
|
487
550
|
passkeyButton: "\u4F7F\u7528 Passkey \u7E7C\u7E8C",
|
|
488
551
|
passkeySignIn: "\u4F7F\u7528 Passkey \u767B\u5165",
|
|
489
552
|
passkeyCreate: "\u5EFA\u7ACB Passkey",
|
|
490
|
-
passkeyFirstTime: "\u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1F\u5EFA\u7ACB
|
|
553
|
+
passkeyFirstTime: "\u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1F\u5EFA\u7ACB\u5E33\u6236",
|
|
491
554
|
passkeyBackToSignIn: "\u8FD4\u56DE\u767B\u5165",
|
|
492
555
|
passkeyWaiting: "\u6B63\u5728\u7B49\u5F85 Passkey\u2026",
|
|
493
556
|
passkeyCreating: "\u6B63\u5728\u5EFA\u7ACB Passkey\u2026",
|
|
494
|
-
passkeyNoLocal: "\
|
|
557
|
+
passkeyNoLocal: "\u9019\u6B21\u672A\u5B8C\u6210 Passkey \u9A57\u8B49\u3002\u8ACB\u91CD\u8A66\u3001\u4F7F\u7528\u5176\u4ED6\u88DD\u7F6E\uFF0C\u6216\u9078\u64C7\u5176\u4ED6\u767B\u5165\u65B9\u5F0F\u3002",
|
|
495
558
|
passkeyUseAnotherDevice: "\u7528\u5176\u4ED6\u88DD\u7F6E\u767B\u5165",
|
|
496
|
-
passkeyCreateInstead: "\
|
|
559
|
+
passkeyCreateInstead: "\u7B2C\u4E00\u6B21\u4F7F\u7528\uFF1F\u5EFA\u7ACB\u5E33\u6236",
|
|
497
560
|
didWalletButton: "\u4F7F\u7528 DID Wallet \u7E7C\u7E8C",
|
|
498
561
|
didWalletButtonShort: "DID Wallet",
|
|
499
562
|
scanWithWallet: "\u8ACB\u4F7F\u7528 DID Wallet \u6383\u78BC",
|
|
@@ -527,25 +590,46 @@ var messages = {
|
|
|
527
590
|
emailSubmit: "\u50B3\u9001\u9A57\u8B49\u78BC",
|
|
528
591
|
emailSending: "\u6B63\u5728\u50B3\u9001\u9A57\u8B49\u78BC...",
|
|
529
592
|
emailSent: "\u9A57\u8B49\u78BC\u5DF2\u50B3\u9001\u81F3 {email}",
|
|
530
|
-
emailCodePlaceholder: "
|
|
593
|
+
emailCodePlaceholder: "8 \u4F4D\u6578\u9A57\u8B49\u78BC",
|
|
531
594
|
emailVerify: "\u9A57\u8B49",
|
|
532
595
|
emailResend: "\u91CD\u65B0\u50B3\u9001",
|
|
533
596
|
emailInvalid: "\u8ACB\u8F38\u5165\u6709\u6548\u7684\u96FB\u5B50\u90F5\u4EF6\u5730\u5740\u3002",
|
|
534
|
-
emailCodeInvalid: "\u8ACB\u8F38\u5165
|
|
597
|
+
emailCodeInvalid: "\u8ACB\u8F38\u5165 8 \u4F4D\u6578\u9A57\u8B49\u78BC\u3002"
|
|
535
598
|
},
|
|
536
599
|
ja: {
|
|
537
|
-
|
|
538
|
-
|
|
600
|
+
emailExplanation: "\u78BA\u8A8D\u30B3\u30FC\u30C9\u3092\u9001\u4FE1\u3057\u307E\u3059\u3002\u30D1\u30B9\u30EF\u30FC\u30C9\u306F\u4E0D\u8981\u3067\u3059\u3002",
|
|
601
|
+
otherMethods: "\u4ED6\u306E\u30ED\u30B0\u30A4\u30F3\u65B9\u6CD5",
|
|
602
|
+
passkeyCreateAccount: "Passkey \u3067\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210",
|
|
603
|
+
lastUsed: "\u524D\u56DE\u4F7F\u7528",
|
|
604
|
+
passkeyExplanation: "\u6307\u7D0B\u3001\u9854\u8A8D\u8A3C\u3001\u307E\u305F\u306F\u30C7\u30D0\u30A4\u30B9\u306E\u30ED\u30C3\u30AF\u89E3\u9664\u3067\u65B0\u3057\u3044\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210\u3057\u307E\u3059\u3002\u30D1\u30B9\u30EF\u30FC\u30C9\u306F\u4E0D\u8981\u3067\u3059\u3002\u30C7\u30D0\u30A4\u30B9\u304C Passkey \u306E\u4FDD\u5B58\u3092\u6C42\u3081\u307E\u3059\u3002\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u304A\u6301\u3061\u3067\u3059\u304B\uFF1F\u30ED\u30B0\u30A4\u30F3\u306B\u623B\u3063\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
605
|
+
continueWith: "{provider} \u3067\u7D9A\u3051\u308B",
|
|
606
|
+
oauthWaiting: "{provider} \u306E\u30A6\u30A3\u30F3\u30C9\u30A6\u3067\u7D9A\u3051\u3066\u304F\u3060\u3055\u3044",
|
|
607
|
+
oauthIncomplete: "\u30ED\u30B0\u30A4\u30F3\u306F\u307E\u3060\u5B8C\u4E86\u3057\u3066\u3044\u307E\u305B\u3093\u3002\u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u958B\u304D\u76F4\u3059\u304B\u3001\u5225\u306E\u65B9\u6CD5\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044\u3002",
|
|
608
|
+
oauthReopen: "\u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u958B\u304D\u76F4\u3059",
|
|
609
|
+
oauthUnconfirmed: "\u30ED\u30B0\u30A4\u30F3\u7D50\u679C\u3092\u78BA\u8A8D\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u518D\u8A66\u884C\u306E\u524D\u306B\u30DA\u30FC\u30B8\u3092\u518D\u8AAD\u307F\u8FBC\u307F\u3057\u3066\u72B6\u614B\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
610
|
+
oauthCheckSession: "\u518D\u8AAD\u307F\u8FBC\u307F\u3057\u3066\u30ED\u30B0\u30A4\u30F3\u3092\u78BA\u8A8D",
|
|
611
|
+
oauthCancelled: "\u30ED\u30B0\u30A4\u30F3\u306E\u5F85\u6A5F\u3092\u7D42\u4E86\u3057\u307E\u3057\u305F\u3002\u8A8D\u8A3C\u30A6\u30A3\u30F3\u30C9\u30A6\u304C\u958B\u3044\u3066\u3044\u308B\u5834\u5408\u306F\u3001\u9589\u3058\u3066\u304B\u3089\u518D\u8A66\u884C\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
612
|
+
oauthDenied: "\u8A8D\u8A3C\u304C\u8A31\u53EF\u3055\u308C\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u518D\u8A66\u884C\u3059\u308B\u304B\u3001\u5225\u306E\u65B9\u6CD5\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044\u3002",
|
|
613
|
+
oauthBlocked: "\u30D6\u30E9\u30A6\u30B6\u304C\u30ED\u30B0\u30A4\u30F3\u753B\u9762\u3092\u30D6\u30ED\u30C3\u30AF\u3057\u307E\u3057\u305F\u3002\u958B\u304D\u76F4\u3059\u304B\u3001\u3053\u306E\u30DA\u30FC\u30B8\u3067\u7D9A\u3051\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
614
|
+
oauthTimedOut: "\u30ED\u30B0\u30A4\u30F3\u304C\u30BF\u30A4\u30E0\u30A2\u30A6\u30C8\u3057\u307E\u3057\u305F\u3002\u30A6\u30A3\u30F3\u30C9\u30A6\u3092\u958B\u304D\u76F4\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
615
|
+
oauthFailed: "\u30ED\u30B0\u30A4\u30F3\u3092\u5B8C\u4E86\u3067\u304D\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u518D\u8A66\u884C\u3059\u308B\u304B\u3001\u5225\u306E\u65B9\u6CD5\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044\u3002",
|
|
616
|
+
oauthFullPage: "\u3053\u306E\u30DA\u30FC\u30B8\u3067\u7D9A\u3051\u308B",
|
|
617
|
+
emailEdit: "\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5909\u66F4",
|
|
618
|
+
emailCooldown: "{seconds} \u79D2\u5F8C\u306B\u518D\u9001\u4FE1",
|
|
619
|
+
walletRetry: "\u30A6\u30A9\u30EC\u30C3\u30C8\u304C\u958B\u304B\u306A\u3044\u5834\u5408\u306F\u3001\u518D\u8A66\u884C\u3059\u308B\u304B QR \u30B3\u30FC\u30C9\u3092\u30B9\u30AD\u30E3\u30F3\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
620
|
+
tryAgain: "\u518D\u8A66\u884C",
|
|
621
|
+
signIn: "\u30ED\u30B0\u30A4\u30F3\u307E\u305F\u306F\u30A2\u30AB\u30A6\u30F3\u30C8\u4F5C\u6210",
|
|
622
|
+
signInTo: "{appName} \u306B\u30ED\u30B0\u30A4\u30F3\u307E\u305F\u306F\u53C2\u52A0",
|
|
539
623
|
passkeyButton: "Passkey \u3067\u7D9A\u3051\u308B",
|
|
540
624
|
passkeySignIn: "Passkey \u3067\u30ED\u30B0\u30A4\u30F3",
|
|
541
625
|
passkeyCreate: "Passkey \u3092\u4F5C\u6210",
|
|
542
|
-
passkeyFirstTime: "\u521D\u3081\u3066\u3067\u3059\u304B\
|
|
626
|
+
passkeyFirstTime: "\u521D\u3081\u3066\u3067\u3059\u304B\uFF1F\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210",
|
|
543
627
|
passkeyBackToSignIn: "\u30ED\u30B0\u30A4\u30F3\u306B\u623B\u308B",
|
|
544
628
|
passkeyWaiting: "Passkey \u3092\u5F85\u3063\u3066\u3044\u307E\u3059\u2026",
|
|
545
629
|
passkeyCreating: "Passkey \u3092\u4F5C\u6210\u3057\u3066\u3044\u307E\u3059\u2026",
|
|
546
|
-
passkeyNoLocal: "\
|
|
630
|
+
passkeyNoLocal: "\u4ECA\u56DE\u306F Passkey \u306E\u78BA\u8A8D\u304C\u5B8C\u4E86\u3057\u307E\u305B\u3093\u3067\u3057\u305F\u3002\u518D\u8A66\u884C\u3059\u308B\u304B\u3001\u5225\u306E\u30C7\u30D0\u30A4\u30B9\u3092\u4F7F\u3046\u304B\u3001\u5225\u306E\u65B9\u6CD5\u3092\u9078\u3093\u3067\u304F\u3060\u3055\u3044\u3002",
|
|
547
631
|
passkeyUseAnotherDevice: "\u5225\u306E\u30C7\u30D0\u30A4\u30B9\u3092\u4F7F\u7528",
|
|
548
|
-
passkeyCreateInstead: "\
|
|
632
|
+
passkeyCreateInstead: "\u521D\u3081\u3066\u3067\u3059\u304B\uFF1F\u30A2\u30AB\u30A6\u30F3\u30C8\u3092\u4F5C\u6210",
|
|
549
633
|
didWalletButton: "DID Wallet \u3067\u7D9A\u3051\u308B",
|
|
550
634
|
didWalletButtonShort: "DID Wallet",
|
|
551
635
|
scanWithWallet: "DID Wallet \u3067\u30B9\u30AD\u30E3\u30F3\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
@@ -578,12 +662,12 @@ var messages = {
|
|
|
578
662
|
emailTitle: "\u30E1\u30FC\u30EB\u3067\u7D9A\u3051\u308B",
|
|
579
663
|
emailSubmit: "\u30B3\u30FC\u30C9\u3092\u9001\u4FE1",
|
|
580
664
|
emailSending: "\u30B3\u30FC\u30C9\u3092\u9001\u4FE1\u3057\u3066\u3044\u307E\u3059...",
|
|
581
|
-
emailSent: "{email} \u306B
|
|
582
|
-
emailCodePlaceholder: "
|
|
665
|
+
emailSent: "{email} \u306B 8 \u6841\u306E\u30B3\u30FC\u30C9\u3092\u9001\u4FE1\u3057\u307E\u3057\u305F",
|
|
666
|
+
emailCodePlaceholder: "8 \u6841\u306E\u30B3\u30FC\u30C9",
|
|
583
667
|
emailVerify: "\u78BA\u8A8D",
|
|
584
668
|
emailResend: "\u30B3\u30FC\u30C9\u3092\u518D\u9001\u4FE1",
|
|
585
669
|
emailInvalid: "\u6709\u52B9\u306A\u30E1\u30FC\u30EB\u30A2\u30C9\u30EC\u30B9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
586
|
-
emailCodeInvalid: "
|
|
670
|
+
emailCodeInvalid: "8 \u6841\u306E\u30B3\u30FC\u30C9\u3092\u5165\u529B\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
587
671
|
}
|
|
588
672
|
};
|
|
589
673
|
var SUPPORTED_LOCALES = [
|
|
@@ -592,7 +676,7 @@ var SUPPORTED_LOCALES = [
|
|
|
592
676
|
];
|
|
593
677
|
function t(key, locale, params) {
|
|
594
678
|
const lang = locale && messages[locale] ? locale : "en";
|
|
595
|
-
let msg = messages[lang][key] ?? key;
|
|
679
|
+
let msg = messages[lang][key] ?? messages.en[key] ?? key;
|
|
596
680
|
if (params) {
|
|
597
681
|
for (const [k, v] of Object.entries(params)) {
|
|
598
682
|
msg = msg.replace(new RegExp(`\\{${k}\\}`, "g"), v);
|
|
@@ -2258,8 +2342,17 @@ var qrcode_default = qrcode;
|
|
|
2258
2342
|
var stringToBytes = qrcode.stringToBytes;
|
|
2259
2343
|
|
|
2260
2344
|
// dist/ui/qr-renderer.js
|
|
2345
|
+
var QR_PLATE_DARK = "#e8e8e8";
|
|
2346
|
+
var QR_PLATE_LIGHT = "#ffffff";
|
|
2347
|
+
function plateColor(container) {
|
|
2348
|
+
const root = container.getRootNode();
|
|
2349
|
+
const themeEl = root instanceof ShadowRoot ? root.host : document.documentElement;
|
|
2350
|
+
const theme = themeEl instanceof Element ? themeEl.getAttribute("data-theme") : null;
|
|
2351
|
+
return theme === "light" ? QR_PLATE_LIGHT : QR_PLATE_DARK;
|
|
2352
|
+
}
|
|
2261
2353
|
function renderQR(container, url, size = 200) {
|
|
2262
2354
|
clearQR(container);
|
|
2355
|
+
container.dataset.qrUrl = url;
|
|
2263
2356
|
const canvas = document.createElement("canvas");
|
|
2264
2357
|
const ctx = canvas.getContext?.("2d") ?? null;
|
|
2265
2358
|
if (!ctx) {
|
|
@@ -2283,7 +2376,7 @@ function renderQR(container, url, size = 200) {
|
|
|
2283
2376
|
canvas.style.width = `${size}px`;
|
|
2284
2377
|
canvas.style.height = `${size}px`;
|
|
2285
2378
|
canvas.style.imageRendering = "pixelated";
|
|
2286
|
-
const bgColor =
|
|
2379
|
+
const bgColor = plateColor(container);
|
|
2287
2380
|
ctx.fillStyle = bgColor;
|
|
2288
2381
|
ctx.fillRect(0, 0, actualSize, actualSize);
|
|
2289
2382
|
ctx.fillStyle = "#000000";
|
|
@@ -2295,12 +2388,18 @@ function renderQR(container, url, size = 200) {
|
|
|
2295
2388
|
}
|
|
2296
2389
|
}
|
|
2297
2390
|
const wrapper = document.createElement("div");
|
|
2298
|
-
wrapper.
|
|
2391
|
+
wrapper.className = "qr-plate";
|
|
2392
|
+
wrapper.style.display = "inline-block";
|
|
2393
|
+
wrapper.style.padding = "10px";
|
|
2394
|
+
wrapper.style.backgroundColor = bgColor;
|
|
2395
|
+
wrapper.style.borderRadius = "8px";
|
|
2396
|
+
wrapper.style.lineHeight = "0";
|
|
2299
2397
|
wrapper.appendChild(canvas);
|
|
2300
2398
|
container.appendChild(wrapper);
|
|
2301
2399
|
}
|
|
2302
2400
|
function clearQR(container) {
|
|
2303
2401
|
container.innerHTML = "";
|
|
2402
|
+
delete container.dataset.qrUrl;
|
|
2304
2403
|
}
|
|
2305
2404
|
|
|
2306
2405
|
// dist/ui/styles.js
|
|
@@ -2317,6 +2416,14 @@ body {
|
|
|
2317
2416
|
justify-content: center;
|
|
2318
2417
|
background-image: radial-gradient(ellipse at 50% 0%, rgba(108,71,255,0.08) 0%, transparent 60%);
|
|
2319
2418
|
}
|
|
2419
|
+
/* The body rule does not apply inside a shadow tree. The host page must
|
|
2420
|
+
give the LoginPage container a height for this centering to show. */
|
|
2421
|
+
:host {
|
|
2422
|
+
display: flex;
|
|
2423
|
+
align-items: center;
|
|
2424
|
+
justify-content: center;
|
|
2425
|
+
min-height: 100%;
|
|
2426
|
+
}
|
|
2320
2427
|
|
|
2321
2428
|
/* Card \u2014 fixed width prevents shrinking when views switch */
|
|
2322
2429
|
.card {
|
|
@@ -2614,19 +2721,25 @@ h1 {
|
|
|
2614
2721
|
/* Email section (methods view) */
|
|
2615
2722
|
.email-section { margin-top: 8px; }
|
|
2616
2723
|
|
|
2617
|
-
/* Back button
|
|
2724
|
+
/* Back button \u2014 same top row as Cancel and the locale/theme tools.
|
|
2725
|
+
A float sat inside the 48px content padding, so Back landed below the
|
|
2726
|
+
absolute locale pill. */
|
|
2618
2727
|
.back-btn {
|
|
2728
|
+
position: absolute;
|
|
2729
|
+
top: 16px;
|
|
2730
|
+
left: 40px;
|
|
2619
2731
|
display: inline-flex;
|
|
2620
2732
|
align-items: center;
|
|
2621
2733
|
gap: 4px;
|
|
2734
|
+
height: 28px;
|
|
2622
2735
|
background: none;
|
|
2623
2736
|
border: none;
|
|
2624
2737
|
color: var(--text-secondary);
|
|
2625
2738
|
font-size: 14px;
|
|
2739
|
+
line-height: 1;
|
|
2626
2740
|
cursor: pointer;
|
|
2627
2741
|
padding: 0;
|
|
2628
|
-
margin
|
|
2629
|
-
float: left;
|
|
2742
|
+
margin: 0;
|
|
2630
2743
|
}
|
|
2631
2744
|
.back-btn:hover { color: var(--text); }
|
|
2632
2745
|
.back-btn svg { width: 18px; height: 18px; }
|
|
@@ -2651,6 +2764,7 @@ h1 {
|
|
|
2651
2764
|
line-height: 1;
|
|
2652
2765
|
text-decoration: none;
|
|
2653
2766
|
cursor: pointer;
|
|
2767
|
+
height: 28px;
|
|
2654
2768
|
padding: 2px 4px 2px 0;
|
|
2655
2769
|
transition: color 0.15s ease;
|
|
2656
2770
|
}
|
|
@@ -2663,7 +2777,6 @@ h1 {
|
|
|
2663
2777
|
font-weight: 600;
|
|
2664
2778
|
color: var(--text-white);
|
|
2665
2779
|
margin-bottom: 8px;
|
|
2666
|
-
clear: both;
|
|
2667
2780
|
padding-top: 8px;
|
|
2668
2781
|
}
|
|
2669
2782
|
|
|
@@ -2837,23 +2950,32 @@ h1 {
|
|
|
2837
2950
|
[data-theme="light"] body {
|
|
2838
2951
|
background-image: radial-gradient(ellipse at 50% 0%, rgba(108,71,255,0.05) 0%, transparent 60%);
|
|
2839
2952
|
}
|
|
2840
|
-
[data-theme="light"] .card
|
|
2953
|
+
[data-theme="light"] .card,
|
|
2954
|
+
:host([data-theme="light"]) .card {
|
|
2841
2955
|
box-shadow: var(--shadow-lg), 0 0 80px -20px rgba(108,71,255,0.08);
|
|
2842
2956
|
}
|
|
2843
|
-
[data-theme="light"] .btn:hover
|
|
2957
|
+
[data-theme="light"] .btn:hover,
|
|
2958
|
+
:host([data-theme="light"]) .btn:hover {
|
|
2844
2959
|
box-shadow: 0 2px 8px rgba(108,71,255,0.15);
|
|
2845
2960
|
}
|
|
2846
2961
|
|
|
2847
|
-
/* Language
|
|
2848
|
-
.
|
|
2962
|
+
/* Language sits on the same top row as Back / Cancel. */
|
|
2963
|
+
.card-tools {
|
|
2849
2964
|
position: absolute;
|
|
2850
2965
|
top: 16px;
|
|
2851
2966
|
right: 16px;
|
|
2967
|
+
z-index: 2;
|
|
2968
|
+
display: flex;
|
|
2969
|
+
align-items: flex-start;
|
|
2970
|
+
}
|
|
2971
|
+
.locale-switcher {
|
|
2972
|
+
position: relative;
|
|
2852
2973
|
}
|
|
2853
2974
|
.locale-btn {
|
|
2854
2975
|
display: inline-flex;
|
|
2855
2976
|
align-items: center;
|
|
2856
2977
|
gap: 4px;
|
|
2978
|
+
height: 28px;
|
|
2857
2979
|
padding: 4px 10px;
|
|
2858
2980
|
font-size: 12px;
|
|
2859
2981
|
color: var(--text-secondary);
|
|
@@ -2872,6 +2994,7 @@ h1 {
|
|
|
2872
2994
|
display: inline-flex;
|
|
2873
2995
|
align-items: center;
|
|
2874
2996
|
gap: 4px;
|
|
2997
|
+
height: 28px;
|
|
2875
2998
|
padding: 4px 10px;
|
|
2876
2999
|
font-size: 12px;
|
|
2877
3000
|
color: var(--text-secondary);
|
|
@@ -2885,22 +3008,89 @@ h1 {
|
|
|
2885
3008
|
border-color: var(--border-strong);
|
|
2886
3009
|
}
|
|
2887
3010
|
.locale-trigger svg { width: 14px; height: 14px; flex-shrink: 0; }
|
|
3011
|
+
/* One compact menu surface; rows are choices, not nested pill buttons. */
|
|
2888
3012
|
.locale-menu {
|
|
2889
|
-
display: none;
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
margin-top: 4px;
|
|
3013
|
+
display: none; position: absolute; top: calc(100% + 6px); right: 0; z-index: 4;
|
|
3014
|
+
min-width: 144px; width: max-content; max-width: min(240px,calc(100vw - 64px));
|
|
3015
|
+
padding: 4px; background: var(--bg-card); border: 1px solid var(--border);
|
|
3016
|
+
border-radius: 10px; box-shadow: var(--shadow-lg);
|
|
2894
3017
|
}
|
|
2895
|
-
.locale-switcher.open .locale-menu {
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
border-color: var(--border-strong);
|
|
3018
|
+
.locale-switcher.open .locale-menu { display: flex; flex-direction: column; }
|
|
3019
|
+
.locale-menu .locale-btn {
|
|
3020
|
+
width: 100%; min-height: 36px; height: auto; padding: 7px 10px; gap: 8px;
|
|
3021
|
+
border: 0; border-radius: 6px; background: transparent; text-align: left;
|
|
3022
|
+
font-size: 13px; line-height: 1.4; overflow-wrap: anywhere;
|
|
2901
3023
|
}
|
|
2902
|
-
.locale-
|
|
3024
|
+
.locale-menu .locale-btn:hover, .locale-menu .locale-btn:focus-visible { background: var(--bg-hover); color: var(--text); }
|
|
3025
|
+
.locale-menu .locale-btn[aria-checked="true"] { color: var(--text); font-weight: 600; }
|
|
3026
|
+
.locale-check { width: 14px; flex: 0 0 14px; font-size: 12px; line-height: 1; }
|
|
3027
|
+
@media (pointer: coarse) { .locale-menu .locale-btn { min-height: 44px; } }
|
|
3028
|
+
.has-last-used { flex-wrap: wrap; }
|
|
3029
|
+
.last-used { display: inline-block; font-size: 11px; font-weight: 400; line-height: 1.4;
|
|
3030
|
+
padding: 2px 6px; border: 1px solid currentColor; border-radius: 4px; opacity: .72; white-space: nowrap; }
|
|
2903
3031
|
.card { position: relative; }
|
|
3032
|
+
|
|
3033
|
+
/* Long localized method labels remain readable on narrow screens. */
|
|
3034
|
+
.methods-grid { grid-template-columns: 1fr; }
|
|
3035
|
+
.method-btn { min-height: 48px; white-space: normal; }
|
|
3036
|
+
.passkey-alt { min-height: 44px; font-weight: 600; }
|
|
3037
|
+
.oauth-recovery { font-size: 14px; line-height: 1.5; margin: 12px 0; padding: 12px;
|
|
3038
|
+
border: 1px solid var(--border); border-radius: var(--radius); color: var(--text-secondary); text-align: left; }
|
|
3039
|
+
.oauth-recovery p { margin: 0 0 8px; overflow-wrap: anywhere; }
|
|
3040
|
+
.oauth-recovery-actions { display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: center; }
|
|
3041
|
+
.oauth-recovery-actions button, .oauth-recovery-actions a { font: inherit; font-size: 13px; min-height: 44px;
|
|
3042
|
+
display: inline-flex; align-items: center; padding: 4px 0; color: var(--text); border: 0; background: none; cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
|
|
3043
|
+
.oauth-recovery-actions[hidden], .oauth-recovery-actions [hidden] { display: none; }
|
|
3044
|
+
.oauth-recovery-actions :focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }
|
|
3045
|
+
|
|
3046
|
+
@media (max-width: 480px) { .card { padding: 24px 20px; width: 100%; box-sizing: border-box; } h1 { font-size: 24px; } }
|
|
3047
|
+
|
|
3048
|
+
.passkey-create-entry { display: flex; justify-content: center; align-items: center; width: 100%; box-sizing: border-box; border: 1px solid var(--border); border-radius: var(--radius); background: var(--bg-surface); color: var(--text); padding: 10px 14px; }
|
|
3049
|
+
.passkey-create-entry:hover { background: var(--bg-hover); text-decoration: none; }
|
|
3050
|
+
.status:empty { min-height: 0; padding: 0; margin: 0; }
|
|
3051
|
+
.passkey-section .subtitle { margin: 8px 0 16px; font-size: 14px; line-height: 1.5; }
|
|
3052
|
+
@media (max-width: 480px) { .card { padding: 20px 18px; } .brand-header { margin-bottom: 20px; } }
|
|
3053
|
+
|
|
3054
|
+
.card-modal .card-tools { left: 20px; right: 80px; top: 12px; }
|
|
3055
|
+
.card-modal .locale-switcher { min-width: 0; max-width: 100%; }
|
|
3056
|
+
.card-modal .locale-trigger { max-width: 100%; height: 36px; }
|
|
3057
|
+
.locale-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
3058
|
+
.card-modal .locale-menu { left: 0; right: auto; }
|
|
3059
|
+
|
|
3060
|
+
.card { padding: 52px 24px 24px; box-sizing: border-box; }
|
|
3061
|
+
.card-modal { width: 100%; }
|
|
3062
|
+
.card-modal { padding-top: 68px; border: 0; border-radius: 0; }
|
|
3063
|
+
/* The dialog frame owns the shadow in both palettes. */
|
|
3064
|
+
.card.card-modal, :host([data-theme="light"]) .card.card-modal { box-shadow: none; }
|
|
3065
|
+
/* A translateY entrance expands an auto scroller by 4px until it finishes. */
|
|
3066
|
+
.card-modal[data-view] { animation: none; }
|
|
3067
|
+
/* Sub-step navigation shares the close button's center line in both modes. */
|
|
3068
|
+
.back-btn { top: 8px; left: 24px; height: 44px; max-width: calc(100% - 160px); line-height: 1.4; text-align: left; }
|
|
3069
|
+
.back-btn:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; border-radius: 4px; }
|
|
3070
|
+
.card-modal .back-btn { position: absolute; top: 8px; left: 24px; margin: 0; }
|
|
3071
|
+
.card-modal:has(.back-btn) .card-tools { left: auto; right: 64px; max-width: calc(100% - 160px); }
|
|
3072
|
+
.passkey-creating > .home-link { display: none; }
|
|
3073
|
+
@media (max-width:480px) { .back-btn, .card-modal .back-btn { left: 18px; } }
|
|
3074
|
+
.card-modal h1 { display: block; overflow: visible; -webkit-line-clamp: unset; line-clamp: unset; line-height: 1.4; padding-block: 2px; }
|
|
3075
|
+
.card-modal h1[tabindex="-1"]:focus { outline: none; }
|
|
3076
|
+
.card-modal h1, .card-modal h2, .card-modal .subtitle, .card-modal .status { overflow-wrap: anywhere; }
|
|
3077
|
+
.card-modal .btn { height: auto; min-height: 44px; line-height: 1.4; padding-block: 10px; white-space: normal; }
|
|
3078
|
+
.card-modal button, .card-modal a { overflow-wrap: anywhere; }
|
|
3079
|
+
.social-first .divider { margin: 14px 0; }
|
|
3080
|
+
.social-first .app-logo { width: 32px; height: 32px; margin-bottom: 12px; }
|
|
3081
|
+
.social-first .login-footer { margin-top: 16px; padding-top: 12px; }
|
|
3082
|
+
.other-methods { margin-top: 8px; color: var(--text-secondary); font-size: 14px; }
|
|
3083
|
+
.other-methods summary { min-height: 44px; padding: 12px 0; cursor: pointer; }
|
|
3084
|
+
.other-methods summary:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
|
|
3085
|
+
@media (max-width:480px) { .card { padding: 52px 18px 24px; } }
|
|
3086
|
+
@media (max-width:480px) { .card-modal { padding-top: 68px; } .card-modal .card-tools { left: 18px; } }
|
|
3087
|
+
@media (prefers-reduced-motion: reduce) { .card[data-view], .qr-view { animation: none; } }
|
|
3088
|
+
|
|
3089
|
+
.passkey-creating > .methods-grid, .passkey-creating > .divider, .passkey-creating > .other-methods { display: none; }
|
|
3090
|
+
|
|
3091
|
+
.input-label { display: block; text-align: left; margin: 8px 0; color: var(--text); font-size: 14px; font-weight: 500; }
|
|
3092
|
+
.input-label[hidden], .email-help[hidden] { display: none; }
|
|
3093
|
+
.email-help { text-align: left; font-size: 13px; line-height: 1.5; color: var(--text-secondary); margin: 0 0 12px; }
|
|
2904
3094
|
`;
|
|
2905
3095
|
|
|
2906
3096
|
// dist/ui/webauthn-browser.js
|
|
@@ -2962,7 +3152,7 @@ async function runPasskeyAuth(options) {
|
|
|
2962
3152
|
const txtCreating = texts.creating || t("passkeyCreating", locale);
|
|
2963
3153
|
const txtVerifying = texts.verifying || t("verifying", locale);
|
|
2964
3154
|
async function fetchAuthChallenge() {
|
|
2965
|
-
const res = await fetch(`${prefix}/auth`, { method: "GET" });
|
|
3155
|
+
const res = await fetch(`${prefix}/auth`, { method: "GET", signal: options.signal });
|
|
2966
3156
|
if (!res.ok)
|
|
2967
3157
|
throw new Error(t("authFailed", locale));
|
|
2968
3158
|
return res.json();
|
|
@@ -2974,7 +3164,7 @@ async function runPasskeyAuth(options) {
|
|
|
2974
3164
|
if (invitationId)
|
|
2975
3165
|
params.push(`invitationId=${encodeURIComponent(invitationId)}`);
|
|
2976
3166
|
const qs = params.length ? `?${params.join("&")}` : "";
|
|
2977
|
-
const res = await fetch(`${prefix}/register${qs}`, { method: "GET" });
|
|
3167
|
+
const res = await fetch(`${prefix}/register${qs}`, { method: "GET", signal: options.signal });
|
|
2978
3168
|
if (!res.ok)
|
|
2979
3169
|
throw new Error(t("registrationFailed", locale));
|
|
2980
3170
|
return res.json();
|
|
@@ -2988,6 +3178,7 @@ async function runPasskeyAuth(options) {
|
|
|
2988
3178
|
async function verifyAuth(challengeId, credential) {
|
|
2989
3179
|
const res = await fetch(`${prefix}/auth`, {
|
|
2990
3180
|
method: "POST",
|
|
3181
|
+
signal: options.signal,
|
|
2991
3182
|
headers: { "Content-Type": "application/json" },
|
|
2992
3183
|
body: JSON.stringify({ challengeId, credential })
|
|
2993
3184
|
});
|
|
@@ -3000,6 +3191,7 @@ async function runPasskeyAuth(options) {
|
|
|
3000
3191
|
async function verifyRegister(challengeId, credential, name) {
|
|
3001
3192
|
const res = await fetch(`${prefix}/register`, {
|
|
3002
3193
|
method: "POST",
|
|
3194
|
+
signal: options.signal,
|
|
3003
3195
|
headers: { "Content-Type": "application/json" },
|
|
3004
3196
|
body: JSON.stringify({ challengeId, credential, name: name || void 0 })
|
|
3005
3197
|
});
|
|
@@ -3016,6 +3208,7 @@ async function runPasskeyAuth(options) {
|
|
|
3016
3208
|
const { challengeId } = challengeResult;
|
|
3017
3209
|
const authentication = challengeResult.authentication || challengeResult;
|
|
3018
3210
|
const authOpts = {
|
|
3211
|
+
signal: options.signal,
|
|
3019
3212
|
publicKey: {
|
|
3020
3213
|
...authentication,
|
|
3021
3214
|
challenge: b64urlDecode(authentication.challenge),
|
|
@@ -3033,6 +3226,10 @@ async function runPasskeyAuth(options) {
|
|
|
3033
3226
|
return { success: false, error: "no_passkey" };
|
|
3034
3227
|
}
|
|
3035
3228
|
}
|
|
3229
|
+
if (!raw && authOnly)
|
|
3230
|
+
return { success: false, error: "no_passkey" };
|
|
3231
|
+
if (options.signal?.aborted)
|
|
3232
|
+
return { success: false, error: "cancelled" };
|
|
3036
3233
|
if (raw) {
|
|
3037
3234
|
const credential = serializeAuthentication(raw);
|
|
3038
3235
|
onStatus(txtVerifying, false);
|
|
@@ -3046,15 +3243,16 @@ async function runPasskeyAuth(options) {
|
|
|
3046
3243
|
return { success: false, error: errMsg, code: errCode };
|
|
3047
3244
|
}
|
|
3048
3245
|
}
|
|
3049
|
-
const challengeData = await fetchRegisterChallenge(options.name || "");
|
|
3050
|
-
if (challengeData.registrationAllowed === false) {
|
|
3051
|
-
onStatus(t("regClosed", locale), true);
|
|
3052
|
-
return { success: false, error: "registration_closed" };
|
|
3053
|
-
}
|
|
3054
|
-
onStatus(txtCreating, false);
|
|
3055
3246
|
try {
|
|
3247
|
+
const challengeData = await fetchRegisterChallenge(options.name || "");
|
|
3248
|
+
if (challengeData.registrationAllowed === false) {
|
|
3249
|
+
onStatus(t("regClosed", locale), true);
|
|
3250
|
+
return { success: false, error: "registration_closed" };
|
|
3251
|
+
}
|
|
3252
|
+
onStatus(txtCreating, false);
|
|
3056
3253
|
const registration = challengeData.registration || challengeData;
|
|
3057
3254
|
const regOpts = {
|
|
3255
|
+
signal: options.signal,
|
|
3058
3256
|
publicKey: {
|
|
3059
3257
|
...registration,
|
|
3060
3258
|
challenge: b64urlDecode(registration.challenge),
|
|
@@ -3069,12 +3267,14 @@ async function runPasskeyAuth(options) {
|
|
|
3069
3267
|
}
|
|
3070
3268
|
};
|
|
3071
3269
|
const raw = await navigator.credentials.create(regOpts);
|
|
3270
|
+
if (!raw || options.signal?.aborted)
|
|
3271
|
+
return { success: false, error: t("passkeyNoLocal", locale) };
|
|
3072
3272
|
const credential = serializeRegistration(raw);
|
|
3073
3273
|
onStatus(txtVerifying, false);
|
|
3074
3274
|
await verifyRegister(challengeData.challengeId, credential, options.name || "");
|
|
3075
3275
|
return { success: true };
|
|
3076
3276
|
} catch (regErr) {
|
|
3077
|
-
const errMsg = regErr instanceof Error ? regErr.message : t("passkeySetupFailed", locale);
|
|
3277
|
+
const errMsg = regErr instanceof Error && (regErr.name === "NotAllowedError" || regErr.name === "AbortError") ? t("passkeyNoLocal", locale) : regErr instanceof Error ? regErr.message : t("passkeySetupFailed", locale);
|
|
3078
3278
|
const errCode = regErr instanceof Error ? regErr.code : void 0;
|
|
3079
3279
|
onStatus(errMsg, true);
|
|
3080
3280
|
return { success: false, error: errMsg, code: errCode };
|
|
@@ -3113,15 +3313,42 @@ var LoginPage = class {
|
|
|
3113
3313
|
tokenSession = null;
|
|
3114
3314
|
styleEl = null;
|
|
3115
3315
|
mounted = false;
|
|
3316
|
+
completed = false;
|
|
3317
|
+
generation = 0;
|
|
3318
|
+
activeMethod;
|
|
3319
|
+
activeProvider;
|
|
3320
|
+
oauthCleanup;
|
|
3321
|
+
emailTimer;
|
|
3322
|
+
emailDraft = "";
|
|
3323
|
+
abort;
|
|
3324
|
+
emit(name) {
|
|
3325
|
+
try {
|
|
3326
|
+
this.options.onEvent?.({ name, method: this.activeMethod, mode: this.options.mode ?? "page" });
|
|
3327
|
+
} catch {
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
select(method) {
|
|
3331
|
+
this.generation++;
|
|
3332
|
+
this.oauthCleanup?.();
|
|
3333
|
+
this.abort?.abort();
|
|
3334
|
+
this.abort = new AbortController();
|
|
3335
|
+
this.activeMethod = method;
|
|
3336
|
+
this.activeProvider = void 0;
|
|
3337
|
+
this.emit("method_selected");
|
|
3338
|
+
}
|
|
3116
3339
|
currentView = "methods";
|
|
3117
3340
|
cardEl = null;
|
|
3118
3341
|
mediaQuery = null;
|
|
3119
3342
|
mediaListener = null;
|
|
3120
3343
|
localeOutsideClick = null;
|
|
3344
|
+
/** Open shadow root when isolate is set. Reused across destroy/mount. */
|
|
3345
|
+
shadow = null;
|
|
3346
|
+
isolate;
|
|
3121
3347
|
constructor(container, config, http, options) {
|
|
3122
3348
|
this.container = container;
|
|
3123
3349
|
this.config = config;
|
|
3124
3350
|
this.http = http;
|
|
3351
|
+
this.isolate = options?.isolate === true;
|
|
3125
3352
|
this.options = {
|
|
3126
3353
|
methods: ["passkey", "did-connect"],
|
|
3127
3354
|
locale: "en",
|
|
@@ -3142,32 +3369,50 @@ var LoginPage = class {
|
|
|
3142
3369
|
if (this.mounted)
|
|
3143
3370
|
return;
|
|
3144
3371
|
this.mounted = true;
|
|
3372
|
+
this.completed = false;
|
|
3373
|
+
this.emit("view");
|
|
3145
3374
|
this.styleEl = document.createElement("style");
|
|
3146
3375
|
this.styleEl.textContent = LOGIN_PAGE_STYLES;
|
|
3147
|
-
this.
|
|
3376
|
+
this.mountRoot().appendChild(this.styleEl);
|
|
3377
|
+
this.adoptStoredTheme();
|
|
3148
3378
|
this.applyTheme();
|
|
3149
3379
|
const available = this.filterMethods(this.options.methods);
|
|
3150
3380
|
this.renderCard(available);
|
|
3151
3381
|
}
|
|
3152
3382
|
/** Destroy the login UI, clean up timers and listeners. Idempotent. */
|
|
3153
3383
|
destroy() {
|
|
3384
|
+
this.generation++;
|
|
3385
|
+
this.oauthCleanup?.();
|
|
3386
|
+
this.abort?.abort();
|
|
3387
|
+
clearInterval(this.emailTimer);
|
|
3154
3388
|
this.detachLocaleMenu();
|
|
3155
3389
|
if (this.tokenSession) {
|
|
3156
3390
|
this.tokenSession.destroy();
|
|
3157
3391
|
this.tokenSession = null;
|
|
3158
3392
|
}
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
this.
|
|
3162
|
-
|
|
3393
|
+
this.detachMedia();
|
|
3394
|
+
if (this.isolate) {
|
|
3395
|
+
const shadow = this.shadow ?? this.container.shadowRoot;
|
|
3396
|
+
if (shadow)
|
|
3397
|
+
shadow.innerHTML = "";
|
|
3398
|
+
this.container.removeAttribute("data-theme");
|
|
3399
|
+
} else {
|
|
3400
|
+
document.documentElement.removeAttribute("data-theme");
|
|
3401
|
+
this.container.innerHTML = "";
|
|
3163
3402
|
}
|
|
3164
|
-
document.documentElement.removeAttribute("data-theme");
|
|
3165
|
-
this.container.innerHTML = "";
|
|
3166
3403
|
this.styleEl = null;
|
|
3167
3404
|
this.cardEl = null;
|
|
3168
3405
|
this.currentView = "methods";
|
|
3169
3406
|
this.mounted = false;
|
|
3170
3407
|
}
|
|
3408
|
+
/**
|
|
3409
|
+
* Apply a light or dark theme without remounting.
|
|
3410
|
+
* The host writes `web-mode` itself. Isolate updates the shadow host only.
|
|
3411
|
+
*/
|
|
3412
|
+
setTheme(theme) {
|
|
3413
|
+
this.options.theme = theme;
|
|
3414
|
+
this.applyTheme();
|
|
3415
|
+
}
|
|
3171
3416
|
/** Update options and re-render. */
|
|
3172
3417
|
update(newOptions) {
|
|
3173
3418
|
Object.assign(this.options, newOptions);
|
|
@@ -3177,24 +3422,82 @@ var LoginPage = class {
|
|
|
3177
3422
|
}
|
|
3178
3423
|
}
|
|
3179
3424
|
// ─── Private: theme ────────────────────────────────────────────────
|
|
3425
|
+
/** Light DOM themes <html>. Isolate themes the shadow host only. */
|
|
3426
|
+
themeTarget() {
|
|
3427
|
+
return this.isolate ? this.container : document.documentElement;
|
|
3428
|
+
}
|
|
3429
|
+
/** Shadow root created once. attachShadow throws if called again. */
|
|
3430
|
+
mountRoot() {
|
|
3431
|
+
if (!this.isolate)
|
|
3432
|
+
return this.container;
|
|
3433
|
+
if (this.shadow)
|
|
3434
|
+
return this.shadow;
|
|
3435
|
+
this.shadow = this.container.shadowRoot ?? this.container.attachShadow({ mode: "open" });
|
|
3436
|
+
return this.shadow;
|
|
3437
|
+
}
|
|
3438
|
+
detachMedia() {
|
|
3439
|
+
if (this.mediaQuery && this.mediaListener) {
|
|
3440
|
+
this.mediaQuery.removeEventListener("change", this.mediaListener);
|
|
3441
|
+
}
|
|
3442
|
+
this.mediaQuery = null;
|
|
3443
|
+
this.mediaListener = null;
|
|
3444
|
+
}
|
|
3445
|
+
/**
|
|
3446
|
+
* Same order as theme/init.js: `?theme=` is a preview and wins;
|
|
3447
|
+
* otherwise `localStorage["web-mode"]`. Neither write happens here.
|
|
3448
|
+
*/
|
|
3449
|
+
adoptStoredTheme() {
|
|
3450
|
+
if (this.options.mode === "dialog")
|
|
3451
|
+
return;
|
|
3452
|
+
if (typeof window === "undefined")
|
|
3453
|
+
return;
|
|
3454
|
+
const urlTheme = new URLSearchParams(window.location.search).get("theme");
|
|
3455
|
+
if (urlTheme === "light" || urlTheme === "dark" || urlTheme === "auto") {
|
|
3456
|
+
this.options.theme = urlTheme;
|
|
3457
|
+
return;
|
|
3458
|
+
}
|
|
3459
|
+
try {
|
|
3460
|
+
const stored = localStorage.getItem("web-mode");
|
|
3461
|
+
if (stored === "light" || stored === "dark" || stored === "auto")
|
|
3462
|
+
this.options.theme = stored;
|
|
3463
|
+
} catch {
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
/** light sets data-theme. dark removes it. auto follows the OS. */
|
|
3180
3467
|
applyTheme() {
|
|
3468
|
+
this.detachMedia();
|
|
3181
3469
|
const theme = this.options.theme ?? "dark";
|
|
3470
|
+
const target = this.themeTarget();
|
|
3182
3471
|
if (theme === "light") {
|
|
3183
|
-
|
|
3472
|
+
target.dataset.theme = "light";
|
|
3184
3473
|
} else if (theme === "auto" && typeof window.matchMedia === "function") {
|
|
3185
3474
|
const mq = window.matchMedia("(prefers-color-scheme: light)");
|
|
3186
3475
|
this.mediaQuery = mq;
|
|
3187
3476
|
const apply = (light) => {
|
|
3188
|
-
if (light)
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3477
|
+
if (light)
|
|
3478
|
+
target.dataset.theme = "light";
|
|
3479
|
+
else
|
|
3480
|
+
target.removeAttribute("data-theme");
|
|
3481
|
+
this.repaintQRPlate();
|
|
3193
3482
|
};
|
|
3194
3483
|
apply(mq.matches);
|
|
3195
3484
|
this.mediaListener = (e) => apply(e.matches);
|
|
3196
3485
|
mq.addEventListener("change", this.mediaListener);
|
|
3486
|
+
return;
|
|
3487
|
+
} else {
|
|
3488
|
+
target.removeAttribute("data-theme");
|
|
3197
3489
|
}
|
|
3490
|
+
this.repaintQRPlate();
|
|
3491
|
+
}
|
|
3492
|
+
/** Redraw a visible QR so its plate matches the theme. Leaves a timeout overlay alone. */
|
|
3493
|
+
repaintQRPlate() {
|
|
3494
|
+
const qr = this.mountRoot().querySelector("#did-connect-qr");
|
|
3495
|
+
if (!(qr instanceof HTMLElement))
|
|
3496
|
+
return;
|
|
3497
|
+
const url = qr.dataset.qrUrl;
|
|
3498
|
+
if (!url || !qr.querySelector("canvas") || qr.querySelector(".qr-timeout-overlay"))
|
|
3499
|
+
return;
|
|
3500
|
+
renderQR(qr, url);
|
|
3198
3501
|
}
|
|
3199
3502
|
// ─── Private: rendering ──────────────────────────────────────────────
|
|
3200
3503
|
filterMethods(methods) {
|
|
@@ -3207,11 +3510,20 @@ var LoginPage = class {
|
|
|
3207
3510
|
}
|
|
3208
3511
|
renderCard(methods) {
|
|
3209
3512
|
const card = document.createElement("div");
|
|
3210
|
-
card.className = "card";
|
|
3513
|
+
card.className = this.options.mode === "dialog" ? "card card-modal" : "card";
|
|
3211
3514
|
this.cardEl = card;
|
|
3212
|
-
this.
|
|
3515
|
+
this.mountRoot().appendChild(card);
|
|
3213
3516
|
this.switchView("methods", methods);
|
|
3214
3517
|
}
|
|
3518
|
+
/** Locale sits on the card's top-right row, level with Back / Cancel. */
|
|
3519
|
+
renderCardTools(card) {
|
|
3520
|
+
if (!(this.options.showLocaleSelector ?? this.options.mode !== "dialog"))
|
|
3521
|
+
return;
|
|
3522
|
+
const tools = document.createElement("div");
|
|
3523
|
+
tools.className = "card-tools";
|
|
3524
|
+
this.renderLocaleSwitcher(tools);
|
|
3525
|
+
card.appendChild(tools);
|
|
3526
|
+
}
|
|
3215
3527
|
renderLocaleSwitcher(card) {
|
|
3216
3528
|
const passed = this.options.locales;
|
|
3217
3529
|
if (passed) {
|
|
@@ -3250,7 +3562,7 @@ var LoginPage = class {
|
|
|
3250
3562
|
trigger.className = "locale-trigger";
|
|
3251
3563
|
trigger.setAttribute("aria-haspopup", "menu");
|
|
3252
3564
|
trigger.setAttribute("aria-expanded", "false");
|
|
3253
|
-
trigger.innerHTML = `${ICON_GLOBE}
|
|
3565
|
+
trigger.innerHTML = `${ICON_GLOBE} <span class="locale-label">${escapeHtml(current.label)}</span>`;
|
|
3254
3566
|
trigger.title = current.label;
|
|
3255
3567
|
const menu = document.createElement("div");
|
|
3256
3568
|
menu.className = "locale-menu";
|
|
@@ -3264,6 +3576,8 @@ var LoginPage = class {
|
|
|
3264
3576
|
trigger.addEventListener("click", (event) => {
|
|
3265
3577
|
event.stopPropagation();
|
|
3266
3578
|
setOpen(menu.hidden);
|
|
3579
|
+
if (!menu.hidden)
|
|
3580
|
+
menu.querySelector('[aria-checked="true"]')?.focus();
|
|
3267
3581
|
});
|
|
3268
3582
|
for (const item of locales) {
|
|
3269
3583
|
const checked = item.code === current.code;
|
|
@@ -3273,12 +3587,13 @@ var LoginPage = class {
|
|
|
3273
3587
|
btn.setAttribute("role", "menuitemradio");
|
|
3274
3588
|
btn.setAttribute("aria-checked", checked ? "true" : "false");
|
|
3275
3589
|
btn.dataset.locale = item.code;
|
|
3590
|
+
btn.tabIndex = -1;
|
|
3276
3591
|
btn.title = item.label;
|
|
3277
|
-
|
|
3592
|
+
{
|
|
3278
3593
|
const mark = document.createElement("span");
|
|
3279
3594
|
mark.className = "locale-check";
|
|
3280
3595
|
mark.setAttribute("aria-hidden", "true");
|
|
3281
|
-
mark.textContent = "\u2713";
|
|
3596
|
+
mark.textContent = checked ? "\u2713" : "";
|
|
3282
3597
|
btn.appendChild(mark);
|
|
3283
3598
|
}
|
|
3284
3599
|
const label = document.createElement("span");
|
|
@@ -3290,9 +3605,31 @@ var LoginPage = class {
|
|
|
3290
3605
|
});
|
|
3291
3606
|
menu.appendChild(btn);
|
|
3292
3607
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3608
|
+
wrapper.addEventListener("keydown", (event) => {
|
|
3609
|
+
const items = [...menu.querySelectorAll(".locale-btn")];
|
|
3610
|
+
const active = this.shadow?.activeElement ?? document.activeElement;
|
|
3611
|
+
const index = items.indexOf(active);
|
|
3612
|
+
if (event.key === "Escape") {
|
|
3613
|
+
event.preventDefault();
|
|
3614
|
+
event.stopPropagation();
|
|
3615
|
+
setOpen(false);
|
|
3616
|
+
trigger.focus();
|
|
3617
|
+
} else if (event.key === "Tab")
|
|
3295
3618
|
setOpen(false);
|
|
3619
|
+
else if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
|
|
3620
|
+
event.preventDefault();
|
|
3621
|
+
setOpen(true);
|
|
3622
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : index < 0 ? event.key === "ArrowUp" ? items.length - 1 : 0 : (index + (event.key === "ArrowUp" ? -1 : 1) + items.length) % items.length;
|
|
3623
|
+
items[next]?.focus();
|
|
3624
|
+
}
|
|
3625
|
+
});
|
|
3626
|
+
const onOutsideClick = (event) => {
|
|
3627
|
+
const path = event.composedPath();
|
|
3628
|
+
if (path.includes(wrapper))
|
|
3629
|
+
return;
|
|
3630
|
+
if (this.isolate && path.includes(this.container))
|
|
3631
|
+
return;
|
|
3632
|
+
setOpen(false);
|
|
3296
3633
|
};
|
|
3297
3634
|
document.addEventListener("click", onOutsideClick);
|
|
3298
3635
|
this.localeOutsideClick = onOutsideClick;
|
|
@@ -3316,10 +3653,14 @@ var LoginPage = class {
|
|
|
3316
3653
|
this.tokenSession = null;
|
|
3317
3654
|
}
|
|
3318
3655
|
}
|
|
3656
|
+
this.generation++;
|
|
3657
|
+
this.oauthCleanup?.();
|
|
3658
|
+
this.abort?.abort();
|
|
3659
|
+
clearInterval(this.emailTimer);
|
|
3319
3660
|
this.currentView = view;
|
|
3320
3661
|
this.cardEl.innerHTML = "";
|
|
3321
3662
|
this.cardEl.dataset.view = view;
|
|
3322
|
-
this.
|
|
3663
|
+
this.renderCardTools(this.cardEl);
|
|
3323
3664
|
switch (view) {
|
|
3324
3665
|
case "methods":
|
|
3325
3666
|
this.renderMethodsView(this.cardEl, locale, methods ?? this.filterMethods(this.options.methods));
|
|
@@ -3332,8 +3673,24 @@ var LoginPage = class {
|
|
|
3332
3673
|
break;
|
|
3333
3674
|
}
|
|
3334
3675
|
}
|
|
3676
|
+
/** Keep recognition next to the action, without a second page subtitle. */
|
|
3677
|
+
markLastUsed(button, method, locale) {
|
|
3678
|
+
if (!this.options.preferenceKey)
|
|
3679
|
+
return;
|
|
3680
|
+
try {
|
|
3681
|
+
if (localStorage.getItem(`did-login-method:${this.options.preferenceKey}`) !== method)
|
|
3682
|
+
return;
|
|
3683
|
+
const hint = document.createElement("span");
|
|
3684
|
+
hint.className = "last-used";
|
|
3685
|
+
hint.textContent = t("lastUsed", locale);
|
|
3686
|
+
button.classList.add("has-last-used");
|
|
3687
|
+
button.appendChild(hint);
|
|
3688
|
+
} catch {
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3335
3691
|
renderMethodsView(card, locale, methods) {
|
|
3336
|
-
this.
|
|
3692
|
+
if (this.options.mode !== "dialog")
|
|
3693
|
+
this.renderHomeLink(card, locale);
|
|
3337
3694
|
this.renderBrandHeader(card, locale);
|
|
3338
3695
|
const requestedPasskey = this.options.methods.includes("passkey");
|
|
3339
3696
|
if (requestedPasskey && !methods.includes("passkey")) {
|
|
@@ -3342,6 +3699,12 @@ var LoginPage = class {
|
|
|
3342
3699
|
warn.textContent = t("passkeyUnsupported", locale);
|
|
3343
3700
|
card.appendChild(warn);
|
|
3344
3701
|
}
|
|
3702
|
+
const socialFirst = this.options.preferredLayout !== "passkey-first" && methods.includes("oauth") && this.options.oauthProviders?.some((p) => p.enabled && ["google", "apple"].includes(p.id));
|
|
3703
|
+
card.classList.toggle("social-first", !!socialFirst);
|
|
3704
|
+
if (socialFirst) {
|
|
3705
|
+
this.renderSecondaryGrid(card, locale, ["oauth"], "primary");
|
|
3706
|
+
this.renderDivider(card, locale);
|
|
3707
|
+
}
|
|
3345
3708
|
const hasPrimary = methods.includes("passkey");
|
|
3346
3709
|
if (hasPrimary) {
|
|
3347
3710
|
this.renderPasskeySection(card, locale);
|
|
@@ -3351,7 +3714,20 @@ var LoginPage = class {
|
|
|
3351
3714
|
this.renderDivider(card, locale);
|
|
3352
3715
|
}
|
|
3353
3716
|
if (secondary.length > 0) {
|
|
3354
|
-
|
|
3717
|
+
if (socialFirst) {
|
|
3718
|
+
if (methods.includes("email"))
|
|
3719
|
+
this.renderSecondaryGrid(card, locale, ["email"]);
|
|
3720
|
+
const details = document.createElement("details");
|
|
3721
|
+
details.className = "other-methods";
|
|
3722
|
+
const summary = document.createElement("summary");
|
|
3723
|
+
summary.textContent = t("otherMethods", locale);
|
|
3724
|
+
details.appendChild(summary);
|
|
3725
|
+
this.renderSecondaryGrid(details, locale, methods.filter((m) => m !== "email"), "secondary");
|
|
3726
|
+
if (details.querySelector("button"))
|
|
3727
|
+
card.appendChild(details);
|
|
3728
|
+
} else {
|
|
3729
|
+
this.renderSecondaryGrid(card, locale, methods);
|
|
3730
|
+
}
|
|
3355
3731
|
}
|
|
3356
3732
|
if (methods.length === 0 && !requestedPasskey) {
|
|
3357
3733
|
const msg = document.createElement("p");
|
|
@@ -3378,10 +3754,14 @@ var LoginPage = class {
|
|
|
3378
3754
|
walletBtn.id = "did-wallet-open-btn";
|
|
3379
3755
|
walletBtn.textContent = t("openInWallet", locale);
|
|
3380
3756
|
walletBtn.disabled = true;
|
|
3381
|
-
|
|
3757
|
+
if (isMobile())
|
|
3758
|
+
card.insertBefore(walletBtn, qrContainer);
|
|
3759
|
+
else
|
|
3760
|
+
card.appendChild(walletBtn);
|
|
3382
3761
|
const statusEl = document.createElement("div");
|
|
3383
3762
|
statusEl.id = "did-connect-status";
|
|
3384
3763
|
statusEl.className = "status";
|
|
3764
|
+
statusEl.setAttribute("role", "status");
|
|
3385
3765
|
card.appendChild(statusEl);
|
|
3386
3766
|
this.renderTrustFooter(card, locale);
|
|
3387
3767
|
this.startDIDConnectInView(statusEl, qrContainer, walletBtn, locale);
|
|
@@ -3393,9 +3773,26 @@ var LoginPage = class {
|
|
|
3393
3773
|
title.className = "view-title";
|
|
3394
3774
|
title.textContent = t("emailTitle", locale);
|
|
3395
3775
|
card.appendChild(title);
|
|
3776
|
+
const emailLabel = document.createElement("label");
|
|
3777
|
+
emailLabel.className = "input-label";
|
|
3778
|
+
emailLabel.htmlFor = "login-email-address";
|
|
3779
|
+
emailLabel.textContent = t("emailPlaceholder", locale);
|
|
3780
|
+
card.appendChild(emailLabel);
|
|
3781
|
+
const emailHelp = document.createElement("p");
|
|
3782
|
+
emailHelp.className = "email-help";
|
|
3783
|
+
emailHelp.textContent = t("emailExplanation", locale);
|
|
3784
|
+
card.appendChild(emailHelp);
|
|
3396
3785
|
const emailInput = document.createElement("input");
|
|
3397
3786
|
emailInput.className = "input";
|
|
3787
|
+
emailInput.id = "login-email-address";
|
|
3398
3788
|
emailInput.type = "email";
|
|
3789
|
+
emailInput.required = true;
|
|
3790
|
+
emailInput.autocomplete = "email";
|
|
3791
|
+
emailInput.value = this.emailDraft;
|
|
3792
|
+
emailInput.setAttribute("aria-label", t("emailPlaceholder", locale));
|
|
3793
|
+
emailInput.addEventListener("input", () => {
|
|
3794
|
+
this.emailDraft = emailInput.value;
|
|
3795
|
+
});
|
|
3399
3796
|
emailInput.placeholder = t("emailPlaceholder", locale);
|
|
3400
3797
|
card.appendChild(emailInput);
|
|
3401
3798
|
const sendBtn = document.createElement("button");
|
|
@@ -3404,15 +3801,23 @@ var LoginPage = class {
|
|
|
3404
3801
|
card.appendChild(sendBtn);
|
|
3405
3802
|
const status = document.createElement("div");
|
|
3406
3803
|
status.className = "status";
|
|
3804
|
+
status.setAttribute("role", "status");
|
|
3407
3805
|
card.appendChild(status);
|
|
3408
3806
|
const codeSection = document.createElement("div");
|
|
3409
3807
|
codeSection.className = "email-code-section";
|
|
3410
3808
|
codeSection.style.display = "none";
|
|
3809
|
+
const codeLabel = document.createElement("label");
|
|
3810
|
+
codeLabel.className = "input-label";
|
|
3811
|
+
codeLabel.htmlFor = "login-email-code";
|
|
3812
|
+
codeLabel.textContent = t("emailCodePlaceholder", locale);
|
|
3813
|
+
codeSection.appendChild(codeLabel);
|
|
3411
3814
|
const codeInput = document.createElement("input");
|
|
3815
|
+
codeInput.id = "login-email-code";
|
|
3412
3816
|
codeInput.className = "input";
|
|
3413
3817
|
codeInput.type = "text";
|
|
3414
3818
|
codeInput.inputMode = "numeric";
|
|
3415
3819
|
codeInput.maxLength = 8;
|
|
3820
|
+
codeInput.setAttribute("aria-label", t("emailCodePlaceholder", locale));
|
|
3416
3821
|
codeInput.autocomplete = "one-time-code";
|
|
3417
3822
|
codeInput.placeholder = t("emailCodePlaceholder", locale);
|
|
3418
3823
|
codeSection.appendChild(codeInput);
|
|
@@ -3427,6 +3832,27 @@ var LoginPage = class {
|
|
|
3427
3832
|
codeSection.appendChild(resendBtn);
|
|
3428
3833
|
card.insertBefore(codeSection, status);
|
|
3429
3834
|
this.renderTrustFooter(card, locale);
|
|
3835
|
+
const epoch = this.generation;
|
|
3836
|
+
const isCurrent = () => this.mounted && epoch === this.generation;
|
|
3837
|
+
const editBtn = document.createElement("button");
|
|
3838
|
+
editBtn.className = "passkey-alt";
|
|
3839
|
+
editBtn.textContent = t("emailEdit", locale);
|
|
3840
|
+
codeSection.appendChild(editBtn);
|
|
3841
|
+
editBtn.addEventListener("click", () => this.switchView("email"));
|
|
3842
|
+
const cooldown = () => {
|
|
3843
|
+
let remaining = 60;
|
|
3844
|
+
clearInterval(this.emailTimer);
|
|
3845
|
+
resendBtn.disabled = true;
|
|
3846
|
+
resendBtn.textContent = t("emailCooldown", locale, { seconds: String(remaining) });
|
|
3847
|
+
this.emailTimer = setInterval(() => {
|
|
3848
|
+
remaining--;
|
|
3849
|
+
resendBtn.textContent = remaining > 0 ? t("emailCooldown", locale, { seconds: String(remaining) }) : t("emailResend", locale);
|
|
3850
|
+
if (remaining <= 0) {
|
|
3851
|
+
clearInterval(this.emailTimer);
|
|
3852
|
+
resendBtn.disabled = false;
|
|
3853
|
+
}
|
|
3854
|
+
}, 1e3);
|
|
3855
|
+
};
|
|
3430
3856
|
let currentEmail = "";
|
|
3431
3857
|
let currentCodeId;
|
|
3432
3858
|
const servicePrefix = this.config.servicePrefix ?? "/.well-known/service";
|
|
@@ -3434,7 +3860,7 @@ var LoginPage = class {
|
|
|
3434
3860
|
if (sendBtn.disabled)
|
|
3435
3861
|
return;
|
|
3436
3862
|
const email = emailInput.value.trim();
|
|
3437
|
-
if (!
|
|
3863
|
+
if (!emailInput.checkValidity()) {
|
|
3438
3864
|
status.textContent = t("emailInvalid", locale);
|
|
3439
3865
|
status.className = "status error";
|
|
3440
3866
|
return;
|
|
@@ -3442,11 +3868,17 @@ var LoginPage = class {
|
|
|
3442
3868
|
sendBtn.disabled = true;
|
|
3443
3869
|
status.textContent = t("emailSending", locale);
|
|
3444
3870
|
status.className = "status";
|
|
3871
|
+
status.setAttribute("role", "status");
|
|
3445
3872
|
try {
|
|
3446
3873
|
const sendResult = await this.http.post(`${servicePrefix}/api/email/sendCode`, { email, locale });
|
|
3874
|
+
if (!isCurrent())
|
|
3875
|
+
return;
|
|
3876
|
+
cooldown();
|
|
3447
3877
|
currentEmail = email;
|
|
3448
3878
|
currentCodeId = sendResult?.id;
|
|
3449
3879
|
emailInput.style.display = "none";
|
|
3880
|
+
emailLabel.hidden = true;
|
|
3881
|
+
emailHelp.hidden = true;
|
|
3450
3882
|
sendBtn.style.display = "none";
|
|
3451
3883
|
codeSection.style.display = "";
|
|
3452
3884
|
title.textContent = t("emailVerify", locale);
|
|
@@ -3454,6 +3886,8 @@ var LoginPage = class {
|
|
|
3454
3886
|
status.className = "status";
|
|
3455
3887
|
codeInput.focus();
|
|
3456
3888
|
} catch (err) {
|
|
3889
|
+
if (!isCurrent())
|
|
3890
|
+
return;
|
|
3457
3891
|
const msg = err instanceof Error ? err.message : "Failed to send email";
|
|
3458
3892
|
status.textContent = msg;
|
|
3459
3893
|
status.className = "status error";
|
|
@@ -3464,7 +3898,7 @@ var LoginPage = class {
|
|
|
3464
3898
|
if (verifyBtn.disabled)
|
|
3465
3899
|
return;
|
|
3466
3900
|
const code = codeInput.value.trim();
|
|
3467
|
-
if (
|
|
3901
|
+
if (!/^\d{8}$/.test(code)) {
|
|
3468
3902
|
status.textContent = t("emailCodeInvalid", locale);
|
|
3469
3903
|
status.className = "status error";
|
|
3470
3904
|
return;
|
|
@@ -3474,10 +3908,14 @@ var LoginPage = class {
|
|
|
3474
3908
|
status.className = "status";
|
|
3475
3909
|
try {
|
|
3476
3910
|
await this.http.post(`${servicePrefix}/api/email/login`, { code, id: currentCodeId });
|
|
3911
|
+
if (!isCurrent())
|
|
3912
|
+
return;
|
|
3477
3913
|
status.textContent = t("success", locale);
|
|
3478
3914
|
status.className = "status";
|
|
3479
3915
|
this.handleSuccess();
|
|
3480
3916
|
} catch (err) {
|
|
3917
|
+
if (!isCurrent())
|
|
3918
|
+
return;
|
|
3481
3919
|
if (err instanceof Error && "code" in err && err.code === "FORBIDDEN") {
|
|
3482
3920
|
this.showFullError(friendlyError(err, locale));
|
|
3483
3921
|
} else {
|
|
@@ -3500,27 +3938,35 @@ var LoginPage = class {
|
|
|
3500
3938
|
verifyCode();
|
|
3501
3939
|
});
|
|
3502
3940
|
resendBtn.addEventListener("click", async () => {
|
|
3941
|
+
if (resendBtn.disabled)
|
|
3942
|
+
return;
|
|
3503
3943
|
resendBtn.disabled = true;
|
|
3504
3944
|
status.textContent = t("emailSending", locale);
|
|
3505
3945
|
status.className = "status";
|
|
3506
3946
|
try {
|
|
3507
3947
|
const resendResult = await this.http.post(`${servicePrefix}/api/email/sendCode`, { email: currentEmail, locale });
|
|
3948
|
+
if (!isCurrent())
|
|
3949
|
+
return;
|
|
3950
|
+
cooldown();
|
|
3508
3951
|
currentCodeId = resendResult?.id;
|
|
3509
3952
|
status.textContent = t("emailSent", locale, { email: currentEmail });
|
|
3510
3953
|
status.className = "status";
|
|
3511
3954
|
} catch (err) {
|
|
3955
|
+
if (!isCurrent())
|
|
3956
|
+
return;
|
|
3957
|
+
resendBtn.disabled = false;
|
|
3512
3958
|
const msg = err instanceof Error ? err.message : "Failed to resend";
|
|
3513
3959
|
status.textContent = msg;
|
|
3514
3960
|
status.className = "status error";
|
|
3515
3961
|
}
|
|
3516
|
-
resendBtn.disabled = false;
|
|
3517
3962
|
});
|
|
3518
3963
|
}
|
|
3519
|
-
renderBackButton(parent, locale) {
|
|
3964
|
+
renderBackButton(parent, locale, onBack = () => this.switchView("methods")) {
|
|
3520
3965
|
const btn = document.createElement("button");
|
|
3966
|
+
btn.type = "button";
|
|
3521
3967
|
btn.className = "back-btn";
|
|
3522
3968
|
btn.innerHTML = `${ICON_BACK_ARROW} ${escapeHtml(t("back", locale))}`;
|
|
3523
|
-
btn.addEventListener("click",
|
|
3969
|
+
btn.addEventListener("click", onBack);
|
|
3524
3970
|
parent.appendChild(btn);
|
|
3525
3971
|
}
|
|
3526
3972
|
/**
|
|
@@ -3535,6 +3981,8 @@ var LoginPage = class {
|
|
|
3535
3981
|
* sub-view back buttons that mean "return to the method list".
|
|
3536
3982
|
*/
|
|
3537
3983
|
renderHomeLink(parent, locale) {
|
|
3984
|
+
if (this.options.dismissible === false)
|
|
3985
|
+
return;
|
|
3538
3986
|
const { homeUrl, onCancel } = this.options;
|
|
3539
3987
|
if (!onCancel && !homeUrl)
|
|
3540
3988
|
return;
|
|
@@ -3544,7 +3992,10 @@ var LoginPage = class {
|
|
|
3544
3992
|
btn.type = "button";
|
|
3545
3993
|
btn.className = "home-link";
|
|
3546
3994
|
btn.innerHTML = label;
|
|
3547
|
-
btn.addEventListener("click", () =>
|
|
3995
|
+
btn.addEventListener("click", () => {
|
|
3996
|
+
this.emit("cancel");
|
|
3997
|
+
this.options.onCancel?.();
|
|
3998
|
+
});
|
|
3548
3999
|
parent.appendChild(btn);
|
|
3549
4000
|
} else {
|
|
3550
4001
|
const link = document.createElement("a");
|
|
@@ -3572,7 +4023,16 @@ var LoginPage = class {
|
|
|
3572
4023
|
} else {
|
|
3573
4024
|
h1.textContent = t("signIn", locale);
|
|
3574
4025
|
}
|
|
4026
|
+
if (this.options.purpose)
|
|
4027
|
+
h1.textContent = this.options.purpose.title;
|
|
4028
|
+
h1.id = "login-title";
|
|
3575
4029
|
header.appendChild(h1);
|
|
4030
|
+
if (this.options.purpose?.description) {
|
|
4031
|
+
const description = document.createElement("p");
|
|
4032
|
+
description.className = "subtitle";
|
|
4033
|
+
description.textContent = this.options.purpose.description;
|
|
4034
|
+
header.appendChild(description);
|
|
4035
|
+
}
|
|
3576
4036
|
if (this.options.masterAppName) {
|
|
3577
4037
|
const sub = document.createElement("p");
|
|
3578
4038
|
sub.className = "federated-hint";
|
|
@@ -3613,6 +4073,12 @@ var LoginPage = class {
|
|
|
3613
4073
|
}
|
|
3614
4074
|
footer.appendChild(links);
|
|
3615
4075
|
}
|
|
4076
|
+
if (this.currentView !== "methods" && this.options.mode !== "dialog") {
|
|
4077
|
+
this.renderHomeLink(footer, locale);
|
|
4078
|
+
const cancel = footer.querySelector(".home-link");
|
|
4079
|
+
if (cancel)
|
|
4080
|
+
cancel.style.cssText = "position:static;display:inline-flex;margin-top:12px";
|
|
4081
|
+
}
|
|
3616
4082
|
parent.appendChild(footer);
|
|
3617
4083
|
}
|
|
3618
4084
|
renderDivider(parent, locale) {
|
|
@@ -3673,14 +4139,24 @@ var LoginPage = class {
|
|
|
3673
4139
|
retryBtn.addEventListener("click", () => doSignIn());
|
|
3674
4140
|
const createLink = document.createElement("button");
|
|
3675
4141
|
createLink.type = "button";
|
|
3676
|
-
createLink.className = "passkey-alt";
|
|
4142
|
+
createLink.className = "passkey-alt passkey-create-entry";
|
|
3677
4143
|
createLink.textContent = t("passkeyCreateInstead", locale);
|
|
3678
4144
|
section.appendChild(createLink);
|
|
3679
4145
|
createLink.addEventListener("click", () => renderCreateState());
|
|
3680
4146
|
};
|
|
3681
4147
|
const renderCreateState = () => {
|
|
4148
|
+
this.cardEl?.classList.add("passkey-creating");
|
|
4149
|
+
this.abort?.abort();
|
|
4150
|
+
this.generation++;
|
|
3682
4151
|
section.innerHTML = "";
|
|
3683
4152
|
clearStatus();
|
|
4153
|
+
const heading = this.cardEl?.querySelector("h1");
|
|
4154
|
+
if (heading)
|
|
4155
|
+
heading.textContent = t("passkeyCreateAccount", locale);
|
|
4156
|
+
const explanation = document.createElement("p");
|
|
4157
|
+
explanation.className = "subtitle";
|
|
4158
|
+
explanation.textContent = t("passkeyExplanation", locale);
|
|
4159
|
+
section.appendChild(explanation);
|
|
3684
4160
|
const nameRow = document.createElement("div");
|
|
3685
4161
|
nameRow.className = "name-row visible";
|
|
3686
4162
|
nameRow.id = "passkey-name-row";
|
|
@@ -3688,6 +4164,7 @@ var LoginPage = class {
|
|
|
3688
4164
|
nameInput.className = "input";
|
|
3689
4165
|
nameInput.id = "passkey-name-input";
|
|
3690
4166
|
nameInput.type = "text";
|
|
4167
|
+
nameInput.setAttribute("aria-label", t("namePlaceholder", locale));
|
|
3691
4168
|
nameInput.placeholder = t("namePlaceholder", locale);
|
|
3692
4169
|
nameInput.autocomplete = "name";
|
|
3693
4170
|
nameRow.appendChild(nameInput);
|
|
@@ -3697,17 +4174,17 @@ var LoginPage = class {
|
|
|
3697
4174
|
createBtn.id = "passkey-btn";
|
|
3698
4175
|
createBtn.innerHTML = `${ICON_PASSKEY} ${escapeHtml(t("passkeyCreate", locale))}`;
|
|
3699
4176
|
section.appendChild(createBtn);
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
backLink.className = "passkey-alt";
|
|
3703
|
-
backLink.textContent = t("passkeyBackToSignIn", locale);
|
|
3704
|
-
section.appendChild(backLink);
|
|
4177
|
+
parent.querySelector(".back-btn")?.remove();
|
|
4178
|
+
this.renderBackButton(parent, locale, () => renderSignInState());
|
|
3705
4179
|
const doCreate = async () => {
|
|
3706
4180
|
if (createBtn.disabled)
|
|
3707
4181
|
return;
|
|
3708
4182
|
createBtn.disabled = true;
|
|
4183
|
+
this.select("passkey");
|
|
4184
|
+
const epoch = this.generation;
|
|
3709
4185
|
const result = await runPasskeyAuth({
|
|
3710
4186
|
registerOnly: true,
|
|
4187
|
+
signal: this.abort?.signal,
|
|
3711
4188
|
name: nameInput.value.trim(),
|
|
3712
4189
|
onStatus: setStatus,
|
|
3713
4190
|
texts: statusTexts,
|
|
@@ -3715,6 +4192,8 @@ var LoginPage = class {
|
|
|
3715
4192
|
prefix: this.options.passkeyPrefix,
|
|
3716
4193
|
invitationId: this.options.invitationId
|
|
3717
4194
|
});
|
|
4195
|
+
if (!this.mounted || epoch !== this.generation || this.abort?.signal.aborted)
|
|
4196
|
+
return;
|
|
3718
4197
|
if (handleResult(result))
|
|
3719
4198
|
return;
|
|
3720
4199
|
if (result.error)
|
|
@@ -3726,21 +4205,27 @@ var LoginPage = class {
|
|
|
3726
4205
|
if (e.key === "Enter")
|
|
3727
4206
|
doCreate();
|
|
3728
4207
|
});
|
|
3729
|
-
|
|
3730
|
-
nameInput.focus();
|
|
4208
|
+
createBtn.focus();
|
|
3731
4209
|
};
|
|
3732
4210
|
const doSignIn = async () => {
|
|
3733
4211
|
const btn = section.querySelector("#passkey-btn");
|
|
4212
|
+
if (btn?.disabled)
|
|
4213
|
+
return;
|
|
3734
4214
|
if (btn)
|
|
3735
4215
|
btn.disabled = true;
|
|
4216
|
+
this.select("passkey");
|
|
4217
|
+
const epoch = this.generation;
|
|
3736
4218
|
const result = await runPasskeyAuth({
|
|
3737
4219
|
authOnly: true,
|
|
4220
|
+
signal: this.abort?.signal,
|
|
3738
4221
|
onStatus: setStatus,
|
|
3739
4222
|
texts: statusTexts,
|
|
3740
4223
|
locale,
|
|
3741
4224
|
prefix: this.options.passkeyPrefix,
|
|
3742
4225
|
invitationId: this.options.invitationId
|
|
3743
4226
|
});
|
|
4227
|
+
if (!this.mounted || epoch !== this.generation || this.abort?.signal.aborted)
|
|
4228
|
+
return;
|
|
3744
4229
|
if (handleResult(result))
|
|
3745
4230
|
return;
|
|
3746
4231
|
if (result.error && result.error !== "no_passkey") {
|
|
@@ -3749,20 +4234,29 @@ var LoginPage = class {
|
|
|
3749
4234
|
btn.disabled = false;
|
|
3750
4235
|
return;
|
|
3751
4236
|
}
|
|
4237
|
+
this.emit("incomplete");
|
|
3752
4238
|
renderCrossDeviceState();
|
|
3753
4239
|
};
|
|
3754
4240
|
const renderSignInState = () => {
|
|
4241
|
+
this.cardEl?.classList.remove("passkey-creating");
|
|
4242
|
+
parent.querySelector(".back-btn")?.remove();
|
|
4243
|
+
this.abort?.abort();
|
|
4244
|
+
this.generation++;
|
|
4245
|
+
const heading = this.cardEl?.querySelector("h1");
|
|
4246
|
+
if (heading)
|
|
4247
|
+
heading.textContent = this.options.purpose?.title ?? (this.options.appName ? t("signInTo", locale, { appName: this.options.appName }) : t("signIn", locale));
|
|
3755
4248
|
section.innerHTML = "";
|
|
3756
4249
|
clearStatus();
|
|
3757
4250
|
const signInBtn = document.createElement("button");
|
|
3758
|
-
signInBtn.className = "btn";
|
|
4251
|
+
signInBtn.className = this.cardEl?.classList.contains("social-first") ? "btn btn-secondary" : "btn";
|
|
3759
4252
|
signInBtn.id = "passkey-btn";
|
|
3760
4253
|
signInBtn.innerHTML = `${ICON_PASSKEY} ${escapeHtml(t("passkeySignIn", locale))}`;
|
|
4254
|
+
this.markLastUsed(signInBtn, "passkey", locale);
|
|
3761
4255
|
section.appendChild(signInBtn);
|
|
3762
4256
|
signInBtn.addEventListener("click", () => doSignIn());
|
|
3763
4257
|
const createLink = document.createElement("button");
|
|
3764
4258
|
createLink.type = "button";
|
|
3765
|
-
createLink.className = "passkey-alt";
|
|
4259
|
+
createLink.className = "passkey-alt passkey-create-entry";
|
|
3766
4260
|
createLink.textContent = t("passkeyFirstTime", locale);
|
|
3767
4261
|
section.appendChild(createLink);
|
|
3768
4262
|
createLink.addEventListener("click", () => renderCreateState());
|
|
@@ -3777,127 +4271,242 @@ var LoginPage = class {
|
|
|
3777
4271
|
// methods now render through renderSecondaryGrid)
|
|
3778
4272
|
// ─── Private: OAuth popup flow ──────────────────────────────────────
|
|
3779
4273
|
startOAuthPopup(providerId, btn) {
|
|
4274
|
+
this.oauthCleanup?.();
|
|
4275
|
+
const epoch = this.generation;
|
|
4276
|
+
const locale = this.options.locale;
|
|
3780
4277
|
const servicePrefix = this.config.servicePrefix ?? "/.well-known/service";
|
|
3781
4278
|
const oauthBase = this.options.masterOAuthOrigin ? `${this.options.masterOAuthOrigin}${servicePrefix}` : servicePrefix;
|
|
3782
4279
|
const oauthOrigin = new URL(oauthBase, window.location.href).origin;
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
4280
|
+
let returnUrl = window.location.href;
|
|
4281
|
+
if (this.options.mode !== "dialog" && window.location.pathname === `${servicePrefix}/login`) {
|
|
4282
|
+
const requested = new URLSearchParams(window.location.search).get("return_to");
|
|
4283
|
+
try {
|
|
4284
|
+
const target = new URL(requested || "/", window.location.origin);
|
|
4285
|
+
returnUrl = target.origin === window.location.origin && !/^\/[/\\]/.test(target.pathname) ? target.href : `${window.location.origin}/`;
|
|
4286
|
+
} catch {
|
|
4287
|
+
returnUrl = `${window.location.origin}/`;
|
|
4288
|
+
}
|
|
3792
4289
|
}
|
|
4290
|
+
const loginUrl = `${oauthBase}/api/oauth/${encodeURIComponent(providerId)}/login?returnUrl=${encodeURIComponent(returnUrl)}`;
|
|
4291
|
+
const provider = this.options.oauthProviders?.find((p) => p.id === providerId)?.name ?? providerId;
|
|
3793
4292
|
const originalHTML = btn.innerHTML;
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
4293
|
+
let popup = null;
|
|
4294
|
+
let settled = false;
|
|
4295
|
+
let exchanging = false;
|
|
4296
|
+
let probing = false;
|
|
4297
|
+
let pollTimer;
|
|
4298
|
+
let sessionTimer;
|
|
4299
|
+
let abandonTimer;
|
|
4300
|
+
const current = () => !settled && this.mounted && epoch === this.generation;
|
|
4301
|
+
const locked = [];
|
|
4302
|
+
const lockMethods = () => {
|
|
4303
|
+
for (const button of this.cardEl?.querySelectorAll(".method-btn, .passkey-create-entry, #passkey-btn, .locale-trigger, .locale-btn") ?? []) {
|
|
4304
|
+
locked.push({ button, disabled: button.disabled });
|
|
4305
|
+
button.disabled = true;
|
|
4306
|
+
}
|
|
4307
|
+
};
|
|
4308
|
+
const recovery = document.createElement("div");
|
|
4309
|
+
recovery.className = "oauth-recovery";
|
|
4310
|
+
recovery.setAttribute("role", "status");
|
|
4311
|
+
const hint = document.createElement("p");
|
|
4312
|
+
const actions = document.createElement("div");
|
|
4313
|
+
actions.className = "oauth-recovery-actions";
|
|
4314
|
+
const reopen = document.createElement("button");
|
|
4315
|
+
reopen.type = "button";
|
|
4316
|
+
reopen.className = "oauth-retry";
|
|
4317
|
+
reopen.textContent = t("oauthReopen", locale);
|
|
4318
|
+
reopen.hidden = true;
|
|
4319
|
+
reopen.onclick = () => {
|
|
4320
|
+
this.select("oauth");
|
|
4321
|
+
this.activeProvider = providerId;
|
|
4322
|
+
this.startOAuthPopup(providerId, btn);
|
|
4323
|
+
};
|
|
4324
|
+
const fullPage = document.createElement("a");
|
|
4325
|
+
fullPage.href = loginUrl;
|
|
4326
|
+
fullPage.textContent = t("oauthFullPage", locale);
|
|
4327
|
+
fullPage.addEventListener("click", (event) => {
|
|
4328
|
+
const reconcile = recovery.dataset.state === "oauthUnconfirmed";
|
|
4329
|
+
if (exchanging && !settled) {
|
|
4330
|
+
event.preventDefault();
|
|
4331
|
+
return;
|
|
4332
|
+
}
|
|
4333
|
+
if (reconcile)
|
|
4334
|
+
event.preventDefault();
|
|
4335
|
+
this.options.onBeforeRedirect?.();
|
|
4336
|
+
this.oauthCleanup?.();
|
|
4337
|
+
if (reconcile)
|
|
4338
|
+
window.location.reload();
|
|
4339
|
+
});
|
|
4340
|
+
const cancel = document.createElement("button");
|
|
4341
|
+
cancel.type = "button";
|
|
4342
|
+
cancel.className = "oauth-cancel";
|
|
4343
|
+
cancel.textContent = t("cancel", locale);
|
|
4344
|
+
actions.append(reopen, fullPage, cancel);
|
|
4345
|
+
recovery.append(hint, actions);
|
|
4346
|
+
btn.parentElement?.after(recovery);
|
|
4347
|
+
const restoreButton = () => {
|
|
3798
4348
|
btn.innerHTML = originalHTML;
|
|
3799
4349
|
btn.disabled = false;
|
|
3800
4350
|
btn.classList.remove("oauth-btn-loading");
|
|
3801
4351
|
};
|
|
3802
|
-
|
|
3803
|
-
|
|
4352
|
+
const closePopup = () => {
|
|
4353
|
+
try {
|
|
4354
|
+
popup?.close();
|
|
4355
|
+
} catch {
|
|
4356
|
+
}
|
|
4357
|
+
};
|
|
4358
|
+
const cleanup = () => {
|
|
4359
|
+
settled = true;
|
|
4360
|
+
clearInterval(pollTimer);
|
|
4361
|
+
clearInterval(sessionTimer);
|
|
4362
|
+
clearTimeout(abandonTimer);
|
|
4363
|
+
window.removeEventListener("message", onMessage);
|
|
4364
|
+
for (const item of locked.splice(0))
|
|
4365
|
+
item.button.disabled = item.disabled;
|
|
4366
|
+
restoreButton();
|
|
4367
|
+
};
|
|
4368
|
+
const showRecovery = (key) => {
|
|
4369
|
+
hint.textContent = t(key, locale, { provider });
|
|
4370
|
+
recovery.dataset.state = key;
|
|
4371
|
+
actions.hidden = false;
|
|
4372
|
+
reopen.hidden = false;
|
|
4373
|
+
cancel.hidden = true;
|
|
4374
|
+
restoreButton();
|
|
4375
|
+
};
|
|
4376
|
+
const fail = (key) => {
|
|
4377
|
+
if (!current())
|
|
4378
|
+
return;
|
|
4379
|
+
cleanup();
|
|
4380
|
+
closePopup();
|
|
4381
|
+
showRecovery(key);
|
|
4382
|
+
this.emit("incomplete");
|
|
4383
|
+
};
|
|
3804
4384
|
const succeed = () => {
|
|
3805
|
-
if (
|
|
4385
|
+
if (!current())
|
|
3806
4386
|
return;
|
|
3807
|
-
settled = true;
|
|
3808
4387
|
cleanup();
|
|
3809
|
-
|
|
4388
|
+
closePopup();
|
|
4389
|
+
recovery.remove();
|
|
4390
|
+
btn.textContent = t("success", locale);
|
|
4391
|
+
btn.disabled = true;
|
|
3810
4392
|
this.handleSuccess();
|
|
3811
4393
|
};
|
|
3812
|
-
|
|
3813
|
-
if (
|
|
4394
|
+
cancel.onclick = () => {
|
|
4395
|
+
if (exchanging)
|
|
3814
4396
|
return;
|
|
3815
|
-
|
|
4397
|
+
fail("oauthCancelled");
|
|
4398
|
+
btn.focus();
|
|
4399
|
+
};
|
|
4400
|
+
const onMessage = async (event) => {
|
|
4401
|
+
if (event.origin !== oauthOrigin || event.source !== popup || !popup)
|
|
3816
4402
|
return;
|
|
3817
|
-
if (
|
|
4403
|
+
if (!event.data || event.data.type !== "authorization_response" || !current() || exchanging)
|
|
3818
4404
|
return;
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
pollTimer = void 0;
|
|
3822
|
-
}
|
|
3823
|
-
try {
|
|
3824
|
-
popup.close();
|
|
3825
|
-
} catch {
|
|
3826
|
-
}
|
|
4405
|
+
clearInterval(pollTimer);
|
|
4406
|
+
clearInterval(sessionTimer);
|
|
3827
4407
|
if (event.data.error) {
|
|
3828
|
-
|
|
3829
|
-
cleanup();
|
|
4408
|
+
fail(event.data.error === "access_denied" ? "oauthDenied" : event.data.error === "cancelled" ? "oauthCancelled" : "oauthFailed");
|
|
3830
4409
|
return;
|
|
3831
4410
|
}
|
|
3832
4411
|
const { code, state } = event.data.response ?? event.data;
|
|
3833
|
-
if (!code) {
|
|
3834
|
-
|
|
3835
|
-
cleanup();
|
|
4412
|
+
if (typeof code !== "string" || !code || typeof state !== "string" || !state) {
|
|
4413
|
+
fail("oauthFailed");
|
|
3836
4414
|
return;
|
|
3837
4415
|
}
|
|
3838
|
-
|
|
4416
|
+
exchanging = true;
|
|
4417
|
+
lockMethods();
|
|
4418
|
+
actions.hidden = true;
|
|
4419
|
+
closePopup();
|
|
4420
|
+
hint.textContent = t("verifying", locale);
|
|
4421
|
+
btn.textContent = t("verifying", locale);
|
|
3839
4422
|
btn.disabled = true;
|
|
3840
4423
|
try {
|
|
3841
4424
|
const oauthBody = { provider: providerId, code, state };
|
|
3842
4425
|
if (this.options.invitationId)
|
|
3843
4426
|
oauthBody.invitationId = this.options.invitationId;
|
|
3844
|
-
await this.http.post(`${servicePrefix}/api/oauth/login`, oauthBody);
|
|
4427
|
+
await this.http.post(`${servicePrefix}/api/oauth/login`, oauthBody, { signal: this.abort?.signal });
|
|
3845
4428
|
succeed();
|
|
3846
|
-
} catch (
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
4429
|
+
} catch (err) {
|
|
4430
|
+
if (!current())
|
|
4431
|
+
return;
|
|
4432
|
+
const code2 = err instanceof Error && "code" in err ? String(err.code) : "";
|
|
4433
|
+
const message = err instanceof Error ? err.message : "";
|
|
4434
|
+
if (code2 === "FORBIDDEN" || code2 === "registration_closed" || message === "registration_closed") {
|
|
3851
4435
|
cleanup();
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
4436
|
+
recovery.remove();
|
|
4437
|
+
this.showFullError(friendlyError(err, locale));
|
|
4438
|
+
} else
|
|
4439
|
+
fail("oauthFailed");
|
|
4440
|
+
}
|
|
4441
|
+
};
|
|
4442
|
+
this.oauthCleanup = () => {
|
|
4443
|
+
cleanup();
|
|
4444
|
+
recovery.remove();
|
|
4445
|
+
closePopup();
|
|
4446
|
+
};
|
|
4447
|
+
window.addEventListener("message", onMessage);
|
|
4448
|
+
try {
|
|
4449
|
+
popup = window.open(loginUrl, "oauth-login:popup", `left=${window.screenX + (window.innerWidth - 500) / 2},top=${window.screenY + (window.innerHeight - 620) / 2},width=500,height=620,resizable,scrollbars=yes,status=1,popup`);
|
|
4450
|
+
} catch {
|
|
4451
|
+
}
|
|
4452
|
+
if (!popup) {
|
|
4453
|
+
fail("oauthBlocked");
|
|
4454
|
+
return;
|
|
4455
|
+
}
|
|
4456
|
+
hint.textContent = t("oauthWaiting", locale, { provider });
|
|
4457
|
+
btn.textContent = t("oauthWaiting", locale, { provider });
|
|
4458
|
+
btn.disabled = true;
|
|
4459
|
+
btn.classList.add("oauth-btn-loading");
|
|
4460
|
+
const checkSession = async () => {
|
|
4461
|
+
if (!current() || exchanging || probing)
|
|
4462
|
+
return;
|
|
4463
|
+
probing = true;
|
|
4464
|
+
try {
|
|
4465
|
+
const session = await this.http.get(`${servicePrefix}/api/did/session`);
|
|
4466
|
+
if (current() && !exchanging && session?.authenticated === true)
|
|
4467
|
+
succeed();
|
|
4468
|
+
} catch {
|
|
4469
|
+
} finally {
|
|
4470
|
+
probing = false;
|
|
3856
4471
|
}
|
|
3857
4472
|
};
|
|
3858
|
-
|
|
4473
|
+
pollTimer = setInterval(() => {
|
|
3859
4474
|
let closed = false;
|
|
3860
4475
|
try {
|
|
3861
|
-
closed = popup
|
|
4476
|
+
closed = !!popup?.closed;
|
|
3862
4477
|
} catch {
|
|
3863
4478
|
return;
|
|
3864
4479
|
}
|
|
3865
|
-
if (!closed)
|
|
4480
|
+
if (!closed || !current())
|
|
3866
4481
|
return;
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
if (session?.authenticated)
|
|
3875
|
-
succeed();
|
|
3876
|
-
}).catch(() => {
|
|
3877
|
-
});
|
|
3878
|
-
};
|
|
3879
|
-
checkSession();
|
|
3880
|
-
sessionPollTimer = setInterval(checkSession, 500);
|
|
4482
|
+
clearInterval(pollTimer);
|
|
4483
|
+
showRecovery("oauthIncomplete");
|
|
4484
|
+
cancel.hidden = false;
|
|
4485
|
+
void checkSession();
|
|
4486
|
+
sessionTimer = setInterval(() => {
|
|
4487
|
+
void checkSession();
|
|
4488
|
+
}, 2e3);
|
|
3881
4489
|
}, 500);
|
|
3882
|
-
|
|
3883
|
-
if (
|
|
4490
|
+
abandonTimer = setTimeout(() => {
|
|
4491
|
+
if (!current())
|
|
4492
|
+
return;
|
|
4493
|
+
if (!exchanging) {
|
|
4494
|
+
fail("oauthTimedOut");
|
|
3884
4495
|
return;
|
|
4496
|
+
}
|
|
4497
|
+
this.abort?.abort();
|
|
3885
4498
|
cleanup();
|
|
3886
|
-
|
|
4499
|
+
closePopup();
|
|
4500
|
+
lockMethods();
|
|
4501
|
+
hint.textContent = t("oauthUnconfirmed", locale);
|
|
4502
|
+
recovery.dataset.state = "oauthUnconfirmed";
|
|
4503
|
+
actions.hidden = false;
|
|
4504
|
+
reopen.hidden = true;
|
|
4505
|
+
cancel.hidden = true;
|
|
4506
|
+
fullPage.href = window.location.href;
|
|
4507
|
+
fullPage.textContent = t("oauthCheckSession", locale);
|
|
4508
|
+
this.emit("incomplete");
|
|
3887
4509
|
}, 5 * 60 * 1e3);
|
|
3888
|
-
const cleanup = () => {
|
|
3889
|
-
if (pollTimer !== void 0) {
|
|
3890
|
-
clearInterval(pollTimer);
|
|
3891
|
-
pollTimer = void 0;
|
|
3892
|
-
}
|
|
3893
|
-
if (sessionPollTimer !== void 0) {
|
|
3894
|
-
clearInterval(sessionPollTimer);
|
|
3895
|
-
sessionPollTimer = void 0;
|
|
3896
|
-
}
|
|
3897
|
-
clearTimeout(abandonTimer);
|
|
3898
|
-
window.removeEventListener("message", onMessage);
|
|
3899
|
-
};
|
|
3900
|
-
window.addEventListener("message", onMessage);
|
|
3901
4510
|
}
|
|
3902
4511
|
// (removed dead code: renderEmailButton — email now renders through renderSecondaryGrid)
|
|
3903
4512
|
/**
|
|
@@ -3906,19 +4515,25 @@ var LoginPage = class {
|
|
|
3906
4515
|
* Even count: all items in 2 columns (each half width).
|
|
3907
4516
|
* Odd count: DID Wallet spans full width at the end.
|
|
3908
4517
|
*/
|
|
3909
|
-
renderSecondaryGrid(parent, locale, methods) {
|
|
4518
|
+
renderSecondaryGrid(parent, locale, methods, group) {
|
|
3910
4519
|
const grid = document.createElement("div");
|
|
3911
4520
|
grid.className = "methods-grid";
|
|
3912
4521
|
if (methods.includes("oauth") && this.options.oauthProviders?.length) {
|
|
3913
|
-
const
|
|
4522
|
+
const order = this.options.oauthProviderOrder ?? ["google", "apple", "github"];
|
|
4523
|
+
const rank = (id) => order.includes(id) ? order.indexOf(id) : order.length;
|
|
4524
|
+
const explicitOrder = !!this.options.oauthProviderOrder || this.options.oauthProviders?.some((p) => p.order !== void 0 && p.order !== 999);
|
|
4525
|
+
const enabledProviders = (this.options.oauthProviders ?? []).filter((p) => p.enabled && (!group || (explicitOrder || ["google", "apple"].includes(p.id)) === (group === "primary"))).sort((a, b) => this.options.oauthProviderOrder ? rank(a.id) - rank(b.id) : (a.order ?? 999) - (b.order ?? 999) || rank(a.id) - rank(b.id));
|
|
3914
4526
|
for (const provider of enabledProviders) {
|
|
3915
4527
|
const btn = document.createElement("button");
|
|
3916
4528
|
btn.className = "method-btn";
|
|
3917
4529
|
btn.dataset.provider = provider.id;
|
|
3918
4530
|
const icon = getOAuthIcon(provider.id);
|
|
3919
|
-
btn.innerHTML = `${icon} ${escapeHtml(provider.name)}`;
|
|
4531
|
+
btn.innerHTML = `${icon} ${escapeHtml(t("continueWith", locale, { provider: provider.name }))}`;
|
|
4532
|
+
this.markLastUsed(btn, `oauth:${provider.id}`, locale);
|
|
3920
4533
|
grid.appendChild(btn);
|
|
3921
4534
|
btn.addEventListener("click", () => {
|
|
4535
|
+
this.select("oauth");
|
|
4536
|
+
this.activeProvider = provider.id;
|
|
3922
4537
|
this.startOAuthPopup(provider.id, btn);
|
|
3923
4538
|
});
|
|
3924
4539
|
}
|
|
@@ -3928,8 +4543,10 @@ var LoginPage = class {
|
|
|
3928
4543
|
btn.className = "method-btn";
|
|
3929
4544
|
btn.id = "email-btn";
|
|
3930
4545
|
btn.innerHTML = `${ICON_EMAIL} ${escapeHtml(t("emailButton", locale))}`;
|
|
4546
|
+
this.markLastUsed(btn, "email", locale);
|
|
3931
4547
|
grid.appendChild(btn);
|
|
3932
4548
|
btn.addEventListener("click", () => {
|
|
4549
|
+
this.select("email");
|
|
3933
4550
|
this.switchView("email");
|
|
3934
4551
|
});
|
|
3935
4552
|
}
|
|
@@ -3939,8 +4556,10 @@ var LoginPage = class {
|
|
|
3939
4556
|
btn.id = "did-wallet-btn";
|
|
3940
4557
|
const walletLabel = grid.children.length > 0 ? t("didWalletButtonShort", locale) : t("didWalletButton", locale);
|
|
3941
4558
|
btn.innerHTML = `${ICON_DID_WALLET} ${escapeHtml(walletLabel)}`;
|
|
4559
|
+
this.markLastUsed(btn, "did-connect", locale);
|
|
3942
4560
|
grid.appendChild(btn);
|
|
3943
4561
|
btn.addEventListener("click", () => {
|
|
4562
|
+
this.select("did-connect");
|
|
3944
4563
|
this.switchView("qr");
|
|
3945
4564
|
});
|
|
3946
4565
|
}
|
|
@@ -3949,19 +4568,27 @@ var LoginPage = class {
|
|
|
3949
4568
|
// ─── Private: DID Connect flow (QR view) ───────────────────────────
|
|
3950
4569
|
async startDIDConnectInView(statusEl, qrContainer, walletBtn, locale) {
|
|
3951
4570
|
statusEl.textContent = t("connecting", locale);
|
|
4571
|
+
const epoch = this.generation;
|
|
4572
|
+
const isCurrent = () => this.mounted && epoch === this.generation;
|
|
3952
4573
|
try {
|
|
3953
4574
|
this.tokenSession?.destroy();
|
|
3954
4575
|
this.tokenSession = new TokenSession(this.config, this.http);
|
|
3955
4576
|
this.tokenSession.on("succeed", async () => {
|
|
4577
|
+
if (!isCurrent())
|
|
4578
|
+
return;
|
|
3956
4579
|
this.showConnectResult(qrContainer, walletBtn, statusEl, t("verifying", locale));
|
|
3957
4580
|
try {
|
|
3958
4581
|
const servicePrefix = this.config.servicePrefix ?? "/.well-known/service";
|
|
3959
4582
|
await this.http.post(`${servicePrefix}/api/did/connect/complete`, {
|
|
3960
4583
|
token: this.tokenSession.state.token
|
|
3961
4584
|
});
|
|
4585
|
+
if (!isCurrent())
|
|
4586
|
+
return;
|
|
3962
4587
|
this.showConnectSuccess(qrContainer, walletBtn, statusEl, locale);
|
|
3963
4588
|
this.handleSuccess();
|
|
3964
4589
|
} catch (err) {
|
|
4590
|
+
if (!isCurrent())
|
|
4591
|
+
return;
|
|
3965
4592
|
this.showConnectResult(qrContainer, walletBtn, statusEl, friendlyError(err, locale), true);
|
|
3966
4593
|
}
|
|
3967
4594
|
});
|
|
@@ -3992,17 +4619,21 @@ var LoginPage = class {
|
|
|
3992
4619
|
}
|
|
3993
4620
|
});
|
|
3994
4621
|
const state = await this.tokenSession.create({ locale });
|
|
4622
|
+
if (!isCurrent())
|
|
4623
|
+
return;
|
|
3995
4624
|
if (state.url) {
|
|
3996
4625
|
renderQR(qrContainer, state.url);
|
|
3997
4626
|
walletBtn.disabled = false;
|
|
3998
4627
|
walletBtn.addEventListener("click", () => {
|
|
3999
|
-
|
|
4628
|
+
statusEl.textContent = t("walletRetry", locale);
|
|
4000
4629
|
this.openInWallet(state.url, locale);
|
|
4001
4630
|
});
|
|
4002
4631
|
}
|
|
4003
4632
|
statusEl.textContent = t("scanWithWallet", locale);
|
|
4004
4633
|
this.tokenSession.startPolling();
|
|
4005
4634
|
} catch (err) {
|
|
4635
|
+
if (!isCurrent())
|
|
4636
|
+
return;
|
|
4006
4637
|
statusEl.textContent = friendlyError(err, locale);
|
|
4007
4638
|
clearQR(qrContainer);
|
|
4008
4639
|
}
|
|
@@ -4022,7 +4653,7 @@ var LoginPage = class {
|
|
|
4022
4653
|
}
|
|
4023
4654
|
this.cardEl.innerHTML = "";
|
|
4024
4655
|
this.cardEl.dataset.view = "error";
|
|
4025
|
-
this.
|
|
4656
|
+
this.renderCardTools(this.cardEl);
|
|
4026
4657
|
const resultContainer = document.createElement("div");
|
|
4027
4658
|
resultContainer.className = "connect-result";
|
|
4028
4659
|
resultContainer.innerHTML = `<div class="result-icon result-error">\u2715</div>`;
|
|
@@ -4161,13 +4792,241 @@ var LoginPage = class {
|
|
|
4161
4792
|
}
|
|
4162
4793
|
// ─── Private: success handler ────────────────────────────────────────
|
|
4163
4794
|
handleSuccess() {
|
|
4795
|
+
if (!this.mounted || this.completed)
|
|
4796
|
+
return;
|
|
4797
|
+
this.completed = true;
|
|
4798
|
+
this.emit("success");
|
|
4799
|
+
if (this.options.preferenceKey && this.activeMethod) {
|
|
4800
|
+
try {
|
|
4801
|
+
localStorage.setItem(`did-login-method:${this.options.preferenceKey}`, this.activeMethod === "oauth" && this.activeProvider ? `oauth:${this.activeProvider}` : this.activeMethod);
|
|
4802
|
+
} catch {
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4164
4805
|
if (this.options.onSuccess) {
|
|
4165
|
-
this.
|
|
4806
|
+
const epoch = this.generation;
|
|
4807
|
+
Promise.resolve().then(() => {
|
|
4808
|
+
if (this.mounted && epoch === this.generation)
|
|
4809
|
+
return this.options.onSuccess?.();
|
|
4810
|
+
}).catch(() => {
|
|
4811
|
+
if (!this.mounted || epoch !== this.generation)
|
|
4812
|
+
return;
|
|
4813
|
+
this.completed = false;
|
|
4814
|
+
this.showFullError(t("authFailed", this.options.locale));
|
|
4815
|
+
});
|
|
4166
4816
|
} else {
|
|
4167
4817
|
setTimeout(() => location.reload(), 300);
|
|
4168
4818
|
}
|
|
4169
4819
|
}
|
|
4170
4820
|
};
|
|
4821
|
+
|
|
4822
|
+
// dist/ui/login-dialog.js
|
|
4823
|
+
var DIALOG_STYLES = `${DESIGN_TOKENS_CSS}${DESIGN_TOKENS_LIGHT_CSS}
|
|
4824
|
+
:host { color-scheme: dark; }
|
|
4825
|
+
:host([data-theme="light"]) { color-scheme: light; }
|
|
4826
|
+
dialog { box-sizing:border-box; position:fixed; inset:0; margin:auto; padding:0;
|
|
4827
|
+
width:min(420px,calc(100vw - 32px)); max-width:none; max-height:calc(100dvh - 32px);
|
|
4828
|
+
border:1px solid var(--border); border-radius:16px; background:var(--bg-card);
|
|
4829
|
+
color:var(--text); color-scheme:inherit; overflow:hidden; box-shadow:var(--shadow-lg); }
|
|
4830
|
+
dialog::backdrop { background:rgba(8,10,18,.56); }
|
|
4831
|
+
:host(:not([data-theme="light"])) dialog::backdrop { background:rgba(0,0,0,.68); }
|
|
4832
|
+
[data-dialog-scroll] { max-height:calc(100dvh - 34px); overflow:auto; overscroll-behavior:contain; }
|
|
4833
|
+
.close { position:absolute; top:8px; right:8px; z-index:3; display:grid; place-items:center;
|
|
4834
|
+
box-sizing:border-box; width:44px; height:44px; padding:0; border:1px solid transparent;
|
|
4835
|
+
border-radius:10px; background:var(--bg-card); color:var(--text-secondary);
|
|
4836
|
+
font:400 24px/1 system-ui,sans-serif; cursor:pointer; }
|
|
4837
|
+
.close:hover { color:var(--text); background:var(--bg-hover); }
|
|
4838
|
+
.close:focus-visible { outline:2px solid var(--blue); outline-offset:-3px; }
|
|
4839
|
+
.host-error { padding:16px 20px; font:14px/1.5 system-ui,sans-serif; overflow-wrap:anywhere; }
|
|
4840
|
+
.retry { padding:10px 16px; margin:0 20px 20px; color:var(--text); background:var(--bg-elevated);
|
|
4841
|
+
border:1px solid var(--border); border-radius:8px; font:inherit; cursor:pointer; }
|
|
4842
|
+
`;
|
|
4843
|
+
var LoginDialog = class {
|
|
4844
|
+
config;
|
|
4845
|
+
http;
|
|
4846
|
+
options;
|
|
4847
|
+
host;
|
|
4848
|
+
dialog;
|
|
4849
|
+
page;
|
|
4850
|
+
previous;
|
|
4851
|
+
themeObserver;
|
|
4852
|
+
systemTheme;
|
|
4853
|
+
syncTheme = () => this.applyTheme(this.hostTheme());
|
|
4854
|
+
constructor(config, http, options = {}) {
|
|
4855
|
+
this.config = config;
|
|
4856
|
+
this.http = http;
|
|
4857
|
+
this.options = options;
|
|
4858
|
+
}
|
|
4859
|
+
mount() {
|
|
4860
|
+
if (this.dialog)
|
|
4861
|
+
return;
|
|
4862
|
+
const hostLocale = document.documentElement.lang;
|
|
4863
|
+
if (hostLocale)
|
|
4864
|
+
this.options.locale = hostLocale;
|
|
4865
|
+
let focused = document.activeElement;
|
|
4866
|
+
while (focused?.shadowRoot?.activeElement)
|
|
4867
|
+
focused = focused.shadowRoot.activeElement;
|
|
4868
|
+
this.previous = this.options.returnFocus ?? (focused instanceof HTMLElement ? focused : void 0);
|
|
4869
|
+
const host = document.createElement("div");
|
|
4870
|
+
host.dataset.didLoginDialogHost = "";
|
|
4871
|
+
host.style.cssText = "position:fixed;width:0;height:0;";
|
|
4872
|
+
this.host = host;
|
|
4873
|
+
const root = host.attachShadow({ mode: "open" });
|
|
4874
|
+
const style = document.createElement("style");
|
|
4875
|
+
style.textContent = DIALOG_STYLES;
|
|
4876
|
+
root.append(style);
|
|
4877
|
+
const dialog = document.createElement("dialog");
|
|
4878
|
+
this.dialog = dialog;
|
|
4879
|
+
dialog.setAttribute("aria-label", this.options.purpose?.title ?? t("signIn", this.options.locale));
|
|
4880
|
+
root.append(dialog);
|
|
4881
|
+
const close = document.createElement("button");
|
|
4882
|
+
close.type = "button";
|
|
4883
|
+
close.className = "close";
|
|
4884
|
+
close.textContent = "\xD7";
|
|
4885
|
+
close.setAttribute("aria-label", t("cancel", this.options.locale));
|
|
4886
|
+
const cancel = () => {
|
|
4887
|
+
if (this.options.dismissible === false || this.dialog !== dialog)
|
|
4888
|
+
return;
|
|
4889
|
+
try {
|
|
4890
|
+
this.options.onEvent?.({ name: "cancel", mode: "dialog" });
|
|
4891
|
+
} catch {
|
|
4892
|
+
}
|
|
4893
|
+
this.destroy();
|
|
4894
|
+
this.options.onCancel?.();
|
|
4895
|
+
};
|
|
4896
|
+
close.addEventListener("click", cancel);
|
|
4897
|
+
if (this.options.dismissible !== false)
|
|
4898
|
+
dialog.append(close);
|
|
4899
|
+
const scroller = document.createElement("div");
|
|
4900
|
+
scroller.dataset.dialogScroll = "";
|
|
4901
|
+
dialog.append(scroller);
|
|
4902
|
+
const container = document.createElement("div");
|
|
4903
|
+
container.dataset.loginDialogMount = "";
|
|
4904
|
+
scroller.append(container);
|
|
4905
|
+
dialog.addEventListener("cancel", (event) => {
|
|
4906
|
+
event.preventDefault();
|
|
4907
|
+
cancel();
|
|
4908
|
+
});
|
|
4909
|
+
document.body.append(host);
|
|
4910
|
+
const theme = this.hostTheme();
|
|
4911
|
+
host.dataset.theme = theme;
|
|
4912
|
+
this.page = new LoginPage(container, this.config, this.http, {
|
|
4913
|
+
...this.options,
|
|
4914
|
+
theme,
|
|
4915
|
+
isolate: true,
|
|
4916
|
+
mode: "dialog",
|
|
4917
|
+
onCancel: cancel,
|
|
4918
|
+
onLocaleChange: (locale) => {
|
|
4919
|
+
this.options.locale = locale;
|
|
4920
|
+
close.setAttribute("aria-label", t("cancel", locale));
|
|
4921
|
+
dialog.setAttribute("aria-label", this.options.purpose?.title ?? t("signIn", locale));
|
|
4922
|
+
this.options.onLocaleChange?.(locale);
|
|
4923
|
+
},
|
|
4924
|
+
onSuccess: async () => {
|
|
4925
|
+
const showFailure = () => {
|
|
4926
|
+
if (!dialog.isConnected || this.dialog !== dialog)
|
|
4927
|
+
return;
|
|
4928
|
+
for (const node of scroller.querySelectorAll(".host-error, .retry"))
|
|
4929
|
+
node.remove();
|
|
4930
|
+
const status = document.createElement("p");
|
|
4931
|
+
status.className = "host-error";
|
|
4932
|
+
status.setAttribute("role", "alert");
|
|
4933
|
+
status.textContent = t("authFailed", this.options.locale);
|
|
4934
|
+
const retry = document.createElement("button");
|
|
4935
|
+
retry.type = "button";
|
|
4936
|
+
retry.className = "retry";
|
|
4937
|
+
retry.textContent = t("tryAgain", this.options.locale);
|
|
4938
|
+
retry.onclick = async () => {
|
|
4939
|
+
retry.disabled = true;
|
|
4940
|
+
try {
|
|
4941
|
+
await this.options.onSuccess?.();
|
|
4942
|
+
if (this.dialog === dialog)
|
|
4943
|
+
this.destroy();
|
|
4944
|
+
} catch {
|
|
4945
|
+
showFailure();
|
|
4946
|
+
}
|
|
4947
|
+
};
|
|
4948
|
+
scroller.append(status, retry);
|
|
4949
|
+
};
|
|
4950
|
+
try {
|
|
4951
|
+
await this.options.onSuccess?.();
|
|
4952
|
+
if (this.dialog === dialog)
|
|
4953
|
+
this.destroy();
|
|
4954
|
+
} catch {
|
|
4955
|
+
showFailure();
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4958
|
+
});
|
|
4959
|
+
this.page.mount();
|
|
4960
|
+
this.themeObserver = new MutationObserver(this.syncTheme);
|
|
4961
|
+
for (const target of [document.documentElement, document.body]) {
|
|
4962
|
+
this.themeObserver.observe(target, { attributes: true, attributeFilter: ["data-theme", "data-mode", "class", "style"] });
|
|
4963
|
+
}
|
|
4964
|
+
if (typeof window.matchMedia === "function") {
|
|
4965
|
+
this.systemTheme = window.matchMedia("(prefers-color-scheme: dark)");
|
|
4966
|
+
this.systemTheme.addEventListener("change", this.syncTheme);
|
|
4967
|
+
}
|
|
4968
|
+
dialog.showModal();
|
|
4969
|
+
const title = container.shadowRoot?.querySelector("h1, h2");
|
|
4970
|
+
if (title) {
|
|
4971
|
+
title.tabIndex = -1;
|
|
4972
|
+
title.focus({ preventScroll: true });
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4975
|
+
hostTheme() {
|
|
4976
|
+
for (const element of [document.documentElement, document.body]) {
|
|
4977
|
+
for (const name of ["data-mode", "data-theme"]) {
|
|
4978
|
+
const value = element.getAttribute(name);
|
|
4979
|
+
if (value === "light" || value === "dark")
|
|
4980
|
+
return value;
|
|
4981
|
+
if (value === "system" || value === "auto")
|
|
4982
|
+
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
for (const element of [document.documentElement, document.body]) {
|
|
4986
|
+
if (element.classList.contains("dark"))
|
|
4987
|
+
return "dark";
|
|
4988
|
+
if (element.classList.contains("light"))
|
|
4989
|
+
return "light";
|
|
4990
|
+
}
|
|
4991
|
+
for (const element of [document.body, document.documentElement]) {
|
|
4992
|
+
const schemes = getComputedStyle(element).colorScheme.split(/\s+/);
|
|
4993
|
+
if (schemes.includes("light") && schemes.includes("dark")) {
|
|
4994
|
+
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
4995
|
+
}
|
|
4996
|
+
if (schemes.includes("light"))
|
|
4997
|
+
return "light";
|
|
4998
|
+
if (schemes.includes("dark"))
|
|
4999
|
+
return "dark";
|
|
5000
|
+
}
|
|
5001
|
+
if (this.options.theme === "light" || this.options.theme === "dark")
|
|
5002
|
+
return this.options.theme;
|
|
5003
|
+
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
5004
|
+
}
|
|
5005
|
+
setTheme(theme) {
|
|
5006
|
+
this.options.theme = theme;
|
|
5007
|
+
this.applyTheme(theme);
|
|
5008
|
+
}
|
|
5009
|
+
applyTheme(theme) {
|
|
5010
|
+
if (this.host)
|
|
5011
|
+
this.host.dataset.theme = theme;
|
|
5012
|
+
this.page?.setTheme(theme);
|
|
5013
|
+
}
|
|
5014
|
+
destroy() {
|
|
5015
|
+
this.themeObserver?.disconnect();
|
|
5016
|
+
this.themeObserver = void 0;
|
|
5017
|
+
this.systemTheme?.removeEventListener("change", this.syncTheme);
|
|
5018
|
+
this.systemTheme = void 0;
|
|
5019
|
+
this.page?.destroy();
|
|
5020
|
+
this.page = void 0;
|
|
5021
|
+
this.dialog?.close();
|
|
5022
|
+
this.dialog?.remove();
|
|
5023
|
+
this.dialog = void 0;
|
|
5024
|
+
this.host?.remove();
|
|
5025
|
+
this.host = void 0;
|
|
5026
|
+
if (this.previous?.isConnected)
|
|
5027
|
+
this.previous.focus({ preventScroll: true });
|
|
5028
|
+
}
|
|
5029
|
+
};
|
|
4171
5030
|
export {
|
|
4172
5031
|
DESIGN_RESET_CSS,
|
|
4173
5032
|
DESIGN_TOKENS_CSS,
|
|
@@ -4177,6 +5036,7 @@ export {
|
|
|
4177
5036
|
ICON_EMAIL,
|
|
4178
5037
|
ICON_PASSKEY,
|
|
4179
5038
|
LOGIN_PAGE_STYLES,
|
|
5039
|
+
LoginDialog,
|
|
4180
5040
|
LoginPage,
|
|
4181
5041
|
OAUTH_ICONS,
|
|
4182
5042
|
SUPPORTED_LOCALES,
|