@enbox/browser 0.3.43 → 0.3.44
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 +144 -154
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/browser-connect-handler.js +17 -20
- package/dist/esm/browser-connect-handler.js.map +1 -1
- package/dist/esm/dweb-connect-client.js +1 -1
- package/dist/esm/dweb-connect-client.js.map +1 -1
- package/dist/esm/index.js +4 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/relay-connect-runner.js +127 -0
- package/dist/esm/relay-connect-runner.js.map +1 -0
- package/dist/esm/ui/connect-modal.js +961 -0
- package/dist/esm/ui/connect-modal.js.map +1 -0
- package/dist/esm/ui/qr.js +468 -0
- package/dist/esm/ui/qr.js.map +1 -0
- package/dist/esm/ui/wallet-icon.js +55 -0
- package/dist/esm/ui/wallet-icon.js.map +1 -0
- package/dist/esm/ui/wallet-well-known.js +61 -0
- package/dist/esm/ui/wallet-well-known.js.map +1 -0
- package/dist/types/browser-connect-handler.d.ts +32 -7
- package/dist/types/browser-connect-handler.d.ts.map +1 -1
- package/dist/types/dweb-connect-client.d.ts +3 -1
- package/dist/types/dweb-connect-client.d.ts.map +1 -1
- package/dist/types/index.d.ts +8 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/relay-connect-runner.d.ts +76 -0
- package/dist/types/relay-connect-runner.d.ts.map +1 -0
- package/dist/types/ui/connect-modal.d.ts +108 -0
- package/dist/types/ui/connect-modal.d.ts.map +1 -0
- package/dist/types/ui/qr.d.ts +55 -0
- package/dist/types/ui/qr.d.ts.map +1 -0
- package/dist/types/ui/wallet-icon.d.ts +13 -0
- package/dist/types/ui/wallet-icon.d.ts.map +1 -0
- package/dist/types/ui/wallet-well-known.d.ts +33 -0
- package/dist/types/ui/wallet-well-known.d.ts.map +1 -0
- package/package.json +3 -1
- package/src/browser-connect-handler.ts +51 -21
- package/src/dweb-connect-client.ts +1 -1
- package/src/index.ts +8 -2
- package/src/relay-connect-runner.ts +201 -0
- package/src/ui/connect-modal.ts +1167 -0
- package/src/ui/qr.ts +549 -0
- package/src/ui/wallet-icon.ts +63 -0
- package/src/ui/wallet-well-known.ts +57 -0
- package/dist/esm/ui/wallet-selector.js +0 -585
- package/dist/esm/ui/wallet-selector.js.map +0 -1
- package/dist/types/ui/wallet-selector.d.ts +0 -44
- package/dist/types/ui/wallet-selector.d.ts.map +0 -1
- package/src/ui/wallet-selector.ts +0 -658
|
@@ -0,0 +1,961 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connect modal — the single surface that owns a whole connect session.
|
|
3
|
+
*
|
|
4
|
+
* One Shadow-DOM dialog whose stage morphs in place through the session:
|
|
5
|
+
* QR (phone path) → pairing code → connected, with the in-browser popup
|
|
6
|
+
* path and the wallet catalog folded in as in-place alternates. The modal
|
|
7
|
+
* never chains into a second modal and stays mounted until the session
|
|
8
|
+
* resolves (connected / denied / cancelled).
|
|
9
|
+
*
|
|
10
|
+
* Layout: three zones —
|
|
11
|
+
*
|
|
12
|
+
* 1. **Header** — requesting app identity + close.
|
|
13
|
+
* 2. **Stage** — a height-stable region where states crossfade: the QR
|
|
14
|
+
* (or deep link on phones), the pairing-code input, progress, success,
|
|
15
|
+
* and error states.
|
|
16
|
+
* 3. **Footer** — the alternate-method link and the wallet switcher
|
|
17
|
+
* disclosure (catalog + custom URL — the entire legacy selector,
|
|
18
|
+
* demoted to an option).
|
|
19
|
+
*
|
|
20
|
+
* The phone path drives the `@enbox/connect` relay transport via
|
|
21
|
+
* {@link runRelayConnect}; the browser path drives the popup transport via
|
|
22
|
+
* the kernel `ConnectClient`. Wallet `connectServerUrl`s are discovered
|
|
23
|
+
* from each wallet's `/.well-known/enbox-connect` document unless
|
|
24
|
+
* overridden.
|
|
25
|
+
*
|
|
26
|
+
* @module
|
|
27
|
+
*/
|
|
28
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
29
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
30
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
31
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
32
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
33
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
34
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
import { ConnectClient } from '@enbox/connect';
|
|
38
|
+
import { buildWalletIcon } from './wallet-icon.js';
|
|
39
|
+
import { collectBrowserClientMetadata, PopupClientTransport, PopupWindowClosedError, } from '../dweb-connect-client.js';
|
|
40
|
+
import { encodeQr, qrToSvg } from './qr.js';
|
|
41
|
+
import { fetchWalletWellKnown, probeWalletWellKnown } from './wallet-well-known.js';
|
|
42
|
+
import { RelayConnectCancelledError, runRelayConnect } from '../relay-connect-runner.js';
|
|
43
|
+
/** localStorage key remembering the last successful method + wallet. */
|
|
44
|
+
const LAST_CHOICE_STORAGE_KEY = 'enbox:connect:lastChoice';
|
|
45
|
+
/** Safety margin subtracted from the relay pointer TTL before re-minting. */
|
|
46
|
+
const REMINT_SAFETY_MS = 30000;
|
|
47
|
+
/** Minimum delay before an automatic re-mint. */
|
|
48
|
+
const REMINT_MIN_MS = 10000;
|
|
49
|
+
/** Pairing code length (matches the wallet's generated PIN). */
|
|
50
|
+
const PIN_LENGTH = 4;
|
|
51
|
+
/**
|
|
52
|
+
* Resolves a wallet origin to its relay `connectServerUrl` via the wallet's
|
|
53
|
+
* `/.well-known/enbox-connect` document ({@link fetchWalletWellKnown} — the
|
|
54
|
+
* same machinery the wallet selector uses for custom-URL validation).
|
|
55
|
+
*/
|
|
56
|
+
export function discoverWalletConnectServerUrl(walletOrigin) {
|
|
57
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
58
|
+
var _a;
|
|
59
|
+
return (_a = (yield fetchWalletWellKnown(walletOrigin))) === null || _a === void 0 ? void 0 : _a.connectServerUrl;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Runs one popup handshake without collapsing early-close into a denial. */
|
|
63
|
+
function runPopupConnect(options) {
|
|
64
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
65
|
+
// The transport constructor calls `window.open` synchronously — callers
|
|
66
|
+
// must invoke this inside the user-gesture call stack.
|
|
67
|
+
const transport = new PopupClientTransport({
|
|
68
|
+
walletUrl: options.walletUrl,
|
|
69
|
+
timeoutMs: options.timeoutMs,
|
|
70
|
+
});
|
|
71
|
+
const client = new ConnectClient({ transport });
|
|
72
|
+
return yield client.connect({
|
|
73
|
+
appName: options.appName,
|
|
74
|
+
appIcon: options.appIcon,
|
|
75
|
+
clientMetadata: collectBrowserClientMetadata(),
|
|
76
|
+
permissionRequests: options.permissionRequests,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function defaultIsMobile() {
|
|
81
|
+
var _a, _b;
|
|
82
|
+
if (typeof window === 'undefined') {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
const coarse = (_b = (_a = window.matchMedia) === null || _a === void 0 ? void 0 : _a.call(window, '(pointer: coarse)').matches) !== null && _b !== void 0 ? _b : false;
|
|
86
|
+
return coarse && Math.min(window.innerWidth, window.innerHeight) < 900;
|
|
87
|
+
}
|
|
88
|
+
function defaultDeps() {
|
|
89
|
+
return {
|
|
90
|
+
runRelay: runRelayConnect,
|
|
91
|
+
runPopup: runPopupConnect,
|
|
92
|
+
discoverConnectServerUrl: discoverWalletConnectServerUrl,
|
|
93
|
+
validateWalletUrl: probeWalletWellKnown,
|
|
94
|
+
isMobile: defaultIsMobile,
|
|
95
|
+
storage: typeof localStorage === 'undefined' ? undefined : localStorage,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function readLastChoice(storage) {
|
|
99
|
+
try {
|
|
100
|
+
const raw = storage === null || storage === void 0 ? void 0 : storage.getItem(LAST_CHOICE_STORAGE_KEY);
|
|
101
|
+
if (raw == null) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const parsed = JSON.parse(raw);
|
|
105
|
+
if ((parsed.method === 'phone' || parsed.method === 'browser') && typeof parsed.walletUrl === 'string') {
|
|
106
|
+
return { method: parsed.method, walletUrl: parsed.walletUrl };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch ( /* unreadable storage — behave as first visit */_a) { /* unreadable storage — behave as first visit */ }
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
function writeLastChoice(storage, choice) {
|
|
113
|
+
try {
|
|
114
|
+
storage === null || storage === void 0 ? void 0 : storage.setItem(LAST_CHOICE_STORAGE_KEY, JSON.stringify(choice));
|
|
115
|
+
}
|
|
116
|
+
catch ( /* best effort */_a) { /* best effort */ }
|
|
117
|
+
}
|
|
118
|
+
function createRelaySession() {
|
|
119
|
+
let cancel;
|
|
120
|
+
const session = { active: true };
|
|
121
|
+
const cancelled = new Promise((_, reject) => {
|
|
122
|
+
cancel = () => {
|
|
123
|
+
if (session.active === true) {
|
|
124
|
+
session.active = false;
|
|
125
|
+
reject(new RelayConnectCancelledError());
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
// Mark handled so an unconsumed cancellation never surfaces as unhandled.
|
|
130
|
+
cancelled.catch(() => undefined);
|
|
131
|
+
session.cancelled = cancelled;
|
|
132
|
+
session.cancel = cancel;
|
|
133
|
+
return session;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Opens the connect modal and resolves when the session ends.
|
|
137
|
+
*
|
|
138
|
+
* @returns The delegated credentials; `undefined` when the user denied the
|
|
139
|
+
* request in the wallet (or dismissed after a denial).
|
|
140
|
+
* @throws When the user cancels the modal, or the session fails and the
|
|
141
|
+
* user dismisses the error.
|
|
142
|
+
*/
|
|
143
|
+
export function runConnectModal(options) {
|
|
144
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
145
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
146
|
+
throw new Error('[@enbox/browser] The connect modal is only available in browser environments.');
|
|
147
|
+
}
|
|
148
|
+
const deps = Object.assign(Object.assign({}, defaultDeps()), options.deps);
|
|
149
|
+
const wallets = (_a = options.wallets) !== null && _a !== void 0 ? _a : [];
|
|
150
|
+
const rememberChoice = (_b = options.rememberChoice) !== null && _b !== void 0 ? _b : true;
|
|
151
|
+
const appName = (_c = options.appName) !== null && _c !== void 0 ? _c : window.location.host;
|
|
152
|
+
const relayWalletPath = (_d = options.relayWalletPath) !== null && _d !== void 0 ? _d : '/connect/app';
|
|
153
|
+
const isMobile = deps.isMobile();
|
|
154
|
+
const lastChoice = rememberChoice ? readLastChoice(deps.storage) : undefined;
|
|
155
|
+
// Wallet resolution order: explicit option → remembered → catalog head.
|
|
156
|
+
const lockedWallet = options.walletUrl !== undefined;
|
|
157
|
+
let walletUrl = (_f = (_e = options.walletUrl) !== null && _e !== void 0 ? _e : (lastChoice !== undefined && (lockedWallet || walletInCatalog(wallets, lastChoice.walletUrl)) ? lastChoice.walletUrl : undefined)) !== null && _f !== void 0 ? _f : (_g = wallets[0]) === null || _g === void 0 ? void 0 : _g.url;
|
|
158
|
+
// Method resolution order: explicit option → remembered → phone.
|
|
159
|
+
let method = (_j = (_h = options.preferredMethod) !== null && _h !== void 0 ? _h : lastChoice === null || lastChoice === void 0 ? void 0 : lastChoice.method) !== null && _j !== void 0 ? _j : 'phone';
|
|
160
|
+
return new Promise((resolve, reject) => {
|
|
161
|
+
var _a, _b;
|
|
162
|
+
// ── Host + shadow root ─────────────────────────────────────
|
|
163
|
+
const host = document.createElement('div');
|
|
164
|
+
host.id = 'enbox-connect-modal';
|
|
165
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
166
|
+
const isDark = (_b = (_a = window.matchMedia) === null || _a === void 0 ? void 0 : _a.call(window, '(prefers-color-scheme: dark)').matches) !== null && _b !== void 0 ? _b : false;
|
|
167
|
+
const style = document.createElement('style');
|
|
168
|
+
style.textContent = buildStyles(isDark);
|
|
169
|
+
shadow.appendChild(style);
|
|
170
|
+
const overlay = document.createElement('div');
|
|
171
|
+
overlay.className = 'overlay';
|
|
172
|
+
const modal = document.createElement('div');
|
|
173
|
+
modal.className = 'modal';
|
|
174
|
+
modal.setAttribute('role', 'dialog');
|
|
175
|
+
modal.setAttribute('aria-modal', 'true');
|
|
176
|
+
modal.setAttribute('aria-label', `Connect to ${appName}`);
|
|
177
|
+
// ── Session state ──────────────────────────────────────────
|
|
178
|
+
let settled = false;
|
|
179
|
+
let relaySession;
|
|
180
|
+
let remintTimer;
|
|
181
|
+
let popupBusy = false;
|
|
182
|
+
let pinResolve;
|
|
183
|
+
const discoveryCache = new Map();
|
|
184
|
+
const cleanup = () => {
|
|
185
|
+
relaySession === null || relaySession === void 0 ? void 0 : relaySession.cancel();
|
|
186
|
+
clearTimeout(remintTimer);
|
|
187
|
+
document.removeEventListener('keydown', onKeydown);
|
|
188
|
+
try {
|
|
189
|
+
document.body.removeChild(host);
|
|
190
|
+
}
|
|
191
|
+
catch ( /* best effort */_a) { /* best effort */ }
|
|
192
|
+
};
|
|
193
|
+
const settle = (settler) => {
|
|
194
|
+
if (settled) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
settled = true;
|
|
198
|
+
cleanup();
|
|
199
|
+
settler();
|
|
200
|
+
};
|
|
201
|
+
const succeed = (result) => {
|
|
202
|
+
if (rememberChoice) {
|
|
203
|
+
writeLastChoice(deps.storage, { method, walletUrl: walletUrl !== null && walletUrl !== void 0 ? walletUrl : '' });
|
|
204
|
+
}
|
|
205
|
+
renderConnected();
|
|
206
|
+
const finish = () => settle(() => resolve(result));
|
|
207
|
+
setTimeout(finish, 1200);
|
|
208
|
+
};
|
|
209
|
+
const cancelModal = () => settle(() => reject(new Error('[@enbox/browser] Connect cancelled.')));
|
|
210
|
+
// ── Zones ──────────────────────────────────────────────────
|
|
211
|
+
modal.appendChild(buildHeader(appName, options.appIcon, cancelModal));
|
|
212
|
+
const stage = document.createElement('div');
|
|
213
|
+
stage.className = 'stage';
|
|
214
|
+
stage.setAttribute('aria-live', 'polite');
|
|
215
|
+
modal.appendChild(stage);
|
|
216
|
+
const footer = document.createElement('div');
|
|
217
|
+
footer.className = 'footer';
|
|
218
|
+
modal.appendChild(footer);
|
|
219
|
+
// ── Stage renderers (all render in place; nothing navigates) ──
|
|
220
|
+
const setStage = (...nodes) => {
|
|
221
|
+
stage.replaceChildren(...nodes);
|
|
222
|
+
stage.classList.remove('fade-in');
|
|
223
|
+
void stage.offsetWidth; // restart the transition
|
|
224
|
+
stage.classList.add('fade-in');
|
|
225
|
+
};
|
|
226
|
+
const renderBusy = (message) => {
|
|
227
|
+
setStage(el('div', 'spinner'), el('p', 'stage-caption', message));
|
|
228
|
+
};
|
|
229
|
+
const renderQr = (handoff) => {
|
|
230
|
+
if (isMobile) {
|
|
231
|
+
const link = document.createElement('a');
|
|
232
|
+
link.className = 'deep-link';
|
|
233
|
+
link.href = handoff.walletUri;
|
|
234
|
+
link.textContent = 'Continue in your wallet';
|
|
235
|
+
setStage(link, el('p', 'stage-caption', 'Your wallet opens to approve this connection.'), el('p', 'stage-subline', 'Come back here when you’re done.'));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const qrBox = document.createElement('div');
|
|
239
|
+
qrBox.className = 'qr-box';
|
|
240
|
+
qrBox.replaceChildren(qrToSvg(encodeQr(handoff.walletUri), {
|
|
241
|
+
dark: isDark ? '#e8e8f0' : '#14141f',
|
|
242
|
+
light: 'transparent',
|
|
243
|
+
quietZone: 2,
|
|
244
|
+
}));
|
|
245
|
+
setStage(qrBox, el('p', 'stage-caption', 'Scan with your phone’s camera'), el('p', 'stage-subline', 'Your wallet stays on your phone, so you can approve things anywhere.'));
|
|
246
|
+
};
|
|
247
|
+
const renderPin = (attempt, previousError) => {
|
|
248
|
+
return new Promise((resolvePin) => {
|
|
249
|
+
pinResolve = resolvePin;
|
|
250
|
+
const boxes = document.createElement('div');
|
|
251
|
+
boxes.className = 'pin-row';
|
|
252
|
+
if (previousError !== undefined) {
|
|
253
|
+
boxes.classList.add('shake');
|
|
254
|
+
}
|
|
255
|
+
const inputs = [];
|
|
256
|
+
for (let i = 0; i < PIN_LENGTH; i++) {
|
|
257
|
+
const input = document.createElement('input');
|
|
258
|
+
input.className = 'pin-input';
|
|
259
|
+
input.type = 'text';
|
|
260
|
+
input.inputMode = 'numeric';
|
|
261
|
+
input.maxLength = 1;
|
|
262
|
+
input.setAttribute('aria-label', `Code digit ${i + 1}`);
|
|
263
|
+
inputs.push(input);
|
|
264
|
+
boxes.appendChild(input);
|
|
265
|
+
}
|
|
266
|
+
const submitIfComplete = () => {
|
|
267
|
+
const code = inputs.map((i) => i.value).join('');
|
|
268
|
+
if (code.length === PIN_LENGTH && pinResolve !== undefined) {
|
|
269
|
+
const resolveOnce = pinResolve;
|
|
270
|
+
pinResolve = undefined;
|
|
271
|
+
renderBusy('Checking the code…');
|
|
272
|
+
resolveOnce(code);
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
inputs.forEach((input, index) => {
|
|
276
|
+
input.addEventListener('input', () => {
|
|
277
|
+
input.value = input.value.replace(/\D/g, '').slice(0, 1);
|
|
278
|
+
if (input.value !== '' && index + 1 < inputs.length) {
|
|
279
|
+
inputs[index + 1].focus();
|
|
280
|
+
}
|
|
281
|
+
submitIfComplete();
|
|
282
|
+
});
|
|
283
|
+
input.addEventListener('keydown', (event) => {
|
|
284
|
+
if (event.key === 'Backspace' && input.value === '' && index > 0) {
|
|
285
|
+
inputs[index - 1].focus();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
input.addEventListener('paste', (event) => {
|
|
289
|
+
var _a, _b, _c;
|
|
290
|
+
const digits = ((_b = (_a = event.clipboardData) === null || _a === void 0 ? void 0 : _a.getData('text')) !== null && _b !== void 0 ? _b : '').replace(/\D/g, '').slice(0, PIN_LENGTH);
|
|
291
|
+
if (digits.length === 0) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
event.preventDefault();
|
|
295
|
+
digits.split('').forEach((digit, digitIndex) => {
|
|
296
|
+
if (inputs[digitIndex] !== undefined) {
|
|
297
|
+
inputs[digitIndex].value = digit;
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
(_c = inputs[Math.min(digits.length, PIN_LENGTH) - 1]) === null || _c === void 0 ? void 0 : _c.focus();
|
|
301
|
+
submitIfComplete();
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
setStage(el('p', 'stage-caption', 'Enter the code shown on your phone'), boxes, ...(previousError !== undefined
|
|
305
|
+
? [el('p', 'stage-error', 'That code doesn’t match — check your phone.')]
|
|
306
|
+
: [el('p', 'stage-subline', attempt === 1 ? 'Phone connected.' : '')]));
|
|
307
|
+
inputs[0].focus();
|
|
308
|
+
});
|
|
309
|
+
};
|
|
310
|
+
const renderConnected = () => {
|
|
311
|
+
const check = el('div', 'check');
|
|
312
|
+
check.textContent = '✓';
|
|
313
|
+
setStage(check, el('p', 'stage-caption', 'You’re connected.'));
|
|
314
|
+
};
|
|
315
|
+
const renderDenied = () => {
|
|
316
|
+
setStage(el('p', 'stage-caption', 'No problem — nothing was shared.'), stageButton('Start over', () => { void startMethod(); }), stageLinkButton('Close', () => settle(() => resolve(undefined))));
|
|
317
|
+
};
|
|
318
|
+
const renderError = (message, error) => {
|
|
319
|
+
setStage(el('p', 'stage-caption', message), stageButton('Start over', () => { void startMethod(); }), stageLinkButton('Close', () => settle(() => reject(error !== null && error !== void 0 ? error : new Error(`[@enbox/browser] ${message}`)))));
|
|
320
|
+
};
|
|
321
|
+
const renderPhoneUnavailable = () => {
|
|
322
|
+
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(); }));
|
|
323
|
+
};
|
|
324
|
+
const renderPopupPrompt = () => {
|
|
325
|
+
setStage(el('p', 'stage-caption', 'Connect with a wallet in this browser'), el('p', 'stage-subline', 'Your wallet opens in a small window to approve this connection.'), stageButton('Open wallet window', () => { startPopup(); }));
|
|
326
|
+
};
|
|
327
|
+
const renderPopupWaiting = () => {
|
|
328
|
+
setStage(el('div', 'spinner'), el('p', 'stage-caption', 'Finish in the wallet window'), el('p', 'stage-subline', 'We’ll wrap up here as soon as you’re done.'));
|
|
329
|
+
};
|
|
330
|
+
const renderPopupInterrupted = (blocked) => {
|
|
331
|
+
setStage(el('p', 'stage-caption', blocked
|
|
332
|
+
? 'Your browser blocked the wallet window.'
|
|
333
|
+
: 'The wallet window was closed.'), stageButton(blocked ? 'Open it now' : 'Reopen window', () => { startPopup(); }), ...(isMobile ? [] : [stageLinkButton('Use your phone instead', () => { void switchMethod('phone'); })]));
|
|
334
|
+
};
|
|
335
|
+
// ── Method drivers ─────────────────────────────────────────
|
|
336
|
+
const resolveConnectServerUrl = (origin) => __awaiter(this, void 0, void 0, function* () {
|
|
337
|
+
if (options.connectServerUrl !== undefined) {
|
|
338
|
+
return options.connectServerUrl;
|
|
339
|
+
}
|
|
340
|
+
let cached = discoveryCache.get(origin);
|
|
341
|
+
if (cached === undefined) {
|
|
342
|
+
cached = deps.discoverConnectServerUrl(origin);
|
|
343
|
+
discoveryCache.set(origin, cached);
|
|
344
|
+
}
|
|
345
|
+
return cached;
|
|
346
|
+
});
|
|
347
|
+
const scheduleRemint = (expiresInSeconds) => {
|
|
348
|
+
clearTimeout(remintTimer);
|
|
349
|
+
const delay = Math.max(REMINT_MIN_MS, expiresInSeconds * 1000 - REMINT_SAFETY_MS);
|
|
350
|
+
remintTimer = setTimeout(() => {
|
|
351
|
+
// Never re-mint once the wallet has responded (pairing in progress).
|
|
352
|
+
if (!settled && pinResolve === undefined && method === 'phone') {
|
|
353
|
+
void startPhone();
|
|
354
|
+
}
|
|
355
|
+
}, delay);
|
|
356
|
+
};
|
|
357
|
+
const startPhone = () => __awaiter(this, void 0, void 0, function* () {
|
|
358
|
+
relaySession === null || relaySession === void 0 ? void 0 : relaySession.cancel();
|
|
359
|
+
clearTimeout(remintTimer);
|
|
360
|
+
if (walletUrl === undefined) {
|
|
361
|
+
renderError('No wallet is configured.');
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
method = 'phone';
|
|
365
|
+
renderFooter();
|
|
366
|
+
renderBusy('Getting things ready…');
|
|
367
|
+
const origin = new URL(walletUrl).origin;
|
|
368
|
+
const connectServerUrl = yield resolveConnectServerUrl(origin);
|
|
369
|
+
if (connectServerUrl === undefined) {
|
|
370
|
+
renderPhoneUnavailable();
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const session = createRelaySession();
|
|
374
|
+
relaySession = session;
|
|
375
|
+
try {
|
|
376
|
+
const result = yield deps.runRelay({
|
|
377
|
+
connectServerUrl,
|
|
378
|
+
walletUri: new URL(relayWalletPath, origin).toString(),
|
|
379
|
+
appName,
|
|
380
|
+
appIcon: options.appIcon,
|
|
381
|
+
clientMetadata: collectBrowserClientMetadata(),
|
|
382
|
+
permissionRequests: options.permissionRequests,
|
|
383
|
+
timeoutMs: options.timeout,
|
|
384
|
+
cancelled: session.cancelled,
|
|
385
|
+
onWalletUriReady: (handoff) => {
|
|
386
|
+
if (session.active && !settled) {
|
|
387
|
+
renderQr(handoff);
|
|
388
|
+
scheduleRemint(handoff.expiresIn);
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
requestPin: (attempt, previousError) => {
|
|
392
|
+
clearTimeout(remintTimer);
|
|
393
|
+
return renderPin(attempt, previousError);
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
if (!session.active || settled) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
if (result === undefined) {
|
|
400
|
+
renderDenied();
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
succeed(result);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
if (!session.active || settled || error instanceof RelayConnectCancelledError) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
411
|
+
renderError(/timed out/i.test(failure.message)
|
|
412
|
+
? 'That took too long, so we stopped for safety.'
|
|
413
|
+
: 'Something went wrong while connecting.', failure);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
const startPopup = () => {
|
|
417
|
+
// Synchronous popup open inside the click's call stack.
|
|
418
|
+
relaySession === null || relaySession === void 0 ? void 0 : relaySession.cancel();
|
|
419
|
+
clearTimeout(remintTimer);
|
|
420
|
+
if (walletUrl === undefined) {
|
|
421
|
+
renderError('No wallet is configured.');
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (popupBusy) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
method = 'browser';
|
|
428
|
+
renderFooter();
|
|
429
|
+
let flow;
|
|
430
|
+
try {
|
|
431
|
+
flow = deps.runPopup({
|
|
432
|
+
walletUrl,
|
|
433
|
+
permissionRequests: options.permissionRequests,
|
|
434
|
+
appName,
|
|
435
|
+
appIcon: options.appIcon,
|
|
436
|
+
timeoutMs: options.timeout,
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
const blocked = error instanceof Error && /popup blocked/i.test(error.message);
|
|
441
|
+
renderPopupInterrupted(blocked);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
popupBusy = true;
|
|
445
|
+
renderPopupWaiting();
|
|
446
|
+
flow
|
|
447
|
+
.then((result) => {
|
|
448
|
+
if (settled) {
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
if (result === undefined) {
|
|
452
|
+
renderDenied();
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
succeed(result);
|
|
456
|
+
}
|
|
457
|
+
})
|
|
458
|
+
.catch((error) => {
|
|
459
|
+
if (settled) {
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
if (error instanceof PopupWindowClosedError) {
|
|
463
|
+
renderPopupInterrupted(false);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
467
|
+
renderError(/timed out/i.test(failure.message)
|
|
468
|
+
? 'That took too long, so we stopped for safety.'
|
|
469
|
+
: 'Something went wrong while connecting.', failure);
|
|
470
|
+
})
|
|
471
|
+
.finally(() => { popupBusy = false; });
|
|
472
|
+
};
|
|
473
|
+
const startMethod = () => __awaiter(this, void 0, void 0, function* () {
|
|
474
|
+
if (method === 'browser') {
|
|
475
|
+
// window.open must stay inside a user gesture — prompt for the click.
|
|
476
|
+
relaySession === null || relaySession === void 0 ? void 0 : relaySession.cancel();
|
|
477
|
+
clearTimeout(remintTimer);
|
|
478
|
+
renderFooter();
|
|
479
|
+
renderPopupPrompt();
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
yield startPhone();
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
const switchMethod = (next) => __awaiter(this, void 0, void 0, function* () {
|
|
486
|
+
method = next;
|
|
487
|
+
yield startMethod();
|
|
488
|
+
});
|
|
489
|
+
// ── Footer: method link + wallet switcher ──────────────────
|
|
490
|
+
const renderFooter = () => {
|
|
491
|
+
footer.replaceChildren();
|
|
492
|
+
if (method === 'phone') {
|
|
493
|
+
const alt = document.createElement('button');
|
|
494
|
+
alt.className = 'footer-link method-link';
|
|
495
|
+
alt.textContent = 'No phone handy? Use this browser →';
|
|
496
|
+
// startPopup runs synchronously in this click handler.
|
|
497
|
+
alt.addEventListener('click', () => { startPopup(); });
|
|
498
|
+
footer.appendChild(alt);
|
|
499
|
+
}
|
|
500
|
+
else if (!isMobile) {
|
|
501
|
+
const alt = document.createElement('button');
|
|
502
|
+
alt.className = 'footer-link method-link';
|
|
503
|
+
alt.textContent = 'Use your phone instead →';
|
|
504
|
+
alt.addEventListener('click', () => { void switchMethod('phone'); });
|
|
505
|
+
footer.appendChild(alt);
|
|
506
|
+
}
|
|
507
|
+
if (!lockedWallet && wallets.length > 0) {
|
|
508
|
+
footer.appendChild(buildWalletSwitcher());
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
const buildWalletSwitcher = () => {
|
|
512
|
+
var _a, _b;
|
|
513
|
+
const wrap = document.createElement('div');
|
|
514
|
+
wrap.className = 'wallet-switcher';
|
|
515
|
+
const current = wallets.find((wallet) => wallet.url === walletUrl);
|
|
516
|
+
const toggle = document.createElement('button');
|
|
517
|
+
toggle.className = 'footer-link wallet-toggle';
|
|
518
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
519
|
+
toggle.textContent = `Wallet: ${(_b = (_a = current === null || current === void 0 ? void 0 : current.name) !== null && _a !== void 0 ? _a : walletUrl) !== null && _b !== void 0 ? _b : 'Choose'} ▾`;
|
|
520
|
+
const panel = document.createElement('div');
|
|
521
|
+
panel.className = 'wallet-panel';
|
|
522
|
+
panel.hidden = true;
|
|
523
|
+
// Search filter — only worth the chrome once the catalog grows.
|
|
524
|
+
const rows = [];
|
|
525
|
+
const empty = document.createElement('div');
|
|
526
|
+
empty.className = 'wallet-empty';
|
|
527
|
+
empty.textContent = 'No wallets match your search.';
|
|
528
|
+
empty.hidden = true;
|
|
529
|
+
if (wallets.length > 5) {
|
|
530
|
+
const search = document.createElement('input');
|
|
531
|
+
search.type = 'search';
|
|
532
|
+
search.className = 'wallet-search';
|
|
533
|
+
search.placeholder = 'Search wallets…';
|
|
534
|
+
search.addEventListener('input', () => {
|
|
535
|
+
const query = search.value.trim().toLowerCase();
|
|
536
|
+
let visible = 0;
|
|
537
|
+
for (const { wallet, el } of rows) {
|
|
538
|
+
const match = query === ''
|
|
539
|
+
|| wallet.name.toLowerCase().includes(query)
|
|
540
|
+
|| wallet.url.toLowerCase().includes(query);
|
|
541
|
+
el.hidden = !match;
|
|
542
|
+
if (match) {
|
|
543
|
+
visible += 1;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
empty.hidden = visible > 0;
|
|
547
|
+
});
|
|
548
|
+
panel.appendChild(search);
|
|
549
|
+
}
|
|
550
|
+
for (const wallet of wallets) {
|
|
551
|
+
const row = document.createElement('button');
|
|
552
|
+
row.className = 'wallet-row';
|
|
553
|
+
row.appendChild(buildWalletIcon(wallet));
|
|
554
|
+
const rowName = document.createElement('span');
|
|
555
|
+
rowName.className = 'wallet-row-name';
|
|
556
|
+
rowName.textContent = wallet.name;
|
|
557
|
+
row.appendChild(rowName);
|
|
558
|
+
if (wallet.url === walletUrl) {
|
|
559
|
+
row.classList.add('selected');
|
|
560
|
+
}
|
|
561
|
+
row.addEventListener('click', () => {
|
|
562
|
+
walletUrl = wallet.url;
|
|
563
|
+
renderFooter();
|
|
564
|
+
void startMethod();
|
|
565
|
+
});
|
|
566
|
+
rows.push({ wallet, el: row });
|
|
567
|
+
panel.appendChild(row);
|
|
568
|
+
}
|
|
569
|
+
panel.appendChild(empty);
|
|
570
|
+
// Custom wallet URL (the power-user path, demoted to the disclosure).
|
|
571
|
+
const custom = document.createElement('div');
|
|
572
|
+
custom.className = 'wallet-custom';
|
|
573
|
+
const input = document.createElement('input');
|
|
574
|
+
input.className = 'url-input';
|
|
575
|
+
input.type = 'url';
|
|
576
|
+
input.placeholder = 'https://your-wallet.example';
|
|
577
|
+
const go = document.createElement('button');
|
|
578
|
+
go.className = 'url-go';
|
|
579
|
+
go.textContent = 'Use';
|
|
580
|
+
const urlError = document.createElement('div');
|
|
581
|
+
urlError.className = 'url-error';
|
|
582
|
+
urlError.hidden = true;
|
|
583
|
+
// When validation fails we let the user override, since the connect
|
|
584
|
+
// handshake is the ultimate gate — a valid wallet behind strict CORS
|
|
585
|
+
// should not be permanently blocked.
|
|
586
|
+
let allowUnverified = false;
|
|
587
|
+
const useWallet = (origin) => {
|
|
588
|
+
walletUrl = origin;
|
|
589
|
+
renderFooter();
|
|
590
|
+
void startMethod();
|
|
591
|
+
};
|
|
592
|
+
const submitCustom = () => __awaiter(this, void 0, void 0, function* () {
|
|
593
|
+
const value = input.value.trim();
|
|
594
|
+
if (value === '') {
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
let origin;
|
|
598
|
+
try {
|
|
599
|
+
origin = new URL(value.includes('://') ? value : `https://${value}`).origin;
|
|
600
|
+
}
|
|
601
|
+
catch (_a) {
|
|
602
|
+
input.classList.add('invalid');
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
if (allowUnverified) {
|
|
606
|
+
useWallet(origin);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
go.disabled = true;
|
|
610
|
+
go.textContent = 'Checking…';
|
|
611
|
+
urlError.hidden = true;
|
|
612
|
+
const valid = yield deps.validateWalletUrl(origin);
|
|
613
|
+
go.disabled = false;
|
|
614
|
+
if (valid) {
|
|
615
|
+
go.textContent = 'Use';
|
|
616
|
+
useWallet(origin);
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
allowUnverified = true;
|
|
620
|
+
go.textContent = 'Use anyway';
|
|
621
|
+
input.classList.add('invalid');
|
|
622
|
+
urlError.textContent = `Couldn't verify a wallet at ${origin}.`;
|
|
623
|
+
urlError.hidden = false;
|
|
624
|
+
});
|
|
625
|
+
input.addEventListener('input', () => {
|
|
626
|
+
allowUnverified = false;
|
|
627
|
+
go.textContent = 'Use';
|
|
628
|
+
input.classList.remove('invalid');
|
|
629
|
+
urlError.hidden = true;
|
|
630
|
+
});
|
|
631
|
+
input.addEventListener('keydown', (e) => {
|
|
632
|
+
if (e.key === 'Enter') {
|
|
633
|
+
void submitCustom();
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
go.addEventListener('click', () => { void submitCustom(); });
|
|
637
|
+
custom.appendChild(input);
|
|
638
|
+
custom.appendChild(go);
|
|
639
|
+
panel.appendChild(custom);
|
|
640
|
+
panel.appendChild(urlError);
|
|
641
|
+
toggle.addEventListener('click', () => {
|
|
642
|
+
panel.hidden = !panel.hidden;
|
|
643
|
+
toggle.setAttribute('aria-expanded', panel.hidden ? 'false' : 'true');
|
|
644
|
+
});
|
|
645
|
+
wrap.appendChild(toggle);
|
|
646
|
+
wrap.appendChild(panel);
|
|
647
|
+
return wrap;
|
|
648
|
+
};
|
|
649
|
+
// ── Global interactions ────────────────────────────────────
|
|
650
|
+
overlay.addEventListener('click', (event) => {
|
|
651
|
+
if (event.target === overlay) {
|
|
652
|
+
cancelModal();
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
const onKeydown = (event) => {
|
|
656
|
+
if (event.key === 'Escape') {
|
|
657
|
+
cancelModal();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (event.key === 'Tab') {
|
|
661
|
+
trapFocus(shadow, event);
|
|
662
|
+
}
|
|
663
|
+
};
|
|
664
|
+
document.addEventListener('keydown', onKeydown);
|
|
665
|
+
overlay.appendChild(modal);
|
|
666
|
+
shadow.appendChild(overlay);
|
|
667
|
+
document.body.appendChild(host);
|
|
668
|
+
renderFooter();
|
|
669
|
+
void startMethod();
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
// ─── Small DOM helpers ───────────────────────────────────────────
|
|
673
|
+
function walletInCatalog(wallets, url) {
|
|
674
|
+
return wallets.some((wallet) => wallet.url === url);
|
|
675
|
+
}
|
|
676
|
+
function el(tag, className, text) {
|
|
677
|
+
const node = document.createElement(tag);
|
|
678
|
+
node.className = className;
|
|
679
|
+
if (text !== undefined) {
|
|
680
|
+
node.textContent = text;
|
|
681
|
+
}
|
|
682
|
+
return node;
|
|
683
|
+
}
|
|
684
|
+
function stageButton(label, onClick) {
|
|
685
|
+
const button = document.createElement('button');
|
|
686
|
+
button.className = 'stage-btn';
|
|
687
|
+
button.textContent = label;
|
|
688
|
+
button.addEventListener('click', onClick);
|
|
689
|
+
return button;
|
|
690
|
+
}
|
|
691
|
+
function stageLinkButton(label, onClick) {
|
|
692
|
+
const button = document.createElement('button');
|
|
693
|
+
button.className = 'footer-link';
|
|
694
|
+
button.textContent = label;
|
|
695
|
+
button.addEventListener('click', onClick);
|
|
696
|
+
return button;
|
|
697
|
+
}
|
|
698
|
+
function buildHeader(appName, appIcon, onCancel) {
|
|
699
|
+
const header = document.createElement('div');
|
|
700
|
+
header.className = 'header';
|
|
701
|
+
const identity = document.createElement('div');
|
|
702
|
+
identity.className = 'header-identity';
|
|
703
|
+
if (appIcon !== undefined) {
|
|
704
|
+
const icon = document.createElement('img');
|
|
705
|
+
icon.className = 'header-icon';
|
|
706
|
+
icon.src = appIcon;
|
|
707
|
+
icon.alt = '';
|
|
708
|
+
icon.width = 24;
|
|
709
|
+
icon.height = 24;
|
|
710
|
+
icon.addEventListener('error', () => icon.remove());
|
|
711
|
+
identity.appendChild(icon);
|
|
712
|
+
}
|
|
713
|
+
const title = document.createElement('h2');
|
|
714
|
+
title.textContent = `Connect to ${appName}`;
|
|
715
|
+
identity.appendChild(title);
|
|
716
|
+
const closeBtn = document.createElement('button');
|
|
717
|
+
closeBtn.className = 'close-btn';
|
|
718
|
+
closeBtn.setAttribute('aria-label', 'Close');
|
|
719
|
+
closeBtn.textContent = '\u00d7';
|
|
720
|
+
closeBtn.addEventListener('click', onCancel);
|
|
721
|
+
header.appendChild(identity);
|
|
722
|
+
header.appendChild(closeBtn);
|
|
723
|
+
return header;
|
|
724
|
+
}
|
|
725
|
+
function trapFocus(shadow, event) {
|
|
726
|
+
const focusables = Array.from(shadow.querySelectorAll('button, input, a[href], [tabindex]:not([tabindex="-1"])')).filter((node) => !node.hasAttribute('disabled') && node.offsetParent !== null);
|
|
727
|
+
if (focusables.length === 0) {
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
const first = focusables[0];
|
|
731
|
+
const last = focusables[focusables.length - 1];
|
|
732
|
+
const active = shadow.activeElement;
|
|
733
|
+
if (event.shiftKey && (active === first || active === null)) {
|
|
734
|
+
event.preventDefault();
|
|
735
|
+
last.focus();
|
|
736
|
+
}
|
|
737
|
+
else if (!event.shiftKey && active === last) {
|
|
738
|
+
event.preventDefault();
|
|
739
|
+
first.focus();
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
// ─── Styles ──────────────────────────────────────────────────────
|
|
743
|
+
function buildStyles(isDark) {
|
|
744
|
+
const bg = isDark ? '#1a1a2e' : '#ffffff';
|
|
745
|
+
const text = isDark ? '#e0e0e0' : '#1a1a2e';
|
|
746
|
+
const muted = isDark ? '#888' : '#666';
|
|
747
|
+
const border = isDark ? '#333' : '#e0e0e0';
|
|
748
|
+
const itemBg = isDark ? '#16213e' : '#f8f9fa';
|
|
749
|
+
const accent = isDark ? '#4a9eff' : '#0066cc';
|
|
750
|
+
const accentSoft = isDark ? 'rgba(74, 158, 255, 0.16)' : 'rgba(0, 102, 204, 0.1)';
|
|
751
|
+
const danger = isDark ? '#ff6b6b' : '#c0392b';
|
|
752
|
+
const success = isDark ? '#43d17c' : '#1e9e5a';
|
|
753
|
+
return `
|
|
754
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
755
|
+
|
|
756
|
+
.overlay {
|
|
757
|
+
position: fixed;
|
|
758
|
+
inset: 0;
|
|
759
|
+
z-index: 2147483647;
|
|
760
|
+
display: flex;
|
|
761
|
+
align-items: center;
|
|
762
|
+
justify-content: center;
|
|
763
|
+
background: rgba(0, 0, 0, 0.5);
|
|
764
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
765
|
+
animation: fadeIn 0.15s ease-out;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
769
|
+
@keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
|
|
770
|
+
@keyframes stageIn { from { opacity: 0; } to { opacity: 1; } }
|
|
771
|
+
@keyframes shake {
|
|
772
|
+
0%, 100% { transform: translateX(0); }
|
|
773
|
+
20%, 60% { transform: translateX(-6px); }
|
|
774
|
+
40%, 80% { transform: translateX(6px); }
|
|
775
|
+
}
|
|
776
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
777
|
+
|
|
778
|
+
.modal {
|
|
779
|
+
background: ${bg};
|
|
780
|
+
color: ${text};
|
|
781
|
+
border-radius: 16px;
|
|
782
|
+
width: 400px;
|
|
783
|
+
max-width: 92vw;
|
|
784
|
+
max-height: 88vh;
|
|
785
|
+
overflow-y: auto;
|
|
786
|
+
padding: 20px 24px 16px;
|
|
787
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
788
|
+
animation: slideUp 0.2s ease-out;
|
|
789
|
+
display: flex;
|
|
790
|
+
flex-direction: column;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.header { display: flex; justify-content: space-between; align-items: center; }
|
|
794
|
+
.header-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
|
795
|
+
.header-icon { border-radius: 6px; }
|
|
796
|
+
h2 { font-size: 16px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
797
|
+
|
|
798
|
+
.close-btn {
|
|
799
|
+
background: none; border: none; font-size: 24px; cursor: pointer;
|
|
800
|
+
color: ${muted}; padding: 4px 8px; border-radius: 8px; line-height: 1;
|
|
801
|
+
}
|
|
802
|
+
.close-btn:hover { color: ${text}; background: ${itemBg}; }
|
|
803
|
+
|
|
804
|
+
.stage {
|
|
805
|
+
min-height: 300px;
|
|
806
|
+
display: flex;
|
|
807
|
+
flex-direction: column;
|
|
808
|
+
align-items: center;
|
|
809
|
+
justify-content: center;
|
|
810
|
+
gap: 12px;
|
|
811
|
+
text-align: center;
|
|
812
|
+
padding: 16px 0;
|
|
813
|
+
}
|
|
814
|
+
.stage.fade-in > * { animation: stageIn 0.15s ease-out; }
|
|
815
|
+
|
|
816
|
+
.qr-box {
|
|
817
|
+
width: 216px;
|
|
818
|
+
height: 216px;
|
|
819
|
+
padding: 12px;
|
|
820
|
+
border-radius: 12px;
|
|
821
|
+
background: ${isDark ? '#10192e' : '#ffffff'};
|
|
822
|
+
border: 1px solid ${border};
|
|
823
|
+
}
|
|
824
|
+
.qr-box svg { width: 100%; height: 100%; display: block; }
|
|
825
|
+
|
|
826
|
+
.stage-caption { font-size: 15px; font-weight: 600; }
|
|
827
|
+
.stage-subline { font-size: 13px; color: ${muted}; max-width: 300px; line-height: 1.45; }
|
|
828
|
+
.stage-error { font-size: 13px; color: ${danger}; }
|
|
829
|
+
|
|
830
|
+
.deep-link {
|
|
831
|
+
display: block;
|
|
832
|
+
width: 100%;
|
|
833
|
+
padding: 14px 16px;
|
|
834
|
+
border-radius: 12px;
|
|
835
|
+
background: ${accent};
|
|
836
|
+
color: #fff;
|
|
837
|
+
font-weight: 600;
|
|
838
|
+
font-size: 15px;
|
|
839
|
+
text-decoration: none;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.pin-row { display: flex; gap: 10px; }
|
|
843
|
+
.pin-row.shake { animation: shake 0.4s ease-in-out; }
|
|
844
|
+
.pin-input {
|
|
845
|
+
width: 48px; height: 56px;
|
|
846
|
+
text-align: center;
|
|
847
|
+
font-size: 24px; font-weight: 600;
|
|
848
|
+
color: ${text};
|
|
849
|
+
background: ${itemBg};
|
|
850
|
+
border: 1px solid ${border};
|
|
851
|
+
border-radius: 10px;
|
|
852
|
+
outline: none;
|
|
853
|
+
}
|
|
854
|
+
.pin-input:focus { border-color: ${accent}; }
|
|
855
|
+
|
|
856
|
+
.check {
|
|
857
|
+
width: 56px; height: 56px;
|
|
858
|
+
display: flex; align-items: center; justify-content: center;
|
|
859
|
+
border-radius: 50%;
|
|
860
|
+
background: ${success};
|
|
861
|
+
color: #fff;
|
|
862
|
+
font-size: 28px;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
.spinner {
|
|
866
|
+
width: 32px; height: 32px;
|
|
867
|
+
border-radius: 50%;
|
|
868
|
+
border: 3px solid ${border};
|
|
869
|
+
border-top-color: ${accent};
|
|
870
|
+
animation: spin 0.8s linear infinite;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.stage-btn {
|
|
874
|
+
padding: 10px 20px;
|
|
875
|
+
border: none;
|
|
876
|
+
border-radius: 10px;
|
|
877
|
+
background: ${accent};
|
|
878
|
+
color: #fff;
|
|
879
|
+
font-size: 14px;
|
|
880
|
+
font-weight: 500;
|
|
881
|
+
cursor: pointer;
|
|
882
|
+
}
|
|
883
|
+
.stage-btn:hover { filter: brightness(1.1); }
|
|
884
|
+
|
|
885
|
+
.footer {
|
|
886
|
+
border-top: 1px solid ${border};
|
|
887
|
+
padding-top: 10px;
|
|
888
|
+
display: flex;
|
|
889
|
+
flex-direction: column;
|
|
890
|
+
gap: 6px;
|
|
891
|
+
align-items: center;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.footer-link {
|
|
895
|
+
background: none;
|
|
896
|
+
border: none;
|
|
897
|
+
color: ${muted};
|
|
898
|
+
font-size: 13px;
|
|
899
|
+
cursor: pointer;
|
|
900
|
+
padding: 4px 8px;
|
|
901
|
+
border-radius: 8px;
|
|
902
|
+
}
|
|
903
|
+
.footer-link:hover { color: ${text}; background: ${itemBg}; }
|
|
904
|
+
.method-link { color: ${accent}; }
|
|
905
|
+
|
|
906
|
+
.wallet-switcher { width: 100%; display: flex; flex-direction: column; align-items: center; }
|
|
907
|
+
.wallet-panel { width: 100%; padding: 6px 0; display: flex; flex-direction: column; gap: 4px; }
|
|
908
|
+
.wallet-row {
|
|
909
|
+
width: 100%;
|
|
910
|
+
text-align: left;
|
|
911
|
+
padding: 8px 12px;
|
|
912
|
+
background: ${itemBg};
|
|
913
|
+
border: 1px solid ${border};
|
|
914
|
+
border-radius: 10px;
|
|
915
|
+
color: ${text};
|
|
916
|
+
font-size: 13px;
|
|
917
|
+
cursor: pointer;
|
|
918
|
+
}
|
|
919
|
+
.wallet-row:hover, .wallet-row.selected { border-color: ${accent}; }
|
|
920
|
+
|
|
921
|
+
.wallet-icon { position: relative; width: 28px; height: 28px; flex-shrink: 0; display: inline-flex; }
|
|
922
|
+
.wallet-badge {
|
|
923
|
+
width: 28px; height: 28px; border-radius: 8px; display: inline-flex;
|
|
924
|
+
align-items: center; justify-content: center; font-size: 13px;
|
|
925
|
+
font-weight: 600; background: ${accentSoft}; color: ${accent};
|
|
926
|
+
}
|
|
927
|
+
.wallet-img { position: absolute; inset: 0; width: 28px; height: 28px; border-radius: 8px; object-fit: cover; }
|
|
928
|
+
.wallet-row-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
929
|
+
.wallet-search {
|
|
930
|
+
width: 100%; box-sizing: border-box; padding: 8px 10px; margin-bottom: 4px;
|
|
931
|
+
border-radius: 8px; border: 1px solid ${border}; background: transparent;
|
|
932
|
+
color: inherit; font: inherit;
|
|
933
|
+
}
|
|
934
|
+
.wallet-search:focus { outline: none; border-color: ${accent}; }
|
|
935
|
+
.wallet-empty { padding: 8px 10px; font-size: 12px; opacity: 0.7; }
|
|
936
|
+
.url-error { padding: 4px 2px 0; font-size: 12px; color: ${danger}; }
|
|
937
|
+
.wallet-custom { display: flex; gap: 6px; margin-top: 4px; }
|
|
938
|
+
.url-input {
|
|
939
|
+
flex: 1;
|
|
940
|
+
padding: 8px 12px;
|
|
941
|
+
border: 1px solid ${border};
|
|
942
|
+
border-radius: 10px;
|
|
943
|
+
font-size: 13px;
|
|
944
|
+
background: ${itemBg};
|
|
945
|
+
color: ${text};
|
|
946
|
+
outline: none;
|
|
947
|
+
}
|
|
948
|
+
.url-input:focus { border-color: ${accent}; }
|
|
949
|
+
.url-input.invalid { border-color: ${danger}; }
|
|
950
|
+
.url-go {
|
|
951
|
+
padding: 8px 14px;
|
|
952
|
+
border: none;
|
|
953
|
+
border-radius: 10px;
|
|
954
|
+
background: ${accent};
|
|
955
|
+
color: #fff;
|
|
956
|
+
font-size: 13px;
|
|
957
|
+
cursor: pointer;
|
|
958
|
+
}
|
|
959
|
+
`;
|
|
960
|
+
}
|
|
961
|
+
//# sourceMappingURL=connect-modal.js.map
|