@enbox/browser 0.3.48 → 0.3.51
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/browser.mjs +58 -46
- package/dist/browser.mjs.map +3 -3
- package/dist/esm/browser-connect-handler.js +10 -0
- package/dist/esm/browser-connect-handler.js.map +1 -1
- package/dist/esm/dweb-connect-client.js +64 -9
- package/dist/esm/dweb-connect-client.js.map +1 -1
- package/dist/esm/dweb-connect-messages.js +7 -1
- package/dist/esm/dweb-connect-messages.js.map +1 -1
- package/dist/esm/dweb-connect-wallet.js +48 -2
- package/dist/esm/dweb-connect-wallet.js.map +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/relay-connect-runner.js +35 -2
- package/dist/esm/relay-connect-runner.js.map +1 -1
- package/dist/esm/ui/connect-modal.js +220 -101
- package/dist/esm/ui/connect-modal.js.map +1 -1
- package/dist/types/browser-connect-handler.d.ts.map +1 -1
- package/dist/types/dweb-connect-client.d.ts +9 -0
- package/dist/types/dweb-connect-client.d.ts.map +1 -1
- package/dist/types/dweb-connect-messages.d.ts +14 -1
- package/dist/types/dweb-connect-messages.d.ts.map +1 -1
- package/dist/types/dweb-connect-wallet.d.ts +23 -1
- package/dist/types/dweb-connect-wallet.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/relay-connect-runner.d.ts +11 -1
- package/dist/types/relay-connect-runner.d.ts.map +1 -1
- package/dist/types/ui/connect-modal.d.ts +11 -10
- package/dist/types/ui/connect-modal.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/browser-connect-handler.ts +10 -0
- package/src/dweb-connect-client.ts +61 -7
- package/src/dweb-connect-messages.ts +16 -1
- package/src/dweb-connect-wallet.ts +50 -1
- package/src/index.ts +2 -0
- package/src/relay-connect-runner.ts +42 -2
- package/src/ui/connect-modal.ts +246 -110
|
@@ -2,21 +2,22 @@
|
|
|
2
2
|
* Connect modal — the single surface that owns a whole connect session.
|
|
3
3
|
*
|
|
4
4
|
* One Shadow-DOM dialog whose stage morphs in place through the session:
|
|
5
|
-
* QR (phone path) → pairing code → connected, with the
|
|
6
|
-
* path and the wallet catalog folded in as in-place
|
|
7
|
-
* never chains into a second modal and stays mounted
|
|
8
|
-
* resolves (connected / denied / cancelled).
|
|
5
|
+
* QR / deep-link handoff (phone path) → pairing code → connected, with the
|
|
6
|
+
* in-browser popup path and the wallet catalog folded in as in-place
|
|
7
|
+
* alternates. The modal never chains into a second modal and stays mounted
|
|
8
|
+
* until the session resolves (connected / denied / cancelled).
|
|
9
9
|
*
|
|
10
10
|
* Layout: three zones —
|
|
11
11
|
*
|
|
12
12
|
* 1. **Header** — requesting app identity + close.
|
|
13
|
-
* 2. **Stage** — a height-stable region where states crossfade: the
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* 2. **Stage** — a height-stable region where states crossfade: the
|
|
14
|
+
* clickable QR (joined by a Continue deep-link button on phones; both
|
|
15
|
+
* open the wallet in a new tab so this session stays alive underneath),
|
|
16
|
+
* the pairing-code input, progress, success, and error states.
|
|
16
17
|
* 3. **Footer** — the wallet identity row ("Connecting with" + the selected
|
|
17
|
-
* wallet and the next catalog wallets as tiles,
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* wallet and the next catalog wallets as tiles), a More tile that expands
|
|
19
|
+
* the remaining wallets as a matching grid (search + custom URL) in
|
|
20
|
+
* place — a wallet is never shown twice — and the alternate-method link.
|
|
20
21
|
*
|
|
21
22
|
* Theming: the stylesheet is static and reads `--ec-*` design tokens.
|
|
22
23
|
* Light/dark follows the visitor's system via `prefers-color-scheme` and
|
|
@@ -52,8 +53,16 @@ import { fetchWalletWellKnown, probeWalletWellKnown } from './wallet-well-known.
|
|
|
52
53
|
import { RelayConnectCancelledError, runRelayConnect } from '../relay-connect-runner.js';
|
|
53
54
|
/** localStorage key remembering the last successful method + wallet. */
|
|
54
55
|
const LAST_CHOICE_STORAGE_KEY = 'enbox:connect:lastChoice';
|
|
55
|
-
/**
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Safety margin subtracted from the relay pointer TTL before re-minting.
|
|
58
|
+
*
|
|
59
|
+
* The pointer is single-use and its TTL starts at mint time, but the wallet
|
|
60
|
+
* only dereferences it after the user scans — and, on a locked wallet, after
|
|
61
|
+
* they finish the unlock ceremony. A scanned code must therefore carry
|
|
62
|
+
* enough remaining life to absorb that whole window, so re-mint well before
|
|
63
|
+
* the deadline rather than just ahead of it.
|
|
64
|
+
*/
|
|
65
|
+
const REMINT_SAFETY_MS = 120000;
|
|
57
66
|
/** Minimum delay before an automatic re-mint. */
|
|
58
67
|
const REMINT_MIN_MS = 10000;
|
|
59
68
|
/** Pairing code length (matches the wallet's generated PIN). */
|
|
@@ -239,6 +248,7 @@ export function runConnectModal(options) {
|
|
|
239
248
|
let settled = false;
|
|
240
249
|
let relaySession;
|
|
241
250
|
let remintTimer;
|
|
251
|
+
let remintAt;
|
|
242
252
|
let popupBusy = false;
|
|
243
253
|
let pinResolve;
|
|
244
254
|
const discoveryCache = new Map();
|
|
@@ -269,8 +279,9 @@ export function runConnectModal(options) {
|
|
|
269
279
|
const cleanup = () => {
|
|
270
280
|
var _a;
|
|
271
281
|
relaySession === null || relaySession === void 0 ? void 0 : relaySession.cancel();
|
|
272
|
-
|
|
282
|
+
clearRemint();
|
|
273
283
|
document.removeEventListener('keydown', onKeydown);
|
|
284
|
+
document.removeEventListener('visibilitychange', onVisibilityReturn);
|
|
274
285
|
(_a = systemDark === null || systemDark === void 0 ? void 0 : systemDark.removeEventListener) === null || _a === void 0 ? void 0 : _a.call(systemDark, 'change', onSchemeChange);
|
|
275
286
|
try {
|
|
276
287
|
document.body.removeChild(host);
|
|
@@ -313,26 +324,50 @@ export function runConnectModal(options) {
|
|
|
313
324
|
const renderBusy = (message) => {
|
|
314
325
|
setStage(el('div', 'spinner'), el('p', 'stage-caption', message));
|
|
315
326
|
};
|
|
327
|
+
/**
|
|
328
|
+
* Schedules the "finish in the wallet" morph after a handoff link is
|
|
329
|
+
* followed. Deferred one tick so the anchor's default new-tab navigation
|
|
330
|
+
* completes before the anchor is replaced, and guarded so a session that
|
|
331
|
+
* moved on meanwhile (re-mint, wallet response, settle) is never
|
|
332
|
+
* overwritten.
|
|
333
|
+
*/
|
|
334
|
+
const morphToAway = (walletName) => {
|
|
335
|
+
const session = relaySession;
|
|
336
|
+
setTimeout(() => {
|
|
337
|
+
if (settled || session !== relaySession || (session === null || session === void 0 ? void 0 : session.active) !== true || pinResolve !== undefined) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
renderAway(walletName);
|
|
341
|
+
}, 0);
|
|
342
|
+
};
|
|
343
|
+
/**
|
|
344
|
+
* Builds one same-device handoff anchor for the wallet URI. The wallet
|
|
345
|
+
* opens in a new tab so this modal — and the relay session under it —
|
|
346
|
+
* stays alive, with the pairing-code entry waiting when the user
|
|
347
|
+
* switches back. `noopener` keeps the wallet tab from reaching back into
|
|
348
|
+
* this one.
|
|
349
|
+
*/
|
|
350
|
+
const buildHandoffLink = (handoff, walletName, className) => {
|
|
351
|
+
const link = document.createElement('a');
|
|
352
|
+
link.className = className;
|
|
353
|
+
link.href = handoff.walletUri;
|
|
354
|
+
link.target = '_blank';
|
|
355
|
+
link.rel = 'noopener noreferrer';
|
|
356
|
+
link.addEventListener('click', () => { morphToAway(walletName); });
|
|
357
|
+
return link;
|
|
358
|
+
};
|
|
316
359
|
const renderQr = (handoff) => {
|
|
317
360
|
var _a;
|
|
318
361
|
const wallet = walletUrl !== undefined ? walletByUrl(walletUrl) : undefined;
|
|
319
362
|
const walletName = (_a = wallet === null || wallet === void 0 ? void 0 : wallet.name) !== null && _a !== void 0 ? _a : 'your wallet';
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
link.appendChild(buildWalletIcon(wallet));
|
|
326
|
-
}
|
|
327
|
-
link.appendChild(el('span', 'deep-link-label', `Continue in ${walletName}`));
|
|
328
|
-
setStage(link, el('p', 'stage-caption', `${walletName} opens to approve this connection.`), el('p', 'stage-subline', 'Come back here when you’re done.'));
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
const qrBox = document.createElement('div');
|
|
332
|
-
qrBox.className = 'qr-box';
|
|
363
|
+
// The QR card is itself a handoff link on every device: opening it on
|
|
364
|
+
// this device runs the exact handshake the camera path runs on another
|
|
365
|
+
// one, and scanning it keeps working either way.
|
|
366
|
+
const qrLink = buildHandoffLink(handoff, walletName, 'qr-box qr-link');
|
|
367
|
+
qrLink.setAttribute('aria-label', `Open ${walletName} on this device`);
|
|
333
368
|
// Fixed colors on the white card: dark-on-light QRs scan reliably in
|
|
334
369
|
// both appearances (inverted codes trip up some camera apps).
|
|
335
|
-
|
|
370
|
+
qrLink.replaceChildren(qrToSvg(encodeQr(handoff.walletUri), {
|
|
336
371
|
dark: '#14141f',
|
|
337
372
|
light: 'transparent',
|
|
338
373
|
quietZone: 2,
|
|
@@ -340,22 +375,46 @@ export function runConnectModal(options) {
|
|
|
340
375
|
// The selected wallet's mark sits at the QR centre — the "this is the
|
|
341
376
|
// wallet you're connecting to" cue. The 44px plate over the ~180px
|
|
342
377
|
// symbol occludes ~5% of modules, well inside ECC level M's 15%
|
|
343
|
-
// recovery budget
|
|
378
|
+
// recovery budget (the compact variant scales the plate down to
|
|
379
|
+
// match).
|
|
344
380
|
if (wallet !== undefined) {
|
|
345
381
|
const logo = el('div', 'qr-logo');
|
|
346
382
|
logo.appendChild(buildWalletIcon(wallet));
|
|
347
|
-
|
|
383
|
+
qrLink.appendChild(logo);
|
|
348
384
|
}
|
|
349
|
-
|
|
385
|
+
if (isMobile) {
|
|
386
|
+
const link = buildHandoffLink(handoff, walletName, 'deep-link');
|
|
387
|
+
if (wallet !== undefined) {
|
|
388
|
+
link.appendChild(buildWalletIcon(wallet));
|
|
389
|
+
}
|
|
390
|
+
link.appendChild(el('span', 'deep-link-label', `Continue in ${walletName}`));
|
|
391
|
+
qrLink.classList.add('compact');
|
|
392
|
+
setStage(link, el('p', 'stage-subline', `${walletName} opens in a new tab — approve there, then come back for your code.`), el('div', 'stage-divider', 'or scan with another phone'), qrLink);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
setStage(qrLink, el('p', 'stage-caption', 'Scan with your phone’s camera'), el('p', 'stage-subline', `${walletName} stays on your phone — or click the code to open it here.`));
|
|
396
|
+
};
|
|
397
|
+
/**
|
|
398
|
+
* Shown once a handoff link is followed on this device: the approval is
|
|
399
|
+
* happening in the wallet tab, this one waits for the code. "Start over"
|
|
400
|
+
* mints a fresh pointer — the right recovery, because the pointer the
|
|
401
|
+
* wallet tab carried away is single-use.
|
|
402
|
+
*/
|
|
403
|
+
const renderAway = (walletName) => {
|
|
404
|
+
setStage(el('div', 'spinner'), el('p', 'stage-caption', `Finish in ${walletName}`), el('p', 'stage-subline', isMobile
|
|
405
|
+
? `Approve the connection in the ${walletName} tab, then come back here for your code.`
|
|
406
|
+
: 'Approve the connection in the tab that just opened, then come back here for your code.'), stageLinkButton('Start over', () => { void startPhone(); }));
|
|
350
407
|
};
|
|
351
408
|
const renderClaimed = () => {
|
|
409
|
+
const walletName = walletUrl !== undefined ? walletByUrl(walletUrl).name : 'your wallet';
|
|
352
410
|
const pulse = document.createElement('div');
|
|
353
411
|
pulse.className = 'claimed-pulse';
|
|
354
|
-
setStage(pulse, el('p', 'stage-caption',
|
|
412
|
+
setStage(pulse, el('p', 'stage-caption', `Request received — approve in ${walletName}`), el('p', 'stage-subline', 'Once you approve, you’ll get a code to enter here.'));
|
|
355
413
|
};
|
|
356
414
|
const renderPin = (attempt, previousError) => {
|
|
357
415
|
return new Promise((resolvePin) => {
|
|
358
416
|
pinResolve = resolvePin;
|
|
417
|
+
const walletName = walletUrl !== undefined ? walletByUrl(walletUrl).name : 'your wallet';
|
|
359
418
|
const boxes = document.createElement('div');
|
|
360
419
|
boxes.className = 'pin-row';
|
|
361
420
|
if (previousError !== undefined) {
|
|
@@ -410,9 +469,9 @@ export function runConnectModal(options) {
|
|
|
410
469
|
submitIfComplete();
|
|
411
470
|
});
|
|
412
471
|
});
|
|
413
|
-
setStage(el('p', 'stage-caption',
|
|
414
|
-
? [el('p', 'stage-error',
|
|
415
|
-
: [el('p', 'stage-subline', attempt === 1 ? '
|
|
472
|
+
setStage(el('p', 'stage-caption', `Enter the code shown in ${walletName}`), boxes, ...(previousError !== undefined
|
|
473
|
+
? [el('p', 'stage-error', `That code doesn’t match — check ${walletName}.`)]
|
|
474
|
+
: [el('p', 'stage-subline', attempt === 1 ? 'This confirms you’re the one approving.' : '')]));
|
|
416
475
|
inputs[0].focus();
|
|
417
476
|
});
|
|
418
477
|
};
|
|
@@ -430,16 +489,21 @@ export function runConnectModal(options) {
|
|
|
430
489
|
const renderPhoneUnavailable = () => {
|
|
431
490
|
setStage(el('p', 'stage-caption', 'This wallet can’t connect by phone here.'), el('p', 'stage-subline', 'You can continue in this browser instead.'), stageButton('Use this browser', () => { startPopup(); }));
|
|
432
491
|
};
|
|
492
|
+
// The popup surface is a small window on desktop but a new tab on
|
|
493
|
+
// phones — the copy names whichever the visitor will actually see.
|
|
494
|
+
const popupSurface = isMobile ? 'tab' : 'window';
|
|
433
495
|
const renderPopupPrompt = () => {
|
|
434
|
-
setStage(el('p', 'stage-caption', 'Connect with a wallet in this browser'), el('p', 'stage-subline',
|
|
496
|
+
setStage(el('p', 'stage-caption', 'Connect with a wallet in this browser'), el('p', 'stage-subline', isMobile
|
|
497
|
+
? 'Your wallet opens in a new tab to approve this connection — no code needed.'
|
|
498
|
+
: 'Your wallet opens in a small window to approve this connection.'), stageButton(isMobile ? 'Open wallet' : 'Open wallet window', () => { startPopup(); }));
|
|
435
499
|
};
|
|
436
500
|
const renderPopupWaiting = () => {
|
|
437
|
-
setStage(el('div', 'spinner'), el('p', 'stage-caption',
|
|
501
|
+
setStage(el('div', 'spinner'), el('p', 'stage-caption', `Finish in the wallet ${popupSurface}`), el('p', 'stage-subline', 'We’ll wrap up here as soon as you’re done.'));
|
|
438
502
|
};
|
|
439
503
|
const renderPopupInterrupted = (blocked) => {
|
|
440
504
|
setStage(el('p', 'stage-caption', blocked
|
|
441
|
-
?
|
|
442
|
-
:
|
|
505
|
+
? `Your browser blocked the wallet ${popupSurface}.`
|
|
506
|
+
: `The wallet ${popupSurface} was closed.`), stageButton(blocked ? 'Open it now' : `Reopen ${popupSurface}`, () => { startPopup(); }), stageLinkButton(isMobile ? 'Use a code instead' : 'Use your phone instead', () => { void switchMethod('phone'); }));
|
|
443
507
|
};
|
|
444
508
|
// ── Method drivers ─────────────────────────────────────────
|
|
445
509
|
const resolveConnectServerUrl = (origin) => __awaiter(this, void 0, void 0, function* () {
|
|
@@ -453,19 +517,28 @@ export function runConnectModal(options) {
|
|
|
453
517
|
}
|
|
454
518
|
return cached;
|
|
455
519
|
});
|
|
456
|
-
const
|
|
520
|
+
const clearRemint = () => {
|
|
457
521
|
clearTimeout(remintTimer);
|
|
522
|
+
remintAt = undefined;
|
|
523
|
+
};
|
|
524
|
+
const remintNow = () => {
|
|
525
|
+
// Never re-mint once the wallet has responded (pairing in progress).
|
|
526
|
+
if (!settled && pinResolve === undefined && method === 'phone') {
|
|
527
|
+
void startPhone();
|
|
528
|
+
}
|
|
529
|
+
};
|
|
530
|
+
const scheduleRemint = (expiresInSeconds) => {
|
|
531
|
+
clearRemint();
|
|
458
532
|
const delay = Math.max(REMINT_MIN_MS, expiresInSeconds * 1000 - REMINT_SAFETY_MS);
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}, delay);
|
|
533
|
+
// The deadline is kept alongside the timer: background tabs (mobile
|
|
534
|
+
// especially) throttle or freeze timers, so the visibility handler
|
|
535
|
+
// re-checks it when the user returns.
|
|
536
|
+
remintAt = Date.now() + delay;
|
|
537
|
+
remintTimer = setTimeout(remintNow, delay);
|
|
465
538
|
};
|
|
466
539
|
const startPhone = () => __awaiter(this, void 0, void 0, function* () {
|
|
467
540
|
relaySession === null || relaySession === void 0 ? void 0 : relaySession.cancel();
|
|
468
|
-
|
|
541
|
+
clearRemint();
|
|
469
542
|
if (walletUrl === undefined) {
|
|
470
543
|
renderError('No wallet is configured.');
|
|
471
544
|
return;
|
|
@@ -499,14 +572,14 @@ export function runConnectModal(options) {
|
|
|
499
572
|
},
|
|
500
573
|
onClaimed: () => {
|
|
501
574
|
if (session.active && !settled) {
|
|
502
|
-
// The
|
|
575
|
+
// The wallet has the request — stop re-minting (a new pointer
|
|
503
576
|
// would orphan the approval in progress) and show progress.
|
|
504
|
-
|
|
577
|
+
clearRemint();
|
|
505
578
|
renderClaimed();
|
|
506
579
|
}
|
|
507
580
|
},
|
|
508
581
|
requestPin: (attempt, previousError) => {
|
|
509
|
-
|
|
582
|
+
clearRemint();
|
|
510
583
|
return renderPin(attempt, previousError);
|
|
511
584
|
},
|
|
512
585
|
});
|
|
@@ -533,7 +606,7 @@ export function runConnectModal(options) {
|
|
|
533
606
|
const startPopup = () => {
|
|
534
607
|
// Synchronous popup open inside the click's call stack.
|
|
535
608
|
relaySession === null || relaySession === void 0 ? void 0 : relaySession.cancel();
|
|
536
|
-
|
|
609
|
+
clearRemint();
|
|
537
610
|
if (walletUrl === undefined) {
|
|
538
611
|
renderError('No wallet is configured.');
|
|
539
612
|
return;
|
|
@@ -591,7 +664,7 @@ export function runConnectModal(options) {
|
|
|
591
664
|
if (method === 'browser') {
|
|
592
665
|
// window.open must stay inside a user gesture — prompt for the click.
|
|
593
666
|
relaySession === null || relaySession === void 0 ? void 0 : relaySession.cancel();
|
|
594
|
-
|
|
667
|
+
clearRemint();
|
|
595
668
|
renderFooter();
|
|
596
669
|
renderPopupPrompt();
|
|
597
670
|
}
|
|
@@ -620,15 +693,17 @@ export function runConnectModal(options) {
|
|
|
620
693
|
if (method === 'phone') {
|
|
621
694
|
const alt = document.createElement('button');
|
|
622
695
|
alt.className = 'footer-link method-link';
|
|
623
|
-
|
|
696
|
+
// On a phone both methods open the wallet on this device — what the
|
|
697
|
+
// popup channel actually buys the visitor is skipping the code.
|
|
698
|
+
alt.textContent = isMobile ? 'Or connect without a code →' : 'No phone? Use this browser →';
|
|
624
699
|
// startPopup runs synchronously in this click handler.
|
|
625
700
|
alt.addEventListener('click', () => { startPopup(); });
|
|
626
701
|
row.appendChild(alt);
|
|
627
702
|
}
|
|
628
|
-
else
|
|
703
|
+
else {
|
|
629
704
|
const alt = document.createElement('button');
|
|
630
705
|
alt.className = 'footer-link method-link';
|
|
631
|
-
alt.textContent = 'Use your phone instead →';
|
|
706
|
+
alt.textContent = isMobile ? 'Use a code instead →' : 'Use your phone instead →';
|
|
632
707
|
alt.addEventListener('click', () => { void switchMethod('phone'); });
|
|
633
708
|
row.appendChild(alt);
|
|
634
709
|
}
|
|
@@ -637,7 +712,7 @@ export function runConnectModal(options) {
|
|
|
637
712
|
}
|
|
638
713
|
};
|
|
639
714
|
const buildWalletSwitcher = () => {
|
|
640
|
-
var _a;
|
|
715
|
+
var _a, _b;
|
|
641
716
|
// Identity row: selected wallet + the next catalog wallets, then the
|
|
642
717
|
// More tile expanding the full catalog in place.
|
|
643
718
|
const row = document.createElement('div');
|
|
@@ -666,26 +741,31 @@ export function runConnectModal(options) {
|
|
|
666
741
|
const panel = document.createElement('div');
|
|
667
742
|
panel.className = 'wallet-panel';
|
|
668
743
|
panel.hidden = true;
|
|
669
|
-
|
|
744
|
+
// The panel grid holds only the wallets NOT already visible in the
|
|
745
|
+
// row, so expanding never shows the same wallet twice.
|
|
746
|
+
const gridWallets = wallets.filter((wallet) => !rowWallets.some((shown) => shown.url === wallet.url));
|
|
670
747
|
const more = document.createElement('button');
|
|
671
748
|
more.className = 'row-tile more-tile';
|
|
672
749
|
more.title = 'All wallets';
|
|
673
750
|
more.setAttribute('aria-expanded', 'false');
|
|
674
|
-
more.appendChild(el('span', 'more-count',
|
|
751
|
+
more.appendChild(el('span', 'more-count', gridWallets.length > 0 ? `+${gridWallets.length}` : '⋯'));
|
|
675
752
|
more.appendChild(el('span', 'row-tile-name', 'More'));
|
|
676
753
|
more.addEventListener('click', () => {
|
|
677
754
|
panel.hidden = !panel.hidden;
|
|
678
755
|
more.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true');
|
|
756
|
+
if (!panel.hidden) {
|
|
757
|
+
updateScrollHint();
|
|
758
|
+
}
|
|
679
759
|
});
|
|
680
760
|
row.appendChild(more);
|
|
681
761
|
const label = rowWallets.length > 0 ? el('div', 'wallet-row-label', 'Connecting with') : undefined;
|
|
682
|
-
// Search filter — appears once the
|
|
762
|
+
// Search filter — appears once the grid outgrows one row of tiles.
|
|
683
763
|
const tiles = [];
|
|
684
764
|
const empty = document.createElement('div');
|
|
685
765
|
empty.className = 'wallet-empty';
|
|
686
|
-
empty.textContent = 'No wallets match your search.';
|
|
766
|
+
empty.textContent = 'No other wallets match your search.';
|
|
687
767
|
empty.hidden = true;
|
|
688
|
-
if (
|
|
768
|
+
if (gridWallets.length > WALLET_SEARCH_THRESHOLD) {
|
|
689
769
|
const search = document.createElement('input');
|
|
690
770
|
search.type = 'search';
|
|
691
771
|
search.className = 'wallet-search';
|
|
@@ -704,28 +784,34 @@ export function runConnectModal(options) {
|
|
|
704
784
|
}
|
|
705
785
|
}
|
|
706
786
|
empty.hidden = visible > 0;
|
|
787
|
+
updateScrollHint();
|
|
707
788
|
});
|
|
708
789
|
panel.appendChild(search);
|
|
709
790
|
}
|
|
710
|
-
//
|
|
711
|
-
// place
|
|
791
|
+
// Tiles matching the identity row, four per row; past three rows the
|
|
792
|
+
// grid scrolls in place, with a bottom fade hinting at more below.
|
|
793
|
+
const scrollWrap = document.createElement('div');
|
|
794
|
+
scrollWrap.className = 'wallet-scroll-wrap';
|
|
795
|
+
scrollWrap.hidden = gridWallets.length === 0;
|
|
712
796
|
const scroll = document.createElement('div');
|
|
713
797
|
scroll.className = 'wallet-scroll';
|
|
714
798
|
const grid = document.createElement('div');
|
|
715
799
|
grid.className = 'wallet-grid';
|
|
716
|
-
|
|
800
|
+
const updateScrollHint = () => {
|
|
801
|
+
const overflowing = scroll.scrollHeight - scroll.clientHeight > 1;
|
|
802
|
+
const atEnd = scroll.scrollTop + scroll.clientHeight >= scroll.scrollHeight - 1;
|
|
803
|
+
scrollWrap.classList.toggle('scroll-hint', overflowing && !atEnd);
|
|
804
|
+
};
|
|
805
|
+
scroll.addEventListener('scroll', updateScrollHint);
|
|
806
|
+
for (const wallet of gridWallets) {
|
|
717
807
|
const tile = document.createElement('button');
|
|
718
808
|
tile.className = 'wallet-tile';
|
|
719
|
-
tile.title = wallet.name;
|
|
809
|
+
tile.title = (_b = wallet.description) !== null && _b !== void 0 ? _b : wallet.name;
|
|
720
810
|
tile.appendChild(buildWalletIcon(wallet));
|
|
721
811
|
const tileName = document.createElement('span');
|
|
722
812
|
tileName.className = 'wallet-tile-name';
|
|
723
813
|
tileName.textContent = wallet.name;
|
|
724
814
|
tile.appendChild(tileName);
|
|
725
|
-
if (wallet.url === walletUrl) {
|
|
726
|
-
tile.classList.add('selected');
|
|
727
|
-
tile.setAttribute('aria-pressed', 'true');
|
|
728
|
-
}
|
|
729
815
|
tile.addEventListener('click', () => {
|
|
730
816
|
selectWallet(wallet.url);
|
|
731
817
|
});
|
|
@@ -734,7 +820,8 @@ export function runConnectModal(options) {
|
|
|
734
820
|
}
|
|
735
821
|
scroll.appendChild(grid);
|
|
736
822
|
scroll.appendChild(empty);
|
|
737
|
-
|
|
823
|
+
scrollWrap.appendChild(scroll);
|
|
824
|
+
panel.appendChild(scrollWrap);
|
|
738
825
|
// Custom wallet URL (the power-user path, demoted to the disclosure).
|
|
739
826
|
const custom = document.createElement('div');
|
|
740
827
|
custom.className = 'wallet-custom';
|
|
@@ -819,6 +906,26 @@ export function runConnectModal(options) {
|
|
|
819
906
|
}
|
|
820
907
|
};
|
|
821
908
|
document.addEventListener('keydown', onKeydown);
|
|
909
|
+
// The visitor typically comes back from the wallet tab mid-session.
|
|
910
|
+
const onVisibilityReturn = () => {
|
|
911
|
+
var _a;
|
|
912
|
+
if (document.visibilityState !== 'visible' || settled) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
// Background tabs throttle (or freeze) timers, so a re-mint that came
|
|
916
|
+
// due while the user was away fires now instead of never.
|
|
917
|
+
if (remintAt !== undefined && Date.now() >= remintAt) {
|
|
918
|
+
remintNow();
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
// Returning with the code in hand: put the caret in the first empty
|
|
922
|
+
// pairing-code box so they can type it straight away.
|
|
923
|
+
if (pinResolve !== undefined) {
|
|
924
|
+
const inputs = Array.from(shadow.querySelectorAll('.pin-input'));
|
|
925
|
+
(_a = inputs.find((input) => input.value === '')) === null || _a === void 0 ? void 0 : _a.focus();
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
document.addEventListener('visibilitychange', onVisibilityReturn);
|
|
822
929
|
overlay.appendChild(modal);
|
|
823
930
|
shadow.appendChild(overlay);
|
|
824
931
|
document.body.appendChild(host);
|
|
@@ -1060,6 +1167,31 @@ const MODAL_STYLES = `
|
|
|
1060
1167
|
.qr-logo .wallet-icon { width: 30px; height: 30px; }
|
|
1061
1168
|
.qr-logo .wallet-badge { border-radius: 8px; font-size: 15px; }
|
|
1062
1169
|
|
|
1170
|
+
/* The QR card doubles as the same-device handoff link. */
|
|
1171
|
+
a.qr-box { display: block; cursor: pointer; }
|
|
1172
|
+
.qr-box.qr-link:hover {
|
|
1173
|
+
border-color: var(--ec-accent);
|
|
1174
|
+
box-shadow: 0 0 0 3px var(--ec-accent-soft);
|
|
1175
|
+
}
|
|
1176
|
+
.qr-box.compact { width: 160px; height: 160px; }
|
|
1177
|
+
.qr-box.compact .qr-logo { width: 36px; height: 36px; }
|
|
1178
|
+
.qr-box.compact .qr-logo .wallet-icon { width: 24px; height: 24px; }
|
|
1179
|
+
|
|
1180
|
+
.stage-divider {
|
|
1181
|
+
display: flex;
|
|
1182
|
+
align-items: center;
|
|
1183
|
+
gap: 10px;
|
|
1184
|
+
width: 100%;
|
|
1185
|
+
color: var(--ec-muted);
|
|
1186
|
+
font-size: 12px;
|
|
1187
|
+
}
|
|
1188
|
+
.stage-divider::before, .stage-divider::after {
|
|
1189
|
+
content: '';
|
|
1190
|
+
flex: 1;
|
|
1191
|
+
height: 1px;
|
|
1192
|
+
background: var(--ec-border);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1063
1195
|
.claimed-pulse {
|
|
1064
1196
|
width: 64px; height: 64px; border-radius: 50%;
|
|
1065
1197
|
background: var(--ec-accent-soft); position: relative;
|
|
@@ -1176,7 +1308,7 @@ const MODAL_STYLES = `
|
|
|
1176
1308
|
grid-template-columns: repeat(4, 1fr);
|
|
1177
1309
|
gap: 8px;
|
|
1178
1310
|
}
|
|
1179
|
-
.row-tile {
|
|
1311
|
+
.row-tile, .wallet-tile {
|
|
1180
1312
|
display: flex;
|
|
1181
1313
|
flex-direction: column;
|
|
1182
1314
|
align-items: center;
|
|
@@ -1191,15 +1323,15 @@ const MODAL_STYLES = `
|
|
|
1191
1323
|
font-size: 11px;
|
|
1192
1324
|
cursor: pointer;
|
|
1193
1325
|
}
|
|
1194
|
-
.row-tile:hover { border-color: var(--ec-accent); }
|
|
1326
|
+
.row-tile:hover, .wallet-tile:hover { border-color: var(--ec-accent); }
|
|
1195
1327
|
.row-tile.selected {
|
|
1196
1328
|
border-color: var(--ec-accent);
|
|
1197
1329
|
background: var(--ec-accent-soft);
|
|
1198
1330
|
box-shadow: inset 0 0 0 1px var(--ec-accent);
|
|
1199
1331
|
}
|
|
1200
1332
|
.row-tile.selected .row-tile-name { font-weight: 600; }
|
|
1201
|
-
.row-tile .wallet-icon { width: 28px; height: 28px; }
|
|
1202
|
-
.row-tile-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1333
|
+
.row-tile .wallet-icon, .wallet-tile .wallet-icon { width: 28px; height: 28px; }
|
|
1334
|
+
.row-tile-name, .wallet-tile-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1203
1335
|
|
|
1204
1336
|
.more-tile { color: var(--ec-muted); }
|
|
1205
1337
|
.more-tile:hover { color: var(--ec-text); }
|
|
@@ -1225,46 +1357,33 @@ const MODAL_STYLES = `
|
|
|
1225
1357
|
}
|
|
1226
1358
|
.wallet-search:focus { outline: none; border-color: var(--ec-accent); }
|
|
1227
1359
|
|
|
1228
|
-
|
|
1360
|
+
/* Cap the grid at three rows of tiles; past that it scrolls in place,
|
|
1361
|
+
and the wrapper's bottom fade hints that more wallets are below. */
|
|
1362
|
+
.wallet-scroll-wrap { position: relative; }
|
|
1363
|
+
.wallet-scroll { max-height: 220px; overflow-y: auto; }
|
|
1364
|
+
.wallet-scroll-wrap.scroll-hint::after {
|
|
1365
|
+
content: '';
|
|
1366
|
+
position: absolute;
|
|
1367
|
+
left: 0;
|
|
1368
|
+
right: 0;
|
|
1369
|
+
bottom: 0;
|
|
1370
|
+
height: 28px;
|
|
1371
|
+
background: linear-gradient(to bottom, transparent, var(--ec-bg));
|
|
1372
|
+
pointer-events: none;
|
|
1373
|
+
}
|
|
1229
1374
|
.wallet-grid {
|
|
1230
1375
|
display: grid;
|
|
1231
|
-
grid-template-columns: repeat(
|
|
1232
|
-
gap: 8px;
|
|
1233
|
-
}
|
|
1234
|
-
.wallet-tile {
|
|
1235
|
-
aspect-ratio: 1;
|
|
1236
|
-
display: flex;
|
|
1237
|
-
flex-direction: column;
|
|
1238
|
-
align-items: center;
|
|
1239
|
-
justify-content: center;
|
|
1376
|
+
grid-template-columns: repeat(4, 1fr);
|
|
1240
1377
|
gap: 8px;
|
|
1241
|
-
padding: 8px;
|
|
1242
|
-
min-width: 0;
|
|
1243
|
-
background: var(--ec-surface);
|
|
1244
|
-
border: 1px solid var(--ec-border);
|
|
1245
|
-
border-radius: 12px;
|
|
1246
|
-
color: var(--ec-text);
|
|
1247
|
-
font-size: 12px;
|
|
1248
|
-
cursor: pointer;
|
|
1249
|
-
}
|
|
1250
|
-
.wallet-tile:hover { border-color: var(--ec-accent); }
|
|
1251
|
-
.wallet-tile.selected {
|
|
1252
|
-
border-color: var(--ec-accent);
|
|
1253
|
-
background: var(--ec-accent-soft);
|
|
1254
|
-
box-shadow: inset 0 0 0 1px var(--ec-accent);
|
|
1255
1378
|
}
|
|
1256
|
-
.wallet-tile-name { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1257
1379
|
|
|
1258
1380
|
.wallet-icon { position: relative; width: 28px; height: 28px; flex-shrink: 0; display: inline-flex; }
|
|
1259
|
-
.wallet-tile .wallet-icon { width: 40px; height: 40px; }
|
|
1260
1381
|
.wallet-badge {
|
|
1261
1382
|
width: 100%; height: 100%; border-radius: 8px; display: inline-flex;
|
|
1262
1383
|
align-items: center; justify-content: center; font-size: 13px;
|
|
1263
1384
|
font-weight: 600; background: var(--ec-accent-soft); color: var(--ec-accent);
|
|
1264
1385
|
}
|
|
1265
|
-
.wallet-tile .wallet-badge { border-radius: 10px; font-size: 17px; }
|
|
1266
1386
|
.wallet-img { position: absolute; inset: 0; width: 100%; height: 100%; border-radius: 8px; object-fit: cover; }
|
|
1267
|
-
.wallet-tile .wallet-img { border-radius: 10px; }
|
|
1268
1387
|
|
|
1269
1388
|
.wallet-empty { padding: 10px 4px; font-size: 12px; color: var(--ec-muted); text-align: center; }
|
|
1270
1389
|
|