Full Code of cashubtc/cashu.me for AI

main 5979114b1a39 cached
228 files
1.7 MB
456.7k tokens
223 symbols
1 requests
Download .txt
Showing preview only (1,908K chars total). Download the full file or copy to clipboard to get everything.
Repository: cashubtc/cashu.me
Branch: main
Commit: 5979114b1a39
Files: 228
Total size: 1.7 MB

Directory structure:
gitextract_4w51wde5/

├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github/
│   └── workflows/
│       ├── build.yaml
│       ├── docker.yaml
│       ├── format.yaml
│       ├── lint.yml
│       └── test.yml
├── .gitignore
├── .npmrc
├── .postcssrc.js
├── .prettierignore
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── AGENTS.md
├── Dockerfile
├── LICENSE.md
├── README.md
├── android/
│   ├── .gitignore
│   ├── app/
│   │   ├── .gitignore
│   │   ├── build.gradle
│   │   ├── capacitor.build.gradle
│   │   ├── proguard-rules.pro
│   │   └── src/
│   │       ├── androidTest/
│   │       │   └── java/
│   │       │       └── com/
│   │       │           └── getcapacitor/
│   │       │               └── myapp/
│   │       │                   └── ExampleInstrumentedTest.java
│   │       ├── main/
│   │       │   ├── AndroidManifest.xml
│   │       │   ├── java/
│   │       │   │   └── me/
│   │       │   │       └── cashu/
│   │       │   │           └── wallet/
│   │       │   │               └── MainActivity.java
│   │       │   └── res/
│   │       │       ├── drawable/
│   │       │       │   └── ic_launcher_background.xml
│   │       │       ├── drawable-v24/
│   │       │       │   └── ic_launcher_foreground.xml
│   │       │       ├── layout/
│   │       │       │   └── activity_main.xml
│   │       │       ├── mipmap-anydpi-v26/
│   │       │       │   ├── ic_launcher.xml
│   │       │       │   └── ic_launcher_round.xml
│   │       │       ├── values/
│   │       │       │   ├── ic_launcher_background.xml
│   │       │       │   ├── strings.xml
│   │       │       │   └── styles.xml
│   │       │       └── xml/
│   │       │           └── file_paths.xml
│   │       └── test/
│   │           └── java/
│   │               └── com/
│   │                   └── getcapacitor/
│   │                       └── myapp/
│   │                           └── ExampleUnitTest.java
│   ├── build.gradle
│   ├── capacitor.settings.gradle
│   ├── gradle/
│   │   └── wrapper/
│   │       ├── gradle-wrapper.jar
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── settings.gradle
│   └── variables.gradle
├── capacitor.config.ts
├── docker-compose.yaml
├── extension/
│   ├── embedder.html
│   ├── manifest.json
│   └── style.css
├── index.html
├── ios/
│   ├── .gitignore
│   └── App/
│       ├── App/
│       │   ├── AppDelegate.swift
│       │   ├── Assets.xcassets/
│       │   │   ├── AppIcon.appiconset/
│       │   │   │   └── Contents.json
│       │   │   ├── Contents.json
│       │   │   └── Splash.imageset/
│       │   │       └── Contents.json
│       │   ├── Base.lproj/
│       │   │   ├── LaunchScreen.storyboard
│       │   │   └── Main.storyboard
│       │   └── Info.plist
│       ├── App.xcodeproj/
│       │   └── project.pbxproj
│       ├── App.xcworkspace/
│       │   ├── contents.xcworkspacedata
│       │   └── xcshareddata/
│       │       └── IDEWorkspaceChecks.plist
│       └── Podfile
├── jsconfig.json
├── package.json
├── quasar.config.js
├── quasar.extensions.json
├── scripts/
│   └── check-i18n.js
├── src/
│   ├── App.vue
│   ├── boot/
│   │   ├── .gitkeep
│   │   ├── axios.js
│   │   ├── base.js
│   │   ├── cashu.js
│   │   ├── global-components.js
│   │   └── i18n.js
│   ├── components/
│   │   ├── ActivityOrb.vue
│   │   ├── AddMintDialog.vue
│   │   ├── AmountInputComponent.vue
│   │   ├── AndroidPWAPrompt.vue
│   │   ├── AnimatedNumber.vue
│   │   ├── BalanceView.vue
│   │   ├── ChooseMint.vue
│   │   ├── CreateInvoiceDialog.vue
│   │   ├── DisplayTokenComponent.vue
│   │   ├── EditMintDialog.vue
│   │   ├── EssentialLink.vue
│   │   ├── FullscreenHeader.vue
│   │   ├── HistoryTable.vue
│   │   ├── InvoiceDetailDialog.vue
│   │   ├── MainHeader.vue
│   │   ├── MeltQuoteInformation.vue
│   │   ├── MintAuditInfo.vue
│   │   ├── MintAuditSwapsBarChart.vue
│   │   ├── MintAuditWarningBox.vue
│   │   ├── MintDiscovery.vue
│   │   ├── MintInfoContainer.vue
│   │   ├── MintMotdMessage.vue
│   │   ├── MintQuoteInformation.vue
│   │   ├── MintRatingsComponent.vue
│   │   ├── MintSettings.vue
│   │   ├── MultinutPaymentDialog.vue
│   │   ├── NWCDialog.vue
│   │   ├── NoMintWarnBanner.vue
│   │   ├── NostrMintRestore.vue
│   │   ├── NumericKeyboard.vue
│   │   ├── P2PKDialog.vue
│   │   ├── ParseInputComponent.vue
│   │   ├── PayInvoiceDialog.vue
│   │   ├── PaymentRequestDialog.vue
│   │   ├── PaymentRequestInfo.vue
│   │   ├── PaymentRequestPayments.vue
│   │   ├── QrcodeReader.vue
│   │   ├── ReceiveDialog.vue
│   │   ├── ReceiveEcashDrawer.vue
│   │   ├── ReceiveTokenDialog.vue
│   │   ├── RemoveMintDialog.vue
│   │   ├── RestoreView.vue
│   │   ├── SendDialog.vue
│   │   ├── SendPaymentRequest.vue
│   │   ├── SendTokenDialog.vue
│   │   ├── SettingsView.vue
│   │   ├── SwapIncomingTokenToKnownMint.vue
│   │   ├── ToggleUnit.vue
│   │   ├── TokenInformation.vue
│   │   ├── TokenStringRender.vue
│   │   ├── ToolTipInfo.vue
│   │   ├── WelcomeDialog.vue
│   │   └── iOSPWAPrompt.vue
│   ├── css/
│   │   ├── app.scss
│   │   ├── base.scss
│   │   ├── mintlist.css
│   │   └── quasar.variables.scss
│   ├── i18n/
│   │   ├── ar-SA/
│   │   │   └── index.ts
│   │   ├── cs-CZ/
│   │   │   └── index.ts
│   │   ├── de-DE/
│   │   │   └── index.ts
│   │   ├── el-GR/
│   │   │   └── index.ts
│   │   ├── en-US/
│   │   │   └── index.ts
│   │   ├── es-ES/
│   │   │   └── index.ts
│   │   ├── fr-FR/
│   │   │   └── index.ts
│   │   ├── index.ts
│   │   ├── it-IT/
│   │   │   └── index.ts
│   │   ├── ja-JP/
│   │   │   └── index.ts
│   │   ├── pt-BR/
│   │   │   └── index.ts
│   │   ├── sv-SE/
│   │   │   └── index.ts
│   │   ├── th-TH/
│   │   │   └── index.ts
│   │   ├── tr-TR/
│   │   │   └── index.ts
│   │   └── zh-CN/
│   │       └── index.ts
│   ├── icons.js
│   ├── js/
│   │   ├── __tests__/
│   │   │   ├── legacy-qr.test.js
│   │   │   └── token.test.js
│   │   ├── base64.js
│   │   ├── dhke.js
│   │   ├── eventBus.js
│   │   ├── legacy-qr.js
│   │   ├── notify.ts
│   │   ├── string-utils.js
│   │   ├── token.ts
│   │   ├── utils.js
│   │   └── wallet-helpers.js
│   ├── layouts/
│   │   ├── BlankLayout.vue
│   │   ├── FullscreenLayout.vue
│   │   └── MainLayout.vue
│   ├── main.js
│   ├── pages/
│   │   ├── AlreadyRunning.vue
│   │   ├── CreateMintReviewPage.vue
│   │   ├── ErrorNotFound.vue
│   │   ├── MintDetailsPage.vue
│   │   ├── MintDiscoveryPage.vue
│   │   ├── MintRatingsPage.vue
│   │   ├── Restore.vue
│   │   ├── Settings.vue
│   │   ├── TermsPage.vue
│   │   ├── WalletPage.vue
│   │   ├── WelcomePage.vue
│   │   └── welcome/
│   │       ├── WelcomeMintSetup.vue
│   │       ├── WelcomeRecoverSeed.vue
│   │       ├── WelcomeRestoreEcash.vue
│   │       ├── WelcomeSlide1.vue
│   │       ├── WelcomeSlide2.vue
│   │       ├── WelcomeSlide3.vue
│   │       ├── WelcomeSlide4.vue
│   │       └── WelcomeSlideChoice.vue
│   ├── router/
│   │   ├── index.js
│   │   └── routes.js
│   └── stores/
│       ├── __tests__/
│       │   └── wallet.test.js
│       ├── camera.ts
│       ├── dexie.ts
│       ├── index.js
│       ├── invoicesWorker.ts
│       ├── migrations.ts
│       ├── mintRecommendations.ts
│       ├── mints.ts
│       ├── nostr.ts
│       ├── nostrMintBackup.ts
│       ├── nostrUser.ts
│       ├── npcv2.ts
│       ├── npubcash.ts
│       ├── nwc.ts
│       ├── p2pk.ts
│       ├── payment-request.ts
│       ├── price.ts
│       ├── proofs.ts
│       ├── receiveTokensStore.ts
│       ├── restore.ts
│       ├── sendTokensStore.ts
│       ├── settings.ts
│       ├── storage.ts
│       ├── store-flag.d.ts
│       ├── swap.ts
│       ├── tokens.ts
│       ├── ui.ts
│       ├── wallet.ts
│       ├── welcome.ts
│       └── workers.ts
├── src-electron/
│   ├── electron-env.d.ts
│   ├── electron-flag.d.ts
│   ├── electron-main.ts
│   ├── electron-preload.ts
│   └── icons/
│       └── icon.icns
├── src-pwa/
│   ├── custom-service-worker.js
│   ├── manifest.json
│   ├── pwa-flag.d.ts
│   └── register-service-worker.js
├── test/
│   └── vitest/
│       ├── __tests__/
│       │   └── bip39seed.test.ts
│       └── setup-file.js
├── tsconfig.json
├── types/
│   └── light-bolt11-decoder/
│       └── index.d.ts
└── vitest.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true


================================================
FILE: .eslintignore
================================================
/dist
/src-bex/www
/src-capacitor
/src-cordova
/.quasar
/node_modules
.eslintrc.js
babel.config.js
**/cashu-ts/dist/**


================================================
FILE: .eslintrc.js
================================================
module.exports = {
  // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy
  // This option interrupts the configuration hierarchy at this file
  // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)
  root: true,

  parserOptions: {
    parser: require.resolve("@typescript-eslint/parser"),
    ecmaVersion: "2021", // Allows for the parsing of modern ECMAScript features
  },

  env: {
    browser: true,
    "vue/setup-compiler-macros": true,
  },

  // Rules order is important, please avoid shuffling them
  extends: [
    // Base ESLint recommended rules
    "eslint:recommended",

    // Uncomment any of the lines below to choose desired strictness,
    // but leave only one uncommented!
    // See https://eslint.vuejs.org/rules/#available-rules
    "plugin:vue/vue3-essential", // Priority A: Essential (Error Prevention)
    // 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)
    // 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)

    // https://github.com/prettier/eslint-config-prettier#installation
    // usage with Prettier, provided by 'eslint-config-prettier'.
    "prettier",
  ],

  plugins: [
    // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files
    // required to lint *.vue files
    "vue",

    // https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674
    // Prettier has not been included as plugin to avoid performance impact
    // add it as an extension for your IDE
  ],

  globals: {
    ga: "readonly", // Google Analytics
    cordova: "readonly",
    __statics: "readonly",
    __QUASAR_SSR__: "readonly",
    __QUASAR_SSR_SERVER__: "readonly",
    __QUASAR_SSR_CLIENT__: "readonly",
    __QUASAR_SSR_PWA__: "readonly",
    process: "readonly",
    Capacitor: "readonly",
    chrome: "readonly",
  },

  // add your custom rules here
  rules: {
    "prefer-promise-reject-errors": "off",

    // allow debugger during development only
    "no-debugger": process.env.NODE_ENV === "production" ? "error" : "off",

    "no-var": "error",
    "no-const-assign": "error",
    "prefer-const": [
      "error",
      {
        destructuring: "any",
        ignoreReadBeforeAssign: false,
      },
    ],

    // remove some warnings/errors from eslint:recommended for now
    // which are quite common in the current codebase
    // we will deal with them later on
    "no-unused-vars": "off",
    "no-undef": "off",
    "no-empty": "off",
    "no-useless-catch": "off",
    "no-constant-condition": "off",
  },
  overrides: [
    {
      files: ["**/*.{js,ts}"],
      // If the `script` part of a Vue component is stored in a separate JS/TS file,
      // as is the case when using DFC (https://testing.quasar.dev/packages/unit-jest/#double-file-components-dfc),
      // Vue ESLint plugin will highlight all public properties as unused
      // as it's not able to detect their usage into the template
      // We disable this rule and only keep it for Vue files
      rules: { "vue/no-unused-properties": "off" },
    },
    {
      files: ["*.vue"],
      parser: "vue-eslint-parser",
      parserOptions: {
        parser: "@typescript-eslint/parser",
      },
      rules: {
        // Disallow <script> blocks without lang="ts"
        "vue/block-lang": ["error", { script: { lang: "ts" } }],
      },
    },
  ],
};


================================================
FILE: .github/workflows/build.yaml
================================================
name: Build

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 25
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run build
        run: |
          npm run build
          npm run build:pwa


================================================
FILE: .github/workflows/docker.yaml
================================================
name: Docker Build

on:
  push:
  pull_request:
    types: [opened, synchronize, reopened]
  release:
    types: [released]

jobs:
  check-secrets:
    runs-on: ubuntu-latest
    outputs:
      secrets_set: ${{ steps.check.outputs.secrets_set }}
    steps:
      - name: Check if secrets are set
        run: |
          if [ -z "${{ secrets.DOCKER_USERNAME }}" ]; then
            echo "secrets_set=false" >> $GITHUB_OUTPUT
          else
            echo "secrets_set=true" >> $GITHUB_OUTPUT
          fi

  build-and-push:
    # run only when secrets are set
    needs: check-secrets
    if: ${{ needs.check-secrets.outputs.secrets_set == 'true' }}
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Cache Docker layers
        uses: actions/cache@v4
        id: cache
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-

      - name: Determine Tag
        id: get_tag
        run: |
          if [[ "${{ github.event_name }}" == "release" ]]; then
            echo "::set-output name=tag::${{ github.event.release.tag_name }}"
          else
            echo "::set-output name=tag::${{ github.sha }}"
          fi

      - name: Build and push on release
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name == 'release' && github.event.action == 'released' }}
          tags: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}:${{ steps.get_tag.outputs.tag }}
          platforms: linux/amd64
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache


================================================
FILE: .github/workflows/format.yaml
================================================
name: Format

on: [push, pull_request]

jobs:
  format:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run checkformat
        run: npm run checkformat


================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint

on: [push, pull_request]

jobs:
  lint:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run lint
        run: npm run lint


================================================
FILE: .github/workflows/test.yml
================================================
name: Test

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main, develop]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        # LTS versions from https://endoflife.date/nodejs
        node-version: [20.x, 22.x, 24.x]

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run test
        run: npm run test:ci


================================================
FILE: .gitignore
================================================
.DS_Store
.thumbs.db
node_modules

# Quasar core related directories
.quasar
/dist

# Cordova related directories and files
/src-cordova/node_modules
/src-cordova/platforms
/src-cordova/plugins
/src-cordova/www

# Capacitor related directories and files
/src-capacitor/www
/src-capacitor/node_modules

# BEX related directories and files
/src-bex/www
/src-bex/js/core

# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln


================================================
FILE: .npmrc
================================================
# pnpm-related options
shamefully-hoist=true
strict-peer-dependencies=false


================================================
FILE: .postcssrc.js
================================================
/* eslint-disable */
// https://github.com/michael-ciniawsky/postcss-load-config

module.exports = {
  plugins: [
    // to edit target browsers: use "browserslist" field in package.json
    require("autoprefixer"),
  ],
};


================================================
FILE: .prettierignore
================================================
dist
src-capacitor
src-cordova
android
ios


================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": [
    "dbaeumer.vscode-eslint",
    "esbenp.prettier-vscode",
    "editorconfig.editorconfig",
    "vue.volar",
    "wayou.vscode-todo-highlight",
    "PeterSchmalfeldt.explorer-exclude"
  ],
  "unwantedRecommendations": [
    "octref.vetur",
    "hookyqr.beautify",
    "dbaeumer.jshint",
    "ms-vscode.vscode-typescript-tslint-plugin"
  ]
}


================================================
FILE: .vscode/settings.json
================================================
{
  "editor.bracketPairColorization.enabled": true,
  "editor.guides.bracketPairs": true,
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  // "editor.codeActionsOnSave": ["source.fixAll.eslint"],
  "eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
  "files.exclude": {
    "**/.git": true,
    "**/.svn": true,
    "**/.hg": true,
    "**/CVS": true,
    "**/.DS_Store": true,
    "**/Thumbs.db": true,
    ".editorconfig": true,
    ".eslintignore": true,
    ".npmrc": true,
    ".postcssrc.js": true,
    ".quasar": true,
    "dist": true,
    "node_modules": true
  },
  "explorerExclude.backup": {}
}


================================================
FILE: AGENTS.md
================================================
# Cashu.me Developer Guide for Agents

This document provides a comprehensive overview of the **Cashu.me** codebase. It is designed to help coding agents understand the architecture, tech stack, conventions, and patterns used in this project.

## 1. Tech Stack

### Core Frameworks

