wallet.component.ts raw
1 import { Component, inject, OnInit, OnDestroy } from '@angular/core';
2 import { Router } from '@angular/router';
3 import { FormsModule } from '@angular/forms';
4 import { CommonModule } from '@angular/common';
5 import {
6 LoggerService,
7 NavComponent,
8 NwcService,
9 NwcConnection_DECRYPTED,
10 CashuService,
11 CashuMint_DECRYPTED,
12 CashuProof,
13 NwcLookupInvoiceResult,
14 BrowserSyncFlow,
15 } from '@common';
16 import * as QRCode from 'qrcode';
17
18 type WalletSection =
19 | 'main'
20 | 'cashu'
21 | 'cashu-detail'
22 | 'cashu-add'
23 | 'cashu-receive'
24 | 'cashu-send'
25 | 'cashu-mint'
26 | 'lightning'
27 | 'lightning-detail'
28 | 'lightning-add'
29 | 'lightning-receive'
30 | 'lightning-pay';
31
32 @Component({
33 selector: 'app-wallet',
34 templateUrl: './wallet.component.html',
35 styleUrl: './wallet.component.scss',
36 imports: [CommonModule, FormsModule],
37 })
38 export class WalletComponent extends NavComponent implements OnInit, OnDestroy {
39 readonly #logger = inject(LoggerService);
40 readonly #router = inject(Router);
41 readonly nwcService = inject(NwcService);
42 readonly cashuService = inject(CashuService);
43
44 activeSection: WalletSection = 'main';
45 selectedConnectionId: string | null = null;
46 selectedMintId: string | null = null;
47
48 // Form fields for adding new NWC connection
49 newWalletName = '';
50 newWalletUrl = '';
51 addingConnection = false;
52 testingConnection = false;
53 connectionError = '';
54 connectionTestResult = '';
55
56 // Form fields for adding new Cashu mint
57 newMintName = '';
58 newMintUrl = '';
59 addingMint = false;
60 testingMint = false;
61 mintError = '';
62 mintTestResult = '';
63
64 // Cashu receive/send fields
65 receiveToken = '';
66 receivingToken = false;
67 receiveError = '';
68 receiveResult = '';
69 sendAmount = 0;
70 sendingToken = false;
71 sendError = '';
72 sendResult = '';
73
74 // Cashu mint (deposit) fields
75 depositAmount = 0;
76 creatingDepositQuote = false;
77 depositQuoteId = '';
78 depositInvoice = '';
79 depositInvoiceQr = '';
80 depositError = '';
81 depositSuccess = '';
82 checkingDepositPayment = false;
83 depositQuoteState: 'UNPAID' | 'PAID' | 'ISSUED' = 'UNPAID';
84 private depositPollingInterval: ReturnType<typeof setInterval> | null = null;
85
86 // Loading states
87 loadingBalances = false;
88 balanceError = '';
89
90 // Lightning transaction history
91 transactions: NwcLookupInvoiceResult[] = [];
92 loadingTransactions = false;
93 transactionsError = '';
94 transactionsNotSupported = false;
95
96 // Lightning receive
97 lnReceiveAmount = 0;
98 lnReceiveDescription = '';
99 generatingInvoice = false;
100 generatedInvoice = '';
101 generatedInvoiceQr = '';
102 lnReceiveError = '';
103 invoiceCopied = false;
104
105 // Lightning pay
106 showPayModal = false;
107 payInput = '';
108 payAmount = 0;
109 paying = false;
110 paymentSuccess = false;
111 paymentError = '';
112
113 // Clipboard feedback
114 addressCopied = false;
115
116 // Cashu onboarding info
117 showCashuInfo = true;
118 currentSyncFlow: BrowserSyncFlow = BrowserSyncFlow.NO_SYNC;
119 readonly BrowserSyncFlow = BrowserSyncFlow; // Expose enum to template
120 readonly browserDownloadSettingsUrl = 'about:preferences#general';
121
122 // Cashu mint refresh
123 refreshingMint = false;
124 refreshError = '';
125
126 get title(): string {
127 switch (this.activeSection) {
128 case 'cashu':
129 return 'Cashu';
130 case 'cashu-detail':
131 return this.selectedMint?.name ?? 'Mint';
132 case 'cashu-add':
133 return 'Add Mint';
134 case 'cashu-receive':
135 return 'Receive';
136 case 'cashu-send':
137 return 'Send';
138 case 'cashu-mint':
139 return 'Deposit';
140 case 'lightning':
141 return 'Lightning';
142 case 'lightning-detail':
143 return this.selectedConnection?.name ?? 'Wallet';
144 case 'lightning-add':
145 return 'Add Wallet';
146 case 'lightning-receive':
147 return 'Receive';
148 case 'lightning-pay':
149 return 'Pay';
150 default:
151 return 'Wallet';
152 }
153 }
154
155 get showBackButton(): boolean {
156 return this.activeSection !== 'main';
157 }
158
159 get connections(): NwcConnection_DECRYPTED[] {
160 return this.nwcService.getConnections();
161 }
162
163 get selectedConnection(): NwcConnection_DECRYPTED | undefined {
164 if (!this.selectedConnectionId) return undefined;
165 return this.nwcService.getConnection(this.selectedConnectionId);
166 }
167
168 get totalLightningBalance(): number {
169 return this.nwcService.getCachedTotalBalance();
170 }
171
172 get mints(): CashuMint_DECRYPTED[] {
173 return this.cashuService.getMints();
174 }
175
176 get selectedMint(): CashuMint_DECRYPTED | undefined {
177 if (!this.selectedMintId) return undefined;
178 return this.cashuService.getMint(this.selectedMintId);
179 }
180
181 get totalCashuBalance(): number {
182 return this.cashuService.getCachedTotalBalance();
183 }
184
185 get selectedMintBalance(): number {
186 if (!this.selectedMintId) return 0;
187 return this.cashuService.getBalance(this.selectedMintId);
188 }
189
190 get selectedMintProofs(): CashuProof[] {
191 if (!this.selectedMintId) return [];
192 return this.cashuService.getProofs(this.selectedMintId);
193 }
194
195 ngOnInit(): void {
196 // Load current sync flow setting
197 this.currentSyncFlow = this.storage.getSyncFlow();
198
199 // Refresh balances on init if we have connections
200 if (this.connections.length > 0) {
201 this.refreshAllBalances();
202 }
203 }
204
205 ngOnDestroy(): void {
206 this.nwcService.disconnectAll();
207 this.stopDepositPolling();
208 }
209
210 setSection(section: WalletSection) {
211 this.activeSection = section;
212 this.connectionError = '';
213 this.connectionTestResult = '';
214 }
215
216 goBack() {
217 switch (this.activeSection) {
218 case 'lightning-detail':
219 case 'lightning-add':
220 this.activeSection = 'lightning';
221 this.selectedConnectionId = null;
222 this.resetAddForm();
223 this.resetLightningForms();
224 break;
225 case 'lightning-receive':
226 case 'lightning-pay':
227 this.activeSection = 'lightning-detail';
228 this.resetLightningForms();
229 break;
230 case 'cashu-detail':
231 case 'cashu-add':
232 this.activeSection = 'cashu';
233 this.selectedMintId = null;
234 this.resetAddMintForm();
235 break;
236 case 'cashu-receive':
237 case 'cashu-send':
238 case 'cashu-mint':
239 this.activeSection = 'cashu-detail';
240 this.resetReceiveSendForm();
241 this.resetDepositForm();
242 break;
243 case 'lightning':
244 case 'cashu':
245 this.activeSection = 'main';
246 break;
247 }
248 }
249
250 selectConnection(connectionId: string) {
251 this.selectedConnectionId = connectionId;
252 this.activeSection = 'lightning-detail';
253 this.loadTransactions(connectionId);
254 }
255
256 private resetLightningForms() {
257 this.lnReceiveAmount = 0;
258 this.lnReceiveDescription = '';
259 this.generatingInvoice = false;
260 this.generatedInvoice = '';
261 this.generatedInvoiceQr = '';
262 this.lnReceiveError = '';
263 this.invoiceCopied = false;
264 this.payInput = '';
265 this.payAmount = 0;
266 this.paying = false;
267 this.paymentSuccess = false;
268 this.paymentError = '';
269 this.showPayModal = false;
270 }
271
272 showAddConnection() {
273 this.resetAddForm();
274 this.activeSection = 'lightning-add';
275 }
276
277 private resetAddForm() {
278 this.newWalletName = '';
279 this.newWalletUrl = '';
280 this.connectionError = '';
281 this.connectionTestResult = '';
282 this.addingConnection = false;
283 this.testingConnection = false;
284 }
285
286 async testConnection() {
287 if (!this.newWalletUrl.trim()) {
288 this.connectionError = 'Please enter an NWC URL';
289 return;
290 }
291
292 this.testingConnection = true;
293 this.connectionError = '';
294 this.connectionTestResult = '';
295 this.nwcService.clearLogs();
296
297 try {
298 const info = await this.nwcService.testConnection(this.newWalletUrl);
299 this.connectionTestResult = `Connected! ${info.alias ? 'Wallet: ' + info.alias : ''}`;
300 // Hide logs on success
301 this.nwcService.clearLogs();
302 } catch (error) {
303 this.connectionError =
304 error instanceof Error ? error.message : 'Connection test failed';
305 // Keep logs visible on failure for debugging
306 } finally {
307 this.testingConnection = false;
308 }
309 }
310
311 async addConnection() {
312 if (!this.newWalletName.trim()) {
313 this.connectionError = 'Please enter a wallet name';
314 return;
315 }
316 if (!this.newWalletUrl.trim()) {
317 this.connectionError = 'Please enter an NWC URL';
318 return;
319 }
320
321 this.addingConnection = true;
322 this.connectionError = '';
323
324 try {
325 await this.nwcService.addConnection(
326 this.newWalletName.trim(),
327 this.newWalletUrl.trim()
328 );
329
330 // Refresh the balance for the new connection
331 const connections = this.nwcService.getConnections();
332 const newConnection = connections[connections.length - 1];
333 if (newConnection) {
334 try {
335 await this.nwcService.getBalance(newConnection.id);
336 } catch {
337 // Ignore balance fetch error
338 }
339 }
340
341 this.goBack();
342 } catch (error) {
343 this.connectionError =
344 error instanceof Error ? error.message : 'Failed to add connection';
345 } finally {
346 this.addingConnection = false;
347 }
348 }
349
350 async deleteConnection() {
351 if (!this.selectedConnectionId) return;
352
353 const connection = this.selectedConnection;
354 if (
355 !confirm(`Delete wallet "${connection?.name}"? This cannot be undone.`)
356 ) {
357 return;
358 }
359
360 try {
361 await this.nwcService.deleteConnection(this.selectedConnectionId);
362 this.goBack();
363 } catch (error) {
364 console.error('Failed to delete connection:', error);
365 }
366 }
367
368 // Cashu methods
369
370 selectMint(mintId: string) {
371 this.selectedMintId = mintId;
372 this.activeSection = 'cashu-detail';
373 // Auto-refresh to check for spent proofs
374 this.refreshMint();
375 }
376
377 async refreshMint() {
378 if (!this.selectedMintId || this.refreshingMint) return;
379
380 this.refreshingMint = true;
381 this.refreshError = '';
382
383 try {
384 const removedAmount = await this.cashuService.checkProofsSpent(this.selectedMintId);
385 if (removedAmount > 0) {
386 // Balance was updated, proofs were spent
387 console.log(`Removed ${removedAmount} sats of spent proofs`);
388 }
389 } catch (error) {
390 this.refreshError = error instanceof Error ? error.message : 'Failed to refresh';
391 console.error('Failed to refresh mint:', error);
392 } finally {
393 this.refreshingMint = false;
394 }
395 }
396
397 showAddMint() {
398 this.resetAddMintForm();
399 this.activeSection = 'cashu-add';
400 }
401
402 showReceive() {
403 this.resetReceiveSendForm();
404 this.activeSection = 'cashu-receive';
405 }
406
407 showSend() {
408 this.resetReceiveSendForm();
409 this.activeSection = 'cashu-send';
410 }
411
412 private resetAddMintForm() {
413 this.newMintName = '';
414 this.newMintUrl = '';
415 this.mintError = '';
416 this.mintTestResult = '';
417 this.addingMint = false;
418 this.testingMint = false;
419 }
420
421 private resetReceiveSendForm() {
422 this.receiveToken = '';
423 this.receivingToken = false;
424 this.receiveError = '';
425 this.receiveResult = '';
426 this.sendAmount = 0;
427 this.sendingToken = false;
428 this.sendError = '';
429 this.sendResult = '';
430 }
431
432 private resetDepositForm() {
433 this.depositAmount = 0;
434 this.creatingDepositQuote = false;
435 this.depositQuoteId = '';
436 this.depositInvoice = '';
437 this.depositInvoiceQr = '';
438 this.depositError = '';
439 this.depositSuccess = '';
440 this.checkingDepositPayment = false;
441 this.depositQuoteState = 'UNPAID';
442 this.stopDepositPolling();
443 }
444
445 private stopDepositPolling() {
446 if (this.depositPollingInterval) {
447 clearInterval(this.depositPollingInterval);
448 this.depositPollingInterval = null;
449 }
450 }
451
452 async testMint() {
453 if (!this.newMintUrl.trim()) {
454 this.mintError = 'Please enter a mint URL';
455 return;
456 }
457
458 this.testingMint = true;
459 this.mintError = '';
460 this.mintTestResult = '';
461
462 try {
463 const info = await this.cashuService.testMintConnection(
464 this.newMintUrl.trim()
465 );
466 this.mintTestResult = `Connected! ${info.name ? 'Mint: ' + info.name : ''}`;
467 } catch (error) {
468 this.mintError =
469 error instanceof Error ? error.message : 'Connection test failed';
470 } finally {
471 this.testingMint = false;
472 }
473 }
474
475 async addMint() {
476 if (!this.newMintName.trim()) {
477 this.mintError = 'Please enter a mint name';
478 return;
479 }
480 if (!this.newMintUrl.trim()) {
481 this.mintError = 'Please enter a mint URL';
482 return;
483 }
484
485 this.addingMint = true;
486 this.mintError = '';
487
488 try {
489 await this.cashuService.addMint(
490 this.newMintName.trim(),
491 this.newMintUrl.trim()
492 );
493 this.goBack();
494 } catch (error) {
495 this.mintError =
496 error instanceof Error ? error.message : 'Failed to add mint';
497 } finally {
498 this.addingMint = false;
499 }
500 }
501
502 async deleteMint() {
503 if (!this.selectedMintId) return;
504
505 const mint = this.selectedMint;
506 if (!confirm(`Delete mint "${mint?.name}"? Any tokens stored will be lost. This cannot be undone.`)) {
507 return;
508 }
509
510 try {
511 await this.cashuService.deleteMint(this.selectedMintId);
512 this.goBack();
513 } catch (error) {
514 console.error('Failed to delete mint:', error);
515 }
516 }
517
518 async receiveTokens() {
519 if (!this.receiveToken.trim()) {
520 this.receiveError = 'Please paste a Cashu token';
521 return;
522 }
523
524 this.receivingToken = true;
525 this.receiveError = '';
526 this.receiveResult = '';
527
528 try {
529 const result = await this.cashuService.receive(this.receiveToken.trim());
530 this.receiveResult = `Received ${result.amount} sats!`;
531 this.receiveToken = '';
532 } catch (error) {
533 this.receiveError =
534 error instanceof Error ? error.message : 'Failed to receive token';
535 } finally {
536 this.receivingToken = false;
537 }
538 }
539
540 async sendTokens() {
541 if (!this.selectedMintId) return;
542
543 if (this.sendAmount <= 0) {
544 this.sendError = 'Please enter a valid amount';
545 return;
546 }
547
548 const balance = this.selectedMintBalance;
549 if (this.sendAmount > balance) {
550 this.sendError = `Insufficient balance. You have ${balance} sats`;
551 return;
552 }
553
554 this.sendingToken = true;
555 this.sendError = '';
556 this.sendResult = '';
557
558 try {
559 const result = await this.cashuService.send(
560 this.selectedMintId,
561 this.sendAmount
562 );
563 this.sendResult = result.token;
564 this.sendAmount = 0;
565 } catch (error) {
566 this.sendError =
567 error instanceof Error ? error.message : 'Failed to create token';
568 } finally {
569 this.sendingToken = false;
570 }
571 }
572
573 copyToken() {
574 if (this.sendResult) {
575 navigator.clipboard.writeText(this.sendResult);
576 }
577 }
578
579 async checkProofs() {
580 if (!this.selectedMintId) return;
581
582 try {
583 const removedAmount = await this.cashuService.checkProofsSpent(
584 this.selectedMintId
585 );
586 if (removedAmount > 0) {
587 alert(`Removed ${removedAmount} sats of spent proofs.`);
588 } else {
589 alert('All proofs are valid.');
590 }
591 } catch (error) {
592 console.error('Failed to check proofs:', error);
593 }
594 }
595
596 // Cashu deposit (mint) methods
597
598 showDeposit() {
599 this.resetDepositForm();
600 this.activeSection = 'cashu-mint';
601 }
602
603 async createDepositInvoice() {
604 if (!this.selectedMintId) return;
605
606 if (this.depositAmount <= 0) {
607 this.depositError = 'Please enter an amount';
608 return;
609 }
610
611 this.creatingDepositQuote = true;
612 this.depositError = '';
613 this.depositInvoice = '';
614 this.depositInvoiceQr = '';
615
616 try {
617 const quote = await this.cashuService.createMintQuote(
618 this.selectedMintId,
619 this.depositAmount
620 );
621
622 this.depositQuoteId = quote.quoteId;
623 this.depositInvoice = quote.invoice;
624 this.depositQuoteState = quote.state;
625
626 // Generate QR code
627 this.depositInvoiceQr = await QRCode.toDataURL(quote.invoice, {
628 width: 200,
629 margin: 2,
630 color: {
631 dark: '#000000',
632 light: '#ffffff',
633 },
634 });
635
636 // Start polling for payment
637 this.startDepositPolling();
638 } catch (error) {
639 this.depositError =
640 error instanceof Error ? error.message : 'Failed to create invoice';
641 } finally {
642 this.creatingDepositQuote = false;
643 }
644 }
645
646 private startDepositPolling() {
647 // Poll every 3 seconds for payment confirmation
648 this.depositPollingInterval = setInterval(async () => {
649 await this.checkDepositPayment();
650 }, 3000);
651 }
652
653 async checkDepositPayment() {
654 if (!this.selectedMintId || !this.depositQuoteId) return;
655
656 this.checkingDepositPayment = true;
657
658 try {
659 const quote = await this.cashuService.checkMintQuote(
660 this.selectedMintId,
661 this.depositQuoteId
662 );
663
664 this.depositQuoteState = quote.state;
665
666 if (quote.state === 'PAID') {
667 // Invoice is paid, claim the tokens
668 this.stopDepositPolling();
669 await this.claimDepositTokens();
670 } else if (quote.state === 'ISSUED') {
671 // Already claimed
672 this.stopDepositPolling();
673 this.depositSuccess = 'Tokens already claimed!';
674 }
675 } catch (error) {
676 // Don't show error for polling failures, just log
677 console.error('Failed to check payment:', error);
678 } finally {
679 this.checkingDepositPayment = false;
680 }
681 }
682
683 async claimDepositTokens() {
684 if (!this.selectedMintId || !this.depositQuoteId) return;
685
686 try {
687 const result = await this.cashuService.mintTokens(
688 this.selectedMintId,
689 this.depositAmount,
690 this.depositQuoteId
691 );
692
693 this.depositSuccess = `Received ${result.amount} sats!`;
694 this.depositQuoteState = 'ISSUED';
695 } catch (error) {
696 this.depositError =
697 error instanceof Error ? error.message : 'Failed to claim tokens';
698 }
699 }
700
701 async copyDepositInvoice() {
702 if (this.depositInvoice) {
703 await navigator.clipboard.writeText(this.depositInvoice);
704 }
705 }
706
707 formatCashuBalance(sats: number | undefined): string {
708 return this.cashuService.formatBalance(sats);
709 }
710
711 async refreshBalance(connectionId: string) {
712 try {
713 await this.nwcService.getBalance(connectionId);
714 } catch (error) {
715 console.error('Failed to refresh balance:', error);
716 }
717 }
718
719 async refreshAllBalances() {
720 this.loadingBalances = true;
721 this.balanceError = '';
722
723 try {
724 await this.nwcService.getAllBalances();
725 } catch {
726 this.balanceError = 'Failed to refresh some balances';
727 } finally {
728 this.loadingBalances = false;
729 }
730 }
731
732 formatBalance(millisats: number | undefined): string {
733 if (millisats === undefined) return '—';
734 // Convert millisats to sats with 3 decimal places
735 const sats = millisats / 1000;
736 return sats.toLocaleString('en-US', {
737 minimumFractionDigits: 0,
738 maximumFractionDigits: 3,
739 });
740 }
741
742 // Lightning transaction methods
743
744 async loadTransactions(connectionId: string) {
745 this.loadingTransactions = true;
746 this.transactionsError = '';
747 this.transactionsNotSupported = false;
748
749 try {
750 this.transactions = await this.nwcService.listTransactions(connectionId, {
751 limit: 20,
752 });
753 } catch (error) {
754 const errorMsg = error instanceof Error ? error.message : 'Unknown error';
755 if (errorMsg.includes('NOT_IMPLEMENTED') || errorMsg.includes('not supported')) {
756 this.transactionsNotSupported = true;
757 } else {
758 this.transactionsError = errorMsg;
759 }
760 this.transactions = [];
761 } finally {
762 this.loadingTransactions = false;
763 }
764 }
765
766 async refreshWallet() {
767 if (!this.selectedConnectionId) return;
768
769 // Refresh balance and transactions in parallel
770 await Promise.all([
771 this.refreshBalance(this.selectedConnectionId),
772 this.loadTransactions(this.selectedConnectionId),
773 ]);
774 }
775
776 showLnReceive() {
777 this.resetLightningForms();
778 this.activeSection = 'lightning-receive';
779 }
780
781 showLnPay() {
782 this.resetLightningForms();
783 this.showPayModal = true;
784 }
785
786 closePayModal() {
787 this.showPayModal = false;
788 this.resetLightningForms();
789 }
790
791 async createReceiveInvoice() {
792 if (!this.selectedConnectionId) return;
793
794 if (this.lnReceiveAmount <= 0) {
795 this.lnReceiveError = 'Please enter an amount';
796 return;
797 }
798
799 this.generatingInvoice = true;
800 this.lnReceiveError = '';
801 this.generatedInvoice = '';
802 this.generatedInvoiceQr = '';
803
804 try {
805 const result = await this.nwcService.makeInvoice(
806 this.selectedConnectionId,
807 this.lnReceiveAmount * 1000, // Convert sats to millisats
808 this.lnReceiveDescription || undefined
809 );
810 this.generatedInvoice = result.invoice;
811
812 // Generate QR code
813 this.generatedInvoiceQr = await QRCode.toDataURL(result.invoice, {
814 width: 200,
815 margin: 2,
816 color: {
817 dark: '#000000',
818 light: '#ffffff',
819 },
820 });
821 } catch (error) {
822 this.lnReceiveError =
823 error instanceof Error ? error.message : 'Failed to create invoice';
824 } finally {
825 this.generatingInvoice = false;
826 }
827 }
828
829 async copyInvoice() {
830 if (this.generatedInvoice) {
831 await navigator.clipboard.writeText(this.generatedInvoice);
832 this.invoiceCopied = true;
833 setTimeout(() => (this.invoiceCopied = false), 2000);
834 }
835 }
836
837 async copyLightningAddress() {
838 const lud16 = this.selectedConnection?.lud16;
839 if (lud16) {
840 await navigator.clipboard.writeText(lud16);
841 this.addressCopied = true;
842 setTimeout(() => (this.addressCopied = false), 2000);
843 }
844 }
845
846 async payInvoiceOrAddress() {
847 if (!this.selectedConnectionId || !this.payInput.trim()) {
848 this.paymentError = 'Please enter a lightning address or invoice';
849 return;
850 }
851
852 this.paying = true;
853 this.paymentError = '';
854 this.paymentSuccess = false;
855
856 try {
857 let invoice = this.payInput.trim();
858
859 // Check if it's a lightning address
860 if (this.nwcService.isLightningAddress(invoice)) {
861 if (this.payAmount <= 0) {
862 this.paymentError = 'Please enter an amount for lightning address payments';
863 this.paying = false;
864 return;
865 }
866 // Resolve lightning address to invoice
867 invoice = await this.nwcService.resolveLightningAddress(
868 invoice,
869 this.payAmount * 1000 // Convert sats to millisats
870 );
871 }
872
873 // Pay the invoice
874 await this.nwcService.payInvoice(
875 this.selectedConnectionId,
876 invoice,
877 this.payAmount > 0 ? this.payAmount * 1000 : undefined
878 );
879
880 this.paymentSuccess = true;
881
882 // Refresh balance and transactions after payment
883 await this.refreshWallet();
884
885 // Close modal after a delay
886 setTimeout(() => {
887 this.closePayModal();
888 }, 2000);
889 } catch (error) {
890 this.paymentError =
891 error instanceof Error ? error.message : 'Payment failed';
892 } finally {
893 this.paying = false;
894 }
895 }
896
897 formatTransactionTime(timestamp: number): string {
898 const date = new Date(timestamp * 1000);
899 const now = new Date();
900 const isToday = date.toDateString() === now.toDateString();
901
902 if (isToday) {
903 return date.toLocaleTimeString('en-US', {
904 hour: '2-digit',
905 minute: '2-digit',
906 });
907 }
908
909 return date.toLocaleDateString('en-US', {
910 month: 'short',
911 day: 'numeric',
912 });
913 }
914
915 formatProofTime(isoTimestamp: string | undefined): string {
916 if (!isoTimestamp) return '—';
917
918 const date = new Date(isoTimestamp);
919 const now = new Date();
920 const isToday = date.toDateString() === now.toDateString();
921
922 if (isToday) {
923 return date.toLocaleTimeString('en-US', {
924 hour: '2-digit',
925 minute: '2-digit',
926 });
927 }
928
929 return date.toLocaleDateString('en-US', {
930 month: 'short',
931 day: 'numeric',
932 hour: '2-digit',
933 minute: '2-digit',
934 });
935 }
936
937 async onClickLock() {
938 this.#logger.logVaultLock();
939 await this.storage.lockVault();
940 this.#router.navigateByUrl('/vault-login');
941 }
942
943 // Cashu onboarding methods
944 dismissCashuInfo() {
945 this.showCashuInfo = false;
946 }
947
948 navigateToSettings() {
949 this.#router.navigateByUrl('/home/settings');
950 }
951 }
952