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