- **Framework:** [Quasar Framework](https://quasar.dev/) (Vue.js 3 + Vite)
- **Language:** TypeScript (mostly) and JavaScript.
- **State Management:** [Pinia](https://pinia.vuejs.org/)
- **Routing:** Vue Router (standard Quasar setup)
- **Build Tool:** Vite (via Quasar CLI)
- **CSS:** SCSS/Sass with Quasar's utility classes.

### Mobile & Desktop

- **Mobile:** [Capacitor](https://capacitorjs.com/) (Android & iOS)
- **Desktop:** Electron (via Quasar mode)
- **PWA:** Supported and primary delivery method for web.

### Cashu & Cryptography

- **Cashu Library:** [`@cashu/cashu-ts`](https://github.com/cashubtc/cashu-ts) (Core Cashu wallet logic)
- **Crypto:** `@cashu/crypto`, `@noble/secp256k1`
- **Lightning/Bitcoin:** `light-bolt11-decoder`, `bech32`

### Persistence

- **Database:** [Dexie.js](https://dexie.org/) (IndexedDB wrapper) for storing Cashu proofs (tokens).
- **Local Storage:** `@vueuse/core` (`useLocalStorage`) for user settings, history, and simpler state.

### Testing & Linting

- **Testing:** Vitest
- **Linting:** ESLint + Prettier

---

## 2. Project Structure

```
.
├── src/
│   ├── assets/          # Static assets (images, icons)
│   ├── boot/            # Quasar boot files (initialization logic)
│   ├── components/      # Vue components (UI elements)
│   ├── css/             # Global styles (SCSS)
│   ├── i18n/            # Internationalization (locales)
│   ├── js/              # Utility functions (non-component logic)
│   ├── layouts/         # App layouts (MainLayout, FullscreenLayout)
│   ├── pages/           # Route pages (WalletPage, Settings, etc.)
│   ├── router/          # Vue Router configuration
│   ├── stores/          # Pinia stores (Critical business logic)
│   ├── App.vue          # Root component
│   └── main.js          # Entry point
├── src-capacitor/       # Capacitor configuration and native projects
├── src-electron/        # Electron main/preload scripts
├── src-pwa/             # PWA service worker and manifest
└── quasar.config.js     # Quasar configuration
```

---

## 3. Architecture & Key Stores

The application logic is heavily centralized in Pinia stores found in `src/stores/`.

### Critical Stores

- **`wallet.ts` (`useWalletStore`):** The **primary controller** for the wallet. It handles:
  - Sending, receiving, melting (paying invoices), and minting tokens.
  - interacting with the `cashu-ts` library (`CashuWallet`, `CashuMint`).
  - managing invoice history.
- **`mints.ts` (`useMintsStore`):** Manages the list of connected mints, their keysets, and URLs.
- **`proofs.ts` (`useProofsStore`):** Manages the collection of proofs (ecash tokens). Handles CRUD operations for proofs in memory and syncs with storage.
- **`tokens.ts` (`useTokensStore`):** Manages token history (spent/received tokens log).
- **`dexie.ts` (`useDexieStore`):** Wrapper around Dexie.js for persistent storage of proofs.
- **`ui.ts` (`useUiStore`):** Manages UI state (loaders, dialog visibility, tab selection).

### Database Schema (Dexie)

The `proofs` table in Dexie stores the actual ecash tokens:

- `secret` (string, PK)
- `amount` (number)
- `C` (string, curve point)
- `id` (string, keyset ID)
- `reserved` (boolean, locked for pending operations)
- `quote` (string, optional)

---

## 4. Coding Conventions

### Component Style

The project predominantly uses the **Options API** with **Pinia mappers** within `.vue` files, even though it is a Vue 3 project.

**Pattern:**

```typescript
import { defineComponent } from "vue";
import { mapState, mapActions, mapWritableState } from "pinia";
import { useWalletStore } from "src/stores/wallet";

export default defineComponent({
  name: "MyComponent",
  mixins: [windowMixin], // Common mixin used for global window props
  components: { ... },
  data() {
    return { ... };
  },
  computed: {
    ...mapState(useWalletStore, ["someState"]),
    ...mapWritableState(useWalletStore, ["someWritableState"]),
  },
  methods: {
    ...mapActions(useWalletStore, ["someAction"]),
    myMethod() {
      // Logic here
    }
  }
});
```

**Note:** While `<script setup>` is the modern Vue 3 standard, this codebase relies heavily on the Options API + Pinia mappers. **Respect this convention when modifying existing components.** For new simple components, `<script setup>` may be acceptable, but consistency is preferred.

### Styling

- Use **Quasar Utility Classes** (e.g., `q-pa-md`, `text-center`, `row`, `col-12`) whenever possible.
- Scoped CSS (`<style scoped>`) is used for component-specific overrides.
- Global variables are in `src/css/quasar.variables.scss`.

### Naming

- **Files:** PascalCase for components (`BalanceView.vue`), camelCase for logic files (`wallet.ts`).
- **Stores:** `use[Name]Store` (e.g., `useWalletStore`).

---

## 5. Common Patterns & Gotchas

### Wallet Operations

Most heavy lifting happens in `wallet.ts`. If you need to implement a new feature involving Cashu logic (e.g., "swap tokens", "pay lnurl"), look there first.

### Mutex Locking

The app uses a global mutex (in `ui.ts` -> `lockMutex`) during critical wallet operations (minting, melting, swapping) to prevent race conditions with the database or network.
**Always** ensure mutexes are released in a `finally` block.

### Notifications

Use the helper in `src/js/notify.ts`:

- `notifySuccess(message)`
- `notifyError(message)`
- `notifyWarning(message)`

### Platform Detection

The app runs on Web, Android, and iOS.

- Use `this.getPwaDisplayMode()` (mixin/helper) to detect if running as PWA or browser.
- Capacitor plugins are often wrapped or used directly in stores/components.

### Assets & Icons

- Icons are typically from `lucide-vue-next` or Quasar's internal Material Icons.
- Imports: `import { X as XIcon } from "lucide-vue-next";`

## 6. Development Workflow

- **Run Dev Server:** `npm run dev` (Starts Vite + Quasar)
- **Lint:** `npm run lint`
- **Format:** `npm run format`
- **Test:** `npm test` (Vitest)

When adding dependencies, prefer `npm install`.


================================================
FILE: Dockerfile
================================================
# Stage 1: Build Phase
FROM node:24 AS builder

WORKDIR /app

COPY package*.json ./

# Install application dependencies
RUN npm install -g @quasar/cli
RUN npm install

# Copy the application code to the container
COPY . .

# Build the PWA (replace 'npm run build' with your actual build command)
RUN npm run build:pwa

# Stage 2: Runtime Phase
FROM nginx

# Copy the built PWA files from the builder stage
COPY --from=builder /app/dist/pwa /usr/share/nginx/html

# Expose the port your app will run on
EXPOSE 80


================================================
FILE: LICENSE.md
================================================
MIT License

Copyright (c) 2023 Cashu

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Cashu (cashu)

Cashu Wallet

## One-liner build & run

```
docker compose up -d
```

access at http://localhost:3000 or serve it behind a reverse proxy.

## Install the dependencies

```bash
npm install
```

### Start the app in development mode (hot-code reloading, error reporting, etc.)

```bash
quasar dev
```

### Run unit tests

```bash
npm test
```

### Lint the files

```bash
npm run lint
```

### Format the files

```bash
npm run format
```

### Check translations

Use this to verify non-English translations are in sync with the English source:

```bash
npm run i18n:check
```

### Build the app for production

```bash
quasar build -m pwa
```

### Capacitor

After updating code, run:

```
quasar build -m pwa
npx cap copy
npx cap sync
npx cap open android / ios
```

Regenerate assets:

```
npx capacitor-assets generate
```

### Customize the configuration

See [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js).

### Reverse proxy

For Quasar Vue Router with history mode, add this fallback URL to allow refreshes: https://router.vuejs.org/guide/essentials/history-mode.html#HTML5-Mode

More info: https://stackoverflow.com/questions/36399319/vue-router-return-404-when-revisit-to-the-url

`Caddyfile`:

```
# CORS snippet by https://kalnytskyi.com/posts/setup-cors-caddy-2/
(cors) {
  @cors_preflight method OPTIONS
  @cors header Origin {args.0}

  handle @cors_preflight {
    header Access-Control-Allow-Origin "{args.0}"
    header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE"
    header Access-Control-Allow-Headers "Content-Type"
    header Access-Control-Max-Age "3600"
    respond "" 204
  }

  handle @cors {
    header Access-Control-Allow-Origin "{args.0}"
    header Access-Control-Expose-Headers "Link"
  }
}
host.com {
    import cors *
    encode gzip

    header /service-worker.js {
            Service-Worker-Allowed "/"
            Cache-Control "no-cache"
    }

    # SPA root
    root * /usr/share/caddy/cashu.me/

    # quasar vue router fallback history mode
    try_files {path} /index.html

    file_server
}
```


================================================
FILE: android/.gitignore
================================================
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore

# Built application files
*.apk
*.aar
*.ap_
*.aab

# Files for the ART/Dalvik VM
*.dex

# Java class files
*.class

# Generated files
bin/
gen/
out/
#  Uncomment the following line in case you need and you don't have the release build type files in your app
# release/

# Gradle files
.gradle/
build/

# Local configuration file (sdk path, etc)
local.properties

# Proguard folder generated by Eclipse
proguard/

# Log Files
*.log

# Android Studio Navigation editor temp files
.navigation/

# Android Studio captures folder
captures/

# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml

# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore

# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/

# Google Services (e.g. APIs or Firebase)
# google-services.json

# Freeline
freeline.py
freeline/
freeline_project_description.json

# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md

# Version control
vcs.xml

# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/

# Android Profiling
*.hprof

# Cordova plugins for Capacitor
capacitor-cordova-android-plugins

# Copied web assets
app/src/main/assets/public

# Generated Config files
app/src/main/assets/capacitor.config.json
app/src/main/assets/capacitor.plugins.json
app/src/main/res/xml/config.xml


================================================
FILE: android/app/.gitignore
================================================
/build/*
!/build/.npmkeep


================================================
FILE: android/app/build.gradle
================================================
apply plugin: 'com.android.application'

android {
    namespace "me.cashu.wallet"
    compileSdk rootProject.ext.compileSdkVersion
    defaultConfig {
        applicationId "me.cashu.wallet"
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        aaptOptions {
             // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
             // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
            ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

repositories {
    flatDir{
        dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
    implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
    implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
    implementation project(':capacitor-android')
    testImplementation "junit:junit:$junitVersion"
    androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
    androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
    implementation project(':capacitor-cordova-android-plugins')
}

apply from: 'capacitor.build.gradle'

try {
    def servicesJSON = file('google-services.json')
    if (servicesJSON.text) {
        apply plugin: 'com.google.gms.google-services'
    }
} catch(Exception e) {
    logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
}


================================================
FILE: android/app/capacitor.build.gradle
================================================
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN

android {
  compileOptions {
      sourceCompatibility JavaVersion.VERSION_17
      targetCompatibility JavaVersion.VERSION_17
  }
}

apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
    implementation project(':capacitor-clipboard')
    implementation project(':capacitor-haptics')
    implementation project(':capacitor-plugin-safe-area')

}


if (hasProperty('postBuildExtras')) {
  postBuildExtras()
}


================================================
FILE: android/app/proguard-rules.pro
================================================
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
#   http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
#   public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile


================================================
FILE: android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
================================================
package com.getcapacitor.myapp;

import static org.junit.Assert.*;

import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import org.junit.Test;
import org.junit.runner.RunWith;

/**
 * Instrumented test, which will execute on an Android device.
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {

    @Test
    public void useAppContext() throws Exception {
        // Context of the app under test.
        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();

        assertEquals("com.getcapacitor.app", appContext.getPackageName());
    }
}


================================================
FILE: android/app/src/main/AndroidManifest.xml
================================================
<?xml version="1.0" encoding="utf-8" ?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-feature
    android:name="android.hardware.camera"
    android:required="false"
  />
    <uses-feature
    android:name="android.hardware.nfc"
    android:required="false"
  />

    <application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
  >
        <activity
      android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
      android:name=".MainActivity"
      android:label="@string/title_activity_main"
      android:theme="@style/AppTheme.NoActionBarLaunch"
      android:launchMode="singleTask"
      android:exported="true"
    >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <provider
      android:name="androidx.core.content.FileProvider"
      android:authorities="${applicationId}.fileprovider"
      android:exported="false"
      android:grantUriPermissions="true"
    >
            <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"
      />
        </provider>
    </application>

    <!-- Permissions -->
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.NFC" />
</manifest>


================================================
FILE: android/app/src/main/java/me/cashu/wallet/MainActivity.java
================================================
package me.cashu.wallet;

import com.getcapacitor.BridgeActivity;

public class MainActivity extends BridgeActivity {}


================================================
FILE: android/app/src/main/res/drawable/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8" ?>
<vector
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:width="108dp"
  android:height="108dp"
  android:viewportHeight="108"
  android:viewportWidth="108"
>
    <path android:fillColor="#26A69A" android:pathData="M0,0h108v108h-108z" />
    <path
    android:fillColor="#00000000"
    android:pathData="M9,0L9,108"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M19,0L19,108"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M29,0L29,108"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M39,0L39,108"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M49,0L49,108"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M59,0L59,108"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M69,0L69,108"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M79,0L79,108"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M89,0L89,108"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M99,0L99,108"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M0,9L108,9"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M0,19L108,19"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M0,29L108,29"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M0,39L108,39"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M0,49L108,49"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M0,59L108,59"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M0,69L108,69"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M0,79L108,79"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M0,89L108,89"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M0,99L108,99"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M19,29L89,29"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M19,39L89,39"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M19,49L89,49"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M19,59L89,59"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M19,69L89,69"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M19,79L89,79"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M29,19L29,89"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M39,19L39,89"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M49,19L49,89"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M59,19L59,89"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M69,19L69,89"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
    <path
    android:fillColor="#00000000"
    android:pathData="M79,19L79,89"
    android:strokeColor="#33FFFFFF"
    android:strokeWidth="0.8"
  />
</vector>


================================================
FILE: android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
================================================
<vector
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:aapt="http://schemas.android.com/aapt"
  android:width="108dp"
  android:height="108dp"
  android:viewportHeight="108"
  android:viewportWidth="108"
>
    <path
    android:fillType="evenOdd"
    android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
    android:strokeColor="#00000000"
    android:strokeWidth="1"
  >
        <aapt:attr name="android:fillColor">
            <gradient
        android:endX="78.5885"
        android:endY="90.9159"
        android:startX="48.7653"
        android:startY="61.0927"
        android:type="linear"
      >
                <item android:color="#44000000" android:offset="0.0" />
                <item android:color="#00000000" android:offset="1.0" />
            </gradient>
        </aapt:attr>
    </path>
    <path
    android:fillColor="#FFFFFF"
    android:fillType="nonZero"
    android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
    android:strokeColor="#00000000"
    android:strokeWidth="1"
  />
</vector>


================================================
FILE: android/app/src/main/res/layout/activity_main.xml
================================================
<?xml version="1.0" encoding="utf-8" ?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  tools:context=".MainActivity"
>

    <WebView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
  />
</androidx.coordinatorlayout.widget.CoordinatorLayout>


================================================
FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
================================================
<?xml version="1.0" encoding="utf-8" ?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background>
        <inset
      android:drawable="@mipmap/ic_launcher_background"
      android:inset="16.7%"
    />
    </background>
    <foreground>
        <inset
      android:drawable="@mipmap/ic_launcher_foreground"
      android:inset="16.7%"
    />
    </foreground>
</adaptive-icon>


================================================
FILE: android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
================================================
<?xml version="1.0" encoding="utf-8" ?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
    <background>
        <inset
      android:drawable="@mipmap/ic_launcher_background"
      android:inset="16.7%"
    />
    </background>
    <foreground>
        <inset
      android:drawable="@mipmap/ic_launcher_foreground"
      android:inset="16.7%"
    />
    </foreground>
</adaptive-icon>


================================================
FILE: android/app/src/main/res/values/ic_launcher_background.xml
================================================
<?xml version="1.0" encoding="utf-8" ?>
<resources>
    <color name="ic_launcher_background">#FFFFFF</color>
</resources>


================================================
FILE: android/app/src/main/res/values/strings.xml
================================================
<?xml version='1.0' encoding='utf-8' ?>
<resources>
    <string name="app_name">Cashu.me</string>
    <string name="title_activity_main">Cashu.me</string>
    <string name="package_name">me.cashu.wallet</string>
    <string name="custom_url_scheme">me.cashu.wallet</string>
</resources>


================================================
FILE: android/app/src/main/res/values/styles.xml
================================================
<?xml version="1.0" encoding="utf-8" ?>
<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

    <style
    name="AppTheme.NoActionBar"
    parent="Theme.AppCompat.DayNight.NoActionBar"
  >
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
        <item name="android:background">@null</item>
    </style>


    <style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
        <item name="android:background">@drawable/splash</item>
    </style>
</resources>


================================================
FILE: android/app/src/main/res/xml/file_paths.xml
================================================
<?xml version="1.0" encoding="utf-8" ?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images" path="." />
    <cache-path name="my_cache_images" path="." />
</paths>


================================================
FILE: android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
================================================
package com.getcapacitor.myapp;

import static org.junit.Assert.*;

import org.junit.Test;

/**
 * Example local unit test, which will execute on the development machine (host).
 *
 * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
 */
public class ExampleUnitTest {

    @Test
    public void addition_isCorrect() throws Exception {
        assertEquals(4, 2 + 2);
    }
}


================================================
FILE: android/build.gradle
================================================
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:8.2.1'
        classpath 'com.google.gms:google-services:4.4.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

apply from: "variables.gradle"

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}


================================================
FILE: android/capacitor.settings.gradle
================================================
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')

include ':capacitor-clipboard'
project(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android')

include ':capacitor-haptics'
project(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android')

include ':capacitor-plugin-safe-area'
project(':capacitor-plugin-safe-area').projectDir = new File('../node_modules/capacitor-plugin-safe-area/android')


================================================
FILE: android/gradle/wrapper/gradle-wrapper.properties
================================================
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists


================================================
FILE: android/gradle.properties
================================================
# Project-wide Gradle settings.

# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.

# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html

# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m

# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true

# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true


================================================
FILE: android/gradlew
================================================
#!/bin/sh

#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

##############################################################################
#
#   Gradle start up script for POSIX generated by Gradle.
#
#   Important for running:
#
#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
#       noncompliant, but you have some other compliant shell such as ksh or
#       bash, then to run this script, type that shell name before the whole
#       command line, like:
#
#           ksh Gradle
#
#       Busybox and similar reduced shells will NOT work, because this script
#       requires all of these POSIX shell features:
#         * functions;
#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
#         * compound commands having a testable exit status, especially «case»;
#         * various built-in commands including «command», «set», and «ulimit».
#
#   Important for patching:
#
#   (2) This script targets any POSIX shell, so it avoids extensions provided
#       by Bash, Ksh, etc; in particular arrays are avoided.
#
#       The "traditional" practice of packing multiple parameters into a
#       space-separated string is a well documented source of bugs and security
#       problems, so this is (mostly) avoided, by progressively accumulating
#       options in "$@", and eventually passing that to Java.
#
#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
#       see the in-line comments for details.
#
#       There are tweaks for specific operating systems such as AIX, CygWin,
#       Darwin, MinGW, and NonStop.
#
#   (3) This script is generated from the Groovy template
#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
#       within the Gradle project.
#
#       You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################

# Attempt to set APP_HOME

# Resolve links: $0 may be a link
app_path=$0

# Need this for daisy-chained symlinks.
while
    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
    [ -h "$app_path" ]
do
    ls=$( ls -ld "$app_path" )
    link=${ls#*' -> '}
    case $link in             #(
      /*)   app_path=$link ;; #(
      *)    app_path=$APP_HOME$link ;;
    esac
done

# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit

# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

warn () {
    echo "$*"
} >&2

die () {
    echo
    echo "$*"
    echo
    exit 1
} >&2

# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in                #(
  CYGWIN* )         cygwin=true  ;; #(
  Darwin* )         darwin=true  ;; #(
  MSYS* | MINGW* )  msys=true    ;; #(
  NONSTOP* )        nonstop=true ;;
esac

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar


# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
        # IBM's JDK on AIX uses strange locations for the executables
        JAVACMD=$JAVA_HOME/jre/sh/java
    else
        JAVACMD=$JAVA_HOME/bin/java
    fi
    if [ ! -x "$JAVACMD" ] ; then
        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
else
    JAVACMD=java
    if ! command -v java >/dev/null 2>&1
    then
        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.

Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
    fi
fi

# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
    case $MAX_FD in #(
      max*)
        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC3045
        MAX_FD=$( ulimit -H -n ) ||
            warn "Could not query maximum file descriptor limit"
    esac
    case $MAX_FD in  #(
      '' | soft) :;; #(
      *)
        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
        # shellcheck disable=SC3045
        ulimit -n "$MAX_FD" ||
            warn "Could not set maximum file descriptor limit to $MAX_FD"
    esac
fi

# Collect all arguments for the java command, stacking in reverse order:
#   * args from the command line
#   * the main class name
#   * -classpath
#   * -D...appname settings
#   * --module-path (only if needed)
#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.

# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )

    JAVACMD=$( cygpath --unix "$JAVACMD" )

    # Now convert the arguments - kludge to limit ourselves to /bin/sh
    for arg do
        if
            case $arg in                                #(
              -*)   false ;;                            # don't mess with options #(
              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
                    [ -e "$t" ] ;;                      #(
              *)    false ;;
            esac
        then
            arg=$( cygpath --path --ignore --mixed "$arg" )
        fi
        # Roll the args list around exactly as many times as the number of
        # args, so each arg winds up back in the position where it started, but
        # possibly modified.
        #
        # NB: a `for` loop captures its iteration list before it begins, so
        # changing the positional parameters here affects neither the number of
        # iterations, nor the values presented in `arg`.
        shift                   # remove old arg
        set -- "$@" "$arg"      # push replacement arg
    done
fi


# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'

# Collect all arguments for the java command;
#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
#     shell script including quotes and variable substitutions, so put them in
#     double quotes to make sure that they get re-expanded; and
#   * put everything else in single quotes, so that it's not re-expanded.

set -- \
        "-Dorg.gradle.appname=$APP_BASE_NAME" \
        -classpath "$CLASSPATH" \
        org.gradle.wrapper.GradleWrapperMain \
        "$@"

# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
    die "xargs is not available"
fi

# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
#   set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#

eval "set -- $(
        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
        xargs -n1 |
        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
        tr '\n' ' '
    )" '"$@"'

exec "$JAVACMD" "$@"


================================================
FILE: android/gradlew.bat
================================================
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem      https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem

@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem  Gradle startup script for Windows
@rem
@rem ##########################################################################

@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal

set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%

@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi

@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"

@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome

set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute

echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe

if exist "%JAVA_EXE%" goto execute

echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.

goto fail

:execute
@rem Setup the command line

set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar


@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd

:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%

:mainEnd
if "%OS%"=="Windows_NT" endlocal

:omega


================================================
FILE: android/settings.gradle
================================================
include ':app'
include ':capacitor-cordova-android-plugins'
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')

apply from: 'capacitor.settings.gradle'

================================================
FILE: android/variables.gradle
================================================
ext {
    minSdkVersion = 22
    compileSdkVersion = 34
    targetSdkVersion = 34
    androidxActivityVersion = '1.8.0'
    androidxAppCompatVersion = '1.6.1'
    androidxCoordinatorLayoutVersion = '1.2.0'
    androidxCoreVersion = '1.12.0'
    androidxFragmentVersion = '1.6.2'
    coreSplashScreenVersion = '1.0.1'
    androidxWebkitVersion = '1.9.0'
    junitVersion = '4.13.2'
    androidxJunitVersion = '1.1.5'
    androidxEspressoCoreVersion = '3.5.1'
    cordovaAndroidVersion = '10.1.1'
}

================================================
FILE: capacitor.config.ts
================================================
import type { CapacitorConfig } from "@capacitor/cli";

const config: CapacitorConfig = {
  appId: "me.cashu.wallet",
  appName: "Cashu.me",
  webDir: "dist/pwa/",
};

export default config;


================================================
FILE: docker-compose.yaml
================================================
services:
  cashu.me:
    image: cashu.me
    build: .
    container_name: cashu.me
    restart: always
    ports:
      - "127.0.0.1:3000:80"


================================================
FILE: extension/embedder.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" type="text/css" href="style.css" />
  </head>
  <body>
    <iframe id="iframe" src="https://wallet.cashu.me"></iframe>
  </body>
</html>


================================================
FILE: extension/manifest.json
================================================
{
  "name": "Cashu.me",
  "description": "A privacy-preserving ecash wallet for Bitcoin Lightning",
  "icons": { "128": "128.png", "32": "32.png", "16": "16.png" },
  "manifest_version": 3,
  "version": "0.1",
  "action": {
    "default_icon": "32.png",
    "default_popup": "embedder.html",
    "default_title": "Cashu.me"
  },
  "permissions": ["unlimitedStorage", "storage"]
}


================================================
FILE: extension/style.css
================================================
body {
  background: #350a60;
  margin: 0;
  width: 580px;
  height: 595px;
}
iframe {
  width: 100%;
  height: 100%;
  border: 0;
}


================================================
FILE: index.html
================================================
<!DOCTYPE html>
<html>
  <head>
    <title><%= productName %></title>

    <meta charset="utf-8" />
    <meta name="description" content="<%= productDescription %>" />
    <meta name="format-detection" content="telephone=no" />
    <meta name="msapplication-tap-highlight" content="no" />
    <meta
      name="viewport"
      content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width<% if (ctx.mode.cordova || ctx.mode.capacitor) { %>, viewport-fit=cover<% } %>"
    />

    <link
      rel="icon"
      type="image/png"
      sizes="128x128"
      href="icons/128x128.png"
    />
    <link rel="icon" type="image/png" sizes="96x96" href="icons/96x96.png" />
    <link rel="icon" type="image/png" sizes="32x32" href="icons/32x32.png" />
    <link rel="icon" type="image/png" sizes="16x16" href="icons/16x16.png" />
    <link rel="icon" type="image/ico" href="favicon.ico" />
  </head>
  <body>
    <!-- quasar:entry-point -->
  </body>
</html>


================================================
FILE: ios/.gitignore
================================================
App/build
App/Pods
App/output
App/App/public
DerivedData
xcuserdata

# Cordova plugins for Capacitor
capacitor-cordova-ios-plugins

# Generated Config files
App/App/capacitor.config.json
App/App/config.xml


================================================
FILE: ios/App/App/AppDelegate.swift
================================================
import UIKit
import Capacitor

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        return true
    }

    func applicationWillResignActive(_ application: UIApplication) {
        // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.
    }

    func applicationDidEnterBackground(_ application: UIApplication) {
        // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
    }

    func applicationWillEnterForeground(_ application: UIApplication) {
        // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
    }

    func applicationDidBecomeActive(_ application: UIApplication) {
        // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    }

    func applicationWillTerminate(_ application: UIApplication) {
        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
    }

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
        // Called when the app was launched with a url. Feel free to add additional processing here,
        // but if you want the App API to support tracking app url opens, make sure to keep this call
        return ApplicationDelegateProxy.shared.application(app, open: url, options: options)
    }

    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        // Called when the app was launched with an activity, including Universal Links.
        // Feel free to add additional processing here, but if you want the App API to support
        // tracking app url opens, make sure to keep this call
        return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
    }

}


================================================
FILE: ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json
================================================
{
  "images": [
    {
      "idiom": "universal",
      "size": "1024x1024",
      "filename": "AppIcon-512@2x.png",
      "platform": "ios"
    }
  ],
  "info": {
    "author": "xcode",
    "version": 1
  }
}


================================================
FILE: ios/App/App/Assets.xcassets/Contents.json
================================================
{
  "info": {
    "version": 1,
    "author": "xcode"
  }
}


================================================
FILE: ios/App/App/Assets.xcassets/Splash.imageset/Contents.json
================================================
{
  "images": [
    {
      "idiom": "universal",
      "filename": "Default@1x~universal~anyany.png",
      "scale": "1x"
    },
    {
      "idiom": "universal",
      "filename": "Default@2x~universal~anyany.png",
      "scale": "2x"
    },
    {
      "idiom": "universal",
      "filename": "Default@3x~universal~anyany.png",
      "scale": "3x"
    },
    {
      "appearances": [
        {
          "appearance": "luminosity",
          "value": "dark"
        }
      ],
      "idiom": "universal",
      "scale": "1x",
      "filename": "Default@1x~universal~anyany-dark.png"
    },
    {
      "appearances": [
        {
          "appearance": "luminosity",
          "value": "dark"
        }
      ],
      "idiom": "universal",
      "scale": "2x",
      "filename": "Default@2x~universal~anyany-dark.png"
    },
    {
      "appearances": [
        {
          "appearance": "luminosity",
          "value": "dark"
        }
      ],
      "idiom": "universal",
      "scale": "3x",
      "filename": "Default@3x~universal~anyany-dark.png"
    }
  ],
  "info": {
    "version": 1,
    "author": "xcode"
  }
}


================================================
FILE: ios/App/App/Base.lproj/LaunchScreen.storyboard
================================================
<?xml version="1.0" encoding="UTF-8" ?>
<document
  type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB"
  version="3.0"
  toolsVersion="17132"
  targetRuntime="iOS.CocoaTouch"
  propertyAccessControl="none"
  useAutolayout="YES"
  launchScreen="YES"
  useTraitCollections="YES"
  useSafeAreas="YES"
  colorMatched="YES"
  initialViewController="01J-lp-oVM"
>
    <device id="retina4_7" orientation="portrait" appearance="light" />
    <dependencies>
        <deployment identifier="iOS" />
        <plugIn
      identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin"
      version="17105"
    />
        <capability
      name="System colors in document resources"
      minToolsVersion="11.0"
    />
        <capability
      name="documents saved in the Xcode 8 format"
      minToolsVersion="8.0"
    />
    </dependencies>
    <scenes>
        <!--View Controller-->
        <scene sceneID="EHf-IW-A2E">
            <objects>
                <viewController id="01J-lp-oVM" sceneMemberID="viewController">
                    <imageView
            key="view"
            userInteractionEnabled="NO"
            contentMode="scaleAspectFill"
            horizontalHuggingPriority="251"
            verticalHuggingPriority="251"
            image="Splash"
            id="snD-IY-ifK"
          >
                        <rect
              key="frame"
              x="0.0"
              y="0.0"
              width="375"
              height="667"
            />
                        <autoresizingMask key="autoresizingMask" />
                        <color
              key="backgroundColor"
              systemColor="systemBackgroundColor"
            />
                    </imageView>
                </viewController>
                <placeholder
          placeholderIdentifier="IBFirstResponder"
          id="iYj-Kq-Ea1"
          userLabel="First Responder"
          sceneMemberID="firstResponder"
        />
            </objects>
            <point key="canvasLocation" x="53" y="375" />
        </scene>
    </scenes>
    <resources>
        <image name="Splash" width="1366" height="1366" />
        <systemColor name="systemBackgroundColor">
            <color
        white="1"
        alpha="1"
        colorSpace="custom"
        customColorSpace="genericGamma22GrayColorSpace"
      />
        </systemColor>
    </resources>
</document>


================================================
FILE: ios/App/App/Base.lproj/Main.storyboard
================================================
<?xml version="1.0" encoding="UTF-8" ?>
<document
  type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB"
  version="3.0"
  toolsVersion="14111"
  targetRuntime="iOS.CocoaTouch"
  propertyAccessControl="none"
  useAutolayout="YES"
  useTraitCollections="YES"
  colorMatched="YES"
  initialViewController="BYZ-38-t0r"
>
    <device id="retina4_7" orientation="portrait">
        <adaptation id="fullscreen" />
    </device>
    <dependencies>
        <deployment identifier="iOS" />
        <plugIn
      identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin"
      version="14088"
    />
    </dependencies>
    <scenes>
        <!--Bridge View Controller-->
        <scene sceneID="tne-QT-ifu">
            <objects>
                <viewController
          id="BYZ-38-t0r"
          customClass="CAPBridgeViewController"
          customModule="Capacitor"
          sceneMemberID="viewController"
        />
                <placeholder
          placeholderIdentifier="IBFirstResponder"
          id="dkx-z0-nzr"
          sceneMemberID="firstResponder"
        />
            </objects>
        </scene>
    </scenes>
</document>


================================================
FILE: ios/App/App/Info.plist
================================================
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>CFBundleDevelopmentRegion</key>
	<string>en</string>
	<key>CFBundleDisplayName</key>
	<string>Cashu.me</string>
	<key>CFBundleExecutable</key>
	<string>$(EXECUTABLE_NAME)</string>
	<key>CFBundleIdentifier</key>
	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
	<key>CFBundleInfoDictionaryVersion</key>
	<string>6.0</string>
	<key>CFBundleName</key>
	<string>$(PRODUCT_NAME)</string>
	<key>CFBundlePackageType</key>
	<string>APPL</string>
	<key>CFBundleShortVersionString</key>
	<string>$(MARKETING_VERSION)</string>
	<key>CFBundleVersion</key>
	<string>$(CURRENT_PROJECT_VERSION)</string>
	<key>LSRequiresIPhoneOS</key>
	<true />
	<key>UILaunchStoryboardName</key>
	<string>LaunchScreen</string>
	<key>UIMainStoryboardFile</key>
	<string>Main</string>
	<key>UIRequiredDeviceCapabilities</key>
	<array>
		<string>armv7</string>
	</array>
	<key>UISupportedInterfaceOrientations</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
	</array>
	<key>UISupportedInterfaceOrientations~ipad</key>
	<array>
		<string>UIInterfaceOrientationPortrait</string>
		<string>UIInterfaceOrientationPortraitUpsideDown</string>
		<string>UIInterfaceOrientationLandscapeLeft</string>
		<string>UIInterfaceOrientationLandscapeRight</string>
	</array>
	<key>UIViewControllerBasedStatusBarAppearance</key>
	<true />
  <key>NSCameraUsageDescription</key>
  <string>The app uses the camera to scan QR codes.</string>
</dict>
</plist>


================================================
FILE: ios/App/App.xcodeproj/project.pbxproj
================================================
// !$*UTF8*$!
{
	archiveVersion = 1;
	classes = {
	};
	objectVersion = 48;
	objects = {

/* Begin PBXBuildFile section */
		2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };
		50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };
		504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };
		504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };
		504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
		504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
		50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
		A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
		2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = "<group>"; };
		50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = "<group>"; };
		504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };
		504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
		504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
		504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
		504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
		504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
		50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
		AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
		AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
		FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
		504EC3011FED79650016851F /* Frameworks */ = {
			isa = PBXFrameworksBuildPhase;
			buildActionMask = 2147483647;
			files = (
				A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXFrameworksBuildPhase section */

/* Begin PBXGroup section */
		27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {
			isa = PBXGroup;
			children = (
				AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,
			);
			name = Frameworks;
			sourceTree = "<group>";
		};
		504EC2FB1FED79650016851F = {
			isa = PBXGroup;
			children = (
				504EC3061FED79650016851F /* App */,
				504EC3051FED79650016851F /* Products */,
				7F8756D8B27F46E3366F6CEA /* Pods */,
				27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
			);
			sourceTree = "<group>";
		};
		504EC3051FED79650016851F /* Products */ = {
			isa = PBXGroup;
			children = (
				504EC3041FED79650016851F /* App.app */,
			);
			name = Products;
			sourceTree = "<group>";
		};
		504EC3061FED79650016851F /* App */ = {
			isa = PBXGroup;
			children = (
				50379B222058CBB4000EE86E /* capacitor.config.json */,
				504EC3071FED79650016851F /* AppDelegate.swift */,
				504EC30B1FED79650016851F /* Main.storyboard */,
				504EC30E1FED79650016851F /* Assets.xcassets */,
				504EC3101FED79650016851F /* LaunchScreen.storyboard */,
				504EC3131FED79650016851F /* Info.plist */,
				2FAD9762203C412B000D30F8 /* config.xml */,
				50B271D01FEDC1A000F3C39B /* public */,
			);
			path = App;
			sourceTree = "<group>";
		};
		7F8756D8B27F46E3366F6CEA /* Pods */ = {
			isa = PBXGroup;
			children = (
				FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,
				AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,
			);
			name = Pods;
			sourceTree = "<group>";
		};
/* End PBXGroup section */

/* Begin PBXNativeTarget section */
		504EC3031FED79650016851F /* App */ = {
			isa = PBXNativeTarget;
			buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */;
			buildPhases = (
				6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,
				504EC3001FED79650016851F /* Sources */,
				504EC3011FED79650016851F /* Frameworks */,
				504EC3021FED79650016851F /* Resources */,
				9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,
			);
			buildRules = (
			);
			dependencies = (
			);
			name = App;
			productName = App;
			productReference = 504EC3041FED79650016851F /* App.app */;
			productType = "com.apple.product-type.application";
		};
/* End PBXNativeTarget section */

/* Begin PBXProject section */
		504EC2FC1FED79650016851F /* Project object */ = {
			isa = PBXProject;
			attributes = {
				LastSwiftUpdateCheck = 920;
				LastUpgradeCheck = 1620;
				TargetAttributes = {
					504EC3031FED79650016851F = {
						CreatedOnToolsVersion = 9.2;
						LastSwiftMigration = 1100;
						ProvisioningStyle = Automatic;
					};
				};
			};
			buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */;
			compatibilityVersion = "Xcode 8.0";
			developmentRegion = en;
			hasScannedForEncodings = 0;
			knownRegions = (
				en,
				Base,
			);
			mainGroup = 504EC2FB1FED79650016851F;
			packageReferences = (
			);
			productRefGroup = 504EC3051FED79650016851F /* Products */;
			projectDirPath = "";
			projectRoot = "";
			targets = (
				504EC3031FED79650016851F /* App */,
			);
		};
/* End PBXProject section */

/* Begin PBXResourcesBuildPhase section */
		504EC3021FED79650016851F /* Resources */ = {
			isa = PBXResourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,
				50B271D11FEDC1A000F3C39B /* public in Resources */,
				504EC30F1FED79650016851F /* Assets.xcassets in Resources */,
				50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,
				504EC30D1FED79650016851F /* Main.storyboard in Resources */,
				2FAD9763203C412B000D30F8 /* config.xml in Resources */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXResourcesBuildPhase section */

/* Begin PBXShellScriptBuildPhase section */
		6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {
			isa = PBXShellScriptBuildPhase;
			buildActionMask = 2147483647;
			files = (
			);
			inputPaths = (
				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
				"${PODS_ROOT}/Manifest.lock",
			);
			name = "[CP] Check Pods Manifest.lock";
			outputPaths = (
				"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt",
			);
			runOnlyForDeploymentPostprocessing = 0;
			shellPath = /bin/sh;
			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
			showEnvVarsInLog = 0;
		};
		9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {
			isa = PBXShellScriptBuildPhase;
			buildActionMask = 2147483647;
			files = (
			);
			inputPaths = (
			);
			name = "[CP] Embed Pods Frameworks";
			outputPaths = (
			);
			runOnlyForDeploymentPostprocessing = 0;
			shellPath = /bin/sh;
			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\"\n";
			showEnvVarsInLog = 0;
		};
/* End PBXShellScriptBuildPhase section */

/* Begin PBXSourcesBuildPhase section */
		504EC3001FED79650016851F /* Sources */ = {
			isa = PBXSourcesBuildPhase;
			buildActionMask = 2147483647;
			files = (
				504EC3081FED79650016851F /* AppDelegate.swift in Sources */,
			);
			runOnlyForDeploymentPostprocessing = 0;
		};
/* End PBXSourcesBuildPhase section */

/* Begin PBXVariantGroup section */
		504EC30B1FED79650016851F /* Main.storyboard */ = {
			isa = PBXVariantGroup;
			children = (
				504EC30C1FED79650016851F /* Base */,
			);
			name = Main.storyboard;
			sourceTree = "<group>";
		};
		504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {
			isa = PBXVariantGroup;
			children = (
				504EC3111FED79650016851F /* Base */,
			);
			name = LaunchScreen.storyboard;
			sourceTree = "<group>";
		};
/* End PBXVariantGroup section */

/* Begin XCBuildConfiguration section */
		504EC3141FED79650016851F /* Debug */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
				CLANG_CXX_LIBRARY = "libc++";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				CODE_SIGN_IDENTITY = "iPhone Developer";
				COPY_PHASE_STRIP = NO;
				DEBUG_INFORMATION_FORMAT = dwarf;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				ENABLE_TESTABILITY = YES;
				GCC_C_LANGUAGE_STANDARD = gnu11;
				GCC_DYNAMIC_NO_PIC = NO;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_OPTIMIZATION_LEVEL = 0;
				GCC_PREPROCESSOR_DEFINITIONS = (
					"DEBUG=1",
					"$(inherited)",
				);
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
				MTL_ENABLE_DEBUG_INFO = YES;
				ONLY_ACTIVE_ARCH = YES;
				SDKROOT = iphoneos;
				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
				SWIFT_OPTIMIZATION_LEVEL = "-Onone";
			};
			name = Debug;
		};
		504EC3151FED79650016851F /* Release */ = {
			isa = XCBuildConfiguration;
			buildSettings = {
				ALWAYS_SEARCH_USER_PATHS = NO;
				CLANG_ANALYZER_NONNULL = YES;
				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
				CLANG_CXX_LIBRARY = "libc++";
				CLANG_ENABLE_MODULES = YES;
				CLANG_ENABLE_OBJC_ARC = YES;
				CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
				CLANG_WARN_BOOL_CONVERSION = YES;
				CLANG_WARN_COMMA = YES;
				CLANG_WARN_CONSTANT_CONVERSION = YES;
				CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
				CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
				CLANG_WARN_EMPTY_BODY = YES;
				CLANG_WARN_ENUM_CONVERSION = YES;
				CLANG_WARN_INFINITE_RECURSION = YES;
				CLANG_WARN_INT_CONVERSION = YES;
				CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
				CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
				CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
				CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
				CLANG_WARN_STRICT_PROTOTYPES = YES;
				CLANG_WARN_SUSPICIOUS_MOVE = YES;
				CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
				CLANG_WARN_UNREACHABLE_CODE = YES;
				CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
				CODE_SIGN_IDENTITY = "iPhone Developer";
				COPY_PHASE_STRIP = NO;
				DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
				ENABLE_NS_ASSERTIONS = NO;
				ENABLE_STRICT_OBJC_MSGSEND = YES;
				GCC_C_LANGUAGE_STANDARD = gnu11;
				GCC_NO_COMMON_BLOCKS = YES;
				GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
				GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
				GCC_WARN_UNDECLARED_SELECTOR = YES;
				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
				GCC_WARN_UNUSED_FUNCTION = YES;
				GCC_WARN_UNUSED_VARIABLE = YES;
				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
				MTL_ENABLE_DEBUG_INFO = NO;
				SDKROOT = iphoneos;
				SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
				VALIDATE_PRODUCT = YES;
			};
			name = Release;
		};
		504EC3171FED79650016851F /* Debug */ = {
			isa = XCBuildConfiguration;
			baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				CODE_SIGN_IDENTITY = "Apple Development";
				CODE_SIGN_STYLE = Automatic;
				CURRENT_PROJECT_VERSION = 1;
				DEVELOPMENT_TEAM = 3XDA5YR8QS;
				INFOPLIST_FILE = App/Info.plist;
				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
				MARKETING_VERSION = 1.0;
				OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
				PRODUCT_BUNDLE_IDENTIFIER = me.cashu.wallet;
				PRODUCT_NAME = "$(TARGET_NAME)";
				PROVISIONING_PROFILE_SPECIFIER = "";
				SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
				SWIFT_VERSION = 5.0;
				TARGETED_DEVICE_FAMILY = "1,2";
			};
			name = Debug;
		};
		504EC3181FED79650016851F /* Release */ = {
			isa = XCBuildConfiguration;
			baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
			buildSettings = {
				ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
				CODE_SIGN_IDENTITY = "Apple Development";
				CODE_SIGN_STYLE = Automatic;
				CURRENT_PROJECT_VERSION = 1;
				DEVELOPMENT_TEAM = 3XDA5YR8QS;
				INFOPLIST_FILE = App/Info.plist;
				IPHONEOS_DEPLOYMENT_TARGET = 13.0;
				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
				MARKETING_VERSION = 1.0;
				PRODUCT_BUNDLE_IDENTIFIER = me.cashu.wallet;
				PRODUCT_NAME = "$(TARGET_NAME)";
				PROVISIONING_PROFILE_SPECIFIER = "";
				SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
				SWIFT_VERSION = 5.0;
				TARGETED_DEVICE_FAMILY = "1,2";
			};
			name = Release;
		};
