@caatinga/cli 2.1.0 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -6
- package/dist/index.js +195 -13
- package/package.json +3 -3
- package/templates/marketplace-with-token/caatinga.template.json +1 -1
- package/templates/marketplace-with-token/package.json +3 -3
- package/templates/react-vite-counter/caatinga.template.json +1 -1
- package/templates/react-vite-counter/package.json +22 -7
- package/templates/react-vite-counter/pnpm-workspace.yaml +16 -9
- package/templates/react-vite-counter/src/App.tsx +17 -3
- package/templates/react-vite-counter/src/components/ContractNotDeployed.tsx +27 -0
- package/templates/react-vite-counter/src/components/CounterCard.tsx +1 -1
- package/templates/react-vite-counter/src/components/WalletButton.tsx +8 -7
- package/templates/react-vite-counter/src/components/WalletModal.tsx +248 -0
- package/templates/react-vite-counter/src/stubs/empty-wallet-dep/index.cjs +3 -0
- package/templates/react-vite-counter/src/stubs/empty-wallet-dep/package.json +6 -0
- package/templates/react-vite-counter/src/stubs/hot-wallet-sdk/index.cjs +7 -0
- package/templates/react-vite-counter/src/stubs/hot-wallet-sdk/package.json +6 -0
- package/templates/react-vite-counter/src/styles.css +261 -0
- package/templates/react-vite-counter/src/wallet-modal-controller.ts +73 -0
- package/templates/react-vite-counter/src/wallet.ts +9 -1
- package/templates/react-vite-counter/vite.config.ts +17 -1
- package/templates/react-vite-counter/src/context/WalletContext.tsx +0 -64
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { formatCaatingaError } from "@caatinga/core/browser";
|
|
3
|
+
import type { ISupportedWallet } from "@creit.tech/stellar-wallets-kit/types";
|
|
4
|
+
import { stellarWalletAdapter } from "../wallet.js";
|
|
5
|
+
import {
|
|
6
|
+
cancelWalletSelection,
|
|
7
|
+
getWalletModalState,
|
|
8
|
+
resolveWalletSelection,
|
|
9
|
+
subscribeWalletModal
|
|
10
|
+
} from "../wallet-modal-controller.js";
|
|
11
|
+
|
|
12
|
+
const LEARN_MORE_URL = "https://developers.stellar.org/docs/build/apps/wallet/overview";
|
|
13
|
+
|
|
14
|
+
// SWK wallet modules reject with plain `{ code, message }` objects, not Errors.
|
|
15
|
+
function describeWalletError(caught: unknown): string {
|
|
16
|
+
if (caught && typeof caught === "object" && "message" in caught) {
|
|
17
|
+
const message = (caught as { message?: unknown }).message;
|
|
18
|
+
if (typeof message === "string" && message) {
|
|
19
|
+
return message;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return formatCaatingaError(caught);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function walletBadge(wallet: ISupportedWallet): string | null {
|
|
26
|
+
if (!wallet.isAvailable) {
|
|
27
|
+
return "Install ↗";
|
|
28
|
+
}
|
|
29
|
+
// Bridge wallets (WalletConnect) hand off to a QR code instead of an extension.
|
|
30
|
+
return wallet.type === "BRIDGE_WALLET" ? "QR code" : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface WalletOptionProps {
|
|
34
|
+
wallet: ISupportedWallet;
|
|
35
|
+
connectingId: string | null;
|
|
36
|
+
onSelect(wallet: ISupportedWallet): void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function WalletOption({ wallet, connectingId, onSelect }: WalletOptionProps) {
|
|
40
|
+
const connecting = connectingId === wallet.id;
|
|
41
|
+
const badge = walletBadge(wallet);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<li>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
className={connecting ? "wallet-option wallet-option--connecting" : "wallet-option"}
|
|
48
|
+
onClick={() => onSelect(wallet)}
|
|
49
|
+
disabled={connectingId !== null && !connecting}
|
|
50
|
+
>
|
|
51
|
+
<img className="wallet-option__icon" src={wallet.icon} alt="" />
|
|
52
|
+
<span className="wallet-option__name">
|
|
53
|
+
{connecting ? `Connecting to ${wallet.name}…` : wallet.name}
|
|
54
|
+
</span>
|
|
55
|
+
{connecting ? (
|
|
56
|
+
<span className="wallet-option__spinner" aria-hidden="true" />
|
|
57
|
+
) : badge ? (
|
|
58
|
+
<span className="wallet-option__badge">{badge}</span>
|
|
59
|
+
) : (
|
|
60
|
+
<span className="status-dot status-dot--on" aria-hidden="true" />
|
|
61
|
+
)}
|
|
62
|
+
</button>
|
|
63
|
+
</li>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function WalletModal() {
|
|
68
|
+
const { open } = useSyncExternalStore(
|
|
69
|
+
subscribeWalletModal,
|
|
70
|
+
getWalletModalState,
|
|
71
|
+
getWalletModalState
|
|
72
|
+
);
|
|
73
|
+
const panelRef = useRef<HTMLDivElement>(null);
|
|
74
|
+
const [wallets, setWallets] = useState<ISupportedWallet[] | null>(null);
|
|
75
|
+
const [connectingId, setConnectingId] = useState<string | null>(null);
|
|
76
|
+
const [error, setError] = useState<string | null>(null);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!open) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setWallets(null);
|
|
84
|
+
setConnectingId(null);
|
|
85
|
+
setError(null);
|
|
86
|
+
|
|
87
|
+
// Availability probes can take a moment; ignore the result if the modal
|
|
88
|
+
// closed (or reopened) before they resolve.
|
|
89
|
+
let cancelled = false;
|
|
90
|
+
stellarWalletAdapter
|
|
91
|
+
.getSupportedWallets()
|
|
92
|
+
.then((list) => {
|
|
93
|
+
if (!cancelled) {
|
|
94
|
+
setWallets(list);
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
.catch((caught: unknown) => {
|
|
98
|
+
if (!cancelled) {
|
|
99
|
+
setWallets([]);
|
|
100
|
+
setError(describeWalletError(caught));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
panelRef.current?.focus();
|
|
105
|
+
|
|
106
|
+
function onKeyDown(event: KeyboardEvent) {
|
|
107
|
+
if (event.key === "Escape") {
|
|
108
|
+
cancelWalletSelection();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
window.addEventListener("keydown", onKeyDown);
|
|
112
|
+
|
|
113
|
+
return () => {
|
|
114
|
+
cancelled = true;
|
|
115
|
+
window.removeEventListener("keydown", onKeyDown);
|
|
116
|
+
};
|
|
117
|
+
}, [open]);
|
|
118
|
+
|
|
119
|
+
if (!open) {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function selectWallet(wallet: ISupportedWallet) {
|
|
124
|
+
if (connectingId) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!wallet.isAvailable) {
|
|
129
|
+
window.open(wallet.url, "_blank", "noopener");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
setError(null);
|
|
134
|
+
setConnectingId(wallet.id);
|
|
135
|
+
try {
|
|
136
|
+
stellarWalletAdapter.setWallet(wallet.id);
|
|
137
|
+
const address = await stellarWalletAdapter.getPublicKey();
|
|
138
|
+
resolveWalletSelection(address);
|
|
139
|
+
} catch (caught) {
|
|
140
|
+
setError(describeWalletError(caught));
|
|
141
|
+
} finally {
|
|
142
|
+
setConnectingId(null);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const detected = wallets?.filter((wallet) => wallet.isAvailable) ?? [];
|
|
147
|
+
const others = wallets?.filter((wallet) => !wallet.isAvailable) ?? [];
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div
|
|
151
|
+
className="wallet-modal"
|
|
152
|
+
role="presentation"
|
|
153
|
+
onClick={(event) => {
|
|
154
|
+
if (event.target === event.currentTarget) {
|
|
155
|
+
cancelWalletSelection();
|
|
156
|
+
}
|
|
157
|
+
}}
|
|
158
|
+
>
|
|
159
|
+
<div
|
|
160
|
+
ref={panelRef}
|
|
161
|
+
className="wallet-modal__panel"
|
|
162
|
+
role="dialog"
|
|
163
|
+
aria-modal="true"
|
|
164
|
+
aria-labelledby="wallet-modal-title"
|
|
165
|
+
tabIndex={-1}
|
|
166
|
+
>
|
|
167
|
+
<header className="wallet-modal__header">
|
|
168
|
+
<h2 id="wallet-modal-title">Connect a wallet</h2>
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
className="wallet-modal__close"
|
|
172
|
+
onClick={() => cancelWalletSelection()}
|
|
173
|
+
aria-label="Close"
|
|
174
|
+
>
|
|
175
|
+
✕
|
|
176
|
+
</button>
|
|
177
|
+
</header>
|
|
178
|
+
|
|
179
|
+
{error ? (
|
|
180
|
+
<p className="wallet-modal__error" role="alert">
|
|
181
|
+
{error}
|
|
182
|
+
</p>
|
|
183
|
+
) : null}
|
|
184
|
+
|
|
185
|
+
<div className="wallet-modal__body">
|
|
186
|
+
{wallets === null ? (
|
|
187
|
+
<ul className="wallet-modal__list" aria-label="Loading wallets">
|
|
188
|
+
{[0, 1, 2].map((row) => (
|
|
189
|
+
<li key={row} className="wallet-option wallet-option--skeleton" aria-hidden="true">
|
|
190
|
+
<span className="wallet-skeleton__icon" />
|
|
191
|
+
<span className="wallet-skeleton__line" />
|
|
192
|
+
</li>
|
|
193
|
+
))}
|
|
194
|
+
</ul>
|
|
195
|
+
) : (
|
|
196
|
+
<>
|
|
197
|
+
{detected.length > 0 ? (
|
|
198
|
+
<>
|
|
199
|
+
<p className="eyebrow wallet-modal__section">Detected</p>
|
|
200
|
+
<ul className="wallet-modal__list">
|
|
201
|
+
{detected.map((wallet) => (
|
|
202
|
+
<WalletOption
|
|
203
|
+
key={wallet.id}
|
|
204
|
+
wallet={wallet}
|
|
205
|
+
connectingId={connectingId}
|
|
206
|
+
onSelect={(selected) => void selectWallet(selected)}
|
|
207
|
+
/>
|
|
208
|
+
))}
|
|
209
|
+
</ul>
|
|
210
|
+
</>
|
|
211
|
+
) : null}
|
|
212
|
+
|
|
213
|
+
{others.length > 0 ? (
|
|
214
|
+
<>
|
|
215
|
+
<p className="eyebrow wallet-modal__section">More wallets</p>
|
|
216
|
+
<ul className="wallet-modal__list">
|
|
217
|
+
{others.map((wallet) => (
|
|
218
|
+
<WalletOption
|
|
219
|
+
key={wallet.id}
|
|
220
|
+
wallet={wallet}
|
|
221
|
+
connectingId={connectingId}
|
|
222
|
+
onSelect={(selected) => void selectWallet(selected)}
|
|
223
|
+
/>
|
|
224
|
+
))}
|
|
225
|
+
</ul>
|
|
226
|
+
</>
|
|
227
|
+
) : null}
|
|
228
|
+
|
|
229
|
+
{detected.length === 0 && others.length === 0 && !error ? (
|
|
230
|
+
<p className="wallet-modal__empty">
|
|
231
|
+
No wallets available. Install a Stellar wallet to continue.
|
|
232
|
+
</p>
|
|
233
|
+
) : null}
|
|
234
|
+
</>
|
|
235
|
+
)}
|
|
236
|
+
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<footer className="wallet-modal__footer">
|
|
240
|
+
<span>New to Stellar wallets?</span>
|
|
241
|
+
<a href={LEARN_MORE_URL} target="_blank" rel="noreferrer">
|
|
242
|
+
Learn more ↗
|
|
243
|
+
</a>
|
|
244
|
+
</footer>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
@@ -239,6 +239,232 @@ button:hover {
|
|
|
239
239
|
background: #f4f2ec;
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
+
.wallet-modal {
|
|
243
|
+
position: fixed;
|
|
244
|
+
inset: 0;
|
|
245
|
+
z-index: 20;
|
|
246
|
+
display: flex;
|
|
247
|
+
align-items: center;
|
|
248
|
+
justify-content: center;
|
|
249
|
+
padding: 16px;
|
|
250
|
+
background: rgba(32, 35, 42, 0.45);
|
|
251
|
+
backdrop-filter: blur(3px);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.wallet-modal__panel {
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
width: min(400px, 100%);
|
|
258
|
+
max-height: min(600px, calc(100vh - 32px));
|
|
259
|
+
border: 1px solid #d9d5ca;
|
|
260
|
+
border-radius: 12px;
|
|
261
|
+
background: #fffdf7;
|
|
262
|
+
box-shadow: 0 18px 60px rgba(32, 35, 42, 0.22);
|
|
263
|
+
outline: none;
|
|
264
|
+
animation: wallet-modal-pop 0.18s ease;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
@keyframes wallet-modal-pop {
|
|
268
|
+
from {
|
|
269
|
+
opacity: 0;
|
|
270
|
+
transform: translateY(8px) scale(0.98);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.wallet-modal__header {
|
|
275
|
+
display: flex;
|
|
276
|
+
align-items: center;
|
|
277
|
+
justify-content: space-between;
|
|
278
|
+
gap: 16px;
|
|
279
|
+
padding: 20px 20px 8px;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.wallet-modal__header h2 {
|
|
283
|
+
margin: 0;
|
|
284
|
+
color: #16181d;
|
|
285
|
+
font-size: 1.2rem;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.wallet-modal__close {
|
|
289
|
+
width: 32px;
|
|
290
|
+
min-height: 32px;
|
|
291
|
+
padding: 0;
|
|
292
|
+
border-radius: 8px;
|
|
293
|
+
background: transparent;
|
|
294
|
+
color: #697076;
|
|
295
|
+
font-size: 0.95rem;
|
|
296
|
+
font-weight: 400;
|
|
297
|
+
line-height: 1;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.wallet-modal__close:hover {
|
|
301
|
+
background: #f4f2ec;
|
|
302
|
+
color: #20232a;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.wallet-modal__body {
|
|
306
|
+
overflow-y: auto;
|
|
307
|
+
padding: 0 12px 16px;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.wallet-modal__section {
|
|
311
|
+
margin: 14px 10px 6px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.wallet-modal__list {
|
|
315
|
+
display: grid;
|
|
316
|
+
gap: 4px;
|
|
317
|
+
margin: 0;
|
|
318
|
+
padding: 0;
|
|
319
|
+
list-style: none;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
.wallet-option {
|
|
323
|
+
display: flex;
|
|
324
|
+
align-items: center;
|
|
325
|
+
gap: 12px;
|
|
326
|
+
width: 100%;
|
|
327
|
+
min-height: 56px;
|
|
328
|
+
padding: 8px 12px;
|
|
329
|
+
border: 1px solid transparent;
|
|
330
|
+
border-radius: 10px;
|
|
331
|
+
background: transparent;
|
|
332
|
+
color: #20232a;
|
|
333
|
+
font-weight: 700;
|
|
334
|
+
text-align: left;
|
|
335
|
+
transition:
|
|
336
|
+
background 0.15s ease,
|
|
337
|
+
border-color 0.15s ease;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.wallet-option:hover {
|
|
341
|
+
background: #f4f2ec;
|
|
342
|
+
border-color: #d9d5ca;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.wallet-option:disabled {
|
|
346
|
+
background: transparent;
|
|
347
|
+
border-color: transparent;
|
|
348
|
+
opacity: 0.5;
|
|
349
|
+
cursor: default;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.wallet-option--connecting,
|
|
353
|
+
.wallet-option--connecting:hover {
|
|
354
|
+
background: #f4f2ec;
|
|
355
|
+
border-color: #d9d5ca;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.wallet-option__icon {
|
|
359
|
+
width: 36px;
|
|
360
|
+
height: 36px;
|
|
361
|
+
border: 1px solid #eee9dd;
|
|
362
|
+
border-radius: 8px;
|
|
363
|
+
background: #ffffff;
|
|
364
|
+
object-fit: contain;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.wallet-option__name {
|
|
368
|
+
flex: 1;
|
|
369
|
+
min-width: 0;
|
|
370
|
+
overflow: hidden;
|
|
371
|
+
text-overflow: ellipsis;
|
|
372
|
+
white-space: nowrap;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.wallet-option__badge {
|
|
376
|
+
border: 1px solid #d9d5ca;
|
|
377
|
+
border-radius: 999px;
|
|
378
|
+
padding: 3px 10px;
|
|
379
|
+
background: #ffffff;
|
|
380
|
+
color: #45515a;
|
|
381
|
+
font-size: 0.7rem;
|
|
382
|
+
font-weight: 800;
|
|
383
|
+
letter-spacing: 0.04em;
|
|
384
|
+
text-transform: uppercase;
|
|
385
|
+
white-space: nowrap;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.wallet-option__spinner {
|
|
389
|
+
width: 16px;
|
|
390
|
+
height: 16px;
|
|
391
|
+
border: 3px solid #d9d5ca;
|
|
392
|
+
border-top-color: #1d6154;
|
|
393
|
+
border-radius: 50%;
|
|
394
|
+
animation: loading-spin 0.7s linear infinite;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.wallet-option--skeleton {
|
|
398
|
+
pointer-events: none;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.wallet-skeleton__icon,
|
|
402
|
+
.wallet-skeleton__line {
|
|
403
|
+
border-radius: 8px;
|
|
404
|
+
background: #ece8dc;
|
|
405
|
+
animation: wallet-skeleton-pulse 1.2s ease-in-out infinite;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.wallet-skeleton__icon {
|
|
409
|
+
flex: none;
|
|
410
|
+
width: 36px;
|
|
411
|
+
height: 36px;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.wallet-skeleton__line {
|
|
415
|
+
width: 45%;
|
|
416
|
+
height: 14px;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
@keyframes wallet-skeleton-pulse {
|
|
420
|
+
50% {
|
|
421
|
+
opacity: 0.55;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.wallet-modal__empty {
|
|
426
|
+
margin: 14px 10px;
|
|
427
|
+
color: #45515a;
|
|
428
|
+
font-size: 0.88rem;
|
|
429
|
+
line-height: 1.4;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
.wallet-modal__error {
|
|
433
|
+
margin: 0 20px 4px;
|
|
434
|
+
max-height: 72px;
|
|
435
|
+
overflow-y: auto;
|
|
436
|
+
border: 1px solid #e7c8c8;
|
|
437
|
+
border-radius: 8px;
|
|
438
|
+
padding: 8px 10px;
|
|
439
|
+
background: #fbf1f1;
|
|
440
|
+
color: #8b1e1e;
|
|
441
|
+
font-size: 0.82rem;
|
|
442
|
+
line-height: 1.4;
|
|
443
|
+
white-space: pre-wrap;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
.wallet-modal__footer {
|
|
447
|
+
display: flex;
|
|
448
|
+
align-items: center;
|
|
449
|
+
justify-content: space-between;
|
|
450
|
+
gap: 8px;
|
|
451
|
+
padding: 14px 20px;
|
|
452
|
+
border-top: 1px solid #eee9dd;
|
|
453
|
+
color: #697076;
|
|
454
|
+
font-size: 0.82rem;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.wallet-modal__footer a {
|
|
458
|
+
color: #1d6154;
|
|
459
|
+
font-weight: 700;
|
|
460
|
+
text-decoration: none;
|
|
461
|
+
white-space: nowrap;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.wallet-modal__footer a:hover {
|
|
465
|
+
text-decoration: underline;
|
|
466
|
+
}
|
|
467
|
+
|
|
242
468
|
@media (max-width: 560px) {
|
|
243
469
|
.topbar,
|
|
244
470
|
.counter-panel__header {
|
|
@@ -254,4 +480,39 @@ button:hover {
|
|
|
254
480
|
width: 100%;
|
|
255
481
|
justify-content: center;
|
|
256
482
|
}
|
|
483
|
+
|
|
484
|
+
.wallet-modal {
|
|
485
|
+
align-items: flex-end;
|
|
486
|
+
padding: 0;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.wallet-modal__panel {
|
|
490
|
+
width: 100%;
|
|
491
|
+
max-height: 80vh;
|
|
492
|
+
border-bottom: 0;
|
|
493
|
+
border-radius: 16px 16px 0 0;
|
|
494
|
+
animation: wallet-modal-rise 0.2s ease;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
@keyframes wallet-modal-rise {
|
|
499
|
+
from {
|
|
500
|
+
opacity: 0;
|
|
501
|
+
transform: translateY(24px);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
@media (prefers-reduced-motion: reduce) {
|
|
506
|
+
.wallet-modal__panel {
|
|
507
|
+
animation: none;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.wallet-option__spinner {
|
|
511
|
+
animation-duration: 1.6s;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
.wallet-skeleton__icon,
|
|
515
|
+
.wallet-skeleton__line {
|
|
516
|
+
animation: none;
|
|
517
|
+
}
|
|
257
518
|
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// Promise bridge between the wallet adapter's `openModal()` capability and the
|
|
2
|
+
// <WalletModal> component. The adapter resolves the same promise the wallet
|
|
3
|
+
// session awaits, so connect/persist/restore flows stay untouched.
|
|
4
|
+
|
|
5
|
+
export interface WalletModalState {
|
|
6
|
+
open: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `error.name` used when the user closes the modal without picking a wallet.
|
|
11
|
+
* Closing on purpose is not a failure, so the UI can suppress this one.
|
|
12
|
+
*/
|
|
13
|
+
export const WALLET_SELECTION_CLOSED_ERROR = "WalletSelectionClosedError";
|
|
14
|
+
|
|
15
|
+
interface PendingSelection {
|
|
16
|
+
resolve(address: string): void;
|
|
17
|
+
reject(error: Error): void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let state: WalletModalState = { open: false };
|
|
21
|
+
let pending: PendingSelection | null = null;
|
|
22
|
+
const listeners = new Set<() => void>();
|
|
23
|
+
|
|
24
|
+
function setState(next: WalletModalState): void {
|
|
25
|
+
state = next;
|
|
26
|
+
for (const listener of listeners) {
|
|
27
|
+
listener();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getWalletModalState(): WalletModalState {
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function subscribeWalletModal(listener: () => void): () => void {
|
|
36
|
+
listeners.add(listener);
|
|
37
|
+
return () => {
|
|
38
|
+
listeners.delete(listener);
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Opens the modal. Resolves with the connected address, rejects when closed. */
|
|
43
|
+
export function requestWalletSelection(): Promise<string> {
|
|
44
|
+
// A connect while the modal is already open supersedes the previous request.
|
|
45
|
+
cancelWalletSelection();
|
|
46
|
+
|
|
47
|
+
return new Promise<string>((resolve, reject) => {
|
|
48
|
+
pending = { resolve, reject };
|
|
49
|
+
setState({ open: true });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveWalletSelection(address: string): void {
|
|
54
|
+
const current = pending;
|
|
55
|
+
pending = null;
|
|
56
|
+
if (state.open) {
|
|
57
|
+
setState({ open: false });
|
|
58
|
+
}
|
|
59
|
+
current?.resolve(address);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function cancelWalletSelection(): void {
|
|
63
|
+
const current = pending;
|
|
64
|
+
pending = null;
|
|
65
|
+
if (state.open) {
|
|
66
|
+
setState({ open: false });
|
|
67
|
+
}
|
|
68
|
+
if (current) {
|
|
69
|
+
const error = new Error("Wallet selection closed.");
|
|
70
|
+
error.name = WALLET_SELECTION_CLOSED_ERROR;
|
|
71
|
+
current.reject(error);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -4,12 +4,20 @@ import {
|
|
|
4
4
|
WalletNetwork,
|
|
5
5
|
type StellarWalletsKitMetadata
|
|
6
6
|
} from "@caatinga/client/stellar-wallets-kit";
|
|
7
|
+
import { requestWalletSelection } from "./wallet-modal-controller.js";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
const baseWalletAdapter = createStellarWalletsKitAdapter({
|
|
9
10
|
network: WalletNetwork.TESTNET,
|
|
10
11
|
walletConnectMetadata: getWalletConnectMetadata()
|
|
11
12
|
});
|
|
12
13
|
|
|
14
|
+
// Route connect() through the custom <WalletModal> instead of SWK's built-in
|
|
15
|
+
// authModal. Everything else (persistence, restore, signing) is untouched.
|
|
16
|
+
export const stellarWalletAdapter = {
|
|
17
|
+
...baseWalletAdapter,
|
|
18
|
+
openModal: () => requestWalletSelection()
|
|
19
|
+
};
|
|
20
|
+
|
|
13
21
|
export { WalletNetwork };
|
|
14
22
|
|
|
15
23
|
function getWalletConnectMetadata(): StellarWalletsKitMetadata | undefined {
|
|
@@ -2,6 +2,15 @@ import { defineConfig } from "vite";
|
|
|
2
2
|
import react from "@vitejs/plugin-react";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
|
|
5
|
+
// Empty CJS module. Aliasing browser-hostile wallet sub-deps here keeps them out
|
|
6
|
+
// of the bundle on yarn/bun (and as a safety net on npm/pnpm). Install-time
|
|
7
|
+
// overrides in package.json / pnpm-workspace.yaml block the same deps from
|
|
8
|
+
// being installed. CJS interop lets any named import resolve to `undefined`
|
|
9
|
+
// without an esbuild "not exported" error.
|
|
10
|
+
const emptyStub = fileURLToPath(
|
|
11
|
+
new URL("./src/stubs/empty-wallet-dep/index.cjs", import.meta.url)
|
|
12
|
+
);
|
|
13
|
+
|
|
5
14
|
export default defineConfig({
|
|
6
15
|
plugins: [react()],
|
|
7
16
|
resolve: {
|
|
@@ -9,7 +18,14 @@ export default defineConfig({
|
|
|
9
18
|
// Stellar Wallets Kit drags NEAR's @hot-wallet/sdk (Node-only crypto) into
|
|
10
19
|
// the browser bundle. The adapter filters HOT Wallet out, so stub the SDK
|
|
11
20
|
// to keep the NEAR chain out of the build. See src/stubs/hot-wallet.ts.
|
|
12
|
-
"@hot-wallet/sdk": fileURLToPath(new URL("./src/stubs/hot-wallet.ts", import.meta.url))
|
|
21
|
+
"@hot-wallet/sdk": fileURLToPath(new URL("./src/stubs/hot-wallet.ts", import.meta.url)),
|
|
22
|
+
// SWK + Reown/WalletConnect pull Trezor Connect (Node-only) and Safe Global
|
|
23
|
+
// SDKs that none of the wallets Caatinga ships actually use.
|
|
24
|
+
"@trezor/connect-web": emptyStub,
|
|
25
|
+
"@trezor/connect-plugin-stellar": emptyStub,
|
|
26
|
+
"@safe-global/safe-apps-sdk": emptyStub,
|
|
27
|
+
"@safe-global/safe-apps-provider": emptyStub,
|
|
28
|
+
"@safe-global/safe-gateway-typescript-sdk": emptyStub
|
|
13
29
|
}
|
|
14
30
|
}
|
|
15
31
|
});
|