wallet.component.html raw
1 <div class="sam-text-header">
2 <div class="header-buttons">
3 <button class="header-btn" title="Lock" (click)="onClickLock()">
4 <span class="emoji">🔒</span>
5 </button>
6 @if (devMode) {
7 <button class="header-btn" title="Test Permission Prompt" (click)="onTestPrompt()">
8 <span class="emoji">✨</span>
9 </button>
10 }
11 </div>
12 @if (showBackButton) {
13 <button class="back-btn" title="Go Back" (click)="goBack()">
14 <span class="emoji">←</span>
15 </button>
16 }
17 <span>{{ title }}</span>
18 <div class="section-btns">
19 <button
20 class="section-btn"
21 [class.active]="activeSection.startsWith('cashu')"
22 title="Cashu"
23 (click)="setSection('cashu')"
24 >
25 <span class="emoji">🥜</span>
26 </button>
27 <button
28 class="section-btn"
29 [class.active]="activeSection.startsWith('lightning')"
30 title="Lightning"
31 (click)="setSection('lightning')"
32 >
33 <span class="emoji">⚡</span>
34 </button>
35 </div>
36 </div>
37
38 <div class="wallet-container">
39 <!-- Main wallet menu -->
40 @if (activeSection === 'main') {
41 <div class="wallet-menu">
42 <button class="wallet-menu-item" (click)="setSection('cashu')">
43 <span class="emoji">🥜</span>
44 <span class="label">Cashu</span>
45 <span class="balance">{{ formatCashuBalance(totalCashuBalance) }} sats</span>
46 </button>
47 <button class="wallet-menu-item" (click)="setSection('lightning')">
48 <span class="emoji">⚡</span>
49 <span class="label">Lightning</span>
50 <span class="balance">{{ formatBalance(totalLightningBalance) }} sats</span>
51 </button>
52 </div>
53 }
54
55 <!-- Cashu mint list -->
56 @else if (activeSection === 'cashu') {
57 <div class="lightning-section">
58 @if (mints.length === 0) {
59 <div class="cashu-onboarding">
60 @if (showCashuInfo) {
61 <div class="info-panel">
62 <h3>Welcome to Cashu Wallet</h3>
63
64 <div class="info-section">
65 <h4>Storage Considerations</h4>
66 @if (currentSyncFlow === BrowserSyncFlow.BROWSER_SYNC) {
67 <div class="warning-box">
68 <p><strong>Browser Sync is enabled</strong></p>
69 <p>
70 Sync storage is limited to ~100KB shared across all your vault data
71 (identities, permissions, relays, and Cashu tokens). This limits
72 your Cashu wallet to approximately 300-400 tokens.
73 </p>
74 <p>
75 For larger Cashu holdings, consider disabling browser sync which
76 provides ~5MB of local storage (~18,000+ tokens).
77 </p>
78 <button class="link-btn" (click)="navigateToSettings()">
79 Change Sync Settings
80 </button>
81 </div>
82 } @else {
83 <div class="success-box">
84 <p><strong>Local Storage Mode</strong></p>
85 <p>
86 You have ~5MB of local storage available, which can hold
87 thousands of Cashu tokens. Your data stays on this device only.
88 </p>
89 </div>
90 }
91 </div>
92
93 <div class="info-section">
94 <h4>Backup Your Wallet</h4>
95 <p>
96 <strong>Important:</strong> Cashu tokens are bearer assets.
97 If you lose your vault backup, you lose your tokens permanently.
98 </p>
99 <p>
100 Vault exports are saved to your browser's downloads folder.
101 Configure this to point to either:
102 </p>
103 <ul>
104 <li>Your backup storage device (external drive, NAS)</li>
105 <li>A folder synced by your backup tool (Syncthing, rsync, etc.)</li>
106 </ul>
107 <p class="browser-url">
108 <code>{{ browserDownloadSettingsUrl }}</code>
109 </p>
110 <button class="link-btn" (click)="navigateToSettings()">
111 Go to Backup Settings
112 </button>
113 </div>
114
115 <button class="dismiss-btn" (click)="dismissCashuInfo()">
116 Got it, let me add a mint
117 </button>
118 </div>
119 } @else {
120 <div class="empty-state">
121 <span class="sam-text-muted">No mints connected yet.</span>
122 <button class="show-info-btn" (click)="showCashuInfo = true">
123 Show storage info
124 </button>
125 </div>
126 }
127 </div>
128 } @else {
129 <div class="wallet-list">
130 @for (mint of mints; track mint.id) {
131 <button class="wallet-list-item" (click)="selectMint(mint.id)">
132 <span class="wallet-name">{{ mint.name }}</span>
133 <span class="wallet-balance">{{ formatCashuBalance(mint.cachedBalance) }} sats</span>
134 </button>
135 }
136 </div>
137 }
138 <button class="add-wallet-btn" (click)="showAddMint()">
139 <span class="emoji">+</span>
140 <span>Add Mint</span>
141 </button>
142 </div>
143 }
144
145 <!-- Cashu mint detail -->
146 @else if (activeSection === 'cashu-detail' && selectedMint) {
147 <div class="wallet-detail">
148 <div class="balance-row">
149 <div class="balance-display compact">
150 <span class="balance-value">{{ formatCashuBalance(selectedMintBalance) }}</span>
151 <span class="balance-unit">sats</span>
152 </div>
153 <button
154 class="refresh-icon-btn"
155 (click)="refreshMint()"
156 [disabled]="refreshingMint"
157 title="Refresh"
158 >
159 <span class="emoji" [class.spinning]="refreshingMint">🔄</span>
160 </button>
161 </div>
162 @if (refreshError) {
163 <div class="error-message small">{{ refreshError }}</div>
164 }
165 <div class="action-buttons">
166 <button class="action-btn deposit-btn" (click)="showDeposit()">
167 Deposit
168 </button>
169 <button class="action-btn receive-btn" (click)="showReceive()">
170 Receive
171 </button>
172 <button class="action-btn send-btn" (click)="showSend()" [disabled]="selectedMintBalance === 0">
173 Send
174 </button>
175 </div>
176
177 <!-- Token viewer section -->
178 <div class="token-section">
179 <div class="section-title">Tokens ({{ selectedMintProofs.length }})</div>
180 @if (selectedMintProofs.length === 0) {
181 <div class="empty-text">No tokens stored</div>
182 } @else {
183 <div class="token-list">
184 @for (proof of selectedMintProofs; track proof.secret) {
185 <div class="token-item">
186 <span class="token-amount">{{ proof.amount }}</span>
187 <span class="token-time">{{ formatProofTime(proof.receivedAt) }}</span>
188 </div>
189 }
190 </div>
191 }
192 </div>
193
194 <div class="wallet-info">
195 <div class="info-row">
196 <span class="info-label">Mint URL</span>
197 <span class="info-value">{{ selectedMint.mintUrl }}</span>
198 </div>
199 <div class="info-row">
200 <span class="info-label">Unit</span>
201 <span class="info-value">{{ selectedMint.unit }}</span>
202 </div>
203 </div>
204 <button class="delete-btn" (click)="deleteMint()">
205 Delete Mint
206 </button>
207 </div>
208 }
209
210 <!-- Cashu add mint form -->
211 @else if (activeSection === 'cashu-add') {
212 <div class="add-wallet-form">
213 <div class="form-group">
214 <label for="mintName">Mint Name</label>
215 <input
216 id="mintName"
217 type="text"
218 [(ngModel)]="newMintName"
219 placeholder="My Mint"
220 [disabled]="addingMint"
221 />
222 </div>
223 <div class="form-group">
224 <label for="mintUrl">Mint URL</label>
225 <input
226 id="mintUrl"
227 type="text"
228 [(ngModel)]="newMintUrl"
229 placeholder="https://mint.example.com"
230 [disabled]="addingMint"
231 />
232 </div>
233 @if (mintError) {
234 <div class="error-message">{{ mintError }}</div>
235 }
236 @if (mintTestResult) {
237 <div class="success-message">{{ mintTestResult }}</div>
238 }
239 <div class="form-actions">
240 <button
241 class="test-btn"
242 (click)="testMint()"
243 [disabled]="testingMint || addingMint"
244 >
245 {{ testingMint ? 'Testing...' : 'Test Connection' }}
246 </button>
247 <button
248 class="add-btn"
249 (click)="addMint()"
250 [disabled]="addingMint"
251 >
252 {{ addingMint ? 'Adding...' : 'Add Mint' }}
253 </button>
254 </div>
255 </div>
256 }
257
258 <!-- Cashu receive token -->
259 @else if (activeSection === 'cashu-receive') {
260 <div class="add-wallet-form">
261 <div class="form-group">
262 <label for="receiveToken">Paste Cashu Token</label>
263 <textarea
264 id="receiveToken"
265 [(ngModel)]="receiveToken"
266 placeholder="cashuB..."
267 rows="5"
268 [disabled]="receivingToken"
269 ></textarea>
270 </div>
271 @if (receiveError) {
272 <div class="error-message">{{ receiveError }}</div>
273 }
274 @if (receiveResult) {
275 <div class="success-message">{{ receiveResult }}</div>
276 }
277 <div class="form-actions">
278 <button
279 class="add-btn full-width"
280 (click)="receiveTokens()"
281 [disabled]="receivingToken"
282 >
283 {{ receivingToken ? 'Receiving...' : 'Receive Tokens' }}
284 </button>
285 </div>
286 </div>
287 }
288
289 <!-- Cashu send token -->
290 @else if (activeSection === 'cashu-send') {
291 <div class="add-wallet-form">
292 <div class="balance-info">
293 Available: {{ formatCashuBalance(selectedMintBalance) }} sats
294 </div>
295 <div class="form-group">
296 <label for="sendAmount">Amount (sats)</label>
297 <input
298 id="sendAmount"
299 type="number"
300 [(ngModel)]="sendAmount"
301 placeholder="0"
302 min="1"
303 [max]="selectedMintBalance"
304 [disabled]="sendingToken"
305 />
306 </div>
307 @if (sendError) {
308 <div class="error-message">{{ sendError }}</div>
309 }
310 @if (sendResult) {
311 <div class="token-result">
312 <span class="token-label">Token to Share</span>
313 <textarea readonly rows="4">{{ sendResult }}</textarea>
314 <button class="copy-btn" (click)="copyToken()">
315 Copy Token
316 </button>
317 </div>
318 }
319 @if (!sendResult) {
320 <div class="form-actions">
321 <button
322 class="add-btn full-width"
323 (click)="sendTokens()"
324 [disabled]="sendingToken || sendAmount <= 0"
325 >
326 {{ sendingToken ? 'Creating...' : 'Create Token' }}
327 </button>
328 </div>
329 }
330 </div>
331 }
332
333 <!-- Cashu deposit (mint via Lightning) -->
334 @else if (activeSection === 'cashu-mint' && selectedMint) {
335 <div class="add-wallet-form">
336 @if (!depositInvoice) {
337 <div class="form-group">
338 <label for="depositAmount">Amount (sats)</label>
339 <input
340 id="depositAmount"
341 type="number"
342 [(ngModel)]="depositAmount"
343 placeholder="1000"
344 min="1"
345 [disabled]="creatingDepositQuote"
346 />
347 </div>
348 @if (depositError) {
349 <div class="error-message">{{ depositError }}</div>
350 }
351 <div class="form-actions">
352 <button
353 class="add-btn full-width"
354 (click)="createDepositInvoice()"
355 [disabled]="creatingDepositQuote || depositAmount <= 0"
356 >
357 {{ creatingDepositQuote ? 'Creating...' : 'Create Invoice' }}
358 </button>
359 </div>
360 }
361 @if (depositInvoice) {
362 <div class="invoice-result">
363 @if (depositInvoiceQr) {
364 <img [src]="depositInvoiceQr" alt="Invoice QR Code" class="qr-code" />
365 }
366 <div class="deposit-status">
367 @if (depositQuoteState === 'UNPAID') {
368 <span class="status-waiting">Waiting for payment...</span>
369 @if (checkingDepositPayment) {
370 <span class="status-checking">checking</span>
371 }
372 } @else if (depositQuoteState === 'PAID') {
373 <span class="status-paid">Payment received! Claiming tokens...</span>
374 } @else if (depositQuoteState === 'ISSUED') {
375 <span class="status-issued">✓ Tokens received!</span>
376 }
377 </div>
378 @if (depositError) {
379 <div class="error-message">{{ depositError }}</div>
380 }
381 @if (depositSuccess) {
382 <div class="success-message">{{ depositSuccess }}</div>
383 }
384 @if (depositQuoteState === 'UNPAID') {
385 <div class="invoice-text">{{ depositInvoice }}</div>
386 <button class="copy-btn" (click)="copyDepositInvoice()">
387 Copy Invoice
388 </button>
389 }
390 </div>
391 }
392 </div>
393 }
394
395 <!-- Lightning wallet list -->
396 @else if (activeSection === 'lightning') {
397 <div class="lightning-section">
398 @if (connections.length === 0) {
399 <div class="empty-state">
400 <span class="sam-text-muted">
401 No wallets connected yet.
402 </span>
403 </div>
404 } @else {
405 <div class="wallet-list">
406 @for (conn of connections; track conn.id) {
407 <button class="wallet-list-item" (click)="selectConnection(conn.id)">
408 <span class="wallet-name">{{ conn.name }}</span>
409 <span class="wallet-balance">{{ formatBalance(conn.cachedBalance) }} sats</span>
410 </button>
411 }
412 </div>
413 }
414 <button class="add-wallet-btn" (click)="showAddConnection()">
415 <span class="emoji">+</span>
416 <span>Add NWC Connection</span>
417 </button>
418 </div>
419 }
420
421 <!-- Lightning wallet detail -->
422 @else if (activeSection === 'lightning-detail' && selectedConnection) {
423 <div class="wallet-detail">
424 <div class="balance-row">
425 <div class="balance-display compact">
426 <span class="balance-value">{{ formatBalance(selectedConnection.cachedBalance) }}</span>
427 <span class="balance-unit">sats</span>
428 </div>
429 <button class="refresh-icon-btn" (click)="refreshWallet()" title="Refresh">
430 <span class="emoji">🔄</span>
431 </button>
432 </div>
433
434 <div class="action-buttons">
435 <button class="action-btn receive-btn" (click)="showLnReceive()">
436 Receive
437 </button>
438 <button class="action-btn send-btn" (click)="showLnPay()">
439 Pay
440 </button>
441 </div>
442
443 <div class="wallet-info">
444 <div class="info-row">
445 <span class="info-label">Relay</span>
446 <span class="info-value">{{ selectedConnection.relayUrl }}</span>
447 </div>
448 @if (selectedConnection.lud16) {
449 <button class="info-row-btn" (click)="copyLightningAddress()">
450 <span class="info-label">Lightning Address</span>
451 <span class="info-value">
452 {{ selectedConnection.lud16 }}
453 <span class="copy-hint">{{ addressCopied ? '✓ Copied' : '(tap to copy)' }}</span>
454 </span>
455 </button>
456 }
457 </div>
458
459 <!-- Transaction History -->
460 <div class="transaction-section">
461 <div class="section-title">Transactions</div>
462 @if (loadingTransactions) {
463 <div class="loading-text">Loading...</div>
464 } @else if (transactionsNotSupported) {
465 <div class="not-supported-text">Transaction history not supported by this wallet</div>
466 } @else if (transactionsError) {
467 <div class="error-text">{{ transactionsError }}</div>
468 } @else if (transactions.length === 0) {
469 <div class="empty-text">No transactions yet</div>
470 } @else {
471 <div class="transaction-list">
472 @for (tx of transactions; track tx.payment_hash) {
473 <div class="transaction-item" [class.incoming]="tx.type === 'incoming'" [class.outgoing]="tx.type === 'outgoing'">
474 <span class="tx-icon">{{ tx.type === 'incoming' ? '⬇' : '⬆' }}</span>
475 <span class="tx-type">{{ tx.type === 'incoming' ? 'Received' : 'Sent' }}</span>
476 <span class="tx-amount">{{ formatBalance(tx.amount) }}</span>
477 <span class="tx-time">{{ formatTransactionTime(tx.created_at) }}</span>
478 </div>
479 }
480 </div>
481 }
482 </div>
483
484 <button class="delete-btn-small" (click)="deleteConnection()">
485 Delete Wallet
486 </button>
487 </div>
488 }
489
490 <!-- Lightning receive invoice -->
491 @else if (activeSection === 'lightning-receive' && selectedConnection) {
492 <div class="add-wallet-form">
493 <div class="form-group">
494 <label for="lnReceiveAmount">Amount (sats)</label>
495 <input
496 id="lnReceiveAmount"
497 type="number"
498 [(ngModel)]="lnReceiveAmount"
499 placeholder="1000"
500 min="1"
501 [disabled]="generatingInvoice"
502 />
503 </div>
504 <div class="form-group">
505 <label for="lnReceiveDescription">Description (optional)</label>
506 <input
507 id="lnReceiveDescription"
508 type="text"
509 [(ngModel)]="lnReceiveDescription"
510 placeholder="Payment for..."
511 [disabled]="generatingInvoice"
512 />
513 </div>
514 @if (lnReceiveError) {
515 <div class="error-message">{{ lnReceiveError }}</div>
516 }
517 @if (!generatedInvoice) {
518 <div class="form-actions">
519 <button
520 class="add-btn full-width"
521 (click)="createReceiveInvoice()"
522 [disabled]="generatingInvoice || lnReceiveAmount <= 0"
523 >
524 {{ generatingInvoice ? 'Generating...' : 'Generate Invoice' }}
525 </button>
526 </div>
527 }
528 @if (generatedInvoice) {
529 <div class="invoice-result">
530 @if (generatedInvoiceQr) {
531 <img [src]="generatedInvoiceQr" alt="Invoice QR Code" class="qr-code" />
532 }
533 <div class="invoice-text">{{ generatedInvoice }}</div>
534 <button class="copy-btn" (click)="copyInvoice()">
535 {{ invoiceCopied ? 'Copied!' : 'Copy Invoice' }}
536 </button>
537 </div>
538 }
539 </div>
540 }
541
542 <!-- Pay Modal Overlay -->
543 @if (showPayModal && selectedConnection) {
544 <div class="modal-overlay" role="dialog" aria-modal="true" tabindex="-1" (click)="closePayModal()" (keydown.escape)="closePayModal()">
545 <div class="modal-content" role="document" (click)="$event.stopPropagation()" (keydown)="$event.stopPropagation()">
546 <div class="modal-header">
547 <span>Pay Invoice</span>
548 <button class="modal-close" (click)="closePayModal()">×</button>
549 </div>
550 <div class="modal-body">
551 <div class="form-group">
552 <label for="payInput">Lightning Address or Invoice</label>
553 <textarea
554 id="payInput"
555 [(ngModel)]="payInput"
556 placeholder="user@domain.com or lnbc1..."
557 rows="3"
558 [disabled]="paying"
559 ></textarea>
560 </div>
561 <div class="form-group">
562 <label for="payAmount">Amount (sats) - required for addresses</label>
563 <input
564 id="payAmount"
565 type="number"
566 [(ngModel)]="payAmount"
567 placeholder="Optional for invoices"
568 min="1"
569 [disabled]="paying"
570 />
571 </div>
572 @if (paymentError) {
573 <div class="error-message">{{ paymentError }}</div>
574 }
575 @if (paymentSuccess) {
576 <div class="success-message payment-success">Payment Successful!</div>
577 }
578 @if (!paymentSuccess) {
579 <div class="form-actions">
580 <button class="test-btn" (click)="closePayModal()" [disabled]="paying">
581 Cancel
582 </button>
583 <button
584 class="add-btn"
585 (click)="payInvoiceOrAddress()"
586 [disabled]="paying || !payInput.trim()"
587 >
588 {{ paying ? 'Paying...' : 'Pay' }}
589 </button>
590 </div>
591 }
592 </div>
593 </div>
594 </div>
595 }
596
597 <!-- Add wallet form -->
598 @else if (activeSection === 'lightning-add') {
599 <div class="add-wallet-form">
600 <div class="form-group">
601 <label for="walletName">Wallet Name</label>
602 <input
603 id="walletName"
604 type="text"
605 [(ngModel)]="newWalletName"
606 placeholder="My Lightning Wallet"
607 [disabled]="addingConnection"
608 />
609 </div>
610 <div class="form-group">
611 <label for="walletUrl">NWC Connection URL</label>
612 <textarea
613 id="walletUrl"
614 [(ngModel)]="newWalletUrl"
615 placeholder="nostr+walletconnect://..."
616 rows="3"
617 [disabled]="addingConnection"
618 ></textarea>
619 </div>
620 @if (connectionError) {
621 <div class="error-message">{{ connectionError }}</div>
622 }
623 @if (connectionTestResult) {
624 <div class="success-message">{{ connectionTestResult }}</div>
625 }
626 @if (nwcService.logs.length > 0) {
627 <div class="nwc-log">
628 <div class="log-header">
629 <span>Connection Log</span>
630 <button class="log-clear-btn" (click)="nwcService.clearLogs()">Clear</button>
631 </div>
632 <div class="log-entries">
633 @for (entry of nwcService.logs; track entry.timestamp) {
634 <div class="log-entry" [class.log-warn]="entry.level === 'warn'" [class.log-error]="entry.level === 'error'">
635 <span class="log-time">{{ entry.timestamp | date:'HH:mm:ss' }}</span>
636 <span class="log-message">{{ entry.message }}</span>
637 </div>
638 }
639 </div>
640 </div>
641 }
642 <div class="form-actions">
643 <button
644 class="test-btn"
645 (click)="testConnection()"
646 [disabled]="testingConnection || addingConnection"
647 >
648 {{ testingConnection ? 'Testing...' : 'Test Connection' }}
649 </button>
650 <button
651 class="add-btn"
652 (click)="addConnection()"
653 [disabled]="addingConnection"
654 >
655 {{ addingConnection ? 'Adding...' : 'Add Wallet' }}
656 </button>
657 </div>
658 </div>
659 }
660 </div>
661