/* End XCBuildConfiguration section */

/* Begin XCConfigurationList section */
		504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				504EC3141FED79650016851F /* Debug */,
				504EC3151FED79650016851F /* Release */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
		504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = {
			isa = XCConfigurationList;
			buildConfigurations = (
				504EC3171FED79650016851F /* Debug */,
				504EC3181FED79650016851F /* Release */,
			);
			defaultConfigurationIsVisible = 0;
			defaultConfigurationName = Release;
		};
/* End XCConfigurationList section */
	};
	rootObject = 504EC2FC1FED79650016851F /* Project object */;
}


================================================
FILE: ios/App/App.xcworkspace/contents.xcworkspacedata
================================================
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
   version = "1.0">
   <FileRef
      location = "group:App.xcodeproj">
   </FileRef>
   <FileRef
      location = "group:Pods/Pods.xcodeproj">
   </FileRef>
</Workspace>


================================================
FILE: ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>IDEDidComputeMac32BitWarning</key>
	<true/>
</dict>
</plist>


================================================
FILE: ios/App/Podfile
================================================
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'

platform :ios, '13.0'
use_frameworks!

# workaround to avoid Xcode caching of Pods that requires
# Product -> Clean Build Folder after new Cordova plugins installed
# Requires CocoaPods 1.6 or newer
install! 'cocoapods', :disable_input_output_paths => true

def capacitor_pods
  pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
  pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
  pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'
  pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
  pod 'CapacitorPluginSafeArea', :path => '../../node_modules/capacitor-plugin-safe-area'
end

target 'App' do
  capacitor_pods
  # Add your Pods here
end

post_install do |installer|
  assertDeploymentTarget(installer)
end


================================================
FILE: jsconfig.json
================================================
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "src/*": ["src/*"],
      "app/*": ["*"],
      "components/*": ["src/components/*"],
      "layouts/*": ["src/layouts/*"],
      "pages/*": ["src/pages/*"],
      "assets/*": ["src/assets/*"],
      "boot/*": ["src/boot/*"],
      "stores/*": ["src/stores/*"],
      "vue$": ["node_modules/vue/dist/vue.runtime.esm-bundler.js"]
    }
  },
  "exclude": ["dist", ".quasar", "node_modules", "android", "ios"]
}


================================================
FILE: package.json
================================================
{
  "name": "cashu",
  "version": "0.0.1",
  "description": "Cashu.me Wallet",
  "productName": "Cashu.me",
  "author": "cashu.me",
  "private": true,
  "scripts": {
    "i18n:check": "node scripts/check-i18n.js",
    "dev": "quasar dev",
    "build": "quasar build",
    "build:pwa": "quasar build -m pwa",
    "build:electron": "quasar build -m electron",
    "quasar": "quasar",
    "lint": "eslint --ext .js,.vue,.ts ./",
    "format": "prettier --write .",
    "checkformat": "prettier --check .",
    "test": "vitest",
    "test:ci": "vitest run"
  },
  "dependencies": {
    "@capacitor/clipboard": "^6.0.2",
    "@capacitor/core": "^6.2.0",
    "@capacitor/haptics": "^6.0.2",
    "@cashu/cashu-ts": "^3.5.0",
    "@chenfengyuan/vue-qrcode": "^2.0.0",
    "@gandlaf21/bc-ur": "^1.1.12",
    "@nostr-dev-kit/ndk": "^2.8.1",
    "@quasar/extras": "^1.17.0",
    "@scure/bip39": "^1.4.0",
    "@vueuse/core": "^10.9.0",
    "axios": "^1.6.8",
    "bech32": "^2.0.0",
    "capacitor-plugin-safe-area": "^3.0.4",
    "core-js": "^3.37.0",
    "date-fns": "^3.6.0",
    "dexie": "^4.0.9",
    "light-bolt11-decoder": "^3.1.1",
    "lucide-vue-next": "^0.453.0",
    "nostr-tools": "^2.5.2",
    "pinia": "^2.1.7",
    "qr-scanner": "^1.4.2",
    "qrcode": "^1.5.3",
    "quasar": "^2.18.2",
    "underscore": "^1.13.6",
    "vue": "^3.4.27",
    "vue-i18n": "^11.1.3",
    "vue-router": "^4.3.2"
  },
  "devDependencies": {
    "@capacitor/android": "^6.2.0",
    "@capacitor/assets": "^3.0.5",
    "@capacitor/cli": "^6.2.0",
    "@capacitor/ios": "^6.0.0",
    "@electron/packager": "^18.4.4",
    "@quasar/app-vite": "^1.11.0",
    "@quasar/icongenie": "^4.0.0",
    "@types/node": "^20.12.11",
    "@types/underscore": "^1.11.15",
    "@types/uuid": "^10.0.0",
    "@typescript-eslint/eslint-plugin": "^5.62.0",
    "@typescript-eslint/parser": "^5.62.0",
    "@vue/test-utils": "^2.4.6",
    "autoprefixer": "^10.4.19",
    "electron": "^38.1.0",
    "eslint": "^8.57.0",
    "eslint-config-prettier": "^8.10.0",
    "eslint-plugin-vue": "^9.26.0",
    "happy-dom": "^17.4.4",
    "prettier": "^2.8.8",
    "typescript": "^5.4.5",
    "vite-jsconfig-paths": "^2.0.1",
    "vitest": "^3.1.1",
    "workbox-build": "^6.6.1",
    "workbox-cacheable-response": "^6.6.1",
    "workbox-core": "^6.6.1",
    "workbox-expiration": "^6.6.1",
    "workbox-precaching": "^6.6.1",
    "workbox-routing": "^6.6.1",
    "workbox-strategies": "^6.6.1"
  },
  "engines": {
    "node": ">=16.11.0",
    "npm": ">= 6.13.4",
    "yarn": ">= 1.21.1"
  }
}


================================================
FILE: quasar.config.js
================================================
/* eslint-env node */

/*
 * This file runs in a Node context (it's NOT transpiled by Babel), so use only
 * the ES6 features that are supported by your Node version. https://node.green/
 */

// Configuration for your app
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js

const { configure } = require("quasar/wrappers");
const { execSync } = require("child_process");

function resolveGitCommit() {
  try {
    return execSync("git describe --always --dirty", {
      cwd: __dirname,
      stdio: "pipe",
    })
      .toString()
      .trim();
  } catch (err) {
    console.warn("Unable to resolve git commit via `git describe`");
    return "unknown";
  }
}

