@enbox/browser 0.1.2 → 0.1.4
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/esm/browser-connect-handler.js +66 -0
- package/dist/esm/browser-connect-handler.js.map +1 -0
- package/dist/esm/dweb-connect-client.js +171 -0
- package/dist/esm/dweb-connect-client.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/ui/wallet-selector.js +313 -0
- package/dist/esm/ui/wallet-selector.js.map +1 -0
- package/dist/types/browser-connect-handler.d.ts +62 -0
- package/dist/types/browser-connect-handler.d.ts.map +1 -0
- package/dist/types/dweb-connect-client.d.ts +63 -0
- package/dist/types/dweb-connect-client.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/ui/wallet-selector.d.ts +12 -0
- package/dist/types/ui/wallet-selector.d.ts.map +1 -0
- package/package.json +7 -6
- package/src/browser-connect-handler.ts +103 -0
- package/src/dweb-connect-client.ts +214 -0
- package/src/index.ts +5 -1
- package/src/ui/wallet-selector.ts +349 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wallet selector modal — a self-contained, framework-agnostic UI component.
|
|
3
|
+
*
|
|
4
|
+
* Injected into the DOM as a Shadow DOM element to prevent style conflicts.
|
|
5
|
+
* Inspired by WalletConnect's Web3Modal pattern.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { WalletOption } from '../browser-connect-handler.js';
|
|
11
|
+
|
|
12
|
+
/** Shows the wallet selector modal and resolves with the chosen wallet URL. */
|
|
13
|
+
export function showWalletSelector(wallets: WalletOption[]): Promise<string> {
|
|
14
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
15
|
+
throw new Error('[@enbox/auth] Wallet selector is only available in browser environments.');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return new Promise<string>((resolve, reject) => {
|
|
19
|
+
// Create the host element with Shadow DOM isolation.
|
|
20
|
+
const host = document.createElement('div');
|
|
21
|
+
host.id = 'enbox-wallet-selector';
|
|
22
|
+
const shadow = host.attachShadow({ mode: 'open' });
|
|
23
|
+
|
|
24
|
+
const cleanup = (): void => {
|
|
25
|
+
try { document.body.removeChild(host); } catch { /* best effort */ }
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Detect dark mode.
|
|
29
|
+
const isDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches;
|
|
30
|
+
|
|
31
|
+
// Build styles.
|
|
32
|
+
const style = document.createElement('style');
|
|
33
|
+
style.textContent = buildStyles(isDark);
|
|
34
|
+
shadow.appendChild(style);
|
|
35
|
+
|
|
36
|
+
// Build modal DOM.
|
|
37
|
+
const overlay = document.createElement('div');
|
|
38
|
+
overlay.className = 'overlay';
|
|
39
|
+
|
|
40
|
+
const modal = document.createElement('div');
|
|
41
|
+
modal.className = 'modal';
|
|
42
|
+
|
|
43
|
+
// Header
|
|
44
|
+
const header = document.createElement('div');
|
|
45
|
+
header.className = 'header';
|
|
46
|
+
|
|
47
|
+
const title = document.createElement('h2');
|
|
48
|
+
title.textContent = 'Connect Wallet';
|
|
49
|
+
|
|
50
|
+
const closeBtn = document.createElement('button');
|
|
51
|
+
closeBtn.className = 'close-btn';
|
|
52
|
+
closeBtn.innerHTML = '×';
|
|
53
|
+
closeBtn.addEventListener('click', () => {
|
|
54
|
+
cleanup();
|
|
55
|
+
reject(new Error('[@enbox/auth] Wallet selection cancelled.'));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
header.appendChild(title);
|
|
59
|
+
header.appendChild(closeBtn);
|
|
60
|
+
modal.appendChild(header);
|
|
61
|
+
|
|
62
|
+
// Wallet list
|
|
63
|
+
const list = document.createElement('div');
|
|
64
|
+
list.className = 'wallet-list';
|
|
65
|
+
|
|
66
|
+
for (const wallet of wallets) {
|
|
67
|
+
const item = document.createElement('button');
|
|
68
|
+
item.className = 'wallet-item';
|
|
69
|
+
|
|
70
|
+
const icon = document.createElement('img');
|
|
71
|
+
icon.src = wallet.icon ?? faviconUrl(wallet.url);
|
|
72
|
+
icon.alt = wallet.name;
|
|
73
|
+
icon.width = 32;
|
|
74
|
+
icon.height = 32;
|
|
75
|
+
icon.onerror = (): void => { icon.style.display = 'none'; };
|
|
76
|
+
|
|
77
|
+
const name = document.createElement('span');
|
|
78
|
+
name.className = 'wallet-name';
|
|
79
|
+
name.textContent = wallet.name;
|
|
80
|
+
|
|
81
|
+
const arrow = document.createElement('span');
|
|
82
|
+
arrow.className = 'arrow';
|
|
83
|
+
arrow.textContent = '\u203A'; // ›
|
|
84
|
+
|
|
85
|
+
item.appendChild(icon);
|
|
86
|
+
item.appendChild(name);
|
|
87
|
+
item.appendChild(arrow);
|
|
88
|
+
|
|
89
|
+
item.addEventListener('click', () => {
|
|
90
|
+
cleanup();
|
|
91
|
+
resolve(wallet.url);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
list.appendChild(item);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
modal.appendChild(list);
|
|
98
|
+
|
|
99
|
+
// Separator
|
|
100
|
+
const sep = document.createElement('div');
|
|
101
|
+
sep.className = 'separator';
|
|
102
|
+
|
|
103
|
+
const sepLine1 = document.createElement('div');
|
|
104
|
+
sepLine1.className = 'sep-line';
|
|
105
|
+
const sepText = document.createElement('span');
|
|
106
|
+
sepText.textContent = 'or';
|
|
107
|
+
const sepLine2 = document.createElement('div');
|
|
108
|
+
sepLine2.className = 'sep-line';
|
|
109
|
+
|
|
110
|
+
sep.appendChild(sepLine1);
|
|
111
|
+
sep.appendChild(sepText);
|
|
112
|
+
sep.appendChild(sepLine2);
|
|
113
|
+
modal.appendChild(sep);
|
|
114
|
+
|
|
115
|
+
// Custom URL input
|
|
116
|
+
const inputGroup = document.createElement('div');
|
|
117
|
+
inputGroup.className = 'input-group';
|
|
118
|
+
|
|
119
|
+
const input = document.createElement('input');
|
|
120
|
+
input.type = 'url';
|
|
121
|
+
input.placeholder = 'Enter wallet URL...';
|
|
122
|
+
input.className = 'url-input';
|
|
123
|
+
|
|
124
|
+
const goBtn = document.createElement('button');
|
|
125
|
+
goBtn.className = 'go-btn';
|
|
126
|
+
goBtn.textContent = 'Connect';
|
|
127
|
+
goBtn.disabled = true;
|
|
128
|
+
|
|
129
|
+
input.addEventListener('input', () => {
|
|
130
|
+
goBtn.disabled = !isValidUrl(input.value);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
input.addEventListener('keydown', (e) => {
|
|
134
|
+
if (e.key === 'Enter' && isValidUrl(input.value)) {
|
|
135
|
+
cleanup();
|
|
136
|
+
resolve(normalizeUrl(input.value));
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
goBtn.addEventListener('click', () => {
|
|
141
|
+
if (isValidUrl(input.value)) {
|
|
142
|
+
cleanup();
|
|
143
|
+
resolve(normalizeUrl(input.value));
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
inputGroup.appendChild(input);
|
|
148
|
+
inputGroup.appendChild(goBtn);
|
|
149
|
+
modal.appendChild(inputGroup);
|
|
150
|
+
|
|
151
|
+
// Close on overlay click.
|
|
152
|
+
overlay.addEventListener('click', (e) => {
|
|
153
|
+
if (e.target === overlay) {
|
|
154
|
+
cleanup();
|
|
155
|
+
reject(new Error('[@enbox/auth] Wallet selection cancelled.'));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Close on Escape.
|
|
160
|
+
const onKeydown = (e: KeyboardEvent): void => {
|
|
161
|
+
if (e.key === 'Escape') {
|
|
162
|
+
document.removeEventListener('keydown', onKeydown);
|
|
163
|
+
cleanup();
|
|
164
|
+
reject(new Error('[@enbox/auth] Wallet selection cancelled.'));
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
document.addEventListener('keydown', onKeydown);
|
|
168
|
+
|
|
169
|
+
overlay.appendChild(modal);
|
|
170
|
+
shadow.appendChild(overlay);
|
|
171
|
+
document.body.appendChild(host);
|
|
172
|
+
|
|
173
|
+
// Focus the input for keyboard-first users.
|
|
174
|
+
input.focus();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── Helpers ─────────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
function faviconUrl(walletUrl: string): string {
|
|
181
|
+
return `https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=${walletUrl}&size=128`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function isValidUrl(value: string): boolean {
|
|
185
|
+
try {
|
|
186
|
+
const url = new URL(value.includes('://') ? value : `https://${value}`);
|
|
187
|
+
return url.protocol === 'https:' || url.protocol === 'http:';
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function normalizeUrl(value: string): string {
|
|
194
|
+
if (!value.includes('://')) {
|
|
195
|
+
value = `https://${value}`;
|
|
196
|
+
}
|
|
197
|
+
const url = new URL(value);
|
|
198
|
+
// Return origin only (strip path/trailing slash).
|
|
199
|
+
return url.origin;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildStyles(isDark: boolean): string {
|
|
203
|
+
const bg = isDark ? '#1a1a2e' : '#ffffff';
|
|
204
|
+
const text = isDark ? '#e0e0e0' : '#1a1a2e';
|
|
205
|
+
const muted = isDark ? '#888' : '#666';
|
|
206
|
+
const border = isDark ? '#333' : '#e0e0e0';
|
|
207
|
+
const itemBg = isDark ? '#16213e' : '#f8f9fa';
|
|
208
|
+
const itemHover = isDark ? '#0f3460' : '#e9ecef';
|
|
209
|
+
const accent = isDark ? '#4a9eff' : '#0066cc';
|
|
210
|
+
const overlayBg = 'rgba(0, 0, 0, 0.5)';
|
|
211
|
+
|
|
212
|
+
return `
|
|
213
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
214
|
+
|
|
215
|
+
.overlay {
|
|
216
|
+
position: fixed;
|
|
217
|
+
inset: 0;
|
|
218
|
+
z-index: 2147483647;
|
|
219
|
+
display: flex;
|
|
220
|
+
align-items: center;
|
|
221
|
+
justify-content: center;
|
|
222
|
+
background: ${overlayBg};
|
|
223
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
224
|
+
animation: fadeIn 0.15s ease-out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@keyframes fadeIn {
|
|
228
|
+
from { opacity: 0; }
|
|
229
|
+
to { opacity: 1; }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@keyframes slideUp {
|
|
233
|
+
from { transform: translateY(20px); opacity: 0; }
|
|
234
|
+
to { transform: translateY(0); opacity: 1; }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.modal {
|
|
238
|
+
background: ${bg};
|
|
239
|
+
color: ${text};
|
|
240
|
+
border-radius: 16px;
|
|
241
|
+
width: 380px;
|
|
242
|
+
max-width: 90vw;
|
|
243
|
+
max-height: 80vh;
|
|
244
|
+
overflow-y: auto;
|
|
245
|
+
padding: 24px;
|
|
246
|
+
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
247
|
+
animation: slideUp 0.2s ease-out;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.header {
|
|
251
|
+
display: flex;
|
|
252
|
+
justify-content: space-between;
|
|
253
|
+
align-items: center;
|
|
254
|
+
margin-bottom: 20px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
h2 {
|
|
258
|
+
font-size: 18px;
|
|
259
|
+
font-weight: 600;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.close-btn {
|
|
263
|
+
background: none;
|
|
264
|
+
border: none;
|
|
265
|
+
font-size: 24px;
|
|
266
|
+
cursor: pointer;
|
|
267
|
+
color: ${muted};
|
|
268
|
+
padding: 4px 8px;
|
|
269
|
+
border-radius: 8px;
|
|
270
|
+
line-height: 1;
|
|
271
|
+
}
|
|
272
|
+
.close-btn:hover { color: ${text}; background: ${itemBg}; }
|
|
273
|
+
|
|
274
|
+
.wallet-list {
|
|
275
|
+
display: flex;
|
|
276
|
+
flex-direction: column;
|
|
277
|
+
gap: 8px;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.wallet-item {
|
|
281
|
+
display: flex;
|
|
282
|
+
align-items: center;
|
|
283
|
+
gap: 12px;
|
|
284
|
+
padding: 12px 16px;
|
|
285
|
+
background: ${itemBg};
|
|
286
|
+
border: 1px solid ${border};
|
|
287
|
+
border-radius: 12px;
|
|
288
|
+
cursor: pointer;
|
|
289
|
+
font-size: 15px;
|
|
290
|
+
color: ${text};
|
|
291
|
+
transition: background 0.15s;
|
|
292
|
+
width: 100%;
|
|
293
|
+
text-align: left;
|
|
294
|
+
}
|
|
295
|
+
.wallet-item:hover { background: ${itemHover}; }
|
|
296
|
+
|
|
297
|
+
.wallet-name { flex: 1; font-weight: 500; }
|
|
298
|
+
|
|
299
|
+
.arrow {
|
|
300
|
+
font-size: 20px;
|
|
301
|
+
color: ${muted};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.separator {
|
|
305
|
+
display: flex;
|
|
306
|
+
align-items: center;
|
|
307
|
+
gap: 12px;
|
|
308
|
+
margin: 20px 0;
|
|
309
|
+
color: ${muted};
|
|
310
|
+
font-size: 13px;
|
|
311
|
+
}
|
|
312
|
+
.sep-line { flex: 1; height: 1px; background: ${border}; }
|
|
313
|
+
|
|
314
|
+
.input-group {
|
|
315
|
+
display: flex;
|
|
316
|
+
gap: 8px;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.url-input {
|
|
320
|
+
flex: 1;
|
|
321
|
+
padding: 10px 14px;
|
|
322
|
+
border: 1px solid ${border};
|
|
323
|
+
border-radius: 10px;
|
|
324
|
+
font-size: 14px;
|
|
325
|
+
background: ${itemBg};
|
|
326
|
+
color: ${text};
|
|
327
|
+
outline: none;
|
|
328
|
+
}
|
|
329
|
+
.url-input:focus { border-color: ${accent}; }
|
|
330
|
+
.url-input::placeholder { color: ${muted}; }
|
|
331
|
+
|
|
332
|
+
.go-btn {
|
|
333
|
+
padding: 10px 18px;
|
|
334
|
+
border: none;
|
|
335
|
+
border-radius: 10px;
|
|
336
|
+
background: ${accent};
|
|
337
|
+
color: #fff;
|
|
338
|
+
font-size: 14px;
|
|
339
|
+
font-weight: 500;
|
|
340
|
+
cursor: pointer;
|
|
341
|
+
white-space: nowrap;
|
|
342
|
+
}
|
|
343
|
+
.go-btn:disabled {
|
|
344
|
+
opacity: 0.4;
|
|
345
|
+
cursor: not-allowed;
|
|
346
|
+
}
|
|
347
|
+
.go-btn:not(:disabled):hover { filter: brightness(1.1); }
|
|
348
|
+
`;
|
|
349
|
+
}
|