module.exports = configure(function (/* ctx */) {
  return {
    eslint: {
      // fix: true,
      // include: [],
      // exclude: [],
      // rawOptions: {},
      warnings: true,
      errors: true,
    },

    // https://v2.quasar.dev/quasar-cli/prefetch-feature
    // preFetch: true,

    // app boot file (/src/boot)
    // --> boot files are part of "main.js"
    // https://v2.quasar.dev/quasar-cli/boot-files
    boot: ["base", "global-components", "cashu", "i18n"],

    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
    css: ["app.scss", "base.scss"],

    // https://github.com/quasarframework/quasar/tree/dev/extras
    extras: [
      // 'ionicons-v4',
      // 'mdi-v5',
      // 'fontawesome-v6',
      // 'eva-icons',
      // 'themify',
      // 'line-awesome',
      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!

      "roboto-font", // optional, you are not bound to it
      "material-icons", // optional, you are not bound to it
    ],

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
    build: {
      target: {
        browser: ["esnext"],
        node: "node16",
      },

      vueRouterMode: "history", // available values: 'hash', 'history'
      // vueRouterBase,
      // vueDevtools,
      // vueOptionsAPI: false,

      // rebuildCache: true, // rebuilds Vite/linter/etc cache on startup

      // publicPath: '/',
      // analyze: true,
      // env: {},
      // rawDefine: {}
      // ignorePublicFolder: true,
      // minify: false,
      // polyfillModulePreload: true,
      // distDir

      extendViteConf(viteConf) {
        viteConf.define = viteConf.define || {};
        viteConf.define.GIT_COMMIT = JSON.stringify(resolveGitCommit());
      },
      // viteVuePluginOptions: {},

      // vitePlugins: [
      //   [ 'package-name', { ..options.. } ]
      // ]
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
    devServer: {
      https: true,
      open: true, // opens browser window automatically
      port: 8080,
    },

    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
    framework: {
      config: {},

      iconSet: "material-icons", // Quasar icon set
      // lang: 'en-US', // Quasar language pack

      // For special cases outside of where the auto-import strategy can have an impact
      // (like functional components as one of the examples),
      // you can manually specify Quasar components/directives to be available everywhere:
      //
      // components: [],
      // directives: [],

      // Quasar plugins
      plugins: ["LocalStorage", "Notify"],
    },

    animations: "all", // --- includes all animations
    // https://v2.quasar.dev/options/animations
    // animations: [],

    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
    // sourceFiles: {
    //   rootComponent: 'src/App.vue',
    //   router: 'src/router/index',
    //   store: 'src/store/index',
    //   registerServiceWorker: 'src-pwa/register-service-worker',
    //   serviceWorker: 'src-pwa/custom-service-worker',
    //   pwaManifestFile: 'src-pwa/manifest.json',
    //   electronMain: 'src-electron/electron-main',
    //   electronPreload: 'src-electron/electron-preload'
    // },

    // https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr
    ssr: {
      // ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
      // will mess up SSR

      // extendSSRWebserverConf (esbuildConf) {},
      // extendPackageJson (json) {},

      pwa: false,

      // manualStoreHydration: true,
      // manualPostHydrationTrigger: true,

      prodPort: 3000, // The default port that the production server should use
      // (gets superseded if process.env.PORT is specified at runtime)

      middlewares: [
        "render", // keep this as last one
      ],
    },

    // https://v2.quasar.dev/quasar-cli/developing-pwa/configuring-pwa
    pwa: {
      workboxMode: "generateSW", // or 'injectManifest'
      injectPwaMetaTags: true,
      swFilename: "sw.js",
      manifestFilename: "manifest.json",
      useCredentialsForManifestTag: false,
      workboxOptions: {
        skipWaiting: true,
        clientsClaim: true,
      },
      // useFilenameHashes: true,
      // extendGenerateSWOptions (cfg) {}
      // extendInjectManifestOptions (cfg) {},
      // extendManifestJson (json) {}
      // extendPWACustomSWConf (esbuildConf) {}
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
    cordova: {
      // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
    capacitor: {
      hideSplashscreen: false,
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
    electron: {
      // extendElectronMainConf (esbuildConf)
      // extendElectronPreloadConf (esbuildConf)

      inspectPort: 5858,

      bundler: "packager", // 'packager' or 'builder'

      packager: {
        // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
        // OS X / Mac App Store
        // appBundleId: '',
        // appCategoryType: '',
        // osxSign: '',
        // protocol: 'myapp://path',
        // Windows only
        // win32metadata: { ... }
        asar: true,
        prune: true,
        ignore: [
          /(^|[\\/])node_modules([\\/]|$)/,
          /(^|[\\/])screenshots([\\/]|$)/,
          /(^|[\\/])package-lock\.json$/,
          /(^|[\\/])yarn\.lock$/,
          /(^|[\\/])pnpm-lock\.yaml$/,
          /(^|[\\/])vitest\.config\.js$/,
          /(^|[\\/])test([\\/]|$)/,
        ],
      },

      builder: {
        // https://www.electron.build/configuration/configuration

        appId: "me.cashu",
      },
    },

    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
    bex: {
      contentScripts: ["my-content-script"],

      // extendBexScriptsConf (esbuildConf) {}
      // extendBexManifestJson (json) {}
    },
  };
});


================================================
FILE: quasar.extensions.json
================================================
{}


================================================
FILE: scripts/check-i18n.js
================================================
#!/usr/bin/env node
// Simple i18n keys auditor for src/i18n/*/index.ts
// - Compares key sets across locales
// - Reports missing/extra keys per locale
// - Detects duplicate object paths within a single locale file by parsing once

const fs = require("fs");
const path = require("path");

const i18nDir = path.join(process.cwd(), "src", "i18n");

function flatten(obj, prefix = "") {
  const res = {};
  for (const [k, v] of Object.entries(obj || {})) {
    const p = prefix ? `${prefix}.${k}` : k;
    if (v && typeof v === "object" && !Array.isArray(v)) {
      Object.assign(res, flatten(v, p));
    } else {
      res[p] = true;
    }
  }
  return res;
}

function loadLocale(file) {
  // Load TypeScript via ts-node/register if available; otherwise do a crude eval
  const src = fs.readFileSync(file, "utf8");
  // Transform "export default { ... }" to "module.exports = { ... }"
  const transformed = src.replace(/export default /, "module.exports = ");
  const mod = { exports: {} };
  const fn = new Function("module", "exports", transformed);
  fn(mod, mod.exports);
  return mod.exports;
}

function main() {
  const entries = fs
    .readdirSync(i18nDir, { withFileTypes: true })
    .filter((d) => d.isDirectory())
    .map((d) => ({
      code: d.name,
      file: path.join(i18nDir, d.name, "index.ts"),
    }))
    .filter((e) => fs.existsSync(e.file));

  if (entries.length === 0) {
    console.error("No locale files found.");
    process.exit(1);
  }

  const locales = entries.map((e) => ({
    code: e.code,
    data: loadLocale(e.file),
  }));
  const flatMaps = locales.map(({ code, data }) => ({
    code,
    flat: flatten(data),
  }));

  // Choose reference: en-US if present, else first
  const ref = flatMaps.find((l) => l.code === "en-US") || flatMaps[0];

  let hadError = false;
  const diffs = {};
  for (const { code, flat } of flatMaps) {
    const missing = Object.keys(ref.flat).filter((k) => !(k in flat));
    const extra = Object.keys(flat).filter((k) => !(k in ref.flat));
    if (missing.length || extra.length) {
      diffs[code] = { missing: missing.length, extra: extra.length };
      hadError = true;
      console.log(`Locale ${code}:`);
      if (missing.length) {
        console.log(`  Missing (${missing.length}):`);
        missing.slice(0, 50).forEach((k) => console.log(`    - ${k}`));
        if (missing.length > 50)
          console.log(`    ... and ${missing.length - 50} more`);
      }
      if (extra.length) {
        console.log(`  Extra (${extra.length}):`);
        extra.slice(0, 50).forEach((k) => console.log(`    + ${k}`));
        if (extra.length > 50)
          console.log(`    ... and ${extra.length - 50} more`);
      }
    }
  }

  if (hadError) {
    console.log("\nDifferences found.");
    console.log("Number of differences per file:");
    for (const [code, { missing, extra }] of Object.entries(diffs)) {
      console.log(`  ${code}: missing ${missing}, extra ${extra}`);
    }
    process.exit(2);
  } else {
    console.log("All locale keys are consistent.");
  }
}

main();


================================================
FILE: src/App.vue
================================================
<template>
  <router-view />
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "App",
});
</script>


================================================
FILE: src/boot/.gitkeep
================================================


================================================
FILE: src/boot/axios.js
================================================
// src/boot/axios.js

import { boot } from "quasar/wrappers";
import axios from "axios";

// const api = axios.create({ baseURL: 'https://api.example.com' })

export default boot(({ app }) => {
  // for use inside Vue files (Options API) through this.$axios and this.$api

  app.config.globalProperties.$axios = axios;
  // ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)
  //       so you won't necessarily have to import axios in each vue file

  //   app.config.globalProperties.$api = api
  // ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)
  //       so you can easily perform requests against your app's API
});

// export { axios, api }
export { axios };


================================================
FILE: src/boot/base.js
================================================
import { copyToClipboard } from "quasar";
import { useUiStore } from "stores/ui";
import { Clipboard } from "@capacitor/clipboard";
import { SafeArea } from "capacitor-plugin-safe-area";
import { useSettingsStore } from "stores/settings";
window.LOCALE = "en";
// window.EventHub = new Vue();

// Ensure we capture the PWA install prompt as early as possible
if (typeof window !== "undefined") {
  if (!window.__deferredBeforeInstallPrompt) {
    window.__deferredBeforeInstallPrompt = null;
  }
  window.addEventListener(
    "beforeinstallprompt",
    (e) => {
      // Allow custom install UI by deferring the prompt
      e.preventDefault();
      window.__deferredBeforeInstallPrompt = e;
      // Notify any listeners that install is available
      try {
        window.dispatchEvent(new CustomEvent("bip-available"));
      } catch (err) {
        // noop
      }
    },
    { once: false }
  );
  window.addEventListener("appinstalled", () => {
    window.__deferredBeforeInstallPrompt = null;
  });
}

// ---- PWA status bar color sync helpers ----
function ensureMetaTag(name, initialContent) {
  let el = document.querySelector(`meta[name="${name}"]`);
  if (!el) {
    el = document.createElement("meta");
    el.setAttribute("name", name);
    if (initialContent != null) {
      el.setAttribute("content", initialContent);
    }
    document.head.appendChild(el);
  }
  return el;
}

function resolveEffectiveTopBackgroundColor() {
  const header = document.querySelector(".q-header");
  const isTransparent = (val) =>
    !val ||
    val === "transparent" ||
    (val.startsWith("rgba") && parseFloat(val.split(",")[3]) === 0);

  const getBg = (el) =>
    el ? window.getComputedStyle(el).backgroundColor || "" : "";

  let color = getBg(header);
  if (isTransparent(color)) {
    const layout = document.querySelector(".q-layout");
    color = getBg(layout);
    if (isTransparent(color)) {
      const pageContainer = document.querySelector(".q-page-container");
      color = getBg(pageContainer);
    }
    if (isTransparent(color)) {
      color = getBg(document.body);
    }
  }
  return color || "#000000";
}

function updateStatusBarMeta() {
  try {
    const iosBar = ensureMetaTag(
      "apple-mobile-web-app-status-bar-style",
      "black-translucent"
    );
    iosBar.setAttribute("content", "black-translucent");

    const themeMeta = ensureMetaTag("theme-color", "#000000");
    const color = resolveEffectiveTopBackgroundColor();
    themeMeta.setAttribute("content", color);
  } catch {
    // noop
  }
}
// -------------------------------------------

window.windowMixin = {
  data: function () {
    return {
      g: {
        offline: !navigator.onLine,
        visibleDrawer: false,
        extensions: [],
        user: null,
        wallet: null,
        payments: [],
        allowedThemes: null,
      },
    };
  },
  methods: {
    changeColor: function (newValue) {
      document.body.setAttribute("data-theme", newValue);
      this.$q.localStorage.set("cashu.theme", newValue);
      updateStatusBarMeta();
    },
    changeLanguage: function (e) {
      this.$q.localStorage.set("cashu.language", e.target.value);
    },
    toggleDarkMode: function () {
      this.$q.dark.toggle();
      this.$q.localStorage.set("cashu.darkMode", this.$q.dark.isActive);
      updateStatusBarMeta();
    },
    copyText: function (text, message, position) {
      const notify = this.$q.notify;
      const i18n = this.$i18n;
      copyToClipboard(text).then(function () {
        notify({
          message:
            message ||
            (i18n && i18n.t("global.copy_to_clipboard.success")) ||
            "Copied to clipboard!",
          position: position || "bottom",
        });
      });
    },
    pasteFromClipboard: async function () {
      let text = "";
      if (window?.Capacitor) {
        const { value } = await Clipboard.read();
        text = value;
      } else {
        text = await navigator.clipboard.readText();
      }
      return text;
    },
    formatCurrency: function (value, currency, showBalance = false) {
      if (currency == undefined) {
        currency = "sat";
      }
      if (useUiStore().hideBalance && !showBalance) {
        return "****";
      }
      if (currency == "sat") return this.formatSat(value);
      if (currency == "msat") return this.fromMsat(value);
      if (currency == "usd") value = value / 100;
      if (currency == "eur") value = value / 100;
      return new Intl.NumberFormat(window.LOCALE, {
        style: "currency",
        currency: currency,
      }).format(value);
      // + " " +
      // currency.toUpperCase()
    },
    formatSat: function (value) {
      // convert value to integer
      value = parseInt(value);
      if (useSettingsStore().bip177BitcoinSymbol) {
        if (value >= 0) {
          return "₿" + new Intl.NumberFormat(window.LOCALE).format(value);
        } else {
          return (
            "-₿" + new Intl.NumberFormat(window.LOCALE).format(Math.abs(value))
          );
        }
      }
      return new Intl.NumberFormat(window.LOCALE).format(value) + " sat";
    },
    fromMsat: function (value) {
      value = parseInt(value);
      return new Intl.NumberFormat(window.LOCALE).format(value) + " msat";
    },
    notifyApiError: function (error) {
      const types = {
        400: "warning",
        401: "warning",
        500: "negative",
      };
      this.$q.notify({
        timeout: 5000,
        type: types[error.response.status] || "warning",
        message:
          error.message ||
          error.response.data.message ||
          error.response.data.detail ||
          null,
        caption:
          [error.response.status, " ", error.response.statusText]
            .join("")
            .toUpperCase() || null,
        icon: null,
      });
    },
    notifySuccess: async function (message, position = "top") {
      this.$q.notify({
        timeout: 5000,
        type: "positive",
        message: message,
        position: position,
        progress: true,
        actions: [
          {
            icon: "close",
            color: "white",
            handler: () => {},
          },
        ],
      });
    },
    notifyRefreshed: async function (message, position = "top") {
      this.$q.notify({
        timeout: 500,
        type: "positive",
        message: message,
        position: position,
        actions: [
          {
            color: "white",
            handler: () => {},
          },
        ],
      });
    },
    notifyError: async function (message, caption = null) {
      this.$q.notify({
        color: "red",
        message: message,
        caption: caption,
        position: "top",
        progress: true,
        actions: [
          {
            icon: "close",
            color: "white",
            handler: () => {},
          },
        ],
      });
    },
    notifyWarning: async function (message, caption = null, timeout = 5000) {
      this.$q.notify({
        timeout: timeout,
        type: "warning",
        message: message,
        caption: caption,
        position: "top",
        progress: true,
        actions: [
          {
            icon: "close",
            color: "black",
            handler: () => {},
          },
        ],
      });
    },
    notify: async function (
      message,
      type = "null",
      position = "top",
      caption = null,
      color = null
    ) {
      // failure
      this.$q.notify({
        timeout: 5000,
        type: "nuill",
        color: "grey",
        message: message,
        caption: null,
        position: "top",
        actions: [
          {
            icon: "close",
            color: "white",
            handler: () => {},
          },
        ],
      });
    },
  },
  created: function () {
    if (
      this.$q.localStorage.getItem("cashu.darkMode") == true ||
      this.$q.localStorage.getItem("cashu.darkMode") == false
    ) {
      this.$q.dark.set(this.$q.localStorage.getItem("cashu.darkMode"));
    } else {
      this.$q.dark.set(true);
    }
    this.g.allowedThemes = window.allowedThemes ?? ["classic"];

    addEventListener("offline", (event) => {
      this.g.offline = true;
    });

    addEventListener("online", (event) => {
      this.g.offline = false;
    });

    // addEventListener("beforeunload", (event) => {
    //   event.preventDefault();
    //   const dialogText = "Are you sure about this?";
    //   event.returnValue = dialogText;
    //   return dialogText;
    // });

    if (this.$q.localStorage.getItem("cashu.theme")) {
      document.body.setAttribute(
        "data-theme",
        this.$q.localStorage.getItem("cashu.theme")
      );
    } else {
      this.changeColor("monochrome");
    }

    // Initial status bar sync and observers for changes
    updateStatusBarMeta();
    window.addEventListener("resize", updateStatusBarMeta);
    window.addEventListener("orientationchange", updateStatusBarMeta);
    try {
      const header = document.querySelector(".q-header");
      const observer = new MutationObserver(updateStatusBarMeta);
      if (header) {
        observer.observe(header, {
          attributes: true,
          attributeFilter: ["class", "style"],
        });
      }
      observer.observe(document.body, {
        attributes: true,
        attributeFilter: ["class", "style", "data-theme"],
      });
    } catch {
      // noop
    }

    const language = this.$q.localStorage.getItem("cashu.language");
    if (language) {
      this.$i18n.locale = language;
    }

    // only for iOS
    if (window.Capacitor && Capacitor.getPlatform() === "ios") {
      SafeArea.getStatusBarHeight().then(({ statusBarHeight }) => {
        document.documentElement.style.setProperty(
          `--safe-area-inset-top`,
          `${statusBarHeight}px`
        );
      });

      SafeArea.removeAllListeners();

      // when safe-area changed
      SafeArea.addListener("safeAreaChanged", (data) => {
        const { insets } = data;
        for (const [key, value] of Object.entries(insets)) {
          document.documentElement.style.setProperty(
            `--safe-area-inset-${key}`,
            `${value}px`
          );
        }
      });
    }
  },
};


================================================
FILE: src/boot/cashu.js
================================================
/**
 * Configures the Cashu-ts library axios client
 */
// import { setupAxios } from "@cashu/cashu-ts";

// export default () => {
//   setupAxios({
//     // Default timeout for any interaction using the cashu-ts library to interact with a mint
//     timeout: 15 * 1000, // 15 seconds
//   });
// };


================================================
FILE: src/boot/global-components.js
================================================
import { boot } from "quasar/wrappers";

import VueQrcode from "@chenfengyuan/vue-qrcode";

// "async" is optional;
// more info on params: https://v2.quasar.dev/quasar-cli/boot-files
export default boot(async ({ app }) => {
  app.component(VueQrcode.name, VueQrcode);
});


================================================
FILE: src/boot/i18n.js
================================================
import { boot } from "quasar/wrappers";
import { createI18n } from "vue-i18n";
import messages from "src/i18n";

// Get stored locale from localStorage or fallback to browser language or en-US
const storedLocale =
  localStorage.getItem("cashu.language") || navigator.language || "en-US";

export const i18n = createI18n({
  locale: storedLocale,
  fallbackLocale: "en-US",
  globalInjection: true,
  messages,
});

export default boot(async ({ app }) => {
  app.use(i18n);
});


================================================
FILE: src/components/ActivityOrb.vue
================================================
<template>
  <div
    class="row justify-center q-mb-none"
    style="position: absolute; top: 60px; left: 0; width: 100%"
  >
    <div>
      <transition
        appear
        enter-active-class="animated fadeIn"
        leave-active-class="animated fadeOut"
      >
        <q-icon
          v-if="enableSpinner"
          name="adjust"
          color="primary"
          size="1.5em"
        />
      </transition>
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { mapState, mapWritableState } from "pinia";
import { useUiStore } from "stores/ui";
import { useWalletStore } from "../stores/wallet";
import { useReceiveTokensStore } from "src/stores/receiveTokensStore";
import { set } from "@vueuse/core";

export default defineComponent({
  name: "ActivityOrb",
  mixins: [windowMixin],
  components: {},
  props: {},
  data: function () {
    return {
      enableSpinner: false,
    };
  },
  mounted() {},
  computed: {
    ...mapWritableState(useUiStore, ["activityOrb"]),
    ...mapState(useWalletStore, ["activeWebsocketConnections"]),
    ...mapState(useReceiveTokensStore, ["scanningCard"]),
  },
  watch: {
    activityOrb: function () {
      if (this.activityOrb) {
        this.enableSpinner = true;
        setTimeout(() => {
          this.activityOrb = false;
          this.enableSpinner = false;
        }, 2000);
      } else {
        this.enableSpinner = false;
      }
    },
    scanningCard: function () {
      if (this.scanningCard) {
        this.enableSpinner = true;
      } else {
        this.enableSpinner = false;
      }
    },
  },
  methods: {},
});
</script>
<style scoped>
.animated.pulse {
  animation-duration: 0.5s;
}
.animated.fadeInDown {
  animation-duration: 0.3s;
}
.animated.fadeOut {
  animation-duration: 1s;
}
.animated.fadeIn {
  animation-duration: 1s;
}
</style>


================================================
FILE: src/components/AddMintDialog.vue
================================================
<template>
  <q-dialog
    v-model="showAddMintDialogLocal"
    @keydown.enter.prevent="addMintLocal"
    backdrop-filter="blur(4px) brightness(50%)"
    transition-show="fade"
    transition-hide="fade"
    scrollable
  >
    <q-card class="add-mint-dialog">
      <!-- Header Section -->
      <q-card-section class="add-mint-header q-pa-md">
        <div class="add-mint-title-row">
          <h4 class="add-mint-title q-my-none">
            {{ $t("AddMintDialog.title") }}
          </h4>
        </div>
      </q-card-section>

      <!-- Scrollable Content Section -->
      <q-card-section
        class="add-mint-content q-px-md scroll"
        style="max-height: 60vh"
      >
        <p class="add-mint-description q-mb-lg">
          {{ $t("AddMintDialog.description") }}
        </p>

        <div class="q-mb-lg">
          <label class="input-label">{{
            $t("AddMintDialog.inputs.mint_url.label")
          }}</label>
          <q-input
            outlined
            readonly
            :model-value="mintUrl"
            dense
            class="mint-input"
            filled
            type="textarea"
            autogrow
            style="font-family: monospace; font-size: 0.9em"
          ></q-input>
        </div>

        <!-- Audit Info Section -->
        <div v-if="mintUrl" class="q-mb-lg">
          <div class="audit-info-section">
            <q-btn
              flat
              class="audit-info-btn"
              @click="showAuditInfo = !showAuditInfo"
            >
              <info-icon size="16" class="q-mr-xs" />
              {{
                showAuditInfo ? "Hide Mint Audit Info" : "View Mint Audit Info"
              }}
            </q-btn>

            <!-- Audit Info Component -->
            <transition
              enter-active-class="animated fadeIn"
              leave-active-class="animated fadeOut"
            >
              <MintAuditInfo
                v-if="showAuditInfo"
                :mintUrl="mintUrl"
                class="q-mt-md"
              />
            </transition>
          </div>
        </div>
      </q-card-section>

      <!-- Fixed Action Buttons Section -->
      <q-card-actions class="action-buttons flex q-pa-md">
        <q-btn flat class="cancel-btn" v-close-popup>
          {{ $t("AddMintDialog.actions.cancel.label") }}
        </q-btn>
        <q-spacer></q-spacer>
        <q-btn
          color="primary"
          class="add-btn"
          @click="addMintLocal"
          v-close-popup
          :loading="addMintBlocking"
          icon="check"
        >
          {{ $t("AddMintDialog.actions.add_mint.label") }}
          <template v-slot:loading>
            <q-spinner />
            {{ $t("AddMintDialog.actions.add_mint.in_progress") }}
          </template>
        </q-btn>
      </q-card-actions>
    </q-card>
  </q-dialog>
</template>

<script lang="ts">
import { defineComponent, computed, ref } from "vue";
import { useSettingsStore } from "src/stores/settings";
import MintAuditInfo from "./MintAuditInfo.vue";
import { Info as InfoIcon } from "lucide-vue-next";

export default defineComponent({
  name: "AddMintDialog",
  components: {
    MintAuditInfo,
    InfoIcon,
  },
  props: {
    addMintData: {
      type: Object,
      required: true,
    },
    showAddMintDialog: {
      type: Boolean,
      required: true,
    },
    addMintBlocking: {
      type: Boolean,
      required: true,
    },
  },
  emits: ["add", "update:showAddMintDialog"],
  setup(props, { emit }) {
    const settings = useSettingsStore();
    const showAuditInfo = ref(false);

    const showAddMintDialogLocal = computed({
      get: () => props.showAddMintDialog,
      set: (value) => emit("update:showAddMintDialog", value),
    });

    const addMintLocal = () => {
      emit("add", props.addMintData, true); // Pass verbose = true
    };

    const mintUrl = computed(() => props.addMintData.url);

    return {
      addMintLocal,
      showAddMintDialogLocal,
      mintUrl,
      settings,
      showAuditInfo,
    };
  },
});
</script>

<style scoped>
.add-mint-dialog {
  width: 100%;
  max-width: 450px;
  max-height: 80vh;
  border-radius: 16px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.add-mint-header {
  position: relative;
  padding-top: 20px;
  flex-shrink: 0;
}

.add-mint-title-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.add-mint-title {
  font-size: 24px;
  font-weight: 700;
  letter-spacing: -0.5px;
  font-family: "Inter", sans-serif;
}

.add-mint-content {
  padding-top: 0;
  flex: 1;
  overflow-y: auto;
}

.add-mint-description {
  font-size: 15px;
  line-height: 1.5;
  font-weight: 400;
  margin-top: 0;
  opacity: 0.7;
  font-family: "Inter", sans-serif;
}

.input-label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 8px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  opacity: 0.7;
  font-family: "Inter", sans-serif;
}

.mint-data-display {
  padding: 12px;
  background-color: rgba(0, 0, 0, 0.03);
  border-radius: 8px;
  min-height: 48px;
  display: flex;
  align-items: center;
  font-family: "Inter", sans-serif;
}

.body--dark .mint-data-display {
  background-color: rgba(255, 255, 255, 0.05);
}

.mint-input {
  border-radius: 8px;
  height: 48px;
  font-size: 16px;
  font-family: "Inter", sans-serif;
}

/* Completely remove all input animations */
:deep(.mint-input) {
  /* Disable all transitions on the input and its children except for background-color */
  * {
    transition: none !important;
    animation: none !important;
  }

  /* Add a smooth transition just for the background-color */
  transition: background-color 0.2s ease-in-out !important;
}

:deep(.mint-input .q-field__focus-target) {
  border-radius: 8px;
}

:deep(.mint-input .q-focus-helper) {
  /* Remove animation completely */
  opacity: 0 !important;
  display: none !important; /* Hide it completely */
}

/* Add subtle focus/active state - theme responsive */
:deep(.mint-input.q-field--focused) {
  background-color: rgba(255, 255, 255, 0.1);
}

/* For dark mode, adjust the focus color */
:deep(.body--dark .mint-input.q-field--focused) {
  background-color: rgba(255, 255, 255, 0.07);
}

/* For light mode, use a darker shade for contrast */
:deep(.body--light .mint-input.q-field--focused) {
  background-color: rgba(0, 0, 0, 0.05);
}

/* Remove any ripple effects */
:deep(.mint-input .q-ripple) {
  display: none !important;
}

/* Remove any before/after pseudo-elements that might animate */
:deep(.mint-input .q-field__control:before),
:deep(.mint-input .q-field__control:after) {
  display: none !important;
}

/* Ensure no border animations */
:deep(.mint-input .q-field__control) {
  height: 48px;
  border-radius: 8px;
  transition: none !important;
}

:deep(.mint-input .q-field__native) {
  padding: 12px;
  font-family: "Inter", sans-serif;
}

/* Make sure input placeholders use Inter font */
:deep(.mint-input .q-field__native),
:deep(.mint-input .q-field__input),
:deep(.mint-input .q-placeholder) {
  font-family: "Inter", sans-serif;
}

.action-buttons {
  display: flex;
  justify-content: space-between;
  margin-top: 32px;
}

.cancel-btn {
  font-weight: 600;
  padding: 8px 16px;
  border-radius: 8px;
  font-family: "Inter", sans-serif;
}

.add-btn {
  font-weight: 700;
  padding: 8px 20px;
  border-radius: 8px;
  transition: all 0.2s ease;
  font-family: "Inter", sans-serif;
}

.add-btn:hover {
  transform: translateY(-1px);
}
</style>


================================================
FILE: src/components/AmountInputComponent.vue
================================================
<template>
  <div class="amount-input-root">
    <slot name="overlay" />
    <transition name="swap-primary">
      <div
        :key="`primary-${fiatMode}`"
        ref="amountDisplayRef"
        class="amount-display text-weight-bold text-center"
        :class="{ 'text-grey-6': muted }"
      >
        {{ primaryAmountDisplay }}
      </div>
    </transition>
    <div v-if="showFiatConversion" class="fiat-container">
      <div class="fiat-wrapper">
        <transition name="swap-secondary">
          <div :key="`secondary-${fiatMode}`" class="fiat-wrapper-content">
            <div
              class="fiat-display text-grey-6"
              :class="{ invisible: !secondaryDisplay }"
              @click="toggleFiatMode"
            >
              {{ secondaryDisplay || " " }}
            </div>
            <q-icon
              v-if="showSwap"
              name="swap_vert"
              size="24px"
              class="fiat-icon text-grey-6 cursor-pointer"
              @click="toggleFiatMode"
              aria-label="Swap amount/fiat input mode"
              :aria-pressed="fiatMode ? 'true' : 'false'"
              role="button"
            />
          </div>
        </transition>
      </div>
    </div>
  </div>
  <!-- amount-input-root keeps structure minimal so parents can place it within their layout -->
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { mapState } from "pinia";
import { useMintsStore } from "src/stores/mints";
import { useSettingsStore } from "src/stores/settings";
import { usePriceStore } from "src/stores/price";
declare const windowMixin: any;

const MAX_AMOUNT = 999_999_999;

export default defineComponent({
  name: "AmountInputComponent",
  mixins: [windowMixin],
  props: {
    modelValue: {
      type: Number,
      default: null,
    },
    // gray out amount (e.g. insufficient funds)
    muted: {
      type: Boolean,
      default: false,
    },
    // whether the component should react to global keyboard input
    enabled: {
      type: Boolean,
      default: true,
    },
    showFiatConversion: {
      type: Boolean,
      default: true,
    },
    // maximum allowed amount (in base units, before currency multiplier)
    maxAmount: {
      type: Number,
      default: null,
    },
    // minimum allowed amount (in base units, before currency multiplier)
    minAmount: {
      type: Number,
      default: null,
    },
  },
  emits: ["update:modelValue", "enter", "fiat-mode-changed"],
  data() {
    return {
      amountEditBuffer: "" as string,
      fiatEditBuffer: "" as string,
      fiatMode: false as boolean,
      isFiatTyping: false as boolean,
    };
  },
  computed: {
    ...mapState(
      useMintsStore as any,
      ["activeUnit", "activeUnitLabel", "activeUnitCurrencyMultiplyer"] as any
    ),
    ...mapState(useSettingsStore, ["bitcoinPriceCurrency"]),
    ...mapState(usePriceStore, ["bitcoinPrice", "currentCurrencyPrice"]),
    formattedAmountDisplay(): string {
      const amount = this.modelValue || 0;
      return (this as any).formatCurrency(
        amount * this.activeUnitCurrencyMultiplyer,
        this.activeUnit,
        true
      );
    },
    primaryAmountDisplay(): string {
      if (this.fiatMode) {
        const fiat = this.getFiatBufferNumber();
        return (this as any).formatCurrency(
          fiat,
          this.bitcoinPriceCurrency,
          true
        );
      }
      return this.formattedAmountDisplay;
    },
    secondaryFiatDisplay(): string {
      if (!this.bitcoinPrice || this.activeUnit !== "sat") {
        return "";
      }
      const baseAmount = this.modelValue ?? 0;
      const fiat = (this as any).formatCurrency(
        (this.currentCurrencyPrice / 100000000) *
          baseAmount *
          this.activeUnitCurrencyMultiplyer,
        this.bitcoinPriceCurrency,
        true
      );
      return fiat;
    },
    secondaryDisplay(): string {
      if (this.fiatMode) {
        // show converted sats (actual emitted amount)
        if (!this.currentCurrencyPrice || this.activeUnit !== "sat") return "";
        const sats = this.derivedSatsFromFiatBuffer;
        return (this as any).formatCurrency(
          sats * this.activeUnitCurrencyMultiplyer,
          this.activeUnit,
          true
        );
      }
      return this.secondaryFiatDisplay;
    },
    showSwap(): boolean {
      // Show when fiat pricing is available and unit is sats (or currently in fiat mode)
      return (
        (this.activeUnit === "sat" && !!this.currentCurrencyPrice) ||
        this.fiatMode
      );
    },
    derivedSatsFromFiatBuffer(): number {
      if (!this.bitcoinPrice || !this.currentCurrencyPrice) return 0;
      const fiat = this.getFiatBufferNumber();
      if (!isFinite(fiat) || fiat <= 0) return 0;
      // sats = fiat * 100_000_000 / price_per_BTC
      let sats = Math.round((fiat * 100000000) / this.currentCurrencyPrice);
      sats = Math.max(0, Math.min(sats, MAX_AMOUNT));
      // Apply min/max constraints
      if (this.minAmount != null && sats < this.minAmount) {
        sats = this.minAmount;
      } else if (this.maxAmount != null && sats > this.maxAmount) {
        sats = this.maxAmount;
      }
      return sats;
    },
  },
  watch: {
    formattedAmountDisplay() {
      this.$nextTick(() => this.adjustAmountFontSize());
    },
    modelValue(newVal: number | null) {
      if (newVal == null) return;
      // Apply min/max constraints
      let clampedVal = newVal;
      if (this.minAmount != null && clampedVal < this.minAmount) {
        clampedVal = this.minAmount;
      } else if (this.maxAmount != null && clampedVal > this.maxAmount) {
        clampedVal = this.maxAmount;
      } else if (clampedVal > MAX_AMOUNT) {
        clampedVal = MAX_AMOUNT;
      }
      if (clampedVal !== newVal) {
        this.$emit("update:modelValue", clampedVal);
        const isFiatInput =
          this.fiatMode || this.activeUnitCurrencyMultiplyer === 100;
        this.amountEditBuffer = isFiatInput
          ? Number(clampedVal).toFixed(2)
          : String(clampedVal);
        if (this.fiatMode) {
          // keep fiat buffer in sync with clamped sat value
          const fiat = this.fiatFromSats(clampedVal);
          this.fiatEditBuffer = this.numberToFiatBuffer(fiat);
        }
        return;
      }
      // Sync buffers when modelValue changes externally (e.g., from NumericKeyboard)
      if (this.fiatMode) {
        // Do not override user's fiat typing buffer during input
        if (this.isFiatTyping) return;
        const fiat = this.fiatFromSats(newVal);
        this.fiatEditBuffer = this.numberToFiatBuffer(fiat);
      } else {
        const isFiatInput =
          this.fiatMode || this.activeUnitCurrencyMultiplyer === 100;
        this.amountEditBuffer = isFiatInput
          ? Number(newVal).toFixed(2)
          : String(newVal);
      }
    },
    enabled(val: boolean) {
      if (val) {
        this.initializeKeyHandling();
      } else {
        this.teardownKeyHandling();
      }
    },
  },
  mounted() {
    this.$nextTick(() => this.adjustAmountFontSize());
    if (this.enabled) {
      this.initializeKeyHandling();
    }
  },
  beforeUnmount() {
    this.teardownKeyHandling();
  },
  methods: {
    adjustAmountFontSize(): void {
      const element = this.$refs.amountDisplayRef as HTMLElement | undefined;
      if (!element) return;
      element.style.fontSize = "";
      const container = element.parentElement as HTMLElement | null;
      if (!container) return;
      const containerWidth = container.offsetWidth;
      const scrollWidth = element.scrollWidth;
      if (scrollWidth > containerWidth) {
        const baseFontSize = parseFloat(
          window.getComputedStyle(element).fontSize
        );
        const scaleFactor = containerWidth / scrollWidth;
        const newFontSize = Math.max(baseFontSize * scaleFactor * 0.95, 24);
        element.style.fontSize = `${newFontSize}px`;
      }
    },
    toggleFiatMode(): void {
      // Only allow switching when price data is available and activeUnit is sat
      if (!this.currentCurrencyPrice || this.activeUnit !== "sat") return;
      this.fiatMode = !this.fiatMode;
      if (this.fiatMode) {
        // initialize fiat buffer from current model value
        const sats = this.modelValue == null ? 0 : this.modelValue;
        const fiat = this.fiatFromSats(sats);
        this.fiatEditBuffer = this.numberToFiatBuffer(fiat);
      } else {
        // initialize sats buffer from current model value
        this.amountEditBuffer =
          this.modelValue == null ? "0" : String(this.modelValue);
      }
      this.$nextTick(() => this.adjustAmountFontSize());
      this.$emit("fiat-mode-changed", this.fiatMode);
    },
    fiatFromSats(sats: number): number {
      // fiat = sats * price_per_BTC / 100_000_000
      return (sats * this.currentCurrencyPrice) / 100000000;
    },
    satsFromFiat(fiat: number): number {
      if (!this.currentCurrencyPrice) return 0;
      const sats = Math.round((fiat * 100000000) / this.currentCurrencyPrice);
      return Math.max(0, Math.min(sats, MAX_AMOUNT));
    },
    getFiatBufferNumber(): number {
      const buf = this.fiatEditBuffer;
      if (!buf || buf === ".") return 0;
      const num = Number(buf.replace(/,/g, "."));
      return isNaN(num) ? 0 : num;
    },
    numberToFiatBuffer(num: number): string {
      // keep exactly 2 decimals for fiat buffer
      return (Math.round(num * 100) / 100).toFixed(2);
    },
    initializeKeyHandling(): void {
      const isFiatInput =
        this.fiatMode || this.activeUnitCurrencyMultiplyer === 100;
      // initialize buffer from current value
      if (this.modelValue == null) {
        this.amountEditBuffer = isFiatInput ? "0.00" : "0";
      } else {
        this.amountEditBuffer = isFiatInput
          ? Number(this.modelValue).toFixed(2)
          : String(this.modelValue);
      }
      if (this.currentCurrencyPrice && this.activeUnit === "sat") {
        const fiat = this.fiatFromSats(this.modelValue || 0);
        this.fiatEditBuffer = this.numberToFiatBuffer(fiat);
      } else {
        this.fiatEditBuffer = "";
      }
      window.addEventListener("keydown", this.onGlobalAmountKeydown);
      window.addEventListener("resize", this.adjustAmountFontSize);
    },
    teardownKeyHandling(): void {
      window.removeEventListener("keydown", this.onGlobalAmountKeydown);
      window.removeEventListener("resize", this.adjustAmountFontSize);
      this.amountEditBuffer = "";
      this.fiatEditBuffer = "";
    },
    onGlobalAmountKeydown(e: KeyboardEvent): void {
      // ignore if an input/textarea/contenteditable is focused
      const ae = document.activeElement as HTMLElement | null;
      if (
        ae &&
        (ae.tagName === "INPUT" ||
          ae.tagName === "TEXTAREA" ||
          ae.getAttribute("contenteditable") === "true")
      ) {
        return;
      }
      if ((e as any).metaKey || (e as any).ctrlKey || (e as any).altKey) return;
      const allowDecimal = this.fiatMode
        ? true
        : this.activeUnit !== "sat" && this.activeUnit !== "msat";
      const isFiatInput =
        this.fiatMode || this.activeUnitCurrencyMultiplyer === 100;
      const key = (e as KeyboardEvent).key;
      let buf = this.fiatMode
        ? this.fiatEditBuffer ||
          this.numberToFiatBuffer(this.fiatFromSats(this.modelValue || 0))
        : this.amountEditBuffer ||
          (this.modelValue == null ? "0" : String(this.modelValue));
      let handled = false;

      if (/^[0-9]$/.test(key)) {
        if (isFiatInput) {
          const num = Number(buf.replace(/,/g, "."));
          const cents = isNaN(num) ? 0 : Math.round(num * 100);
          let centsStr = cents.toString();
          if (centsStr === "0") centsStr = "";
          centsStr += key;
          const parsed = parseInt(centsStr, 10);
          const newCents = isNaN(parsed) ? 0 : parsed;
          buf = (newCents / 100).toFixed(2);
        } else {
          // If buffer represents zero (0, 0.0, 0.00, etc.), reset completely
          const bufNum = allowDecimal
            ? Number(buf.replace(/,/g, "."))
            : Number(buf);
          if (bufNum === 0 || isNaN(bufNum)) {
            buf = key;
          } else {
            buf = buf + key;
          }
        }
        handled = true;
      } else if (key === "Backspace" || key === "Delete") {
        if (isFiatInput) {
          const num = Number(buf.replace(/,/g, "."));
          const cents = isNaN(num) ? 0 : Math.round(num * 100);
          let centsStr = cents.toString();
          centsStr = centsStr.length > 1 ? centsStr.slice(0, -1) : "0";
          const parsed = parseInt(centsStr, 10);
          const newCents = isNaN(parsed) ? 0 : parsed;
          buf = (newCents / 100).toFixed(2);
        } else {
          buf = buf.length > 1 ? buf.slice(0, -1) : "0";
        }
        handled = true;
      } else if ((key === "." || key === ",") && allowDecimal) {
        if (!isFiatInput) {
          if (!buf.includes(".")) {
            buf = buf + ".";
          }
        }
        handled = true;
      } else if (key === "Enter") {
        if (this.modelValue != null && this.modelValue > 0) {
          this.$emit("enter");
        }
        handled = true;
      }
      if (!handled) return;
      (e as Event).preventDefault();

      // sanitize buffer
      if (allowDecimal) {
        if (!isFiatInput) {
          buf = buf.replace(/,/g, ".");
          buf = buf.replace(/[^\d.]/g, "").replace(/^(\d*\.\d*).*$/, "$1");
          if (buf.includes(".")) {
            const parts = buf.split(".");
            const decimals = parts[1] ?? "";
            buf = parts[0] + "." + decimals.slice(0, 2);
          }
        }
      } else {
        buf = buf.replace(/[^\d]/g, "");
      }
      if (
        !isFiatInput &&
        buf.startsWith("0") &&
        buf.length > 1 &&
        buf[1] !== "."
      ) {
        buf = String(parseInt(buf, 10) || 0);
      }
      if (this.fiatMode) {
        this.fiatEditBuffer = buf;
        if (buf === "" || buf === ".") {
          this.$emit("update:modelValue", null);
        } else {
          const fiatNum = Number(buf);
          if (isNaN(fiatNum)) {
            this.$emit("update:modelValue", null);
          } else {
            let sats = this.satsFromFiat(fiatNum);
            if (sats >= MAX_AMOUNT) {
              sats = MAX_AMOUNT;
              // reflect clamp back into fiat buffer
              this.fiatEditBuffer = this.numberToFiatBuffer(
                this.fiatFromSats(MAX_AMOUNT)
              );
            }
            // Apply min/max constraints
            if (this.minAmount != null && sats < this.minAmount) {
              sats = this.minAmount;
              this.fiatEditBuffer = this.numberToFiatBuffer(
                this.fiatFromSats(this.minAmount)
              );
            } else if (this.maxAmount != null && sats > this.maxAmount) {
              sats = this.maxAmount;
              this.fiatEditBuffer = this.numberToFiatBuffer(
                this.fiatFromSats(this.maxAmount)
              );
            }
            this.isFiatTyping = true;
            this.$emit("update:modelValue", sats);
            this.$nextTick(() => {
              this.isFiatTyping = false;
            });
          }
        }
      } else {
        this.amountEditBuffer = buf;
        if (buf === "" || buf === ".") {
          this.$emit("update:modelValue", null);
        } else {
          let num = Number(buf);
          if (isNaN(num)) {
            this.$emit("update:modelValue", null);
          } else {
            // Apply min/max constraints
            if (this.minAmount != null && num < this.minAmount) {
              num = this.minAmount;
              this.amountEditBuffer = isFiatInput
                ? num.toFixed(2)
                : String(this.minAmount);
            } else if (this.maxAmount != null && num > this.maxAmount) {
              num = this.maxAmount;
              this.amountEditBuffer = isFiatInput
                ? num.toFixed(2)
                : String(this.maxAmount);
            } else if (num > MAX_AMOUNT) {
              num = MAX_AMOUNT;
              this.amountEditBuffer = isFiatInput
                ? num.toFixed(2)
                : String(MAX_AMOUNT);
            }
            this.$emit("update:modelValue", num);
          }
        }
      }
    },
  },
});
</script>
<style scoped>
.amount-input-root {
  position: relative;
  width: 100%;
  min-height: 120px; /* Ensure enough space for both displays */
}
.amount-display {
  position: absolute;
  top: 0;
  left: 50%;
  transform: translateX(-50%);
  font-size: clamp(56px, 11vw, 80px);
  line-height: 1.1;
  white-space: nowrap;
  max-width: 90vw;
}
.fiat-container {
  position: absolute;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 18px;
}
.fiat-wrapper {
  position: relative;
  width: 100%;
  height: 18px;
}
.fiat-wrapper-content {
  position: absolute;
  left: 50%;
  top: 50%;
  display: inline-flex;
  align-items: center;
  transform: translate(-50%, -50%);
}
.fiat-display {
  font-size: 20px;
  text-align: center;
}
.fiat-icon {
  position: absolute;
  left: 100%;
  margin-left: 4px;
}
.invisible {
  visibility: hidden;
}
/* Primary amount swap animation (top position) - Apple-like */
.swap-primary-enter-active {
  transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  transform-origin: center;
}
.swap-primary-leave-active {
  transition: all 0.25s cubic-bezier(0.55, 0.06, 0.68, 0.19);
  transform-origin: center;
}
.swap-primary-leave-to {
  opacity: 0;
  transform: translateX(-50%) translateY(40px) scale(0.85);
}
.swap-primary-enter-from {
  opacity: 0;
  transform: translateX(-50%) translateY(-40px) scale(0.85);
}
.swap-primary-enter-to,
.swap-primary-leave-from {
  opacity: 1;
  transform: translateX(-50%) translateY(0) scale(1);
}
/* Secondary amount swap animation (bottom position) - Apple-like */
.swap-secondary-enter-active {
  transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
  transform-origin: center;
}
.swap-secondary-leave-active {
  transition: all 0.25s cubic-bezier(0.55, 0.06, 0.68, 0.19);
  transform-origin: center;
}
.swap-secondary-leave-to {
  opacity: 0;
  transform: translate(-50%, calc(-50% - 40px)) scale(0.85);
}
.swap-secondary-enter-from {
  opacity: 0;
  transform: translate(-50%, calc(-50% + 40px)) scale(0.85);
}
.swap-secondary-enter-to,
.swap-secondary-leave-from {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}
</style>


================================================
FILE: src/components/AndroidPWAPrompt.vue
================================================
<!-- src/components/AndroidPWAPrompt.vue -->
<template>
  <transition appear enter-active-class="animated fadeInDown">
    <div
      v-if="showAndroidPWAPrompt"
      class="pwa-prompt android-pwa-prompt q-pa-md text-center"
    >
      <div class="pwa-prompt-content">
        <i18n-t keypath="AndroidPWAPrompt.text" tag="span">
          <template v-slot:icon>
            <q-icon name="more_vert" size="sm" />
          </template>
          <template v-slot:buttonText>
            <strong>{{ $t("AndroidPWAPrompt.buttonText") }}</strong>
          </template>
        </i18n-t>
        <q-btn
          flat
          icon="close"
          @click="closePrompt"
          size="sm"
          class="close-btn q-px-sm"
        />
      </div>

      <div class="pwa-prompt-arrow"></div>
    </div>
  </transition>
</template>

<script lang="ts">
import { defineComponent } from "vue";

export default defineComponent({
  name: "AndroidPWAPrompt",
  data() {
    return {
      showAndroidPWAPromptLocal:
        localStorage.getItem("cashu.ui.showAndroidPWAPrompt") != "seen",
      showAndroidPWAPrompt: false,
    };
  },
  mounted() {
    if (
      this.showAndroidPWAPromptLocal &&
      this.isChromeOnAndroid() &&
      !this.isInStandaloneMode()
    ) {
      this.showAndroidPWAPrompt = true;
    }
  },
  methods: {
    closePrompt() {
      localStorage.setItem("cashu.ui.showAndroidPWAPrompt", "seen");
      this.showAndroidPWAPrompt = false;
    },
    isChromeOnAndroid() {
      const userAgent = navigator.userAgent.toLowerCase();
      const isAndroid = /android/.test(userAgent);
      const isChrome =
        /chrome/.test(userAgent) && !/edge|edg|opr|opera/.test(userAgent);
      return isAndroid && isChrome;
    },
    isInStandaloneMode() {
      return window.matchMedia("(display-mode: standalone)").matches;
    },
  },
});
</script>

<style scoped>
@keyframes moveUpDown {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(-10px);
  }
}

.pwa-prompt {
  position: fixed;
  top: 20px;
  right: 20px;
  margin: 0 auto;
  z-index: 9999;
  text-align: center;
  display: flex;
  flex-direction: column; /* Add this line */
  align-items: center; /* Add this line */
  justify-content: center;
  animation: moveUpDown 1s infinite; /* Add this line for animation */
}

.pwa-prompt-content {
  display: inline-flex;
  align-items: center;
  background-color: black;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 8px;
}

.pwa-prompt-content q-icon {
  margin-right: 5px;
}

.pwa-prompt-arrow {
  position: relative;
  width: 0;
  height: 0;
  bottom: 60px;
  left: 45%;
  border-left: 10px solid transparent;
  border-right: 10px solid transparent;
  border-bottom: 10px solid white;
  text-align: center;
  margin: 0 auto;
}
</style>


================================================
FILE: src/components/AnimatedNumber.vue
================================================
<template>
  <span @click="$emit('click')">{{ formattedValue }}</span>
</template>

<script lang="ts">
import { defineComponent, ref, watch, computed } from "vue";

export default defineComponent({
  name: "AnimatedNumber",
  props: {
    value: {
      type: Number,
      required: true,
    },
    duration: {
      type: Number,
      default: 1000, // Animation duration in milliseconds
    },
    format: {
      type: Function,
      required: true, // Function to format the number
    },
  },
  setup(props) {
    const displayedValue = ref(props.value);
    // value to remember that we do not want to animate the very first update (when the component is created)
    const initialized = ref(false);

    watch(
      () => props.value,
      (newValue, oldValue) => {
        if (!initialized.value) {
          displayedValue.value = newValue;
          if (newValue > 0) {
            // do not animate until we set the first value
            initialized.value = true;
          }
          return;
        }
        const startTime = performance.now();
        const startValue = oldValue !== undefined ? oldValue : newValue;
        const endValue = newValue;

        const animate = (currentTime: number) => {
          const elapsed = currentTime - startTime;
          const progress = Math.max(Math.min(elapsed / props.duration, 1), 0);
          const currentValue = startValue + (endValue - startValue) * progress;
          displayedValue.value = currentValue;
          if (progress < 1) {
            requestAnimationFrame(animate);
          }
        };

        requestAnimationFrame(animate);
      },
      { immediate: true }
    );

    const formattedValue = computed(() => {
      return props.format(displayedValue.value);
    });

    return {
      formattedValue,
    };
  },
});
</script>


================================================
FILE: src/components/BalanceView.vue
================================================
<template>
  <!-- <q-card class="q-my-md q-py-sm">
    <q-card-section class="q-mt-sm q-py-xs"> -->
  <div class="q-pt-xl q-pb-md">
    <div class="row justify-center q-pb-lg" style="height: 80px">
      <div v-if="globalMutexLock">
        <transition
          appear
          enter-active-class="animated pulse"
          leave-active-class="animated fadeOut"
        >
          <q-spinner class="q-mt-lg q-mb-none" size="lg" color="primary" />
        </transition>
      </div>
      <div v-else>
        <transition
          appear
          enter-active-class="animated pulse"
          leave-active-class="animated fadeOut"
        >
          <ToggleUnit class="q-mt-lg q-mb-none" />
        </transition>
      </div>
    </div>
    <transition
      appear
      enter-active-class="animated fadeInDown"
      leave-active-class="animated fadeInDown"
      mode="out-in"
    >
      <q-carousel
        v-model="this.activeUnit"
        transition-prev="jump-up"
        transition-next="jump-up"
        swipeable
        animated
        :height="$q.screen.width < 390 ? '130px' : '80px'"
        control-color="primary"
        class="bg-transparent rounded-borders q-mb-xl q-mt-xl text-primary"
      >
        <!-- make a q-carousel-slide with v-for for all possible units -->
        <q-carousel-slide
          v-for="unit in balancesOptions"
          :key="unit.value"
          :name="unit.value"
          class="q-pt-none"
        >
          <div class="row">
            <div class="col-12">
              <h3
                class="q-my-none q-py-none cursor-pointer"
                @click="toggleHideBalance"
              >
                <strong>
                  <AnimatedNumber
                    :value="getTotalBalance"
                    :format="(val) => formatCurrency(val, activeUnit)"
                    class="q-my-none q-py-none cursor-pointer"
                  />
                </strong>
              </h3>
              <div v-if="bitcoinPrice" class="q-mt-sm">
                <strong v-if="this.activeUnit == 'sat'">
                  <AnimatedNumber
                    :value="
                      (currentCurrencyPrice / 100000000) * getTotalBalance
                    "
                    :format="(val) => formatCurrency(val, bitcoinPriceCurrency)"
                  />
                </strong>
                <strong
                  v-if="this.activeUnit == 'usd' || this.activeUnit == 'eur'"
                >
                  <AnimatedNumber
                    :value="
                      (getTotalBalance / 100 / currentCurrencyPrice) * 100000000
                    "
                    :format="(val) => formatCurrency(val, 'sat')"
                  />
                </strong>
                <q-tooltip>
                  1 BTC =
                  {{
                    formatCurrency(currentCurrencyPrice, bitcoinPriceCurrency)
                  }}
                </q-tooltip>
              </div>
            </div>
          </div>
        </q-carousel-slide>
      </q-carousel>
    </transition>
    <div
      v-if="activeMint().mint.errored"
      class="row q-mt-md q-mb-none text-secondary"
    >
      <div class="col-12">
        <q-badge outline color="red" class="q-mr-xs q-mt-sm text-weight-bold">
          {{ $t("BalanceView.mintError.label") }}
          <q-icon name="error" class="q-ml-xs" />
        </q-badge>
      </div>
    </div>
    <!-- mint url -->
    <div class="row q-mt-md q-mb-none text-secondary" v-if="activeMintUrl">
      <div class="col-12 cursor-pointer">
        <span class="text-weight-light" @click="setTab('mints')">
          {{ $t("BalanceView.mintUrl.label") }}: <b>{{ activeMintLabel }}</b>
        </span>
      </div>
    </div>
    <!-- mint balance -->
    <div class="row q-mb-none text-secondary" v-if="mints.length > 1">
      <div class="col-12">
        <span class="q-my-none q-py-none text-weight-regular">
          {{ $t("BalanceView.mintBalance.label") }}:
          <b>
            <AnimatedNumber
              :value="getActiveBalance"
              :format="(val) => formatCurrency(val, activeUnit)"
              class="q-my-none q-py-none cursor-pointer"
            />
          </b>
        </span>
      </div>
    </div>
  </div>
  <!-- pending -->
  <div class="row q-mt-xs q-mb-none" v-if="pendingBalance > 0">
    <div class="col-12">
      <q-btn
        name="history"
        size="sm"
        align="between"
        color="secondary"
        dense
        outline
        class="q-mx-none q-mt-xs q-pr-sm cursor-pointer"
        @click="checkPendingTokens()"
        ><q-icon name="history" size="1rem" class="q-mx-xs" />
        {{ $t("BalanceView.pending.label") }}:
        {{ formatCurrency(pendingBalance, this.activeUnit) }}
        <q-tooltip>{{ $t("BalanceView.pending.tooltip") }}</q-tooltip>
      </q-btn>
    </div>
  </div>
  <!-- </q-card-section>
  </q-card> -->
</template>
<script lang="ts">
import { defineComponent, ref } from "vue";
import { getShortUrl } from "src/js/wallet-helpers";
import { mapState, mapWritableState, mapActions } from "pinia";
import { useMintsStore } from "stores/mints";
import { useSettingsStore } from "stores/settings";
import { useTokensStore } from "stores/tokens";
import { useUiStore } from "stores/ui";
import { useWalletStore } from "stores/wallet";
import { usePriceStore } from "stores/price";
import ToggleUnit from "components/ToggleUnit.vue";
import AnimatedNumber from "components/AnimatedNumber.vue";
import axios from "axios";
import { map } from "underscore";

export default defineComponent({
  name: "BalanceView",
  mixins: [windowMixin],
  components: {
    ToggleUnit,
    AnimatedNumber,
  },
  props: {
    setTab: Function,
  },
  computed: {
    ...mapState(useMintsStore, [
      "activeMintUrl",
      "activeProofs",
      "activeBalance",
      "mints",
      "totalUnitBalance",
      "activeUnit",
      "activeMint",
    ]),
    ...mapState(useTokensStore, ["historyTokens"]),
    ...mapState(useUiStore, ["globalMutexLock"]),
    ...mapState(usePriceStore, [
      "bitcoinPrice",
      "bitcoinPrices",
      "currentCurrencyPrice",
    ]),
    ...mapState(useSettingsStore, ["bitcoinPriceCurrency"]),
    ...mapWritableState(useMintsStore, ["activeUnit"]),
    ...mapWritableState(useUiStore, ["hideBalance", "lastBalanceCached"]),
    pendingBalance: function () {
      return -this.historyTokens
        .filter((t) => t.status == "pending")
        .filter((t) => t.unit == this.activeUnit)
        .reduce((sum, el) => (sum += el.amount), 0);
    },
    balancesOptions: function () {
      const mint = this.activeMint();
      return Object.entries(mint.allBalances).map(([key, value]) => ({
        label: key,
        value: key,
      }));
    },
    allMintKeysets: function () {
      return [].concat(...this.mints.map((m) => m.keysets));
    },
    getTotalBalance: function () {
      return this.totalUnitBalance;
    },
    getActiveBalance: function () {
      return this.activeBalance;
    },
    activeMintLabel: function () {
      const mintClass = this.activeMint();

      return (
        mintClass.mint.nickname ||
        mintClass.mint.info?.name ||
        getShortUrl(this.activeMintUrl)
      );
    },
    getBalance: function () {
      const balance = this.activeProofs
        .flat()
        .reduce((sum, el) => (sum += el.amount), 0);
      return balance;
    },
  },
  data() {
    return {
      priceLabel: null,
    };
  },
  mounted() {
    this.fetchBitcoinPrice();
  },
  methods: {
    ...mapActions(useWalletStore, ["checkPendingTokens"]),
    ...mapActions(usePriceStore, ["fetchBitcoinPrice"]),
    toggleUnit: function () {
      const units = this.activeMint().units;
      this.activeUnit =
        units[(units.indexOf(this.activeUnit) + 1) % units.length];
      return this.activeUnit;
    },
    toggleHideBalance() {
      this.hideBalance = !this.hideBalance;
    },
  },
});
</script>
<style scoped>
.animated.pulse {
  animation-duration: 0.5s;
}
.animated.fadeInDown {
  animation-duration: 0.3s;
}
</style>


================================================
FILE: src/components/ChooseMint.vue
================================================
<template>
  <div class="q-pb-md">
    <!-- Main mint selector button -->
    <div
      class="row q-mt-xs q-mb-none"
      v-if="activeMintUrl || !requireActiveMint"
    >
      <div class="col-12">
        <div
          class="mint-selector-btn"
          :class="{ 'mint-selector-dense': dense }"
          :style="style"
          @click="showMintSheet = true"
        >
          <div class="row items-center full-width no-wrap">
            <!-- Mint Icon -->
            <q-avatar
              :size="dense ? '40px' : '48px'"
              class="q-mr-md mint-icon-avatar"
            >
              <q-img
                v-if="chosenMint?.iconUrl"
                :src="chosenMint.iconUrl"
                spinner-color="white"
                spinner-size="xs"
              >
                <template v-slot:error>
                  <div class="row items-center justify-center full-height">
                    <q-icon name="account_balance" color="grey-7" size="24px" />
                  </div>
                </template>
              </q-img>
              <q-icon
                v-else
                name="account_balance"
                color="grey-7"
                size="24px"
              />
            </q-avatar>

            <!-- Mint Info -->
            <div class="col text-left mint-info-section">
              <div class="mint-name-label">
                {{
                  chosenMint?.nickname ||
                  chosenMint?.shorturl ||
                  placeholder ||
                  $t("ChooseMint.placeholder")
                }}
              </div>
              <div v-if="showBalances && chosenMint" class="mint-balance-label">
                <span v-if="!chosenMint.errored" class="text-grey-6">
                  {{ formatCurrency(selectedMintBalance, activeUnit) }}
                  {{ $t("ChooseMint.available_text") }}
                </span>
                <span v-else class="text-red">
                  {{ $t("ChooseMint.badge_mint_error_text") }}
                </span>
              </div>
            </div>

            <!-- Chevron -->
            <q-icon name="expand_more" color="grey-6" size="20px" />
          </div>
        </div>
      </div>
    </div>

    <!-- Bottom Sheet for Mint Selection -->
    <teleport to="body">
      <transition name="mint-overlay">
        <div
          v-if="showMintSheet"
          class="mint-sheet-overlay"
          @click="showMintSheet = false"
        >
          <div class="mint-sheet" @click.stop>
            <!-- Header -->
            <div class="mint-sheet-header">
              <h3>{{ $t("ChooseMint.sheet_title") }}</h3>
              <q-btn
                flat
                round
                icon="close"
                @click="showMintSheet = false"
                class="close-btn"
              />
            </div>

            <!-- Mint List -->
            <div class="mint-options">
              <div
                v-for="mint in chooseMintOptions()"
                :key="mint.url"
                class="mint-option"
                :class="{ active: chosenMint?.url === mint.url }"
                @click="selectMint(mint)"
              >
                <div class="row items-center full-width no-wrap">
                  <!-- Mint Icon -->
                  <q-avatar size="48px" class="q-mr-md">
                    <q-img
                      v-if="mint.iconUrl"
                      :src="mint.iconUrl"
                      spinner-color="white"
                      spinner-size="xs"
                    >
                      <template v-slot:error>
                        <div
                          class="row items-center justify-center full-height"
                        >
                          <q-icon
                            name="account_balance"
                            color="grey-7"
                            size="24px"
                          />
                        </div>
                      </template>
                    </q-img>
                    <q-icon
                      v-else
                      name="account_balance"
                      color="grey-7"
                      size="24px"
                    />
                  </q-avatar>

                  <!-- Mint Info -->
                  <div class="col text-left">
                    <div class="mint-option-name">
                      {{ mint.nickname || mint.shorturl }}
                    </div>
                    <div v-if="showBalances" class="mint-option-balance">
                      <span v-if="!mint.errored" class="text-grey-6">
                        <span
                          v-for="unit in mint.units"
                          :key="unit"
                          class="q-mr-sm"
                        >
                          {{ formatCurrency(mint.balances[unit], unit) }}
                        </span>
                      </span>
                      <span v-else class="text-red">
                        {{ $t("ChooseMint.badge_mint_error_text") }}
                      </span>
                    </div>
                  </div>

                  <!-- Selection Indicator -->
                  <q-icon
                    v-if="chosenMint?.url === mint.url"
                    name="check_circle"
                    color="primary"
                    size="24px"
                  />
                </div>
              </div>
            </div>
          </div>
        </div>
      </transition>
    </teleport>
  </div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
import { getShortUrl } from "src/js/wallet-helpers";
import { mapActions, mapState, mapWritableState } from "pinia";
import { useMintsStore } from "stores/mints";
import { MintClass } from "stores/mints";
import type { StoredMint } from "stores/mints";
import { useUiStore } from "stores/ui";
import { i18n } from "../boot/i18n";

declare const windowMixin: any;

type MintOption = {
  nickname?: string | null;
  url: string;
  shorturl: string | null;
  iconUrl?: string | null;
  balances: Record<string, number>;
  errored?: boolean;
  units: string[];
};

export default defineComponent({
  name: "ChooseMint",
  mixins: [windowMixin],
  props: {
    rounded: {
      type: Boolean,
      default: false,
    },
    dense: {
      type: Boolean,
      default: false,
    },
    title: {
      type: String,
      default: i18n.global.t("ChooseMint.title"),
    },
    style: {
      type: String,
      default: "",
    },
    placeholder: {
      type: String,
      default: "",
    },
    showBalances: {
      type: Boolean,
      default: true,
    },
    requireActiveMint: {
      type: Boolean,
      default: true,
    },
    dryRun: {
      type: Boolean,
      default: false,
    },
    excludeMint: {
      type: String,
      default: null,
    },
    // When provided, this will be used instead of activeMintUrl
    modelValue: {
      type: String,
      default: null,
    },
  },
  emits: ["update:modelValue"],
  data: function () {
    return {
      chosenMint: null as MintOption | null,
      showMintSheet: false,
    };
  },
  mounted() {
    this.initializeChosenMint();
  },
  watch: {
    chosenMint: async function () {
      const selectedUrl = this.chosenMint?.url || "";
      if (this.dryRun || this.modelValue !== null) {
        this.$emit("update:modelValue", selectedUrl);
      }
      if (this.modelValue === null && !this.dryRun) {
        // Use the original behavior when not using v-model
        (this.activeMintUrl as unknown as string) = selectedUrl;
      }
    },
    modelValue: {
      handler() {
        this.initializeChosenMint();
      },
      immediate: true,
    },
    activeMintUrl: {
      handler() {
        if (this.modelValue === null) {
          this.initializeChosenMint();
        }
      },
    },
    excludeMint() {
      this.initializeChosenMint();
    },
  },
  computed: {
    ...mapState(useMintsStore, ["activeProofs", "mints", "activeUnit"]),
    ...mapWritableState(useMintsStore, ["activeMintUrl"]),
    selectedMintBalance(): number {
      const unit = this.activeUnit;
      if (!this.chosenMint || !unit) {
        return 0;
      }
      const balance = this.chosenMint.balances?.[unit];
      return typeof balance === "number" ? balance : 0;
    },
  },
  methods: {
    ...mapActions(useMintsStore, ["activateMintUrl"]),
    formatCurrency(value: number, currency: string) {
      return useUiStore().formatCurrency(value, currency);
    },
    applyChosenMint(option: MintOption | null) {
      if (!option) {
        this.chosenMint = null;
        return;
      }
      this.chosenMint = {
        ...option,
        balances: { ...option.balances },
        units: [...option.units],
      };
    },
    initializeChosenMint() {
      const options = this.chooseMintOptions();
      const fallbackUrl =
        this.chosenMint?.url && this.chosenMint.url !== this.excludeMint
          ? this.chosenMint.url
          : "";
      let targetUrl =
        this.modelValue !== null
          ? this.modelValue
          : fallbackUrl || this.activeMintUrl;
      if (targetUrl && targetUrl === this.excludeMint) {
        targetUrl = "";
      }
      if (targetUrl) {
        const matched = options.find(
          (option: MintOption) => option.url === targetUrl
        );
        if (matched) {
          this.applyChosenMint(matched);
          return;
        }
      }
      if (options.length) {
        this.applyChosenMint(options[0]);
      } else {
        this.applyChosenMint(null);
      }
    },
    selectMint(mint: MintOption) {
      if (this.excludeMint && mint.url === this.excludeMint) {
        return;
      }
      this.applyChosenMint(mint);
      this.showMintSheet = false;
    },
    chooseMintOptions: function () {
      const options: MintOption[] = [];
      const availableMints = Array.isArray(this.mints)
        ? (this.mints as StoredMint[])
        : [];
      for (const mintData of availableMints) {
        const all_units = mintData.keysets.map((r) => r.unit);
        const units = [...new Set(all_units)];
        const mint = new MintClass(mintData);
        if (!this.excludeMint || mint.mint.url !== this.excludeMint) {
          options.push({
            nickname: mint.mint.nickname || mint.mint.info?.name,
            url: mint.mint.url,
            shorturl: getShortUrl(mintData.url),
            iconUrl: mint.mint.info?.icon_url,
            balances: mint.allBalances,
            errored: mint.mint.errored,
            units: units,
          });
        }
      }
      return options;
    },
  },
});
</script>

<style lang="scss" scoped>
.mint-selector-btn {
  width: 100%;
  padding: 16px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 12px;
  cursor: pointer;
  transition: all 0.2s ease;

  &:hover {
    background: rgba(255, 255, 255, 0.08);
    border-color: rgba(255, 255, 255, 0.2);
  }

  &.mint-selector-dense {
    padding: 12px;
  }
}

.mint-icon-avatar {
  flex-shrink: 0;
}

.mint-info-section {
  min-width: 0;
}

.mint-name-label {
  font-size: 16px;
  font-weight: 500;
  line-height: 1.3;
  color: white;
}

.mint-balance-label {
  font-size: 14px;
  line-height: 1.3;
  margin-top: 4px;
}

/* Bottom sheet overlay */
.mint-sheet-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(4px);
  z-index: 9999;
  display: flex;
  align-items: flex-end;
}

/* Bottom sheet */
.mint-sheet {
  width: 100%;
  background: rgba(20, 20, 20, 0.98);
  backdrop-filter: blur(20px);
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 20px 20px 0 0;
  max-height: 70vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.mint-sheet-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px 24px 16px 24px;
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}

.mint-sheet-header h3 {
  color: white;
  font-size: 1.1rem;
  font-weight: 600;
  margin: 0;
}

.close-btn {
  color: rgba(255, 255, 255, 0.7) !important;
}

.mint-options {
  flex: 1;
  overflow-y: auto;
  padding-bottom: env(safe-area-inset-bottom);
}

.mint-option {
  padding: 16px 24px;
  cursor: pointer;
  transition: all 0.2s ease;
  border-bottom: 1px solid rgba(255, 255, 255, 0.05);

  &:hover {
    background: rgba(255, 255, 255, 0.08);
  }

  &.active {
    background: rgba(var(--q-primary-rgb), 0.2);
  }

  &:last-child {
    border-bottom: none;
  }
}

.mint-option-name {
  font-size: 16px;
  font-weight: 500;
  line-height: 1.3;
  color: white;
}

.mint-option-balance {
  font-size: 14px;
  line-height: 1.3;
  margin-top: 4px;
}

/* Vue transition for overlay fade and sheet slide */
.mint-overlay-enter-active,
.mint-overlay-leave-active {
  transition: opacity 0.3s ease;
}
.mint-overlay-enter-from,
.mint-overlay-leave-to {
  opacity: 0;
}
/* Animate the sheet together with the overlay */
.mint-overlay-enter-active .mint-sheet,
.mint-overlay-leave-active .mint-sheet {
  transition: transform 0.3s ease;
}
.mint-overlay-enter-from .mint-sheet,
.mint-overlay-leave-to .mint-sheet {
  transform: translateY(100%);
}
</style>


================================================
FILE: src/components/CreateInvoiceDialog.vue
================================================
<template>
  <q-dialog
    v-model="showCreateInvoiceDialog"
    maximized
    backdrop-filter="blur(2px) brightness(60%)"
    transition-show="fade"
    transition-hide="fade"
    no-backdrop-dismiss
    @keydown.esc="showCreateInvoiceDialog = false"
  >
    <q-card class="q-pa-none q-pt-none qcard">
      <!-- enter invoice amount (full-screen) -->
      <div
        class="column fit send-fullscreen"
        :class="$q.dark.isActive ? 'bg-dark' : 'bg-white'"
      >
        <!-- Header -->
        <div class="row items-center q-pa-md" style="position: relative">
          <q-btn
            v-close-popup
            flat
            round
            icon="close"
            color="grey"
            class="floating-close-btn"
          />
          <div class="col text-center fixed-title-height">
            <q-item-label
              class="dialog-header q-mt-sm"
              :class="$q.dark.isActive ? 'text-white' : 'text-black'"
            >
              {{ $t("InvoiceDetailDialog.title") }}
            </q-item-label>
          </div>
          <div
            class="row items-center q-gutter-sm"
            style="position: absolute; right: 16px"
          >
            <q-btn
              flat
              dense
              size="lg"
              color="primary"
              @click="toggleUnit()"
              :label="activeUnitLabel"
            />
          </div>
        </div>

        <!-- Mint selection -->
        <div class="row justify-center">
          <div
            class="col-12 col-sm-11 col-md-8 q-px-lg q-mb-sm"
            style="max-width: 600px"
          >
            <ChooseMint />
          </div>
        </div>

        <!-- Amount display -->
        <div class="col column items-center justify-center q-px-lg amount-area">
          <AmountInputComponent
            v-model="invoiceData.amount"
            :enabled="true"
            @enter="requestMintButton"
            @fiat-mode-changed="fiatKeyboardMode = $event"
          />
        </div>

        <!-- Numeric keypad -->
        <div class="bottom-panel">
          <div class="keypad-wrapper">
            <NumericKeyboard
              :force-visible="true"
              :hide-close="true"
              :hide-enter="true"
              :hide-comma="
                (activeUnit === 'sat' || activeUnit === 'msat') &&
                !fiatKeyboardMode
              "
              :model-value="String(invoiceData.amount ?? 0)"
              @update:modelValue="(val: string | number) => (invoiceData.amount = Number(val))"
              @done="requestMintButton"
            />
          </div>
          <!-- Create action below keyboard -->
          <div class="row justify-center q-pb-lg q-pt-sm">
            <div
              class="col-12 col-sm-11 col-md-8 q-px-md"
              style="max-width: 600px"
            >
              <q-btn
                class="full-width"
                unelevated
                size="lg"
                :disable="
                  invoiceData.amount == null || Number(invoiceData.amount) <= 0
                "
                @click="requestMintButton"
                color="primary"
                rounded
                type="submit"
                :loading="globalMutexLock || createInvoiceButtonBlocked"
              >
                {{ $t("InvoiceDetailDialog.actions.create.label") }}
                <template v-slot:loading>
                  <q-spinner />
                </template>
              </q-btn>
            </div>
          </div>
        </div>
      </div>
    </q-card>
  </q-dialog>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { mapActions, mapState, mapWritableState } from "pinia";
import ChooseMint from "components/ChooseMint.vue";
import NumericKeyboard from "components/NumericKeyboard.vue";
import AmountInputComponent from "components/AmountInputComponent.vue";
import { useWalletStore } from "src/stores/wallet";
import { useUiStore } from "src/stores/ui";
import { useMintsStore } from "src/stores/mints";
import { useSettingsStore } from "src/stores/settings";
import { usePriceStore } from "src/stores/price";
declare const windowMixin: any;

export default defineComponent({
  name: "CreateInvoiceDialog",
  mixins: [windowMixin],
  components: {
    ChooseMint,
    NumericKeyboard,
    AmountInputComponent,
  },
  props: {},
  data: function () {
    return {
      createInvoiceButtonBlocked: false,
      fiatKeyboardMode: false as boolean,
    };
  },
  computed: {
    ...mapWritableState(useUiStore, [
      "showNumericKeyboard",
      "showInvoiceDetails",
    ]),
    ...mapWritableState(useUiStore, ["tickerShort", "globalMutexLock"]),
    ...mapWritableState(useUiStore, ["showCreateInvoiceDialog"]),
    ...mapWritableState(useWalletStore, ["invoiceData"]),
    ...mapState(useMintsStore, [
      "activeUnit",
      "activeUnitLabel",
      "activeUnitCurrencyMultiplyer",
      "activeMintUrl",
    ]),
    ...mapState(useSettingsStore, [
      "bitcoinPriceCurrency",
      "useNumericKeyboard",
    ]),
    ...mapState(usePriceStore, ["bitcoinPrice", "currentCurrencyPrice"]),
  },
  watch: {
    showCreateInvoiceDialog: function (val) {
      if (val) {
        this.$nextTick(() => {
          this.showNumericKeyboard = true;
        });
      } else {
      }
    },
  },
  methods: {
    ...mapActions(useWalletStore, [
      "requestMint",
      "mintOnPaid",
      "activeWallet",
    ]),
    ...mapActions(useMintsStore, ["toggleUnit"]),
    requestMintButton: async function () {
      if (!this.invoiceData.amount) {
        return;
      }
      try {
        this.showNumericKeyboard = false;
        const amount = Math.floor(
          this.invoiceData.amount * this.activeUnitCurrencyMultiplyer
        );
        this.createInvoiceButtonBlocked = true;
        const wallet = await this.activeWallet(true);
        const mintQuote = await this.requestMint(amount, wallet);
        // Switch to QR display dialog
        this.showCreateInvoiceDialog = false;
        this.showInvoiceDetails = true;
        await this.mintOnPaid(mintQuote.quote);
      } catch (e) {
        console.log("#### requestMintButton", e);
      } finally {
        this.createInvoiceButtonBlocked = false;
      }
    },
  },
});
</script>
<style scoped>
.send-fullscreen {
  height: 100vh;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.floating-close-btn {
  position: absolute;
  left: 16px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 1;
}
.fixed-title-height {
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
}
.amount-area {
  flex: 1;
}
.amount-container {
  position: relative;
  display: inline-block;
  max-width: 90vw;
  overflow: hidden;
}
.amount-display {
  font-size: clamp(56px, 11vw, 80px);
  line-height: 1.1;
  overflow-wrap: break-word;
  word-break: break-all;
  max-width: 100%;
}
.fiat-display {
  font-size: 14px;
}
.bottom-panel {
  margin-top: auto;
  background: var(--q-color-grey-1);
  padding-bottom: env(safe-area-inset-bottom, 0px);
}
.keypad-wrapper {
  width: 100%;
  display: flex;
  justify-content: center;
}
</style>


================================================
FILE: src/components/DisplayTokenComponent.vue
================================================
<template>
  <div
    :class="$q.dark.isActive ? 'bg-dark' : 'bg-white'"
    class="display-token-fullscreen"
  >
    <!-- Header -->
    <div class="row items-center q-pa-md" style="position: relative">
      <q-btn
        v-close-popup
        flat
        round
        icon="close"
        color="grey"
        class="floating-close-btn"
        @click="closeCardScanner"
      />
      <div class="col text-center fixed-title-height">
        <div v-show="!showExpandedButtons">
          <q-item-label
            overline
            class="q-mt-sm text-white"
            style="font-size: 1rem"
          >
            {{
              sendData.historyToken.amount && sendData.historyToken.amount < 0
                ? sendData.historyToken.status === "paid"
                  ? "Sent"
                  : "Pending"
                : "Received"
            }}
            Ecash</q-item-label
          >
        </div>
      </div>
      <!-- Floating actions (expandable) -->
      <div class="floating-actions">
        <div class="row no-wrap items-center">
          <div
            v-if="showExpandedButtons"
            class="row no-wrap items-center q-gutter-xs justify-end q-mr-sm"
          >
            <q-btn
              class="q-mx-xs q-pb-sm"
              size="md"
              flat
              dense
              @click="copyUsingMixin(encodeToPeanut(sendData.tokensBase64))"
            >
              <NutIcon :size="16" />

              <q-tooltip>{{
                $t("SendTokenDialog.actions.copy_emoji.tooltip_text")
              }}</q-tooltip>
            </q-btn>

            <q-btn
              v-if="webShareSupported"
              class="q-mx-xs q-pb-sm"
              size="md"
              dense
              flat
              @click="shareToken"
            >
              <ShareIcon :size="16" />
              <q-tooltip>{{
                $t("SendTokenDialog.actions.share.tooltip_text")
              }}</q-tooltip>
            </q-btn>

            <q-btn
              class="q-mx-none"
              size="md"
              dense
              icon="link"
              flat
              @click="
                copyUsingMixin(baseURL + '#token=' + sendData.tokensBase64)
              "
              ><q-tooltip>{{
                $t("SendTokenDialog.actions.copy_link.tooltip_text")
              }}</q-tooltip></q-btn
            >
            <q-btn
              unelevated
              dense
              size="md"
              class="q-mx-xs"
              v-if="
                hasCamera &&
                !sendData.paymentRequest &&
                sendData.historyAmount < 0
              "
              @click="showCamera"
            >
              <ScanIcon :size="16" />
            </q-btn>
            <q-btn
              unelevated
              dense
              v-if="
                ndefSupported &&
                !sendData.paymentRequest &&
                sendData.historyAmount < 0
              "
              :disabled="scanningCard"
              :loading="scanningCard"
              class="q-mx-xs"
              size="md"
              @click="writeTokensToCard"
              flat
            >
              <NfcIcon :size="16" />
              <q-tooltip>{{
                ndefSupported
                  ? $t(
                      "SendTokenDialog.actions.write_tokens_to_card.tooltips.ndef_supported_text"
                    )
                  : $t(
                      "SendTokenDialog.actions.write_tokens_to_card.tooltips.ndef_unsupported_text"
                    )
              }}</q-tooltip>
              <template v-slot:loading>
                <q-spinner @click="closeCardScanner" />
              </template>
            </q-btn>
            <q-btn
              class="q-mx-none"
              dense
              color="negative"
              icon="delete"
              size="sm"
              @click="
                showDeleteDialog = true;
                closeCardScanner();
              "
              flat
            >
              <q-tooltip>{{
                $t("SendTokenDialog.actions.delete.tooltip_text")
              }}</q-tooltip>
            </q-btn>
          </div>
          <q-btn
            class="q-mx-none"
            size="md"
            flat
            dense
            @click="toggleExpandButtons"
          >
            <q-icon
              name="more_horiz"
              :color="showExpandedButtons ? 'primary' : 'grey'"
            />
          </q-btn>
        </div>
      </div>
    </div>
    <!-- Content area -->
    <div class="content-area">
      <q-card-section class="q-pa-none">
        <div v-if="qrCodeFragment" class="row justify-center q-mb-md">
          <div class="col-12 col-sm-11 col-md-8 q-px-md">
            <q-responsive :ratio="1" class="q-mx-none">
              <vue-qrcode
                :value="qrCodeFragment"
                :options="{ width: 600, height: 600 }"
                class="rounded-borders"
                style="width: 100%; height: 100%"
                @click="copyTokens"
              >
              </vue-qrcode>
            </q-responsive>
            <div style="height: 2px">
              <q-linear-progress
                v-if="runnerActive"
                indeterminate
                color="primary"
              />
            </div>
          </div>
        </div>
        <div class="row justify-center q-pb-xs q-ba-none">
          <div
            class="col-12 col-sm-11 col-md-8 q-px-md"
            style="max-width: 600px; position: relative"
          >
            <div class="row justify-center items-center no-wrap">
              <div class="q-gutter-sm">
                <q-btn
                  v-if="showAnimatedQR"
                  flat
                  style="font-size: 10px"
                  color="grey"
                  class="q-ma-none"
                  @click="changeSpeed"
                >
                  <q-icon name="speed" style="margin-right: 8px"></q-icon>
                  Speed: {{ fragmentSpeedLabel }}
                </q-btn>
                <q-btn
                  v-if="showAnimatedQR"
                  flat
                  style="font-size: 10px"
                  class="q-ma-none"
                  color="grey"
                  @click="changeSize"
                >
                  <q-icon name="zoom_in" style="margin-right: 8px"></q-icon>
                  Size: {{ fragmentLengthLabel }}
                </q-btn>
              </div>
            </div>
          </div>
        </div>
        <q-card-section class="q-pa-sm">
          <div class="row justify-center q-pt-lg">
            <TokenInformation
              :encodedToken="sendData.tokensBase64"
              :payment-request-id="sendData.historyToken?.paymentRequestId"
            />
          </div>
          <div
            v-if="sendData.paymentRequest"
            class="row justify-center q-pt-md"
          >
            <div
              class="col-12 col-sm-11 col-md-8 q-px-md"
              style="max-width: 600px"
            >
              <PaymentRequestInfo :request="sendData.paymentRequest" />
            </div>
          </div>
          <div
            v-if="sendData.historyToken?.meltQuote"
            class="row justify-center q-pt-md"
          >
            <div
              class="col-12 col-sm-11 col-md-8 q-px-md"
              style="max-width: 600px"
            >
              <MeltQuoteInformation
                :melt-quote="sendData.historyToken.meltQuote"
                :invoice="sendData.historyToken"
                :mint-url="sendData.historyToken.mint"
                :history-paid-at="sendData.historyToken.paidDate"
                :show-amount="false"
              />
            </div>
          </div>
          <div
            v-if="
              sendData.paymentRequest &&
              sendData.historyToken &&
              sendData.historyToken.amount < 0 &&
              sendData.historyToken.status === 'pending'
            "
            class="row justify-center q-pt-sm"
          >
            <SendPaymentRequest />
          </div>
        </q-card-section>
      </q-card-section>
    </div>
    <!-- Fixed bottom panel with copy button -->
    <div class="bottom-panel">
      <div class="row justify-center q-pb-md q-pt-sm">
        <div class="col-12 col-sm-11 col-md-8 q-px-md" style="max-width: 600px">
          <q-btn
            class="full-width"
            unelevated
            size="lg"
            color="primary"
            rounded
            @click="copyTokens"
          >
            {{ copyButtonLabel }}
          </q-btn>
        </div>
      </div>
    </div>
  </div>
  <!-- popup dialog to confirm deletion -->
  <q-dialog v-model="showDeleteDialog">
    <q-card class="q-pa-lg q-pt-md qcard">
      <q-card-section class="q-pa-none">
        <div class="row items-center no-wrap q-mb-sm">
          <div class="col-12">
            <span class="text-h6">Delete Ecash</span>
          </div>
        </div>
        <div class="row items-center no-wrap q-my-sm q-py-none">
          <div class="col-12">
            <q-item-label>
              Are you sure you want to delete this transaction from your
              history?
            </q-item-label>
            <q-item-label class="q-pt-md text-weight-bold">
              Warning: This action cannot be undone and there is no way to
              recover the token.
            </q-item-label>
          </div>
        </div>
        <div class="row q-mt-lg">
          <q-btn
            @click="deleteThisToken"
            color="negative"
            rounded
            class="q-mr-sm"
            >Delete</q-btn
          >
          <q-btn v-close-popup rounded flat color="grey" class="q-ml-auto"
            >Cancel</q-btn
          >
        </div>
      </q-card-section>
    </q-card>
  </q-dialog>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { Buffer } from "buffer";
import { UR, UREncoder } from "@gandlaf21/bc-ur";
import { mapActions, mapState, mapWritableState } from "pinia";
import { useSendTokensStore } from "src/stores/sendTokensStore";
import { useWorkersStore } from "src/stores/workers";
import { useUiStore } from "src/stores/ui";
import { useCameraStore } from "src/stores/camera";
import { useSettingsStore } from "src/stores/settings";
import { useTokensStore } from "src/stores/tokens";
import { useMintsStore } from "src/stores/mints";
import TokenInformation from "components/TokenInformation.vue";
import MeltQuoteInformation from "components/MeltQuoteInformation.vue";
import SendPaymentRequest from "./SendPaymentRequest.vue";
import PaymentRequestInfo from "./PaymentRequestInfo.vue";
import {
  getDecodedToken,
  getEncodedTokenBinary,
  getEncodedToken,
  getEncodedTokenV4,
} from "@cashu/cashu-ts";
import token from "src/js/token";
import { notifyError, notifySuccess } from "src/js/notify";
import { copyToClipboard } from "quasar";
import {
  Scan as ScanIcon,
  Nfc as NfcIcon,
  Share as ShareIcon,
  Nut as NutIcon,
} from "lucide-vue-next";

declare const windowMixin: any;

export default defineComponent({
  name: "DisplayTokenComponent",
  mixins: [windowMixin],
  components: {
    TokenInformation,
    MeltQuoteInformation,
    SendPaymentRequest,
    PaymentRequestInfo,
    ScanIcon,
    NfcIcon,
    ShareIcon,
    NutIcon,
  },
  data: function () {
    return {
      baseURL: location.protocol + "//" + location.host + location.pathname,
      showAnimatedQR: false,
      qrCodeFragment: "",
      qrInterval: null as any,
      encoder: null as any,
      // animated QR params
      currentFragmentLength: 150,
      fragmentLengthMedium: 100,
      fragmentLengthShort: 50,
      fragmentLengthLong: 150,
      fragmentLengthLabel: "L",
      currentFragmentInterval: 150,
      fragmentIntervalMedium: 250,
      fragmentIntervalFast: 150,
      framentInervalSlow: 500,
      fragmentSpeedLabel: "F",
      isV4Token: false,
      scanningCard: false,
      showExpandedButtons: false,
      showDeleteDialog: false,
      copyButtonCopied: false,
      copyButtonTimeout: null as any,
    };
  },
  computed: {
    ...mapWritableState(useSendTokensStore, ["showSendTokens", "sendData"]),
    ...mapWritableState(useCameraStore, ["hasCamera"]),
    ...mapState(useUiStore, ["ndefSupported", "webShareSupported"]),
    ...mapState(useWorkersStore, ["tokenWorkerRunning"]),
    ...mapState(useSettingsStore, ["nfcEncoding"]),
    // display helpers
    sumProofs: function () {
      const proofs = token.getProofs(token.decode(this.sendData.tokensBase64));
      return proofs.flat().reduce((sum, el) => (sum += el.amount), 0);
    },
    displayUnit: function () {
      const display = this.formatCurrency(this.sumProofs, this.tokenUnit);
      return display;
    },
    tokenUnit: function () {
      const unit = token.getUnit(token.decode(this.sendData.tokensBase64));
      return unit;
    },
    paidFees: function () {
      return this.sumProofs - Math.abs(this.sendData.historyAmount);
    },
    runnerActive: function () {
      return this.tokenWorkerRunning;
    },
    copyButtonLabel: function () {
      if (this.copyButtonCopied) {
        return "Copied";
      }
      return this.$t("SendTokenDialog.actions.copy_tokens.label");
    },
  },
  watch: {
    "sendData.tokensBase64": function (val: string) {
      this.showAnimatedQR = false;
      if (!val?.length) {
        return;
      }
      const tokenObj = token.decode(val);
      const proofs = tokenObj.proofs || [];
      if (!proofs.length) {
        return;
      } else if (proofs.length <= 2) {
        this.qrCodeFragment = val;
      } else {
        this.showAnimatedQR = true;
        this.qrCodeFragment = "";
        this.startQrCodeLoop();
      }
      this.isV4Token = val.startsWith("cashuB");
    },
  },
  methods: {
    ...mapActions(useWorkersStore, ["clearAllWorkers"]),
    ...mapActions(useTokensStore, ["deleteToken"]),
    ...mapActions(useCameraStore, ["showCamera"]),
    copyUsingMixin(text: string) {
      (this as any).copyText(text);
    },
    toggleExpandButtons() {
      this.showExpandedButtons = !this.showExpandedButtons;
    },
    initQr: function () {
      const val = this.sendData?.tokensBase64;
      this.showAnimatedQR = false;
      if (!val?.length) {
        this.qrCodeFragment = "";
        return;
      }
      const tokenObj = token.decode(val);
      const proofs = tokenObj.proofs || [];
      if (!proofs.length) {
        this.qrCodeFragment = "";
        return;
      } else if (proofs.length <= 2) {
        this.qrCodeFragment = val;
      } else {
        this.showAnimatedQR = true;
        this.qrCodeFragment = "";
        this.startQrCodeLoop();
      }
      this.isV4Token = val.startsWith("cashuB");
    },
    startQrCodeLoop: async function () {
      if (this.sendData.tokensBase64.length == 0) {
        return;
      }
      const messageBuffer = Buffer.from(this.sendData.tokensBase64);
      const ur = UR.fromBuffer(messageBuffer);
      const firstSeqNum = 0;
      this.encoder = new UREncoder(ur, this.currentFragmentLength, firstSeqNum);
      clearInterval(this.qrInterval);
      this.qrInterval = setInterval(() => {
        this.qrCodeFragment = this.encoder.nextPart();
      }, this.currentFragmentInterval);
    },
    updateQrCode: function () {
      this.qrCodeFragment = this.encoder.nextPart();
    },
    changeSpeed: function () {
      if (this.currentFragmentInterval == this.fragmentIntervalMedium) {
        this.currentFragmentInterval = this.framentInervalSlow;
        this.fragmentSpeedLabel = "S";
      } else if (this.currentFragmentInterval == this.framentInervalSlow) {
        this.currentFragmentInterval = this.fragmentIntervalFast;
        this.fragmentSpeedLabel = "F";
      } else {
        this.currentFragmentInterval = this.fragmentIntervalMedium;
        this.fragmentSpeedLabel = "M";
      }
      this.startQrCodeLoop();
    },
    changeSize: function () {
      if (this.currentFragmentLength == this.fragmentLengthMedium) {
        this.currentFragmentLength = this.fragmentLengthShort;
        this.fragmentLengthLabel = "S";
      } else if (this.currentFragmentLength == this.fragmentLengthShort) {
        this.currentFragmentLength = this.fragmentLengthLong;
        this.fragmentLengthLabel = "L";
      } else {
        this.currentFragmentLength = this.fragmentLengthMedium;
        this.fragmentLengthLabel = "M";
      }
      this.startQrCodeLoop();
    },
    toggleTokenEncoding: function () {
      const decodedToken = token.decode(this.sendData.tokensBase64);
      if (this.sendData.tokensBase64.startsWith("cashuA")) {
        try {
          this.sendData.tokensBase64 = getEncodedTokenV4(decodedToken);
        } catch {
          this.sendData.tokensBase64 = getEncodedToken(decodedToken, {
            version: 3,
          });
        }
      } else {
        this.sendData.tokensBase64 = getEncodedToken(decodedToken, {
          version: 3,
        });
      }
    },
    encodeToPeanut: function (tokenStr: string) {
      return (
        "🥜" +
        Array.from(tokenStr)
          .map((char) => {
            const byteValue = char.charCodeAt(0);
            if (byteValue >= 0 && byteValue <= 15) {
              return String.fromCodePoint(0xfe00 + byteValue);
            }
            if (byteValue >= 16 && byteValue <= 255) {
              return String.fromCodePoint(0xe0100 + (byteValue - 16));
            }
            return "";
          })
          .join("")
      );
    },
    shareToken: async function () {
      if (!this.webShareSupported) {
        return;
      }
      const shareData = {
        text: `cashu:${this.sendData.tokensBase64}`,
      };
      try {
        await navigator.share(shareData);
      } catch (error: any) {
        if (error?.name !== "AbortError") {
          console.error("Error sharing token:", error);
        }
      }
    },
    writeTokensToCard: function () {
      if (!this.scanningCard) {
        try {
          // @ts-ignore
          this.ndef = new NDEFReader();
          // @ts-ignore
          this.controller = new AbortController();
          const signal = this.controller.signal;
          this.ndef
            .scan({ signal })
            .then(() => {
              this.ndef.onreadingerror = (error: any) => {
                console.error(`NFC read failed: ${error}`);
                notifyError(`${error?.message || error}`, "NFC read failed");
                this.controller.abort();
                this.scanningCard = false;
              };

              this.ndef.onreading = ({ message, serialNumber }: any) => {
                this.controller.abort();
                this.scanningCard = false;
                try {
                  let records: any[] = [];
                  switch (this.nfcEncoding) {
                    case "text":
                      records = [
                        {
                          recordType: "text",
                          data: `${this.sendData.tokensBase64}`,
                          lang: "en",
                        },
                      ];
                      break;
                    case "weburl":
                      records = [
                        {
                          recordType: "url",
                          data: `${window.location}#token=${this.sendData.tokensBase64}`,
                        },
                      ];
                      break;
                    case "binary": {
                      const decoded = getDecodedToken(
                        this.sendData.tokensBase64,
                        useMintsStore().allMintKeysets
                      );
                      const data = getEncodedTokenBinary(decoded);
                      records = [
                        {
                          recordType: "mime",
                          mediaType: "application/octet-stream",
                          data: data,
                        },
                      ];
                      break;
                    }
                    default:
                     
Download .txt
gitextract_4w51wde5/

├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github/
│   └── workflows/
│       ├── build.yaml
│       ├── docker.yaml
│       ├── format.yaml
│       ├── lint.yml
│       └── test.yml
├── .gitignore
├── .npmrc
├── .postcssrc.js
├── .prettierignore
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── AGENTS.md
├── Dockerfile
├── LICENSE.md
├── README.md
├── android/
│   ├── .gitignore
│   ├── app/
│   │   ├── .gitignore
│   │   ├── build.gradle
│   │   ├── capacitor.build.gradle
│   │   ├── proguard-rules.pro
│   │   └── src/
│   │       ├── androidTest/
│   │       │   └── java/
│   │       │       └── com/
│   │       │           └── getcapacitor/
│   │       │               └── myapp/
│   │       │                   └── ExampleInstrumentedTest.java
│   │       ├── main/
│   │       │   ├── AndroidManifest.xml
│   │       │   ├── java/
│   │       │   │   └── me/
│   │       │   │       └── cashu/
│   │       │   │           └── wallet/
│   │       │   │               └── MainActivity.java
│   │       │   └── res/
│   │       │       ├── drawable/
│   │       │       │   └── ic_launcher_background.xml
│   │       │       ├── drawable-v24/
│   │       │       │   └── ic_launcher_foreground.xml
│   │       │       ├── layout/
│   │       │       │   └── activity_main.xml
│   │       │       ├── mipmap-anydpi-v26/
│   │       │       │   ├── ic_launcher.xml
│   │       │       │   └── ic_launcher_round.xml
│   │       │       ├── values/
│   │       │       │   ├── ic_launcher_background.xml
│   │       │       │   ├── strings.xml
│   │       │       │   └── styles.xml
│   │       │       └── xml/
│   │       │           └── file_paths.xml
│   │       └── test/
│   │           └── java/
│   │               └── com/
│   │                   └── getcapacitor/
│   │                       └── myapp/
│   │                           └── ExampleUnitTest.java
│   ├── build.gradle
│   ├── capacitor.settings.gradle
│   ├── gradle/
│   │   └── wrapper/
│   │       ├── gradle-wrapper.jar
│   │       └── gradle-wrapper.properties
│   ├── gradle.properties
│   ├── gradlew
│   ├── gradlew.bat
│   ├── settings.gradle
│   └── variables.gradle
├── capacitor.config.ts
├── docker-compose.yaml
├── extension/
│   ├── embedder.html
│   ├── manifest.json
│   └── style.css
├── index.html
├── ios/
│   ├── .gitignore
│   └── App/
│       ├── App/
│       │   ├── AppDelegate.swift
│       │   ├── Assets.xcassets/
│       │   │   ├── AppIcon.appiconset/
│       │   │   │   └── Contents.json
│       │   │   ├── Contents.json
│       │   │   └── Splash.imageset/
│       │   │       └── Contents.json
│       │   ├── Base.lproj/
│       │   │   ├── LaunchScreen.storyboard
│       │   │   └── Main.storyboard
│       │   └── Info.plist
│       ├── App.xcodeproj/
│       │   └── project.pbxproj
│       ├── App.xcworkspace/
│       │   ├── contents.xcworkspacedata
│       │   └── xcshareddata/
│       │       └── IDEWorkspaceChecks.plist
│       └── Podfile
├── jsconfig.json
├── package.json
├── quasar.config.js
├── quasar.extensions.json
├── scripts/
│   └── check-i18n.js
├── src/
│   ├── App.vue
│   ├── boot/
│   │   ├── .gitkeep
│   │   ├── axios.js
│   │   ├── base.js
│   │   ├── cashu.js
│   │   ├── global-components.js
│   │   └── i18n.js
│   ├── components/
│   │   ├── ActivityOrb.vue
│   │   ├── AddMintDialog.vue
│   │   ├── AmountInputComponent.vue
│   │   ├── AndroidPWAPrompt.vue
│   │   ├── AnimatedNumber.vue
│   │   ├── BalanceView.vue
│   │   ├── ChooseMint.vue
│   │   ├── CreateInvoiceDialog.vue
│   │   ├── DisplayTokenComponent.vue
│   │   ├── EditMintDialog.vue
│   │   ├── EssentialLink.vue
│   │   ├── FullscreenHeader.vue
│   │   ├── HistoryTable.vue
│   │   ├── InvoiceDetailDialog.vue
│   │   ├── MainHeader.vue
│   │   ├── MeltQuoteInformation.vue
│   │   ├── MintAuditInfo.vue
│   │   ├── MintAuditSwapsBarChart.vue
│   │   ├── MintAuditWarningBox.vue
│   │   ├── MintDiscovery.vue
│   │   ├── MintInfoContainer.vue
│   │   ├── MintMotdMessage.vue
│   │   ├── MintQuoteInformation.vue
│   │   ├── MintRatingsComponent.vue
│   │   ├── MintSettings.vue
│   │   ├── MultinutPaymentDialog.vue
│   │   ├── NWCDialog.vue
│   │   ├── NoMintWarnBanner.vue
│   │   ├── NostrMintRestore.vue
│   │   ├── NumericKeyboard.vue
│   │   ├── P2PKDialog.vue
│   │   ├── ParseInputComponent.vue
│   │   ├── PayInvoiceDialog.vue
│   │   ├── PaymentRequestDialog.vue
│   │   ├── PaymentRequestInfo.vue
│   │   ├── PaymentRequestPayments.vue
│   │   ├── QrcodeReader.vue
│   │   ├── ReceiveDialog.vue
│   │   ├── ReceiveEcashDrawer.vue
│   │   ├── ReceiveTokenDialog.vue
│   │   ├── RemoveMintDialog.vue
│   │   ├── RestoreView.vue
│   │   ├── SendDialog.vue
│   │   ├── SendPaymentRequest.vue
│   │   ├── SendTokenDialog.vue
│   │   ├── SettingsView.vue
│   │   ├── SwapIncomingTokenToKnownMint.vue
│   │   ├── ToggleUnit.vue
│   │   ├── TokenInformation.vue
│   │   ├── TokenStringRender.vue
│   │   ├── ToolTipInfo.vue
│   │   ├── WelcomeDialog.vue
│   │   └── iOSPWAPrompt.vue
│   ├── css/
│   │   ├── app.scss
│   │   ├── base.scss
│   │   ├── mintlist.css
│   │   └── quasar.variables.scss
│   ├── i18n/
│   │   ├── ar-SA/
│   │   │   └── index.ts
│   │   ├── cs-CZ/
│   │   │   └── index.ts
│   │   ├── de-DE/
│   │   │   └── index.ts
│   │   ├── el-GR/
│   │   │   └── index.ts
│   │   ├── en-US/
│   │   │   └── index.ts
│   │   ├── es-ES/
│   │   │   └── index.ts
│   │   ├── fr-FR/
│   │   │   └── index.ts
│   │   ├── index.ts
│   │   ├── it-IT/
│   │   │   └── index.ts
│   │   ├── ja-JP/
│   │   │   └── index.ts
│   │   ├── pt-BR/
│   │   │   └── index.ts
│   │   ├── sv-SE/
│   │   │   └── index.ts
│   │   ├── th-TH/
│   │   │   └── index.ts
│   │   ├── tr-TR/
│   │   │   └── index.ts
│   │   └── zh-CN/
│   │       └── index.ts
│   ├── icons.js
│   ├── js/
│   │   ├── __tests__/
│   │   │   ├── legacy-qr.test.js
│   │   │   └── token.test.js
│   │   ├── base64.js
│   │   ├── dhke.js
│   │   ├── eventBus.js
│   │   ├── legacy-qr.js
│   │   ├── notify.ts
│   │   ├── string-utils.js
│   │   ├── token.ts
│   │   ├── utils.js
│   │   └── wallet-helpers.js
│   ├── layouts/
│   │   ├── BlankLayout.vue
│   │   ├── FullscreenLayout.vue
│   │   └── MainLayout.vue
│   ├── main.js
│   ├── pages/
│   │   ├── AlreadyRunning.vue
│   │   ├── CreateMintReviewPage.vue
│   │   ├── ErrorNotFound.vue
│   │   ├── MintDetailsPage.vue
│   │   ├── MintDiscoveryPage.vue
│   │   ├── MintRatingsPage.vue
│   │   ├── Restore.vue
│   │   ├── Settings.vue
│   │   ├── TermsPage.vue
│   │   ├── WalletPage.vue
│   │   ├── WelcomePage.vue
│   │   └── welcome/
│   │       ├── WelcomeMintSetup.vue
│   │       ├── WelcomeRecoverSeed.vue
│   │       ├── WelcomeRestoreEcash.vue
│   │       ├── WelcomeSlide1.vue
│   │       ├── WelcomeSlide2.vue
│   │       ├── WelcomeSlide3.vue
│   │       ├── WelcomeSlide4.vue
│   │       └── WelcomeSlideChoice.vue
│   ├── router/
│   │   ├── index.js
│   │   └── routes.js
│   └── stores/
│       ├── __tests__/
│       │   └── wallet.test.js
│       ├── camera.ts
│       ├── dexie.ts
│       ├── index.js
│       ├── invoicesWorker.ts
│       ├── migrations.ts
│       ├── mintRecommendations.ts
│       ├── mints.ts
│       ├── nostr.ts
│       ├── nostrMintBackup.ts
│       ├── nostrUser.ts
│       ├── npcv2.ts
│       ├── npubcash.ts
│       ├── nwc.ts
│       ├── p2pk.ts
│       ├── payment-request.ts
│       ├── price.ts
│       ├── proofs.ts
│       ├── receiveTokensStore.ts
│       ├── restore.ts
│       ├── sendTokensStore.ts
│       ├── settings.ts
│       ├── storage.ts
│       ├── store-flag.d.ts
│       ├── swap.ts
│       ├── tokens.ts
│       ├── ui.ts
│       ├── wallet.ts
│       ├── welcome.ts
│       └── workers.ts
├── src-electron/
│   ├── electron-env.d.ts
│   ├── electron-flag.d.ts
│   ├── electron-main.ts
│   ├── electron-preload.ts
│   └── icons/
│       └── icon.icns
├── src-pwa/
│   ├── custom-service-worker.js
│   ├── manifest.json
│   ├── pwa-flag.d.ts
│   └── register-service-worker.js
├── test/
│   └── vitest/
│       ├── __tests__/
│       │   └── bip39seed.test.ts
│       └── setup-file.js
├── tsconfig.json
├── types/
│   └── light-bolt11-decoder/
│       └── index.d.ts
└── vitest.config.js
Download .txt
SYMBOL INDEX (223 symbols across 47 files)

FILE: android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java
  class ExampleInstrumentedTest (line 16) | @RunWith(AndroidJUnit4.class)
    method useAppContext (line 19) | @Test

FILE: android/app/src/main/java/me/cashu/wallet/MainActivity.java
  class MainActivity (line 5) | public class MainActivity extends BridgeActivity {}

FILE: android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java
  class ExampleUnitTest (line 12) | public class ExampleUnitTest {
    method addition_isCorrect (line 14) | @Test

FILE: quasar.config.js
  function resolveGitCommit (line 14) | function resolveGitCommit() {
  method extendViteConf (line 87) | extendViteConf(viteConf) {

FILE: scripts/check-i18n.js
  function flatten (line 12) | function flatten(obj, prefix = "") {
  function loadLocale (line 25) | function loadLocale(file) {
  function main (line 36) | function main() {

FILE: src-electron/electron-env.d.ts
  type ProcessEnv (line 4) | interface ProcessEnv {

FILE: src-electron/electron-flag.d.ts
  type QuasarFeatureFlags (line 7) | interface QuasarFeatureFlags {

FILE: src-electron/electron-main.ts
  function createWindow (line 10) | function createWindow() {

FILE: src-pwa/pwa-flag.d.ts
  type QuasarFeatureFlags (line 7) | interface QuasarFeatureFlags {

FILE: src-pwa/register-service-worker.js
  method ready (line 14) | ready(/* registration */) {
  method registered (line 18) | registered(/* registration */) {
  method cached (line 22) | cached(/* registration */) {
  method updatefound (line 26) | updatefound(/* registration */) {
  method updated (line 30) | updated(/* registration */) {
  method offline (line 34) | offline() {
  method error (line 38) | error(/* err */) {

FILE: src/boot/base.js
  function ensureMetaTag (line 35) | function ensureMetaTag(name, initialContent) {
  function resolveEffectiveTopBackgroundColor (line 48) | function resolveEffectiveTopBackgroundColor() {
  function updateStatusBarMeta (line 73) | function updateStatusBarMeta() {

FILE: src/js/__tests__/token.test.js
  constant VALID_V4_TOKEN (line 4) | const VALID_V4_TOKEN =
  constant VALID_V3_TOKEN (line 6) | const VALID_V3_TOKEN =
  constant VALID_V2_TOKEN (line 8) | const VALID_V2_TOKEN =

FILE: src/js/base64.js
  function unescapeBase64Url (line 1) | function unescapeBase64Url(str) {
  function escapeBase64Url (line 7) | function escapeBase64Url(str) {

FILE: src/js/dhke.js
  function hashToCurve (line 4) | async function hashToCurve(secretMessage) {
  function step1Alice (line 19) | async function step1Alice(secretMessage) {
  function step3Alice (line 30) | function step3Alice(C_, r, A) {

FILE: src/js/eventBus.js
  method on (line 6) | on(event, callback) {
  method off (line 13) | off(event, callback) {
  method emit (line 18) | emit(event, payload) {

FILE: src/js/legacy-qr.js
  constant MERCHANT_PATTERNS (line 3) | const MERCHANT_PATTERNS = [
  function isLegacyRetailQR (line 8) | function isLegacyRetailQR(code) {
  function translateLegacyQRToLightningAddress (line 12) | function translateLegacyQRToLightningAddress(qrCode) {

FILE: src/js/notify.ts
  type StatusMap (line 3) | type StatusMap = { [x: number]: "warning" | "negative" };
  function notifyApiError (line 10) | async function notifyApiError(
  function notifySuccess (line 36) | async function notifySuccess(
  function notifyError (line 56) | async function notifyError(message: string, caption?: string) {
  function notifyWarning (line 73) | async function notifyWarning(
  function notify (line 95) | async function notify(

FILE: src/js/string-utils.js
  function shortenString (line 1) | function shortenString(s, length = 20, lastchars = 5) {

FILE: src/js/token.ts
  function decode (line 15) | function decode(encoded_token: string): TokenMetadata {
  function decodeFull (line 25) | async function decodeFull(encoded_token: string): Promise<Token> {
  function getProofs (line 39) | function getProofs(decoded_token: Token): WalletProof[] {
  function getMint (line 47) | function getMint(decoded_token: Token) {
  function getUnit (line 58) | function getUnit(decoded_token: Token) {
  function getMemo (line 75) | function getMemo(decoded_token: Token) {

FILE: src/js/utils.js
  function splitAmount (line 4) | function splitAmount(value) {
  function bytesToNumber (line 13) | function bytesToNumber(bytes) {
  function bigIntStringify (line 17) | function bigIntStringify(key, value) {
  function hexToNumber (line 21) | function hexToNumber(hex) {
  function currentDateStr (line 28) | function currentDateStr() {

FILE: src/js/wallet-helpers.js
  function getShortUrl (line 1) | function getShortUrl(url) {

FILE: src/stores/__tests__/wallet.test.js
  class WalletMock (line 61) | class WalletMock {
    method constructor (line 62) | constructor(url, options) {
    method selectProofsToSend (line 70) | selectProofsToSend(proofs, amount) {

FILE: src/stores/dexie.ts
  class CashuDexie (line 18) | class CashuDexie extends Dexie {
    method constructor (line 21) | constructor() {

FILE: src/stores/invoicesWorker.ts
  type InvoiceQuote (line 5) | interface InvoiceQuote {
  method startInvoiceCheckerWorker (line 40) | startInvoiceCheckerWorker() {
  method stopInvoiceCheckerWorker (line 48) | stopInvoiceCheckerWorker() {
  method addInvoiceToChecker (line 55) | addInvoiceToChecker(quote: string) {
  method removeInvoiceFromChecker (line 73) | removeInvoiceFromChecker(quote: string) {
  method dueTime (line 79) | dueTime(q: InvoiceQuote) {
  method processQuotes (line 93) | async processQuotes() {
  method checkPendingInvoices (line 121) | async checkPendingInvoices() {

FILE: src/stores/migrations.ts
  type Migration (line 10) | type Migration = {
  method registerMigration (line 23) | registerMigration(migration: Migration) {
  method runMigrations (line 32) | async runMigrations() {
  method migrateStablenutsToCash (line 72) | async migrateStablenutsToCash() {
  method migrateAddPrimalRelayAndEnableBackupAndClearMintRecs (line 98) | async migrateAddPrimalRelayAndEnableBackupAndClearMintRecs() {
  method initMigrations (line 153) | initMigrations() {

FILE: src/stores/mintRecommendations.ts
  type MintReview (line 8) | type MintReview = {
  type MintRecommendation (line 17) | type MintRecommendation = {
  function parseRatingAndComment (line 26) | function parseRatingAndComment(content: string): {
  class MintReviewsDB (line 541) | class MintReviewsDB extends Dexie {
    method constructor (line 545) | constructor() {
  type ReviewRow (line 557) | type ReviewRow = {
  type InfoRow (line 567) | type InfoRow = {
  type HttpInfoRow (line 575) | type HttpInfoRow = {

FILE: src/stores/mints.ts
  type StoredMint (line 21) | type StoredMint = {
  class MintClass (line 35) | class MintClass {
    method constructor (line 37) | constructor(mint: StoredMint) {
    method api (line 40) | get api() {
    method proofs (line 43) | get proofs() {
    method allBalances (line 49) | get allBalances() {
    method keysets (line 58) | get keysets() {
    method units (line 62) | get units() {
    method unitKeysets (line 68) | unitKeysets(unit: string): MintKeyset[] {
    method unitProofs (line 72) | unitProofs(unit: string): WalletProof[] {
    method unitBalance (line 80) | unitBalance(unit: string) {
  type WalletProof (line 87) | type WalletProof = Proof & { reserved: boolean; quote?: string };
  type Balances (line 89) | type Balances = {
  type BlindSignatureAudit (line 93) | type BlindSignatureAudit = {
  method multiMints (line 147) | multiMints({ activeUnit }) {
  method totalUnitBalance (line 171) | totalUnitBalance({ activeUnit }): number {
  method activeBalance (line 184) | activeBalance(): number {
  method activeKeysets (line 189) | activeKeysets({ activeMintUrl, activeUnit }): MintKeyset[] {
  method activeKeys (line 198) | activeKeys({ activeMintUrl, activeUnit }): MintKeys[] {
  method activeInfo (line 207) | activeInfo({ activeMintUrl }): GetInfoResponse {
  method activeUnitLabel (line 213) | activeUnitLabel({ activeUnit }): string {
  method activeUnitCurrencyMultiplyer (line 230) | activeUnitCurrencyMultiplyer({ activeUnit }): number {
  method activeMint (line 244) | activeMint() {
  method mintUnitProofs (line 259) | mintUnitProofs(mint: StoredMint, unit: string): WalletProof[] {
  method mintUnitKeysets (line 266) | mintUnitKeysets(mint: StoredMint, unit: string): MintKeyset[] {
  method toggleActiveUnitForMint (line 275) | toggleActiveUnitForMint(mint: StoredMint) {
  method updateMint (line 285) | updateMint(oldMint: StoredMint, newMint: StoredMint) {
  method updateMintMultinutSelection (line 289) | updateMintMultinutSelection(mintUrl: string, selected: boolean) {
  method checkMintInfoMotdChanged (line 461) | checkMintInfoMotdChanged(newMintInfo: GetInfoResponse, mint: StoredMint) {
  method triggerMintInfoMotdChanged (line 472) | triggerMintInfoMotdChanged(
  function keysetIdToBigInt (line 523) | function keysetIdToBigInt(id: string): bigint {

FILE: src/stores/nostr.ts
  type NostrEventLog (line 41) | type NostrEventLog = {
  type SignerType (line 46) | enum SignerType {

FILE: src/stores/nostrMintBackup.ts
  constant MINT_BACKUP_KIND (line 20) | const MINT_BACKUP_KIND = 30078;
  type MintBackupData (line 22) | type MintBackupData = {
  type DiscoveredMint (line 27) | type DiscoveredMint = {
  method initializeBackupKeys (line 97) | async initializeBackupKeys(): Promise<void> {
  method initializeBackupKeysFromMnemonic (line 107) | async initializeBackupKeysFromMnemonic(
  method backupMintsToNostr (line 126) | async backupMintsToNostr(verbose: boolean = false): Promise<void> {
  method searchMintsOnNostr (line 230) | async searchMintsOnNostr(mnemonic: string): Promise<DiscoveredMint[]> {
  method addSelectedMintsToWallet (line 322) | async addSelectedMintsToWallet(
  method toggleMintSelection (line 367) | toggleMintSelection(mintUrl: string): void {
  method selectAllMints (line 375) | selectAllMints(): void {
  method deselectAllMints (line 382) | deselectAllMints(): void {
  method enableBackup (line 389) | async enableBackup(): Promise<void> {
  method disableBackup (line 403) | disableBackup(): void {
  method forceBackup (line 409) | async forceBackup(): Promise<void> {
  method clearDiscoveredMints (line 422) | clearDiscoveredMints(): void {

FILE: src/stores/nostrUser.ts
  type NostrProfile (line 7) | type NostrProfile = {
  method displayName (line 47) | displayName(state): string {
  method wotCount (line 57) | wotCount(state): number {
  method hasCrawlCheckpoint (line 70) | hasCrawlCheckpoint(state): boolean {
  class NostrUserDB (line 394) | class NostrUserDB extends Dexie {
    method constructor (line 398) | constructor() {

FILE: src/stores/npcv2.ts
  type NPCUser (line 12) | type NPCUser = {
  type NPCV2InfoReponse (line 19) | type NPCV2InfoReponse =
  type NPCV2UsernameReponse (line 31) | type NPCV2UsernameReponse =
  type NPCQuote (line 35) | type NPCQuote = {
  type NPCQuoteResponse (line 47) | type NPCQuoteResponse =
  type UsernameQuote (line 60) | type UsernameQuote = { username: string; creq: string };

FILE: src/stores/npubcash.ts
  type NPCInfo (line 16) | type NPCInfo = {
  type NPCBalance (line 23) | type NPCBalance = {
  type NPCClaim (line 28) | type NPCClaim = {
  type NPCWithdrawl (line 35) | type NPCWithdrawl = {
  type NPCWithdrawals (line 43) | type NPCWithdrawals = {

FILE: src/stores/nwc.ts
  type NWCConnection (line 19) | type NWCConnection = {
  type NWCCommand (line 27) | type NWCCommand = {
  type NWCTransaction (line 32) | type NWCTransaction = {
  type NWCResult (line 45) | type NWCResult = {
  type NWCError (line 50) | type NWCError = {
  method mapToNwcTransaction (line 309) | mapToNwcTransaction(invoice: InvoiceHistory): NWCTransaction {

FILE: src/stores/p2pk.ts
  type P2PKKey (line 8) | type P2PKKey = {

FILE: src/stores/payment-request.ts
  type OurPaymentRequest (line 25) | type OurPaymentRequest = {
  method currentPaymentRequest (line 51) | currentPaymentRequest(state): OurPaymentRequest | undefined {
  method newPaymentRequest (line 61) | newPaymentRequest(
  method ensureStoredRequest (line 111) | ensureStoredRequest(
  method selectPrevRequest (line 140) | selectPrevRequest() {
  method selectNextRequest (line 147) | selectNextRequest() {
  method selectRequestByIndex (line 153) | selectRequestByIndex(index: number) {
  method registerIncomingPaymentForRequest (line 162) | registerIncomingPaymentForRequest(
  method getPaymentsForRequest (line 172) | getPaymentsForRequest(requestId: string) {
  method decodePaymentRequest (line 180) | async decodePaymentRequest(pr: string) {
  method parseAndPayPaymentRequest (line 240) | async parseAndPayPaymentRequest(
  method payNostrPaymentRequest (line 259) | async payNostrPaymentRequest(
  method payPostPaymentRequest (line 293) | async payPostPaymentRequest(

FILE: src/stores/price.ts
  method currentCurrencyPrice (line 71) | currentCurrencyPrice(): number {

FILE: src/stores/proofs.ts
  method proofsToWalletProofs (line 87) | proofsToWalletProofs(proofs: Proof[], quote?: string): WalletProof[] {
  method addProofs (line 96) | async addProofs(proofs: Proof[], quote?: string) {
  method removeProofs (line 104) | async removeProofs(proofs: Proof[]) {
  method getProofsForQuote (line 112) | async getProofsForQuote(quote: string): Promise<WalletProof[]> {

FILE: src/stores/restore.ts
  constant BATCH_SIZE (line 13) | const BATCH_SIZE = 200;
  constant MAX_GAP (line 14) | const MAX_GAP = 2;

FILE: src/stores/sendTokensStore.ts
  method clearSendData (line 30) | clearSendData() {

FILE: src/stores/storage.ts
  method canPasteFromClipboard (line 164) | canPasteFromClipboard() {

FILE: src/stores/store-flag.d.ts
  type QuasarFeatureFlags (line 7) | interface QuasarFeatureFlags {

FILE: src/stores/swap.ts
  type SwapAmountData (line 18) | type SwapAmountData = {
  type HistoryToken (line 24) | type HistoryToken = {

FILE: src/stores/tokens.ts
  type HistoryToken (line 17) | type HistoryToken = {
  method addPaidToken (line 41) | addPaidToken({
  method addPendingToken (line 76) | addPendingToken({
  method editHistoryToken (line 111) | editHistoryToken(
  method setTokenPaid (line 153) | setTokenPaid(token: string) {
  method deleteToken (line 161) | deleteToken(token: string) {
  method tokenAlreadyInHistory (line 167) | tokenAlreadyInHistory(tokenStr: string): HistoryToken | undefined {
  function currentDateStr (line 173) | function currentDateStr() {

FILE: src/stores/ui.ts
  method closeDialogs (line 46) | closeDialogs() {
  method lockMutex (line 53) | async lockMutex() {
  method unlockMutex (line 69) | unlockMutex() {
  method triggerActivityOrb (line 72) | triggerActivityOrb() {
  method setTab (line 75) | setTab(tab: string) {
  method toggleDebugConsole (line 117) | toggleDebugConsole() {
  method enableDebugConsole (line 125) | enableDebugConsole() {
  method disableDebugConsole (line 138) | disableDebugConsole() {
  method tickerShort (line 164) | tickerShort() {
  method ndefSupported (line 172) | ndefSupported(): boolean {
  method canPasteFromClipboard (line 180) | canPasteFromClipboard() {
  method webShareSupported (line 187) | webShareSupported(): boolean {

FILE: src/stores/wallet.ts
  type Invoice (line 66) | type Invoice = {
  type InvoiceHistory (line 73) | type InvoiceHistory = Invoice & {
  type KeysetCounter (line 85) | type KeysetCounter = {
  method seed (line 163) | seed(): Uint8Array {
  method activeWallet (line 175) | async activeWallet(updateKeysets: boolean = false): Promise<Wallet> {
  method mintWallet (line 183) | async mintWallet(
  method mintWalletSync (line 218) | mintWalletSync(url: string, unit: string): Wallet {
  method createWalletInstance (line 226) | createWalletInstance(
  method getKeyset (line 285) | getKeyset(
  method setInvoicePaid (line 327) | setInvoicePaid(quoteId: string) {

FILE: src/stores/welcome.ts
  type WelcomeState (line 6) | type WelcomeState = {
  method initializeWelcome (line 88) | initializeWelcome() {
  method closeWelcome (line 97) | closeWelcome() {
  method setPath (line 105) | setPath(path: "new" | "recover") {
  method setCurrentSlide (line 113) | setCurrentSlide(index: number) {
  method acceptTerms (line 120) | acceptTerms() {
  method validateSeedPhrase (line 127) | validateSeedPhrase() {
  method resetWelcome (line 134) | resetWelcome() {
  method goToPrevSlide (line 148) | goToPrevSlide() {
  method goToNextSlide (line 159) | goToNextSlide() {

FILE: types/light-bolt11-decoder/index.d.ts
  type DecodedSection (line 2) | interface DecodedSection {
  type DecodedBolt11 (line 8) | interface DecodedBolt11 {
Condensed preview — 228 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,976K chars).
[
  {
    "path": ".editorconfig",
    "chars": 147,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".eslintignore",
    "chars": 119,
    "preview": "/dist\n/src-bex/www\n/src-capacitor\n/src-cordova\n/.quasar\n/node_modules\n.eslintrc.js\nbabel.config.js\n**/cashu-ts/dist/**\n"
  },
  {
    "path": ".eslintrc.js",
    "chars": 3507,
    "preview": "module.exports = {\n  // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy\n  // This o"
  },
  {
    "path": ".github/workflows/build.yaml",
    "chars": 431,
    "preview": "name: Build\n\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code"
  },
  {
    "path": ".github/workflows/docker.yaml",
    "chars": 2033,
    "preview": "name: Docker Build\n\non:\n  push:\n  pull_request:\n    types: [opened, synchronize, reopened]\n  release:\n    types: [releas"
  },
  {
    "path": ".github/workflows/format.yaml",
    "chars": 405,
    "preview": "name: Format\n\non: [push, pull_request]\n\njobs:\n  format:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout co"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 387,
    "preview": "name: Lint\n\non: [push, pull_request]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n "
  },
  {
    "path": ".github/workflows/test.yml",
    "chars": 632,
    "preview": "name: Test\n\non:\n  push:\n    branches: [main, develop]\n  pull_request:\n    branches: [main, develop]\n\njobs:\n  test:\n    r"
  },
  {
    "path": ".gitignore",
    "chars": 496,
    "preview": ".DS_Store\n.thumbs.db\nnode_modules\n\n# Quasar core related directories\n.quasar\n/dist\n\n# Cordova related directories and fi"
  },
  {
    "path": ".npmrc",
    "chars": 76,
    "preview": "# pnpm-related options\nshamefully-hoist=true\nstrict-peer-dependencies=false\n"
  },
  {
    "path": ".postcssrc.js",
    "chars": 224,
    "preview": "/* eslint-disable */\n// https://github.com/michael-ciniawsky/postcss-load-config\n\nmodule.exports = {\n  plugins: [\n    //"
  },
  {
    "path": ".prettierignore",
    "chars": 43,
    "preview": "dist\nsrc-capacitor\nsrc-cordova\nandroid\nios\n"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 367,
    "preview": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"editorconfig.editorconfig\",\n  "
  },
  {
    "path": ".vscode/settings.json",
    "chars": 670,
    "preview": "{\n  \"editor.bracketPairColorization.enabled\": true,\n  \"editor.guides.bracketPairs\": true,\n  \"editor.formatOnSave\": true,"
  },
  {
    "path": "AGENTS.md",
    "chars": 6278,
    "preview": "# Cashu.me Developer Guide for Agents\n\nThis document provides a comprehensive overview of the **Cashu.me** codebase. It "
  },
  {
    "path": "Dockerfile",
    "chars": 512,
    "preview": "# Stage 1: Build Phase\nFROM node:24 AS builder\n\nWORKDIR /app\n\nCOPY package*.json ./\n\n# Install application dependencies\n"
  },
  {
    "path": "LICENSE.md",
    "chars": 1062,
    "preview": "MIT License\n\nCopyright (c) 2023 Cashu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof t"
  },
  {
    "path": "README.md",
    "chars": 2115,
    "preview": "# Cashu (cashu)\n\nCashu Wallet\n\n## One-liner build & run\n\n```\ndocker compose up -d\n```\n\naccess at http://localhost:3000 o"
  },
  {
    "path": "android/.gitignore",
    "chars": 1824,
    "preview": "# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore\n\n# Built application"
  },
  {
    "path": "android/app/.gitignore",
    "chars": 26,
    "preview": "/build/*\n!/build/.npmkeep\n"
  },
  {
    "path": "android/app/build.gradle",
    "chars": 2122,
    "preview": "apply plugin: 'com.android.application'\n\nandroid {\n    namespace \"me.cashu.wallet\"\n    compileSdk rootProject.ext.compil"
  },
  {
    "path": "android/app/capacitor.build.gradle",
    "chars": 527,
    "preview": "// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME \"capacitor update\" IS RUN\n\nandroid {\n  compileOptions {\n      source"
  },
  {
    "path": "android/app/proguard-rules.pro",
    "chars": 751,
    "preview": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguar"
  },
  {
    "path": "android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java",
    "chars": 774,
    "preview": "package com.getcapacitor.myapp;\n\nimport static org.junit.Assert.*;\n\nimport android.content.Context;\nimport androidx.test"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "chars": 1817,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <uses-"
  },
  {
    "path": "android/app/src/main/java/me/cashu/wallet/MainActivity.java",
    "chars": 119,
    "preview": "package me.cashu.wallet;\n\nimport com.getcapacitor.BridgeActivity;\n\npublic class MainActivity extends BridgeActivity {}\n"
  },
  {
    "path": "android/app/src/main/res/drawable/ic_launcher_background.xml",
    "chars": 5138,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<vector\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:w"
  },
  {
    "path": "android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "chars": 1729,
    "preview": "<vector\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:aapt=\"http://schemas.android.com/aapt\"\n  an"
  },
  {
    "path": "android/app/src/main/res/layout/activity_main.xml",
    "chars": 523,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n  xmlns:android=\"http://sch"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "chars": 414,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "chars": 414,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <"
  },
  {
    "path": "android/app/src/main/res/values/ic_launcher_background.xml",
    "chars": 122,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<resources>\n    <color name=\"ic_launcher_background\">#FFFFFF</color>\n</resources"
  },
  {
    "path": "android/app/src/main/res/values/strings.xml",
    "chars": 287,
    "preview": "<?xml version='1.0' encoding='utf-8' ?>\n<resources>\n    <string name=\"app_name\">Cashu.me</string>\n    <string name=\"titl"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "chars": 836,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" par"
  },
  {
    "path": "android/app/src/main/res/xml/file_paths.xml",
    "chars": 215,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<paths xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <external"
  },
  {
    "path": "android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java",
    "chars": 402,
    "preview": "package com.getcapacitor.myapp;\n\nimport static org.junit.Assert.*;\n\nimport org.junit.Test;\n\n/**\n * Example local unit te"
  },
  {
    "path": "android/build.gradle",
    "chars": 636,
    "preview": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    \n"
  },
  {
    "path": "android/capacitor.settings.gradle",
    "chars": 623,
    "preview": "// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME \"capacitor update\" IS RUN\ninclude ':capacitor-android'\nproject(':cap"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "chars": 252,
    "preview": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributi"
  },
  {
    "path": "android/gradle.properties",
    "chars": 987,
    "preview": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will o"
  },
  {
    "path": "android/gradlew",
    "chars": 8504,
    "preview": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"Lice"
  },
  {
    "path": "android/gradlew.bat",
    "chars": 2868,
    "preview": "@rem\r\n@rem Copyright 2015 the original author or authors.\r\n@rem\r\n@rem Licensed under the Apache License, Version 2.0 (th"
  },
  {
    "path": "android/settings.gradle",
    "chars": 208,
    "preview": "include ':app'\ninclude ':capacitor-cordova-android-plugins'\nproject(':capacitor-cordova-android-plugins').projectDir = n"
  },
  {
    "path": "android/variables.gradle",
    "chars": 496,
    "preview": "ext {\n    minSdkVersion = 22\n    compileSdkVersion = 34\n    targetSdkVersion = 34\n    androidxActivityVersion = '1.8.0'\n"
  },
  {
    "path": "capacitor.config.ts",
    "chars": 191,
    "preview": "import type { CapacitorConfig } from \"@capacitor/cli\";\n\nconst config: CapacitorConfig = {\n  appId: \"me.cashu.wallet\",\n  "
  },
  {
    "path": "docker-compose.yaml",
    "chars": 143,
    "preview": "services:\n  cashu.me:\n    image: cashu.me\n    build: .\n    container_name: cashu.me\n    restart: always\n    ports:\n     "
  },
  {
    "path": "extension/embedder.html",
    "chars": 196,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\" />\n  </head>\n  <body>\n    <i"
  },
  {
    "path": "extension/manifest.json",
    "chars": 380,
    "preview": "{\n  \"name\": \"Cashu.me\",\n  \"description\": \"A privacy-preserving ecash wallet for Bitcoin Lightning\",\n  \"icons\": { \"128\": "
  },
  {
    "path": "extension/style.css",
    "chars": 133,
    "preview": "body {\n  background: #350a60;\n  margin: 0;\n  width: 580px;\n  height: 595px;\n}\niframe {\n  width: 100%;\n  height: 100%;\n  "
  },
  {
    "path": "index.html",
    "chars": 984,
    "preview": "<!DOCTYPE html>\n<html>\n  <head>\n    <title><%= productName %></title>\n\n    <meta charset=\"utf-8\" />\n    <meta name=\"desc"
  },
  {
    "path": "ios/.gitignore",
    "chars": 206,
    "preview": "App/build\nApp/Pods\nApp/output\nApp/App/public\nDerivedData\nxcuserdata\n\n# Cordova plugins for Capacitor\ncapacitor-cordova-i"
  },
  {
    "path": "ios/App/App/AppDelegate.swift",
    "chars": 3031,
    "preview": "import UIKit\nimport Capacitor\n\n@UIApplicationMain\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n\n    var windo"
  },
  {
    "path": "ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "chars": 210,
    "preview": "{\n  \"images\": [\n    {\n      \"idiom\": \"universal\",\n      \"size\": \"1024x1024\",\n      \"filename\": \"AppIcon-512@2x.png\",\n   "
  },
  {
    "path": "ios/App/App/Assets.xcassets/Contents.json",
    "chars": 60,
    "preview": "{\n  \"info\": {\n    \"version\": 1,\n    \"author\": \"xcode\"\n  }\n}\n"
  },
  {
    "path": "ios/App/App/Assets.xcassets/Splash.imageset/Contents.json",
    "chars": 1125,
    "preview": "{\n  \"images\": [\n    {\n      \"idiom\": \"universal\",\n      \"filename\": \"Default@1x~universal~anyany.png\",\n      \"scale\": \"1"
  },
  {
    "path": "ios/App/App/Base.lproj/LaunchScreen.storyboard",
    "chars": 2381,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<document\n  type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\"\n  versi"
  },
  {
    "path": "ios/App/App/Base.lproj/Main.storyboard",
    "chars": 1146,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<document\n  type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\"\n  versi"
  },
  {
    "path": "ios/App/App/Info.plist",
    "chars": 1598,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\"\n  \"http://www.apple.com/DTD"
  },
  {
    "path": "ios/App/App.xcodeproj/project.pbxproj",
    "chars": 16575,
    "preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 48;\n\tobjects = {\n\n/* Begin PBXBuildFile section *"
  },
  {
    "path": "ios/App/App.xcworkspace/contents.xcworkspacedata",
    "chars": 221,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:App.xcodeproj\""
  },
  {
    "path": "ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "chars": 238,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "ios/App/Podfile",
    "chars": 865,
    "preview": "require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'\n\nplatform :ios, '13.0'\nuse_frameworks!\n\n# work"
  },
  {
    "path": "jsconfig.json",
    "chars": 475,
    "preview": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"src/*\": [\"src/*\"],\n      \"app/*\": [\"*\"],\n      \"compo"
  },
  {
    "path": "package.json",
    "chars": 2543,
    "preview": "{\n  \"name\": \"cashu\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Cashu.me Wallet\",\n  \"productName\": \"Cashu.me\",\n  \"author\": "
  },
  {
    "path": "quasar.config.js",
    "chars": 6983,
    "preview": "/* eslint-env node */\n\n/*\n * This file runs in a Node context (it's NOT transpiled by Babel), so use only\n * the ES6 fea"
  },
  {
    "path": "quasar.extensions.json",
    "chars": 3,
    "preview": "{}\n"
  },
  {
    "path": "scripts/check-i18n.js",
    "chars": 3066,
    "preview": "#!/usr/bin/env node\n// Simple i18n keys auditor for src/i18n/*/index.ts\n// - Compares key sets across locales\n// - Repor"
  },
  {
    "path": "src/App.vue",
    "chars": 163,
    "preview": "<template>\n  <router-view />\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\n\nexport default defi"
  },
  {
    "path": "src/boot/.gitkeep",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/boot/axios.js",
    "chars": 709,
    "preview": "// src/boot/axios.js\n\nimport { boot } from \"quasar/wrappers\";\nimport axios from \"axios\";\n\n// const api = axios.create({ "
  },
  {
    "path": "src/boot/base.js",
    "chars": 10246,
    "preview": "import { copyToClipboard } from \"quasar\";\nimport { useUiStore } from \"stores/ui\";\nimport { Clipboard } from \"@capacitor/"
  },
  {
    "path": "src/boot/cashu.js",
    "chars": 303,
    "preview": "/**\n * Configures the Cashu-ts library axios client\n */\n// import { setupAxios } from \"@cashu/cashu-ts\";\n\n// export defa"
  },
  {
    "path": "src/boot/global-components.js",
    "chars": 273,
    "preview": "import { boot } from \"quasar/wrappers\";\n\nimport VueQrcode from \"@chenfengyuan/vue-qrcode\";\n\n// \"async\" is optional;\n// m"
  },
  {
    "path": "src/boot/i18n.js",
    "chars": 478,
    "preview": "import { boot } from \"quasar/wrappers\";\nimport { createI18n } from \"vue-i18n\";\nimport messages from \"src/i18n\";\n\n// Get "
  },
  {
    "path": "src/components/ActivityOrb.vue",
    "chars": 1870,
    "preview": "<template>\n  <div\n    class=\"row justify-center q-mb-none\"\n    style=\"position: absolute; top: 60px; left: 0; width: 100"
  },
  {
    "path": "src/components/AddMintDialog.vue",
    "chars": 7518,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showAddMintDialogLocal\"\n    @keydown.enter.prevent=\"addMintLocal\"\n    backdrop-filte"
  },
  {
    "path": "src/components/AmountInputComponent.vue",
    "chars": 18683,
    "preview": "<template>\n  <div class=\"amount-input-root\">\n    <slot name=\"overlay\" />\n    <transition name=\"swap-primary\">\n      <div"
  },
  {
    "path": "src/components/AndroidPWAPrompt.vue",
    "chars": 2809,
    "preview": "<!-- src/components/AndroidPWAPrompt.vue -->\n<template>\n  <transition appear enter-active-class=\"animated fadeInDown\">\n "
  },
  {
    "path": "src/components/AnimatedNumber.vue",
    "chars": 1829,
    "preview": "<template>\n  <span @click=\"$emit('click')\">{{ formattedValue }}</span>\n</template>\n\n<script lang=\"ts\">\nimport { defineCo"
  },
  {
    "path": "src/components/BalanceView.vue",
    "chars": 8120,
    "preview": "<template>\n  <!-- <q-card class=\"q-my-md q-py-sm\">\n    <q-card-section class=\"q-mt-sm q-py-xs\"> -->\n  <div class=\"q-pt-x"
  },
  {
    "path": "src/components/ChooseMint.vue",
    "chars": 13324,
    "preview": "<template>\n  <div class=\"q-pb-md\">\n    <!-- Main mint selector button -->\n    <div\n      class=\"row q-mt-xs q-mb-none\"\n "
  },
  {
    "path": "src/components/CreateInvoiceDialog.vue",
    "chars": 7201,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showCreateInvoiceDialog\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%"
  },
  {
    "path": "src/components/DisplayTokenComponent.vue",
    "chars": 23751,
    "preview": "<template>\n  <div\n    :class=\"$q.dark.isActive ? 'bg-dark' : 'bg-white'\"\n    class=\"display-token-fullscreen\"\n  >\n    <!"
  },
  {
    "path": "src/components/EditMintDialog.vue",
    "chars": 6260,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showEditMintDialogLocal\"\n    backdrop-filter=\"blur(4px) brightness(50%)\"\n    transit"
  },
  {
    "path": "src/components/EssentialLink.vue",
    "chars": 724,
    "preview": "<template>\n  <q-item clickable tag=\"a\" target=\"_blank\" :href=\"link\">\n    <q-item-section v-if=\"icon\" avatar>\n      <q-ic"
  },
  {
    "path": "src/components/FullscreenHeader.vue",
    "chars": 609,
    "preview": "<template>\n  <q-header class=\"bg-dark\">\n    <q-toolbar>\n      <q-btn\n        flat\n        dense\n        round\n        ic"
  },
  {
    "path": "src/components/HistoryTable.vue",
    "chars": 13634,
    "preview": "<template>\n  <div class=\"q-pa-xs\" style=\"max-width: 500px; margin: 0 auto\">\n    <q-list>\n      <q-item\n        v-for=\"tr"
  },
  {
    "path": "src/components/InvoiceDetailDialog.vue",
    "chars": 8045,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showInvoiceDetails\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n  "
  },
  {
    "path": "src/components/MainHeader.vue",
    "chars": 6128,
    "preview": "<template>\n  <q-header class=\"bg-transparent\">\n    <q-toolbar>\n      <q-btn\n        flat\n        dense\n        round\n   "
  },
  {
    "path": "src/components/MeltQuoteInformation.vue",
    "chars": 11033,
    "preview": "<template>\n  <div\n    v-if=\"hasQuote\"\n    class=\"melt-quote-information q-mt-md\"\n    :class=\"containerTextClass\"\n  >\n   "
  },
  {
    "path": "src/components/MintAuditInfo.vue",
    "chars": 9995,
    "preview": "<template>\n  <div class=\"q-mt-lg q-mb-lg\">\n    <div class=\"text-caption q-mx-sm\" v-if=\"loading\">\n      <q-spinner color="
  },
  {
    "path": "src/components/MintAuditSwapsBarChart.vue",
    "chars": 14479,
    "preview": "<template>\n  <div class=\"bar-chart-container\" ref=\"chartContainer\">\n    <div class=\"chart-wrapper\">\n      <div class=\"ba"
  },
  {
    "path": "src/components/MintAuditWarningBox.vue",
    "chars": 7951,
    "preview": "<template>\n  <div class=\"warning-container\" v-if=\"hasWarnings\">\n    <div class=\"warning-content\">\n      <q-icon name=\"wa"
  },
  {
    "path": "src/components/MintDiscovery.vue",
    "chars": 10019,
    "preview": "<template>\n  <div class=\"discover-section\">\n    <div class=\"discover-card\">\n      <!-- <div class=\"discover-header\">\n   "
  },
  {
    "path": "src/components/MintInfoContainer.vue",
    "chars": 1467,
    "preview": "<template>\n  <div class=\"row items-center\">\n    <q-avatar v-if=\"iconUrl\" :size=\"avatarSize\" class=\"q-mr-sm\">\n      <q-im"
  },
  {
    "path": "src/components/MintMotdMessage.vue",
    "chars": 3044,
    "preview": "<template>\n  <div class=\"motd-container\" v-if=\"message && !dismissed\">\n    <div class=\"motd-content\">\n      <info-icon s"
  },
  {
    "path": "src/components/MintQuoteInformation.vue",
    "chars": 5896,
    "preview": "<template>\n  <div\n    v-if=\"hasQuote\"\n    class=\"mint-quote-information q-mt-md\"\n    :class=\"containerTextClass\"\n  >\n   "
  },
  {
    "path": "src/components/MintRatingsComponent.vue",
    "chars": 25904,
    "preview": "<template>\n  <q-card\n    class=\"bg-dark\"\n    style=\"min-width: 360px; max-width: 820px; width: 100%\"\n  >\n    <q-card-sec"
  },
  {
    "path": "src/components/MintSettings.vue",
    "chars": 22540,
    "preview": "<template>\n  <AddMintDialog\n    :addMintData=\"addMintData\"\n    :showAddMintDialog=\"showAddMintDialog\"\n    @update:showAd"
  },
  {
    "path": "src/components/MultinutPaymentDialog.vue",
    "chars": 40945,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showMultinutPaymentDialog\"\n    position=\"top\"\n    :maximized=\"true\"\n    transition-s"
  },
  {
    "path": "src/components/NWCDialog.vue",
    "chars": 2526,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showNWCDialog\"\n    position=\"top\"\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n  "
  },
  {
    "path": "src/components/NoMintWarnBanner.vue",
    "chars": 3055,
    "preview": "<template>\n  <q-card class=\"q-ma-lg bg-dark q-pa-md\">\n    <q-card-section>\n      <div class=\"row items-center justify-ce"
  },
  {
    "path": "src/components/NostrMintRestore.vue",
    "chars": 14612,
    "preview": "<template>\n  <div v-if=\"!autoAdd\" class=\"nostr-mint-restore\">\n    <!-- Header -->\n    <div class=\"q-px-xs text-left q-mt"
  },
  {
    "path": "src/components/NumericKeyboard.vue",
    "chars": 4514,
    "preview": "<!-- NumericKeyboard.vue -->\n<template>\n  <transition name=\"slide-up-fade\">\n    <div\n      class=\"numeric-keyboard q-pa-"
  },
  {
    "path": "src/components/P2PKDialog.vue",
    "chars": 5267,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showP2PKDialog\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n    tr"
  },
  {
    "path": "src/components/ParseInputComponent.vue",
    "chars": 4548,
    "preview": "<template>\n  <div class=\"column q-mt-md\">\n    <!-- Input field with paste button -->\n    <div class=\"input-with-paste-wr"
  },
  {
    "path": "src/components/PayInvoiceDialog.vue",
    "chars": 36463,
    "preview": "<template>\n  <q-dialog\n    v-model=\"payInvoiceData.show\"\n    @hide=\"closeParseDialog\"\n    v-if=\"!camera.show\"\n    maximi"
  },
  {
    "path": "src/components/PaymentRequestDialog.vue",
    "chars": 11626,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showPRDialog\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n    tran"
  },
  {
    "path": "src/components/PaymentRequestInfo.vue",
    "chars": 3229,
    "preview": "<template>\n  <div class=\"payment-request-info\">\n    <div class=\"row items-center no-wrap\">\n      <div class=\"icon-circle"
  },
  {
    "path": "src/components/PaymentRequestPayments.vue",
    "chars": 4177,
    "preview": "<template>\n  <div class=\"q-pa-none\">\n    <div v-if=\"payments.length === 0\" class=\"text-center q-mt-sm\">\n      <q-item-la"
  },
  {
    "path": "src/components/QrcodeReader.vue",
    "chars": 3938,
    "preview": "<script lang=\"ts\">\nimport QrScanner from \"qr-scanner\";\nimport { URDecoder } from \"@gandlaf21/bc-ur\";\nimport { useCameraS"
  },
  {
    "path": "src/components/ReceiveDialog.vue",
    "chars": 5301,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showReceiveDialog\"\n    position=\"bottom\"\n    :maximized=\"$q.screen.lt.sm\"\n    transi"
  },
  {
    "path": "src/components/ReceiveEcashDrawer.vue",
    "chars": 9219,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showReceiveEcashDrawer\"\n    position=\"bottom\"\n    :maximized=\"$q.screen.lt.sm\"\n    t"
  },
  {
    "path": "src/components/ReceiveTokenDialog.vue",
    "chars": 29385,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showReceiveTokens\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n   "
  },
  {
    "path": "src/components/RemoveMintDialog.vue",
    "chars": 7504,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showRemoveMintDialogLocal\"\n    backdrop-filter=\"blur(4px) brightness(50%)\"\n    trans"
  },
  {
    "path": "src/components/RestoreView.vue",
    "chars": 16743,
    "preview": "<template>\n  <div style=\"max-width: 800px; margin: 0 auto\">\n    <!-- Mnemonic seed phrase input -->\n    <div v-if=\"!onbo"
  },
  {
    "path": "src/components/SendDialog.vue",
    "chars": 5605,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showSendDialog\"\n    position=\"bottom\"\n    :maximized=\"$q.screen.lt.sm\"\n    transitio"
  },
  {
    "path": "src/components/SendPaymentRequest.vue",
    "chars": 6092,
    "preview": "<template>\n  <div\n    v-if=\"sendData.paymentRequest\"\n    class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n    style=\"max-width:"
  },
  {
    "path": "src/components/SendTokenDialog.vue",
    "chars": 23391,
    "preview": "<template>\n  <q-dialog\n    v-model=\"showSendTokens\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n    tr"
  },
  {
    "path": "src/components/SettingsView.vue",
    "chars": 82842,
    "preview": "<template>\n  <div style=\"max-width: 800px; margin: 0 auto\">\n    <!-- BACKUP & RESTORE SECTION -->\n    <div class=\"sectio"
  },
  {
    "path": "src/components/SwapIncomingTokenToKnownMint.vue",
    "chars": 6799,
    "preview": "<template>\n  <div class=\"swap-section q-mt-md\">\n    <!-- Header with cancel button -->\n    <div class=\"row items-center "
  },
  {
    "path": "src/components/ToggleUnit.vue",
    "chars": 1059,
    "preview": "<template>\n  <q-btn\n    rounded\n    outline\n    :color=\"color\"\n    @click=\"toggleUnit()\"\n    :label=\"activeUnitLabelAdop"
  },
  {
    "path": "src/components/TokenInformation.vue",
    "chars": 10990,
    "preview": "<template>\n  <div class=\"token-information-container q-px-md\">\n    <!-- Amount Header -->\n    <div v-if=\"!hideAmount\" cl"
  },
  {
    "path": "src/components/TokenStringRender.vue",
    "chars": 6877,
    "preview": "<template>\n  <q-card class=\"token-card\">\n    <!-- Token string as background filling entire card -->\n    <div class=\"tok"
  },
  {
    "path": "src/components/ToolTipInfo.vue",
    "chars": 697,
    "preview": "<template>\n  <div class=\"tool-tip-info\">\n    <q-icon name=\"info\" size=\"16px\" class=\"q-mr-xs\" />\n    <span class=\"tool-ti"
  },
  {
    "path": "src/components/WelcomeDialog.vue",
    "chars": 4327,
    "preview": "<template>\n  <q-dialog\n    class=\"z-top\"\n    persistent\n    position=\"top\"\n    @drop=\"dragFile\"\n    @dragover=\"allowDrop"
  },
  {
    "path": "src/components/iOSPWAPrompt.vue",
    "chars": 2623,
    "preview": "<template>\n  <transition enter-active-class=\"animated fadeInUp\">\n    <div\n      v-if=\"showIosPWAPrompt\"\n      class=\"pwa"
  },
  {
    "path": "src/css/app.scss",
    "chars": 2206,
    "preview": "// app global css in SCSS form\n\n// Import Inter font from Google Fonts\n@import url(\"https://fonts.googleapis.com/css2?fa"
  },
  {
    "path": "src/css/base.scss",
    "chars": 4900,
    "preview": "$themes: (\n  \"classic\": (\n    primary: #935af5,\n    secondary: #b45af5,\n    dark: #1f2234,\n    info: #333646,\n    margin"
  },
  {
    "path": "src/css/mintlist.css",
    "chars": 2388,
    "preview": "/* Shared styles for mint list items across MintSettings, RestoreView, and NostrMintRestore */\n\n/* Common mint card styl"
  },
  {
    "path": "src/css/quasar.variables.scss",
    "chars": 746,
    "preview": "// Quasar SCSS (& Sass) Variables\n// --------------------------------------------------\n// To customize the look and fee"
  },
  {
    "path": "src/i18n/ar-SA/index.ts",
    "chars": 41515,
    "preview": "export default {\n  MultinutPicker: {\n    payment: \"دفع متعدد الجوز\",\n    selectMints: \"حدد واحدًا أو أكثر من mints لتنفي"
  },
  {
    "path": "src/i18n/cs-CZ/index.ts",
    "chars": 45037,
    "preview": "export default {\n  global: {\n    copy_to_clipboard: {\n      success: \"Zkopírováno do schránky!\",\n    },\n    actions: {\n "
  },
  {
    "path": "src/i18n/de-DE/index.ts",
    "chars": 46218,
    "preview": "export default {\n  MultinutPicker: {\n    payment: \"Multinut-Zahlung\",\n    selectMints:\n      \"Wählen Sie eine oder mehre"
  },
  {
    "path": "src/i18n/el-GR/index.ts",
    "chars": 46747,
    "preview": "export default {\n  MultinutPicker: {\n    payment: \"Πληρωμή Multinut\",\n    selectMints:\n      \"Επιλέξτε ένα ή περισσότερα"
  },
  {
    "path": "src/i18n/en-US/index.ts",
    "chars": 44252,
    "preview": "export default {\n  global: {\n    copy_to_clipboard: {\n      success: \"Copied to clipboard!\",\n    },\n    actions: {\n     "
  },
  {
    "path": "src/i18n/es-ES/index.ts",
    "chars": 45130,
    "preview": "export default {\n  global: {\n    copy_to_clipboard: {\n      success: \"¡Copiado al portapapeles!\",\n    },\n    actions: {\n"
  },
  {
    "path": "src/i18n/fr-FR/index.ts",
    "chars": 46683,
    "preview": "export default {\n  MultinutPicker: {\n    payment: \"Paiement Multinut\",\n    selectMints:\n      \"Sélectionnez une ou plusi"
  },
  {
    "path": "src/i18n/index.ts",
    "chars": 651,
    "preview": "import enUS from \"./en-US\";\nimport esES from \"./es-ES\";\nimport itIT from \"./it-IT\";\nimport deDE from \"./de-DE\";\nimport f"
  },
  {
    "path": "src/i18n/it-IT/index.ts",
    "chars": 44488,
    "preview": "export default {\n  MultinutPicker: {\n    payment: \"Pagamento Multinut\",\n    selectMints: \"Seleziona una o più mint da cu"
  },
  {
    "path": "src/i18n/ja-JP/index.ts",
    "chars": 35520,
    "preview": "export default {\n  MultinutPicker: {\n    payment: \"マルチナット支払い\",\n    selectMints: \"支払いに使用するミントを一つ以上選択してください。\",\n    totalSe"
  },
  {
    "path": "src/i18n/pt-BR/index.ts",
    "chars": 46745,
    "preview": "export default {\n  global: {\n    copy_to_clipboard: {\n      success: \"Copiado para a área de transferência!\",\n    },\n   "
  },
  {
    "path": "src/i18n/sv-SE/index.ts",
    "chars": 44083,
    "preview": "export default {\n  MultinutPicker: {\n    payment: \"Multinut-betalning\",\n    selectMints: \"Välj en eller flera mints att "
  },
  {
    "path": "src/i18n/th-TH/index.ts",
    "chars": 42200,
    "preview": "export default {\n  MultinutPicker: {\n    payment: \"การชำระเงิน Multinut\",\n    selectMints: \"เลือกหนึ่งหรือหลาย mint เพื่"
  },
  {
    "path": "src/i18n/tr-TR/index.ts",
    "chars": 44150,
    "preview": "export default {\n  MultinutPicker: {\n    payment: \"Multinut ödeme\",\n    selectMints: \"Ödeme yapmak için bir veya birden "
  },
  {
    "path": "src/i18n/zh-CN/index.ts",
    "chars": 33018,
    "preview": "export default {\n  MultinutPicker: {\n    payment: \"多坚果支付\",\n    selectMints: \"选择一个或多个铸币厂来发起支付。\",\n    totalSelectedBalance"
  },
  {
    "path": "src/icons.js",
    "chars": 393,
    "preview": "import { createApp } from \"vue\";\nimport {\n  LucideX,\n  LucideQrCode,\n  LucideWallet,\n  LucideZap,\n  // Add other icons y"
  },
  {
    "path": "src/js/__tests__/legacy-qr.test.js",
    "chars": 2415,
    "preview": "import { describe, expect, it } from \"vitest\";\nimport {\n  isLegacyRetailQR,\n  translateLegacyQRToLightningAddress,\n} fro"
  },
  {
    "path": "src/js/__tests__/token.test.js",
    "chars": 2117,
    "preview": "import token from \"../token\";\nimport { describe, expect, it } from \"vitest\";\n\nconst VALID_V4_TOKEN =\n  \"cashuBo2FteCJodH"
  },
  {
    "path": "src/js/base64.js",
    "chars": 817,
    "preview": "function unescapeBase64Url(str) {\n  return (str + \"===\".slice((str.length + 3) % 4))\n    .replace(/-/g, \"+\")\n    .replac"
  },
  {
    "path": "src/js/dhke.js",
    "chars": 1152,
    "preview": "import { bytesToNumber } from \"./utils\";\nimport * as nobleSecp256k1 from \"@noble/secp256k1\";\n\nasync function hashToCurve"
  },
  {
    "path": "src/js/eventBus.js",
    "chars": 551,
    "preview": "import { reactive } from \"vue\";\n\nexport const EventBus = reactive({\n  events: {},\n\n  on(event, callback) {\n    if (!this"
  },
  {
    "path": "src/js/legacy-qr.js",
    "chars": 704,
    "preview": "// Converts South African retail EMV QR codes to Lightning Address format via cryptoqr.net\n\nconst MERCHANT_PATTERNS = [\n"
  },
  {
    "path": "src/js/notify.ts",
    "chars": 2114,
    "preview": "import { Notify, QNotifyCreateOptions } from \"quasar\";\n\ntype StatusMap = { [x: number]: \"warning\" | \"negative\" };\nconst "
  },
  {
    "path": "src/js/string-utils.js",
    "chars": 243,
    "preview": "function shortenString(s, length = 20, lastchars = 5) {\n  if (s.length > length + lastchars) {\n    return (\n      s.subs"
  },
  {
    "path": "src/js/token.ts",
    "chars": 2182,
    "preview": "import {\n  type Token,\n  getDecodedToken,\n  getTokenMetadata,\n  Mint,\n  TokenMetadata,\n} from \"@cashu/cashu-ts\";\nimport "
  },
  {
    "path": "src/js/utils.js",
    "chars": 813,
    "preview": "import { date } from \"quasar\";\nimport * as nobleSecp256k1 from \"@noble/secp256k1\";\n\nfunction splitAmount(value) {\n  cons"
  },
  {
    "path": "src/js/wallet-helpers.js",
    "chars": 632,
    "preview": "function getShortUrl(url) {\n  url = url.replace(\"https://\", \"\");\n  url = url.replace(\"http://\", \"\");\n  const cut_param ="
  },
  {
    "path": "src/layouts/BlankLayout.vue",
    "chars": 316,
    "preview": "<template>\n  <q-layout view=\"lHh Lpr lFf\">\n    <q-page-container>\n      <router-view />\n    </q-page-container>\n  </q-la"
  },
  {
    "path": "src/layouts/FullscreenLayout.vue",
    "chars": 435,
    "preview": "<template>\n  <q-layout view=\"lHh Lpr lFf\">\n    <FullscreenHeader />\n    <q-page-container>\n      <router-view />\n    </q"
  },
  {
    "path": "src/layouts/MainLayout.vue",
    "chars": 405,
    "preview": "<template>\n  <q-layout view=\"lHh Lpr lFf\">\n    <MainHeader />\n    <q-page-container>\n      <router-view />\n    </q-page-"
  },
  {
    "path": "src/main.js",
    "chars": 148,
    "preview": "import { createApp } from \"vue\";\nimport App from \"./App.vue\";\nimport registerIcons from \"./icons\";\n\nconst app = createAp"
  },
  {
    "path": "src/pages/AlreadyRunning.vue",
    "chars": 644,
    "preview": "<template>\n  <div\n    class=\"fullscreen bg-dark text-white text-center q-pa-md flex flex-center\"\n  >\n    <div>\n      <di"
  },
  {
    "path": "src/pages/CreateMintReviewPage.vue",
    "chars": 12088,
    "preview": "<template>\n  <div class=\"bg-dark text-white q-pa-md flex flex-center\">\n    <div class=\"review-page-content\">\n      <div "
  },
  {
    "path": "src/pages/ErrorNotFound.vue",
    "chars": 676,
    "preview": "<template>\n  <div\n    class=\"fullscreen bg-dark text-white text-center q-pa-md flex flex-center\"\n  >\n    <div>\n      <di"
  },
  {
    "path": "src/pages/MintDetailsPage.vue",
    "chars": 21971,
    "preview": "<template>\n  <div class=\"bg-dark text-white q-pa-md flex flex-center\">\n    <div class=\"mint-details-page-content\">\n     "
  },
  {
    "path": "src/pages/MintDiscoveryPage.vue",
    "chars": 1033,
    "preview": "<template>\n  <div class=\"bg-dark text-white q-pa-md flex flex-center\">\n    <div class=\"page-content q-px-md\">\n      <div"
  },
  {
    "path": "src/pages/MintRatingsPage.vue",
    "chars": 2469,
    "preview": "<template>\n  <div class=\"bg-dark text-white q-pa-md flex flex-center\">\n    <div class=\"mint-ratings-page-content\">\n     "
  },
  {
    "path": "src/pages/Restore.vue",
    "chars": 348,
    "preview": "<template>\n  <div class=\"bg-dark text-white text-center q-pa-md flex flex-center\">\n    <RestoreView />\n  </div>\n</templa"
  },
  {
    "path": "src/pages/Settings.vue",
    "chars": 352,
    "preview": "<template>\n  <div class=\"bg-dark text-white text-center q-pa-md flex flex-center\">\n    <SettingsView />\n  </div>\n</templ"
  },
  {
    "path": "src/pages/TermsPage.vue",
    "chars": 841,
    "preview": "<!-- src/components/WelcomePage.vue -->\n<template>\n  <q-card class=\"bg-dark q-pa-none\" style=\"height: 100%\">\n    <Welcom"
  },
  {
    "path": "src/pages/WalletPage.vue",
    "chars": 22069,
    "preview": "<template>\n  <div class=\"row q-col-gutter-y-md justify-center q-pt-sm q-pb-md\">\n    <div class=\"col-12 col-sm-11 col-md-"
  },
  {
    "path": "src/pages/WelcomePage.vue",
    "chars": 6136,
    "preview": "<!-- src/components/WelcomePage.vue -->\n<template>\n  <q-dialog\n    v-model=\"welcomeStore.showWelcome\"\n    persistent\n   "
  },
  {
    "path": "src/pages/welcome/WelcomeMintSetup.vue",
    "chars": 9898,
    "preview": "<template>\n  <div class=\"mint-setup-slide\">\n    <!-- Main content area -->\n    <div class=\"content\">\n      <!-- Header I"
  },
  {
    "path": "src/pages/welcome/WelcomeRecoverSeed.vue",
    "chars": 8703,
    "preview": "<template>\n  <div class=\"recover-slide\">\n    <!-- Main content area -->\n    <div class=\"content\">\n      <!-- Header Icon"
  },
  {
    "path": "src/pages/welcome/WelcomeRestoreEcash.vue",
    "chars": 2673,
    "preview": "<template>\n  <div class=\"restore-ecash-slide\">\n    <!-- Main content area -->\n    <div class=\"content\">\n      <!-- Heade"
  },
  {
    "path": "src/pages/welcome/WelcomeSlide1.vue",
    "chars": 26934,
    "preview": "<!-- src/components/WelcomeSlide1.vue -->\n<template>\n  <div class=\"welcome-slide\">\n    <!-- Logo -->\n    <div class=\"log"
  },
  {
    "path": "src/pages/welcome/WelcomeSlide2.vue",
    "chars": 11523,
    "preview": "<!-- src/components/WelcomeSlide2.vue -->\n<template>\n  <div class=\"pwa-slide\">\n    <!-- Main content area -->\n    <div c"
  },
  {
    "path": "src/pages/welcome/WelcomeSlide3.vue",
    "chars": 6429,
    "preview": "<!-- src/components/WelcomeSlide3.vue -->\n<template>\n  <div class=\"seed-phrase-slide\">\n    <!-- Main content area -->\n  "
  },
  {
    "path": "src/pages/welcome/WelcomeSlide4.vue",
    "chars": 17499,
    "preview": "<template>\n  <div class=\"q-pa-md flex flex-center\">\n    <div class=\"text-center\">\n      <q-icon name=\"gavel\" size=\"4em\" "
  },
  {
    "path": "src/pages/welcome/WelcomeSlideChoice.vue",
    "chars": 4178,
    "preview": "<template>\n  <div class=\"choice-slide\">\n    <!-- Main content area -->\n    <div class=\"content\">\n      <!-- Header Icon "
  },
  {
    "path": "src/router/index.js",
    "chars": 985,
    "preview": "import { route } from \"quasar/wrappers\";\nimport {\n  createRouter,\n  createMemoryHistory,\n  createWebHistory,\n  createWeb"
  },
  {
    "path": "src/router/routes.js",
    "chars": 2118,
    "preview": "const routes = [\n  {\n    path: \"/\",\n    component: () => import(\"layouts/MainLayout.vue\"),\n    children: [\n      { path:"
  },
  {
    "path": "src/stores/__tests__/wallet.test.js",
    "chars": 12177,
    "preview": "import { beforeEach, describe, expect, it, vi } from \"vitest\";\n\nconst h = vi.hoisted(() => {\n  const notify = vi.fn();\n "
  },
  {
    "path": "src/stores/camera.ts",
    "chars": 511,
    "preview": "import { defineStore } from \"pinia\";\n\nexport const useCameraStore = defineStore(\"camera\", {\n  state: () => ({\n    camera"
  },
  {
    "path": "src/stores/dexie.ts",
    "chars": 2270,
    "preview": "import { defineStore } from \"pinia\";\nimport Dexie, { Table } from \"dexie\";\nimport { useLocalStorage } from \"@vueuse/core"
  },
  {
    "path": "src/stores/index.js",
    "chars": 475,
    "preview": "import { store } from \"quasar/wrappers\";\nimport { createPinia } from \"pinia\";\n\n/*\n * If not building with SSR mode, you "
  },
  {
    "path": "src/stores/invoicesWorker.ts",
    "chars": 4565,
    "preview": "import { defineStore } from \"pinia\";\nimport { useWalletStore } from \"./wallet\";\nimport { useLocalStorage } from \"@vueuse"
  },
  {
    "path": "src/stores/migrations.ts",
    "chars": 5932,
    "preview": "import { defineStore } from \"pinia\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { useMintsStore } from \"./mi"
  },
  {
    "path": "src/stores/mintRecommendations.ts",
    "chars": 19854,
    "preview": "import { defineStore } from \"pinia\";\nimport NDK, { NDKEvent, NDKFilter, NDKKind } from \"@nostr-dev-kit/ndk\";\nimport { us"
  },
  {
    "path": "src/stores/mints.ts",
    "chars": 21877,
    "preview": "import { defineStore } from \"pinia\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { useWorkersStore } from \"./"
  },
  {
    "path": "src/stores/nostr.ts",
    "chars": 20309,
    "preview": "import { defineStore } from \"pinia\";\nimport NDK, {\n  NDKEvent,\n  NDKSigner,\n  NDKNip07Signer,\n  NDKNip46Signer,\n  NDKFil"
  },
  {
    "path": "src/stores/nostrMintBackup.ts",
    "chars": 13187,
    "preview": "import { defineStore } from \"pinia\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { bytesToHex } from \"@noble/"
  },
  {
    "path": "src/stores/nostrUser.ts",
    "chars": 14485,
    "preview": "import { defineStore } from \"pinia\";\nimport NDK, { NDKEvent, NDKFilter, NDKKind } from \"@nostr-dev-kit/ndk\";\nimport { us"
  },
  {
    "path": "src/stores/npcv2.ts",
    "chars": 9564,
    "preview": "import { defineStore } from \"pinia\";\nimport NDK, { NDKEvent } from \"@nostr-dev-kit/ndk\";\nimport { useLocalStorage } from"
  },
  {
    "path": "src/stores/npubcash.ts",
    "chars": 7886,
    "preview": "import { defineStore } from \"pinia\";\nimport NDK, { NDKEvent } from \"@nostr-dev-kit/ndk\";\nimport { useLocalStorage } from"
  },
  {
    "path": "src/stores/nwc.ts",
    "chars": 18258,
    "preview": "import { defineStore } from \"pinia\";\nimport NDK, {\n  NDKEvent,\n  NDKFilter,\n  NDKPrivateKeySigner,\n  NDKKind,\n  NDKSubsc"
  },
  {
    "path": "src/stores/p2pk.ts",
    "chars": 6704,
    "preview": "import { defineStore } from \"pinia\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { generateSecretKey, getPubl"
  },
  {
    "path": "src/stores/payment-request.ts",
    "chars": 11326,
    "preview": "import { defineStore } from \"pinia\";\nimport { useWalletStore } from \"./wallet\";\nimport {\n  decodePaymentRequest,\n  Payme"
  },
  {
    "path": "src/stores/price.ts",
    "chars": 2519,
    "preview": "import { defineStore } from \"pinia\";\nimport { useSettingsStore } from \"./settings\";\nimport { useLocalStorage } from \"@vu"
  }
]

// ... and 28 more files (download for full content)

About this extraction

This page contains the full source code of the cashubtc/cashu.me GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 228 files (1.7 MB), approximately 456.7k tokens, and a symbol index with 223 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!