[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
  },
  {
    "path": ".eslintignore",
    "content": "/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",
    "content": "module.exports = {\n  // https://eslint.org/docs/user-guide/configuring#configuration-cascading-and-hierarchy\n  // This option interrupts the configuration hierarchy at this file\n  // Remove this if you have an higher level ESLint config file (it usually happens into a monorepos)\n  root: true,\n\n  parserOptions: {\n    parser: require.resolve(\"@typescript-eslint/parser\"),\n    ecmaVersion: \"2021\", // Allows for the parsing of modern ECMAScript features\n  },\n\n  env: {\n    browser: true,\n    \"vue/setup-compiler-macros\": true,\n  },\n\n  // Rules order is important, please avoid shuffling them\n  extends: [\n    // Base ESLint recommended rules\n    \"eslint:recommended\",\n\n    // Uncomment any of the lines below to choose desired strictness,\n    // but leave only one uncommented!\n    // See https://eslint.vuejs.org/rules/#available-rules\n    \"plugin:vue/vue3-essential\", // Priority A: Essential (Error Prevention)\n    // 'plugin:vue/vue3-strongly-recommended', // Priority B: Strongly Recommended (Improving Readability)\n    // 'plugin:vue/vue3-recommended', // Priority C: Recommended (Minimizing Arbitrary Choices and Cognitive Overhead)\n\n    // https://github.com/prettier/eslint-config-prettier#installation\n    // usage with Prettier, provided by 'eslint-config-prettier'.\n    \"prettier\",\n  ],\n\n  plugins: [\n    // https://eslint.vuejs.org/user-guide/#why-doesn-t-it-work-on-vue-files\n    // required to lint *.vue files\n    \"vue\",\n\n    // https://github.com/typescript-eslint/typescript-eslint/issues/389#issuecomment-509292674\n    // Prettier has not been included as plugin to avoid performance impact\n    // add it as an extension for your IDE\n  ],\n\n  globals: {\n    ga: \"readonly\", // Google Analytics\n    cordova: \"readonly\",\n    __statics: \"readonly\",\n    __QUASAR_SSR__: \"readonly\",\n    __QUASAR_SSR_SERVER__: \"readonly\",\n    __QUASAR_SSR_CLIENT__: \"readonly\",\n    __QUASAR_SSR_PWA__: \"readonly\",\n    process: \"readonly\",\n    Capacitor: \"readonly\",\n    chrome: \"readonly\",\n  },\n\n  // add your custom rules here\n  rules: {\n    \"prefer-promise-reject-errors\": \"off\",\n\n    // allow debugger during development only\n    \"no-debugger\": process.env.NODE_ENV === \"production\" ? \"error\" : \"off\",\n\n    \"no-var\": \"error\",\n    \"no-const-assign\": \"error\",\n    \"prefer-const\": [\n      \"error\",\n      {\n        destructuring: \"any\",\n        ignoreReadBeforeAssign: false,\n      },\n    ],\n\n    // remove some warnings/errors from eslint:recommended for now\n    // which are quite common in the current codebase\n    // we will deal with them later on\n    \"no-unused-vars\": \"off\",\n    \"no-undef\": \"off\",\n    \"no-empty\": \"off\",\n    \"no-useless-catch\": \"off\",\n    \"no-constant-condition\": \"off\",\n  },\n  overrides: [\n    {\n      files: [\"**/*.{js,ts}\"],\n      // If the `script` part of a Vue component is stored in a separate JS/TS file,\n      // as is the case when using DFC (https://testing.quasar.dev/packages/unit-jest/#double-file-components-dfc),\n      // Vue ESLint plugin will highlight all public properties as unused\n      // as it's not able to detect their usage into the template\n      // We disable this rule and only keep it for Vue files\n      rules: { \"vue/no-unused-properties\": \"off\" },\n    },\n    {\n      files: [\"*.vue\"],\n      parser: \"vue-eslint-parser\",\n      parserOptions: {\n        parser: \"@typescript-eslint/parser\",\n      },\n      rules: {\n        // Disallow <script> blocks without lang=\"ts\"\n        \"vue/block-lang\": [\"error\", { script: { lang: \"ts\" } }],\n      },\n    },\n  ],\n};\n"
  },
  {
    "path": ".github/workflows/build.yaml",
    "content": "name: Build\n\non: [push, pull_request]\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 25\n          cache: npm\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Run build\n        run: |\n          npm run build\n          npm run build:pwa\n"
  },
  {
    "path": ".github/workflows/docker.yaml",
    "content": "name: Docker Build\n\non:\n  push:\n  pull_request:\n    types: [opened, synchronize, reopened]\n  release:\n    types: [released]\n\njobs:\n  check-secrets:\n    runs-on: ubuntu-latest\n    outputs:\n      secrets_set: ${{ steps.check.outputs.secrets_set }}\n    steps:\n      - name: Check if secrets are set\n        run: |\n          if [ -z \"${{ secrets.DOCKER_USERNAME }}\" ]; then\n            echo \"secrets_set=false\" >> $GITHUB_OUTPUT\n          else\n            echo \"secrets_set=true\" >> $GITHUB_OUTPUT\n          fi\n\n  build-and-push:\n    # run only when secrets are set\n    needs: check-secrets\n    if: ${{ needs.check-secrets.outputs.secrets_set == 'true' }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Login to Docker Hub\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKER_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Cache Docker layers\n        uses: actions/cache@v4\n        id: cache\n        with:\n          path: /tmp/.buildx-cache\n          key: ${{ runner.os }}-buildx-${{ github.sha }}\n          restore-keys: |\n            ${{ runner.os }}-buildx-\n\n      - name: Determine Tag\n        id: get_tag\n        run: |\n          if [[ \"${{ github.event_name }}\" == \"release\" ]]; then\n            echo \"::set-output name=tag::${{ github.event.release.tag_name }}\"\n          else\n            echo \"::set-output name=tag::${{ github.sha }}\"\n          fi\n\n      - name: Build and push on release\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          push: ${{ github.event_name == 'release' && github.event.action == 'released' }}\n          tags: ${{ secrets.DOCKER_USERNAME }}/${{ github.event.repository.name }}:${{ steps.get_tag.outputs.tag }}\n          platforms: linux/amd64\n          cache-from: type=local,src=/tmp/.buildx-cache\n          cache-to: type=local,dest=/tmp/.buildx-cache\n"
  },
  {
    "path": ".github/workflows/format.yaml",
    "content": "name: Format\n\non: [push, pull_request]\n\njobs:\n  format:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 24\n          cache: npm\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Run checkformat\n        run: npm run checkformat\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint\n\non: [push, pull_request]\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set up Node.js\n        uses: actions/setup-node@v4\n        with:\n          node-version: 24\n          cache: npm\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Run lint\n        run: npm run lint\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test\n\non:\n  push:\n    branches: [main, develop]\n  pull_request:\n    branches: [main, develop]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n\n    strategy:\n      matrix:\n        # LTS versions from https://endoflife.date/nodejs\n        node-version: [20.x, 22.x, 24.x]\n\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n\n      - name: Set up Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v4\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: npm\n\n      - name: Install dependencies\n        run: npm ci\n\n      - name: Run test\n        run: npm run test:ci\n"
  },
  {
    "path": ".gitignore",
    "content": ".DS_Store\n.thumbs.db\nnode_modules\n\n# Quasar core related directories\n.quasar\n/dist\n\n# Cordova related directories and files\n/src-cordova/node_modules\n/src-cordova/platforms\n/src-cordova/plugins\n/src-cordova/www\n\n# Capacitor related directories and files\n/src-capacitor/www\n/src-capacitor/node_modules\n\n# BEX related directories and files\n/src-bex/www\n/src-bex/js/core\n\n# Log files\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n# Editor directories and files\n.idea\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n"
  },
  {
    "path": ".npmrc",
    "content": "# pnpm-related options\nshamefully-hoist=true\nstrict-peer-dependencies=false\n"
  },
  {
    "path": ".postcssrc.js",
    "content": "/* eslint-disable */\n// https://github.com/michael-ciniawsky/postcss-load-config\n\nmodule.exports = {\n  plugins: [\n    // to edit target browsers: use \"browserslist\" field in package.json\n    require(\"autoprefixer\"),\n  ],\n};\n"
  },
  {
    "path": ".prettierignore",
    "content": "dist\nsrc-capacitor\nsrc-cordova\nandroid\nios\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\n    \"dbaeumer.vscode-eslint\",\n    \"esbenp.prettier-vscode\",\n    \"editorconfig.editorconfig\",\n    \"vue.volar\",\n    \"wayou.vscode-todo-highlight\",\n    \"PeterSchmalfeldt.explorer-exclude\"\n  ],\n  \"unwantedRecommendations\": [\n    \"octref.vetur\",\n    \"hookyqr.beautify\",\n    \"dbaeumer.jshint\",\n    \"ms-vscode.vscode-typescript-tslint-plugin\"\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"editor.bracketPairColorization.enabled\": true,\n  \"editor.guides.bracketPairs\": true,\n  \"editor.formatOnSave\": true,\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  // \"editor.codeActionsOnSave\": [\"source.fixAll.eslint\"],\n  \"eslint.validate\": [\"javascript\", \"javascriptreact\", \"typescript\", \"vue\"],\n  \"files.exclude\": {\n    \"**/.git\": true,\n    \"**/.svn\": true,\n    \"**/.hg\": true,\n    \"**/CVS\": true,\n    \"**/.DS_Store\": true,\n    \"**/Thumbs.db\": true,\n    \".editorconfig\": true,\n    \".eslintignore\": true,\n    \".npmrc\": true,\n    \".postcssrc.js\": true,\n    \".quasar\": true,\n    \"dist\": true,\n    \"node_modules\": true\n  },\n  \"explorerExclude.backup\": {}\n}\n"
  },
  {
    "path": "AGENTS.md",
    "content": "# Cashu.me Developer Guide for Agents\n\nThis 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.\n\n## 1. Tech Stack\n\n### Core Frameworks\n\n- **Framework:** [Quasar Framework](https://quasar.dev/) (Vue.js 3 + Vite)\n- **Language:** TypeScript (mostly) and JavaScript.\n- **State Management:** [Pinia](https://pinia.vuejs.org/)\n- **Routing:** Vue Router (standard Quasar setup)\n- **Build Tool:** Vite (via Quasar CLI)\n- **CSS:** SCSS/Sass with Quasar's utility classes.\n\n### Mobile & Desktop\n\n- **Mobile:** [Capacitor](https://capacitorjs.com/) (Android & iOS)\n- **Desktop:** Electron (via Quasar mode)\n- **PWA:** Supported and primary delivery method for web.\n\n### Cashu & Cryptography\n\n- **Cashu Library:** [`@cashu/cashu-ts`](https://github.com/cashubtc/cashu-ts) (Core Cashu wallet logic)\n- **Crypto:** `@cashu/crypto`, `@noble/secp256k1`\n- **Lightning/Bitcoin:** `light-bolt11-decoder`, `bech32`\n\n### Persistence\n\n- **Database:** [Dexie.js](https://dexie.org/) (IndexedDB wrapper) for storing Cashu proofs (tokens).\n- **Local Storage:** `@vueuse/core` (`useLocalStorage`) for user settings, history, and simpler state.\n\n### Testing & Linting\n\n- **Testing:** Vitest\n- **Linting:** ESLint + Prettier\n\n---\n\n## 2. Project Structure\n\n```\n.\n├── src/\n│   ├── assets/          # Static assets (images, icons)\n│   ├── boot/            # Quasar boot files (initialization logic)\n│   ├── components/      # Vue components (UI elements)\n│   ├── css/             # Global styles (SCSS)\n│   ├── i18n/            # Internationalization (locales)\n│   ├── js/              # Utility functions (non-component logic)\n│   ├── layouts/         # App layouts (MainLayout, FullscreenLayout)\n│   ├── pages/           # Route pages (WalletPage, Settings, etc.)\n│   ├── router/          # Vue Router configuration\n│   ├── stores/          # Pinia stores (Critical business logic)\n│   ├── App.vue          # Root component\n│   └── main.js          # Entry point\n├── src-capacitor/       # Capacitor configuration and native projects\n├── src-electron/        # Electron main/preload scripts\n├── src-pwa/             # PWA service worker and manifest\n└── quasar.config.js     # Quasar configuration\n```\n\n---\n\n## 3. Architecture & Key Stores\n\nThe application logic is heavily centralized in Pinia stores found in `src/stores/`.\n\n### Critical Stores\n\n- **`wallet.ts` (`useWalletStore`):** The **primary controller** for the wallet. It handles:\n  - Sending, receiving, melting (paying invoices), and minting tokens.\n  - interacting with the `cashu-ts` library (`CashuWallet`, `CashuMint`).\n  - managing invoice history.\n- **`mints.ts` (`useMintsStore`):** Manages the list of connected mints, their keysets, and URLs.\n- **`proofs.ts` (`useProofsStore`):** Manages the collection of proofs (ecash tokens). Handles CRUD operations for proofs in memory and syncs with storage.\n- **`tokens.ts` (`useTokensStore`):** Manages token history (spent/received tokens log).\n- **`dexie.ts` (`useDexieStore`):** Wrapper around Dexie.js for persistent storage of proofs.\n- **`ui.ts` (`useUiStore`):** Manages UI state (loaders, dialog visibility, tab selection).\n\n### Database Schema (Dexie)\n\nThe `proofs` table in Dexie stores the actual ecash tokens:\n\n- `secret` (string, PK)\n- `amount` (number)\n- `C` (string, curve point)\n- `id` (string, keyset ID)\n- `reserved` (boolean, locked for pending operations)\n- `quote` (string, optional)\n\n---\n\n## 4. Coding Conventions\n\n### Component Style\n\nThe project predominantly uses the **Options API** with **Pinia mappers** within `.vue` files, even though it is a Vue 3 project.\n\n**Pattern:**\n\n```typescript\nimport { defineComponent } from \"vue\";\nimport { mapState, mapActions, mapWritableState } from \"pinia\";\nimport { useWalletStore } from \"src/stores/wallet\";\n\nexport default defineComponent({\n  name: \"MyComponent\",\n  mixins: [windowMixin], // Common mixin used for global window props\n  components: { ... },\n  data() {\n    return { ... };\n  },\n  computed: {\n    ...mapState(useWalletStore, [\"someState\"]),\n    ...mapWritableState(useWalletStore, [\"someWritableState\"]),\n  },\n  methods: {\n    ...mapActions(useWalletStore, [\"someAction\"]),\n    myMethod() {\n      // Logic here\n    }\n  }\n});\n```\n\n**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.\n\n### Styling\n\n- Use **Quasar Utility Classes** (e.g., `q-pa-md`, `text-center`, `row`, `col-12`) whenever possible.\n- Scoped CSS (`<style scoped>`) is used for component-specific overrides.\n- Global variables are in `src/css/quasar.variables.scss`.\n\n### Naming\n\n- **Files:** PascalCase for components (`BalanceView.vue`), camelCase for logic files (`wallet.ts`).\n- **Stores:** `use[Name]Store` (e.g., `useWalletStore`).\n\n---\n\n## 5. Common Patterns & Gotchas\n\n### Wallet Operations\n\nMost 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.\n\n### Mutex Locking\n\nThe 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.\n**Always** ensure mutexes are released in a `finally` block.\n\n### Notifications\n\nUse the helper in `src/js/notify.ts`:\n\n- `notifySuccess(message)`\n- `notifyError(message)`\n- `notifyWarning(message)`\n\n### Platform Detection\n\nThe app runs on Web, Android, and iOS.\n\n- Use `this.getPwaDisplayMode()` (mixin/helper) to detect if running as PWA or browser.\n- Capacitor plugins are often wrapped or used directly in stores/components.\n\n### Assets & Icons\n\n- Icons are typically from `lucide-vue-next` or Quasar's internal Material Icons.\n- Imports: `import { X as XIcon } from \"lucide-vue-next\";`\n\n## 6. Development Workflow\n\n- **Run Dev Server:** `npm run dev` (Starts Vite + Quasar)\n- **Lint:** `npm run lint`\n- **Format:** `npm run format`\n- **Test:** `npm test` (Vitest)\n\nWhen adding dependencies, prefer `npm install`.\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Stage 1: Build Phase\nFROM node:24 AS builder\n\nWORKDIR /app\n\nCOPY package*.json ./\n\n# Install application dependencies\nRUN npm install -g @quasar/cli\nRUN npm install\n\n# Copy the application code to the container\nCOPY . .\n\n# Build the PWA (replace 'npm run build' with your actual build command)\nRUN npm run build:pwa\n\n# Stage 2: Runtime Phase\nFROM nginx\n\n# Copy the built PWA files from the builder stage\nCOPY --from=builder /app/dist/pwa /usr/share/nginx/html\n\n# Expose the port your app will run on\nEXPOSE 80\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2023 Cashu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Cashu (cashu)\n\nCashu Wallet\n\n## One-liner build & run\n\n```\ndocker compose up -d\n```\n\naccess at http://localhost:3000 or serve it behind a reverse proxy.\n\n## Install the dependencies\n\n```bash\nnpm install\n```\n\n### Start the app in development mode (hot-code reloading, error reporting, etc.)\n\n```bash\nquasar dev\n```\n\n### Run unit tests\n\n```bash\nnpm test\n```\n\n### Lint the files\n\n```bash\nnpm run lint\n```\n\n### Format the files\n\n```bash\nnpm run format\n```\n\n### Check translations\n\nUse this to verify non-English translations are in sync with the English source:\n\n```bash\nnpm run i18n:check\n```\n\n### Build the app for production\n\n```bash\nquasar build -m pwa\n```\n\n### Capacitor\n\nAfter updating code, run:\n\n```\nquasar build -m pwa\nnpx cap copy\nnpx cap sync\nnpx cap open android / ios\n```\n\nRegenerate assets:\n\n```\nnpx capacitor-assets generate\n```\n\n### Customize the configuration\n\nSee [Configuring quasar.config.js](https://v2.quasar.dev/quasar-cli-webpack/quasar-config-js).\n\n### Reverse proxy\n\nFor Quasar Vue Router with history mode, add this fallback URL to allow refreshes: https://router.vuejs.org/guide/essentials/history-mode.html#HTML5-Mode\n\nMore info: https://stackoverflow.com/questions/36399319/vue-router-return-404-when-revisit-to-the-url\n\n`Caddyfile`:\n\n```\n# CORS snippet by https://kalnytskyi.com/posts/setup-cors-caddy-2/\n(cors) {\n  @cors_preflight method OPTIONS\n  @cors header Origin {args.0}\n\n  handle @cors_preflight {\n    header Access-Control-Allow-Origin \"{args.0}\"\n    header Access-Control-Allow-Methods \"GET, POST, PUT, PATCH, DELETE\"\n    header Access-Control-Allow-Headers \"Content-Type\"\n    header Access-Control-Max-Age \"3600\"\n    respond \"\" 204\n  }\n\n  handle @cors {\n    header Access-Control-Allow-Origin \"{args.0}\"\n    header Access-Control-Expose-Headers \"Link\"\n  }\n}\nhost.com {\n    import cors *\n    encode gzip\n\n    header /service-worker.js {\n            Service-Worker-Allowed \"/\"\n            Cache-Control \"no-cache\"\n    }\n\n    # SPA root\n    root * /usr/share/caddy/cashu.me/\n\n    # quasar vue router fallback history mode\n    try_files {path} /index.html\n\n    file_server\n}\n```\n"
  },
  {
    "path": "android/.gitignore",
    "content": "# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore\n\n# Built application files\n*.apk\n*.aar\n*.ap_\n*.aab\n\n# Files for the ART/Dalvik VM\n*.dex\n\n# Java class files\n*.class\n\n# Generated files\nbin/\ngen/\nout/\n#  Uncomment the following line in case you need and you don't have the release build type files in your app\n# release/\n\n# Gradle files\n.gradle/\nbuild/\n\n# Local configuration file (sdk path, etc)\nlocal.properties\n\n# Proguard folder generated by Eclipse\nproguard/\n\n# Log Files\n*.log\n\n# Android Studio Navigation editor temp files\n.navigation/\n\n# Android Studio captures folder\ncaptures/\n\n# IntelliJ\n*.iml\n.idea/workspace.xml\n.idea/tasks.xml\n.idea/gradle.xml\n.idea/assetWizardSettings.xml\n.idea/dictionaries\n.idea/libraries\n# Android Studio 3 in .gitignore file.\n.idea/caches\n.idea/modules.xml\n# Comment next line if keeping position of elements in Navigation Editor is relevant for you\n.idea/navEditor.xml\n\n# Keystore files\n# Uncomment the following lines if you do not want to check your keystore files in.\n#*.jks\n#*.keystore\n\n# External native build folder generated in Android Studio 2.2 and later\n.externalNativeBuild\n.cxx/\n\n# Google Services (e.g. APIs or Firebase)\n# google-services.json\n\n# Freeline\nfreeline.py\nfreeline/\nfreeline_project_description.json\n\n# fastlane\nfastlane/report.xml\nfastlane/Preview.html\nfastlane/screenshots\nfastlane/test_output\nfastlane/readme.md\n\n# Version control\nvcs.xml\n\n# lint\nlint/intermediates/\nlint/generated/\nlint/outputs/\nlint/tmp/\n# lint/reports/\n\n# Android Profiling\n*.hprof\n\n# Cordova plugins for Capacitor\ncapacitor-cordova-android-plugins\n\n# Copied web assets\napp/src/main/assets/public\n\n# Generated Config files\napp/src/main/assets/capacitor.config.json\napp/src/main/assets/capacitor.plugins.json\napp/src/main/res/xml/config.xml\n"
  },
  {
    "path": "android/app/.gitignore",
    "content": "/build/*\n!/build/.npmkeep\n"
  },
  {
    "path": "android/app/build.gradle",
    "content": "apply plugin: 'com.android.application'\n\nandroid {\n    namespace \"me.cashu.wallet\"\n    compileSdk rootProject.ext.compileSdkVersion\n    defaultConfig {\n        applicationId \"me.cashu.wallet\"\n        minSdkVersion rootProject.ext.minSdkVersion\n        targetSdkVersion rootProject.ext.targetSdkVersion\n        versionCode 1\n        versionName \"1.0\"\n        testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"\n        aaptOptions {\n             // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.\n             // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61\n            ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'\n        }\n    }\n    buildTypes {\n        release {\n            minifyEnabled false\n            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'\n        }\n    }\n}\n\nrepositories {\n    flatDir{\n        dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'\n    }\n}\n\ndependencies {\n    implementation fileTree(include: ['*.jar'], dir: 'libs')\n    implementation \"androidx.appcompat:appcompat:$androidxAppCompatVersion\"\n    implementation \"androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion\"\n    implementation \"androidx.core:core-splashscreen:$coreSplashScreenVersion\"\n    implementation project(':capacitor-android')\n    testImplementation \"junit:junit:$junitVersion\"\n    androidTestImplementation \"androidx.test.ext:junit:$androidxJunitVersion\"\n    androidTestImplementation \"androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion\"\n    implementation project(':capacitor-cordova-android-plugins')\n}\n\napply from: 'capacitor.build.gradle'\n\ntry {\n    def servicesJSON = file('google-services.json')\n    if (servicesJSON.text) {\n        apply plugin: 'com.google.gms.google-services'\n    }\n} catch(Exception e) {\n    logger.info(\"google-services.json not found, google-services plugin not applied. Push Notifications won't work\")\n}\n"
  },
  {
    "path": "android/app/capacitor.build.gradle",
    "content": "// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME \"capacitor update\" IS RUN\n\nandroid {\n  compileOptions {\n      sourceCompatibility JavaVersion.VERSION_17\n      targetCompatibility JavaVersion.VERSION_17\n  }\n}\n\napply from: \"../capacitor-cordova-android-plugins/cordova.variables.gradle\"\ndependencies {\n    implementation project(':capacitor-clipboard')\n    implementation project(':capacitor-haptics')\n    implementation project(':capacitor-plugin-safe-area')\n\n}\n\n\nif (hasProperty('postBuildExtras')) {\n  postBuildExtras()\n}\n"
  },
  {
    "path": "android/app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java",
    "content": "package com.getcapacitor.myapp;\n\nimport static org.junit.Assert.*;\n\nimport android.content.Context;\nimport androidx.test.ext.junit.runners.AndroidJUnit4;\nimport androidx.test.platform.app.InstrumentationRegistry;\nimport org.junit.Test;\nimport org.junit.runner.RunWith;\n\n/**\n * Instrumented test, which will execute on an Android device.\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\n@RunWith(AndroidJUnit4.class)\npublic class ExampleInstrumentedTest {\n\n    @Test\n    public void useAppContext() throws Exception {\n        // Context of the app under test.\n        Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();\n\n        assertEquals(\"com.getcapacitor.app\", appContext.getPackageName());\n    }\n}\n"
  },
  {
    "path": "android/app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <uses-feature\n    android:name=\"android.hardware.camera\"\n    android:required=\"false\"\n  />\n    <uses-feature\n    android:name=\"android.hardware.nfc\"\n    android:required=\"false\"\n  />\n\n    <application\n    android:allowBackup=\"true\"\n    android:icon=\"@mipmap/ic_launcher\"\n    android:label=\"@string/app_name\"\n    android:roundIcon=\"@mipmap/ic_launcher_round\"\n    android:supportsRtl=\"true\"\n    android:theme=\"@style/AppTheme\"\n  >\n        <activity\n      android:configChanges=\"orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode\"\n      android:name=\".MainActivity\"\n      android:label=\"@string/title_activity_main\"\n      android:theme=\"@style/AppTheme.NoActionBarLaunch\"\n      android:launchMode=\"singleTask\"\n      android:exported=\"true\"\n    >\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n        </activity>\n\n        <provider\n      android:name=\"androidx.core.content.FileProvider\"\n      android:authorities=\"${applicationId}.fileprovider\"\n      android:exported=\"false\"\n      android:grantUriPermissions=\"true\"\n    >\n            <meta-data\n        android:name=\"android.support.FILE_PROVIDER_PATHS\"\n        android:resource=\"@xml/file_paths\"\n      />\n        </provider>\n    </application>\n\n    <!-- Permissions -->\n    <uses-permission android:name=\"android.permission.CAMERA\" />\n    <uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\" />\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.NFC\" />\n</manifest>\n"
  },
  {
    "path": "android/app/src/main/java/me/cashu/wallet/MainActivity.java",
    "content": "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",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<vector\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  android:width=\"108dp\"\n  android:height=\"108dp\"\n  android:viewportHeight=\"108\"\n  android:viewportWidth=\"108\"\n>\n    <path android:fillColor=\"#26A69A\" android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M9,0L9,108\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,0L19,108\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M29,0L29,108\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M39,0L39,108\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M49,0L49,108\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M59,0L59,108\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M69,0L69,108\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M79,0L79,108\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M89,0L89,108\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M99,0L99,108\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,9L108,9\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,19L108,19\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,29L108,29\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,39L108,39\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,49L108,49\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,59L108,59\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,69L108,69\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,79L108,79\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,89L108,89\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M0,99L108,99\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,29L89,29\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,39L89,39\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,49L89,49\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,59L89,59\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,69L89,69\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M19,79L89,79\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M29,19L29,89\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M39,19L39,89\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M49,19L49,89\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M59,19L59,89\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M69,19L69,89\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n    <path\n    android:fillColor=\"#00000000\"\n    android:pathData=\"M79,19L79,89\"\n    android:strokeColor=\"#33FFFFFF\"\n    android:strokeWidth=\"0.8\"\n  />\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:aapt=\"http://schemas.android.com/aapt\"\n  android:width=\"108dp\"\n  android:height=\"108dp\"\n  android:viewportHeight=\"108\"\n  android:viewportWidth=\"108\"\n>\n    <path\n    android:fillType=\"evenOdd\"\n    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\"\n    android:strokeColor=\"#00000000\"\n    android:strokeWidth=\"1\"\n  >\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n        android:endX=\"78.5885\"\n        android:endY=\"90.9159\"\n        android:startX=\"48.7653\"\n        android:startY=\"61.0927\"\n        android:type=\"linear\"\n      >\n                <item android:color=\"#44000000\" android:offset=\"0.0\" />\n                <item android:color=\"#00000000\" android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n    android:fillColor=\"#FFFFFF\"\n    android:fillType=\"nonZero\"\n    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\"\n    android:strokeColor=\"#00000000\"\n    android:strokeWidth=\"1\"\n  />\n</vector>\n"
  },
  {
    "path": "android/app/src/main/res/layout/activity_main.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<androidx.coordinatorlayout.widget.CoordinatorLayout\n  xmlns:android=\"http://schemas.android.com/apk/res/android\"\n  xmlns:app=\"http://schemas.android.com/apk/res-auto\"\n  xmlns:tools=\"http://schemas.android.com/tools\"\n  android:layout_width=\"match_parent\"\n  android:layout_height=\"match_parent\"\n  tools:context=\".MainActivity\"\n>\n\n    <WebView\n    android:layout_width=\"match_parent\"\n    android:layout_height=\"match_parent\"\n  />\n</androidx.coordinatorlayout.widget.CoordinatorLayout>\n"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background>\n        <inset\n      android:drawable=\"@mipmap/ic_launcher_background\"\n      android:inset=\"16.7%\"\n    />\n    </background>\n    <foreground>\n        <inset\n      android:drawable=\"@mipmap/ic_launcher_foreground\"\n      android:inset=\"16.7%\"\n    />\n    </foreground>\n</adaptive-icon>\n"
  },
  {
    "path": "android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background>\n        <inset\n      android:drawable=\"@mipmap/ic_launcher_background\"\n      android:inset=\"16.7%\"\n    />\n    </background>\n    <foreground>\n        <inset\n      android:drawable=\"@mipmap/ic_launcher_foreground\"\n      android:inset=\"16.7%\"\n    />\n    </foreground>\n</adaptive-icon>\n"
  },
  {
    "path": "android/app/src/main/res/values/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<resources>\n    <color name=\"ic_launcher_background\">#FFFFFF</color>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values/strings.xml",
    "content": "<?xml version='1.0' encoding='utf-8' ?>\n<resources>\n    <string name=\"app_name\">Cashu.me</string>\n    <string name=\"title_activity_main\">Cashu.me</string>\n    <string name=\"package_name\">me.cashu.wallet</string>\n    <string name=\"custom_url_scheme\">me.cashu.wallet</string>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/values/styles.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<resources>\n\n    <!-- Base application theme. -->\n    <style name=\"AppTheme\" parent=\"Theme.AppCompat.Light.DarkActionBar\">\n        <!-- Customize your theme here. -->\n        <item name=\"colorPrimary\">@color/colorPrimary</item>\n        <item name=\"colorPrimaryDark\">@color/colorPrimaryDark</item>\n        <item name=\"colorAccent\">@color/colorAccent</item>\n    </style>\n\n    <style\n    name=\"AppTheme.NoActionBar\"\n    parent=\"Theme.AppCompat.DayNight.NoActionBar\"\n  >\n        <item name=\"windowActionBar\">false</item>\n        <item name=\"windowNoTitle\">true</item>\n        <item name=\"android:background\">@null</item>\n    </style>\n\n\n    <style name=\"AppTheme.NoActionBarLaunch\" parent=\"Theme.SplashScreen\">\n        <item name=\"android:background\">@drawable/splash</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "android/app/src/main/res/xml/file_paths.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n<paths xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <external-path name=\"my_images\" path=\".\" />\n    <cache-path name=\"my_cache_images\" path=\".\" />\n</paths>\n"
  },
  {
    "path": "android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java",
    "content": "package com.getcapacitor.myapp;\n\nimport static org.junit.Assert.*;\n\nimport org.junit.Test;\n\n/**\n * Example local unit test, which will execute on the development machine (host).\n *\n * @see <a href=\"http://d.android.com/tools/testing\">Testing documentation</a>\n */\npublic class ExampleUnitTest {\n\n    @Test\n    public void addition_isCorrect() throws Exception {\n        assertEquals(4, 2 + 2);\n    }\n}\n"
  },
  {
    "path": "android/build.gradle",
    "content": "// Top-level build file where you can add configuration options common to all sub-projects/modules.\n\nbuildscript {\n    \n    repositories {\n        google()\n        mavenCentral()\n    }\n    dependencies {\n        classpath 'com.android.tools.build:gradle:8.2.1'\n        classpath 'com.google.gms:google-services:4.4.0'\n\n        // NOTE: Do not place your application dependencies here; they belong\n        // in the individual module build.gradle files\n    }\n}\n\napply from: \"variables.gradle\"\n\nallprojects {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\n\ntask clean(type: Delete) {\n    delete rootProject.buildDir\n}\n"
  },
  {
    "path": "android/capacitor.settings.gradle",
    "content": "// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME \"capacitor update\" IS RUN\ninclude ':capacitor-android'\nproject(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')\n\ninclude ':capacitor-clipboard'\nproject(':capacitor-clipboard').projectDir = new File('../node_modules/@capacitor/clipboard/android')\n\ninclude ':capacitor-haptics'\nproject(':capacitor-haptics').projectDir = new File('../node_modules/@capacitor/haptics/android')\n\ninclude ':capacitor-plugin-safe-area'\nproject(':capacitor-plugin-safe-area').projectDir = new File('../node_modules/capacitor-plugin-safe-area/android')\n"
  },
  {
    "path": "android/gradle/wrapper/gradle-wrapper.properties",
    "content": "distributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.2.1-all.zip\nnetworkTimeout=10000\nvalidateDistributionUrl=true\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "android/gradle.properties",
    "content": "# Project-wide Gradle settings.\n\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx1536m\n\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n"
  },
  {
    "path": "android/gradlew",
    "content": "#!/bin/sh\n\n#\n# Copyright © 2015-2021 the original authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n#\n#   Gradle start up script for POSIX generated by Gradle.\n#\n#   Important for running:\n#\n#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is\n#       noncompliant, but you have some other compliant shell such as ksh or\n#       bash, then to run this script, type that shell name before the whole\n#       command line, like:\n#\n#           ksh Gradle\n#\n#       Busybox and similar reduced shells will NOT work, because this script\n#       requires all of these POSIX shell features:\n#         * functions;\n#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,\n#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;\n#         * compound commands having a testable exit status, especially «case»;\n#         * various built-in commands including «command», «set», and «ulimit».\n#\n#   Important for patching:\n#\n#   (2) This script targets any POSIX shell, so it avoids extensions provided\n#       by Bash, Ksh, etc; in particular arrays are avoided.\n#\n#       The \"traditional\" practice of packing multiple parameters into a\n#       space-separated string is a well documented source of bugs and security\n#       problems, so this is (mostly) avoided, by progressively accumulating\n#       options in \"$@\", and eventually passing that to Java.\n#\n#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,\n#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;\n#       see the in-line comments for details.\n#\n#       There are tweaks for specific operating systems such as AIX, CygWin,\n#       Darwin, MinGW, and NonStop.\n#\n#   (3) This script is generated from the Groovy template\n#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt\n#       within the Gradle project.\n#\n#       You can find Gradle at https://github.com/gradle/gradle/.\n#\n##############################################################################\n\n# Attempt to set APP_HOME\n\n# Resolve links: $0 may be a link\napp_path=$0\n\n# Need this for daisy-chained symlinks.\nwhile\n    APP_HOME=${app_path%\"${app_path##*/}\"}  # leaves a trailing /; empty if no leading path\n    [ -h \"$app_path\" ]\ndo\n    ls=$( ls -ld \"$app_path\" )\n    link=${ls#*' -> '}\n    case $link in             #(\n      /*)   app_path=$link ;; #(\n      *)    app_path=$APP_HOME$link ;;\n    esac\ndone\n\n# This is normally unused\n# shellcheck disable=SC2034\nAPP_BASE_NAME=${0##*/}\nAPP_HOME=$( cd \"${APP_HOME:-./}\" && pwd -P ) || exit\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=maximum\n\nwarn () {\n    echo \"$*\"\n} >&2\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n} >&2\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"$( uname )\" in                #(\n  CYGWIN* )         cygwin=true  ;; #(\n  Darwin* )         darwin=true  ;; #(\n  MSYS* | MINGW* )  msys=true    ;; #(\n  NONSTOP* )        nonstop=true ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=$JAVA_HOME/jre/sh/java\n    else\n        JAVACMD=$JAVA_HOME/bin/java\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=java\n    if ! command -v java >/dev/null 2>&1\n    then\n        die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nfi\n\n# Increase the maximum file descriptors if we can.\nif ! \"$cygwin\" && ! \"$darwin\" && ! \"$nonstop\" ; then\n    case $MAX_FD in #(\n      max*)\n        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC3045\n        MAX_FD=$( ulimit -H -n ) ||\n            warn \"Could not query maximum file descriptor limit\"\n    esac\n    case $MAX_FD in  #(\n      '' | soft) :;; #(\n      *)\n        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.\n        # shellcheck disable=SC3045\n        ulimit -n \"$MAX_FD\" ||\n            warn \"Could not set maximum file descriptor limit to $MAX_FD\"\n    esac\nfi\n\n# Collect all arguments for the java command, stacking in reverse order:\n#   * args from the command line\n#   * the main class name\n#   * -classpath\n#   * -D...appname settings\n#   * --module-path (only if needed)\n#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif \"$cygwin\" || \"$msys\" ; then\n    APP_HOME=$( cygpath --path --mixed \"$APP_HOME\" )\n    CLASSPATH=$( cygpath --path --mixed \"$CLASSPATH\" )\n\n    JAVACMD=$( cygpath --unix \"$JAVACMD\" )\n\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    for arg do\n        if\n            case $arg in                                #(\n              -*)   false ;;                            # don't mess with options #(\n              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath\n                    [ -e \"$t\" ] ;;                      #(\n              *)    false ;;\n            esac\n        then\n            arg=$( cygpath --path --ignore --mixed \"$arg\" )\n        fi\n        # Roll the args list around exactly as many times as the number of\n        # args, so each arg winds up back in the position where it started, but\n        # possibly modified.\n        #\n        # NB: a `for` loop captures its iteration list before it begins, so\n        # changing the positional parameters here affects neither the number of\n        # iterations, nor the values presented in `arg`.\n        shift                   # remove old arg\n        set -- \"$@\" \"$arg\"      # push replacement arg\n    done\nfi\n\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Collect all arguments for the java command;\n#   * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of\n#     shell script including quotes and variable substitutions, so put them in\n#     double quotes to make sure that they get re-expanded; and\n#   * put everything else in single quotes, so that it's not re-expanded.\n\nset -- \\\n        \"-Dorg.gradle.appname=$APP_BASE_NAME\" \\\n        -classpath \"$CLASSPATH\" \\\n        org.gradle.wrapper.GradleWrapperMain \\\n        \"$@\"\n\n# Stop when \"xargs\" is not available.\nif ! command -v xargs >/dev/null 2>&1\nthen\n    die \"xargs is not available\"\nfi\n\n# Use \"xargs\" to parse quoted args.\n#\n# With -n1 it outputs one arg per line, with the quotes and backslashes removed.\n#\n# In Bash we could simply go:\n#\n#   readarray ARGS < <( xargs -n1 <<<\"$var\" ) &&\n#   set -- \"${ARGS[@]}\" \"$@\"\n#\n# but POSIX shell has neither arrays nor command substitution, so instead we\n# post-process each arg (as a line of input to sed) to backslash-escape any\n# character that might be a shell metacharacter, then use eval to reverse\n# that process (while maintaining the separation between arguments), and wrap\n# the whole thing up as a single \"set\" statement.\n#\n# This will of course break if any of these variables contains a newline or\n# an unmatched quote.\n#\n\neval \"set -- $(\n        printf '%s\\n' \"$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS\" |\n        xargs -n1 |\n        sed ' s~[^-[:alnum:]+,./:=@_]~\\\\&~g; ' |\n        tr '\\n' ' '\n    )\" '\"$@\"'\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "android/gradlew.bat",
    "content": "@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 (the \"License\");\r\n@rem you may not use this file except in compliance with the License.\r\n@rem You may obtain a copy of the License at\r\n@rem\r\n@rem      https://www.apache.org/licenses/LICENSE-2.0\r\n@rem\r\n@rem Unless required by applicable law or agreed to in writing, software\r\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\r\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n@rem See the License for the specific language governing permissions and\r\n@rem limitations under the License.\r\n@rem\r\n\r\n@if \"%DEBUG%\"==\"\" @echo off\r\n@rem ##########################################################################\r\n@rem\r\n@rem  Gradle startup script for Windows\r\n@rem\r\n@rem ##########################################################################\r\n\r\n@rem Set local scope for the variables with windows NT shell\r\nif \"%OS%\"==\"Windows_NT\" setlocal\r\n\r\nset DIRNAME=%~dp0\r\nif \"%DIRNAME%\"==\"\" set DIRNAME=.\r\n@rem This is normally unused\r\nset APP_BASE_NAME=%~n0\r\nset APP_HOME=%DIRNAME%\r\n\r\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\r\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\r\n\r\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\r\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\r\n\r\n@rem Find java.exe\r\nif defined JAVA_HOME goto findJavaFromJavaHome\r\n\r\nset JAVA_EXE=java.exe\r\n%JAVA_EXE% -version >NUL 2>&1\r\nif %ERRORLEVEL% equ 0 goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:findJavaFromJavaHome\r\nset JAVA_HOME=%JAVA_HOME:\"=%\r\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\r\n\r\nif exist \"%JAVA_EXE%\" goto execute\r\n\r\necho.\r\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\r\necho.\r\necho Please set the JAVA_HOME variable in your environment to match the\r\necho location of your Java installation.\r\n\r\ngoto fail\r\n\r\n:execute\r\n@rem Setup the command line\r\n\r\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\r\n\r\n\r\n@rem Execute Gradle\r\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\r\n\r\n:end\r\n@rem End local scope for the variables with windows NT shell\r\nif %ERRORLEVEL% equ 0 goto mainEnd\r\n\r\n:fail\r\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\r\nrem the _cmd.exe /c_ return code!\r\nset EXIT_CODE=%ERRORLEVEL%\r\nif %EXIT_CODE% equ 0 set EXIT_CODE=1\r\nif not \"\"==\"%GRADLE_EXIT_CONSOLE%\" exit %EXIT_CODE%\r\nexit /b %EXIT_CODE%\r\n\r\n:mainEnd\r\nif \"%OS%\"==\"Windows_NT\" endlocal\r\n\r\n:omega\r\n"
  },
  {
    "path": "android/settings.gradle",
    "content": "include ':app'\ninclude ':capacitor-cordova-android-plugins'\nproject(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')\n\napply from: 'capacitor.settings.gradle'"
  },
  {
    "path": "android/variables.gradle",
    "content": "ext {\n    minSdkVersion = 22\n    compileSdkVersion = 34\n    targetSdkVersion = 34\n    androidxActivityVersion = '1.8.0'\n    androidxAppCompatVersion = '1.6.1'\n    androidxCoordinatorLayoutVersion = '1.2.0'\n    androidxCoreVersion = '1.12.0'\n    androidxFragmentVersion = '1.6.2'\n    coreSplashScreenVersion = '1.0.1'\n    androidxWebkitVersion = '1.9.0'\n    junitVersion = '4.13.2'\n    androidxJunitVersion = '1.1.5'\n    androidxEspressoCoreVersion = '3.5.1'\n    cordovaAndroidVersion = '10.1.1'\n}"
  },
  {
    "path": "capacitor.config.ts",
    "content": "import type { CapacitorConfig } from \"@capacitor/cli\";\n\nconst config: CapacitorConfig = {\n  appId: \"me.cashu.wallet\",\n  appName: \"Cashu.me\",\n  webDir: \"dist/pwa/\",\n};\n\nexport default config;\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "services:\n  cashu.me:\n    image: cashu.me\n    build: .\n    container_name: cashu.me\n    restart: always\n    ports:\n      - \"127.0.0.1:3000:80\"\n"
  },
  {
    "path": "extension/embedder.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\" />\n  </head>\n  <body>\n    <iframe id=\"iframe\" src=\"https://wallet.cashu.me\"></iframe>\n  </body>\n</html>\n"
  },
  {
    "path": "extension/manifest.json",
    "content": "{\n  \"name\": \"Cashu.me\",\n  \"description\": \"A privacy-preserving ecash wallet for Bitcoin Lightning\",\n  \"icons\": { \"128\": \"128.png\", \"32\": \"32.png\", \"16\": \"16.png\" },\n  \"manifest_version\": 3,\n  \"version\": \"0.1\",\n  \"action\": {\n    \"default_icon\": \"32.png\",\n    \"default_popup\": \"embedder.html\",\n    \"default_title\": \"Cashu.me\"\n  },\n  \"permissions\": [\"unlimitedStorage\", \"storage\"]\n}\n"
  },
  {
    "path": "extension/style.css",
    "content": "body {\n  background: #350a60;\n  margin: 0;\n  width: 580px;\n  height: 595px;\n}\niframe {\n  width: 100%;\n  height: 100%;\n  border: 0;\n}\n"
  },
  {
    "path": "index.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n    <title><%= productName %></title>\n\n    <meta charset=\"utf-8\" />\n    <meta name=\"description\" content=\"<%= productDescription %>\" />\n    <meta name=\"format-detection\" content=\"telephone=no\" />\n    <meta name=\"msapplication-tap-highlight\" content=\"no\" />\n    <meta\n      name=\"viewport\"\n      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<% } %>\"\n    />\n\n    <link\n      rel=\"icon\"\n      type=\"image/png\"\n      sizes=\"128x128\"\n      href=\"icons/128x128.png\"\n    />\n    <link rel=\"icon\" type=\"image/png\" sizes=\"96x96\" href=\"icons/96x96.png\" />\n    <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"icons/32x32.png\" />\n    <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"icons/16x16.png\" />\n    <link rel=\"icon\" type=\"image/ico\" href=\"favicon.ico\" />\n  </head>\n  <body>\n    <!-- quasar:entry-point -->\n  </body>\n</html>\n"
  },
  {
    "path": "ios/.gitignore",
    "content": "App/build\nApp/Pods\nApp/output\nApp/App/public\nDerivedData\nxcuserdata\n\n# Cordova plugins for Capacitor\ncapacitor-cordova-ios-plugins\n\n# Generated Config files\nApp/App/capacitor.config.json\nApp/App/config.xml\n"
  },
  {
    "path": "ios/App/App/AppDelegate.swift",
    "content": "import UIKit\nimport Capacitor\n\n@UIApplicationMain\nclass AppDelegate: UIResponder, UIApplicationDelegate {\n\n    var window: UIWindow?\n\n    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {\n        // Override point for customization after application launch.\n        return true\n    }\n\n    func applicationWillResignActive(_ application: UIApplication) {\n        // 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.\n        // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.\n    }\n\n    func applicationDidEnterBackground(_ application: UIApplication) {\n        // 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.\n        // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.\n    }\n\n    func applicationWillEnterForeground(_ application: UIApplication) {\n        // 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.\n    }\n\n    func applicationDidBecomeActive(_ application: UIApplication) {\n        // 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.\n    }\n\n    func applicationWillTerminate(_ application: UIApplication) {\n        // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.\n    }\n\n    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {\n        // Called when the app was launched with a url. Feel free to add additional processing here,\n        // but if you want the App API to support tracking app url opens, make sure to keep this call\n        return ApplicationDelegateProxy.shared.application(app, open: url, options: options)\n    }\n\n    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {\n        // Called when the app was launched with an activity, including Universal Links.\n        // Feel free to add additional processing here, but if you want the App API to support\n        // tracking app url opens, make sure to keep this call\n        return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)\n    }\n\n}\n"
  },
  {
    "path": "ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\": [\n    {\n      \"idiom\": \"universal\",\n      \"size\": \"1024x1024\",\n      \"filename\": \"AppIcon-512@2x.png\",\n      \"platform\": \"ios\"\n    }\n  ],\n  \"info\": {\n    \"author\": \"xcode\",\n    \"version\": 1\n  }\n}\n"
  },
  {
    "path": "ios/App/App/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\": {\n    \"version\": 1,\n    \"author\": \"xcode\"\n  }\n}\n"
  },
  {
    "path": "ios/App/App/Assets.xcassets/Splash.imageset/Contents.json",
    "content": "{\n  \"images\": [\n    {\n      \"idiom\": \"universal\",\n      \"filename\": \"Default@1x~universal~anyany.png\",\n      \"scale\": \"1x\"\n    },\n    {\n      \"idiom\": \"universal\",\n      \"filename\": \"Default@2x~universal~anyany.png\",\n      \"scale\": \"2x\"\n    },\n    {\n      \"idiom\": \"universal\",\n      \"filename\": \"Default@3x~universal~anyany.png\",\n      \"scale\": \"3x\"\n    },\n    {\n      \"appearances\": [\n        {\n          \"appearance\": \"luminosity\",\n          \"value\": \"dark\"\n        }\n      ],\n      \"idiom\": \"universal\",\n      \"scale\": \"1x\",\n      \"filename\": \"Default@1x~universal~anyany-dark.png\"\n    },\n    {\n      \"appearances\": [\n        {\n          \"appearance\": \"luminosity\",\n          \"value\": \"dark\"\n        }\n      ],\n      \"idiom\": \"universal\",\n      \"scale\": \"2x\",\n      \"filename\": \"Default@2x~universal~anyany-dark.png\"\n    },\n    {\n      \"appearances\": [\n        {\n          \"appearance\": \"luminosity\",\n          \"value\": \"dark\"\n        }\n      ],\n      \"idiom\": \"universal\",\n      \"scale\": \"3x\",\n      \"filename\": \"Default@3x~universal~anyany-dark.png\"\n    }\n  ],\n  \"info\": {\n    \"version\": 1,\n    \"author\": \"xcode\"\n  }\n}\n"
  },
  {
    "path": "ios/App/App/Base.lproj/LaunchScreen.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<document\n  type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\"\n  version=\"3.0\"\n  toolsVersion=\"17132\"\n  targetRuntime=\"iOS.CocoaTouch\"\n  propertyAccessControl=\"none\"\n  useAutolayout=\"YES\"\n  launchScreen=\"YES\"\n  useTraitCollections=\"YES\"\n  useSafeAreas=\"YES\"\n  colorMatched=\"YES\"\n  initialViewController=\"01J-lp-oVM\"\n>\n    <device id=\"retina4_7\" orientation=\"portrait\" appearance=\"light\" />\n    <dependencies>\n        <deployment identifier=\"iOS\" />\n        <plugIn\n      identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\"\n      version=\"17105\"\n    />\n        <capability\n      name=\"System colors in document resources\"\n      minToolsVersion=\"11.0\"\n    />\n        <capability\n      name=\"documents saved in the Xcode 8 format\"\n      minToolsVersion=\"8.0\"\n    />\n    </dependencies>\n    <scenes>\n        <!--View Controller-->\n        <scene sceneID=\"EHf-IW-A2E\">\n            <objects>\n                <viewController id=\"01J-lp-oVM\" sceneMemberID=\"viewController\">\n                    <imageView\n            key=\"view\"\n            userInteractionEnabled=\"NO\"\n            contentMode=\"scaleAspectFill\"\n            horizontalHuggingPriority=\"251\"\n            verticalHuggingPriority=\"251\"\n            image=\"Splash\"\n            id=\"snD-IY-ifK\"\n          >\n                        <rect\n              key=\"frame\"\n              x=\"0.0\"\n              y=\"0.0\"\n              width=\"375\"\n              height=\"667\"\n            />\n                        <autoresizingMask key=\"autoresizingMask\" />\n                        <color\n              key=\"backgroundColor\"\n              systemColor=\"systemBackgroundColor\"\n            />\n                    </imageView>\n                </viewController>\n                <placeholder\n          placeholderIdentifier=\"IBFirstResponder\"\n          id=\"iYj-Kq-Ea1\"\n          userLabel=\"First Responder\"\n          sceneMemberID=\"firstResponder\"\n        />\n            </objects>\n            <point key=\"canvasLocation\" x=\"53\" y=\"375\" />\n        </scene>\n    </scenes>\n    <resources>\n        <image name=\"Splash\" width=\"1366\" height=\"1366\" />\n        <systemColor name=\"systemBackgroundColor\">\n            <color\n        white=\"1\"\n        alpha=\"1\"\n        colorSpace=\"custom\"\n        customColorSpace=\"genericGamma22GrayColorSpace\"\n      />\n        </systemColor>\n    </resources>\n</document>\n"
  },
  {
    "path": "ios/App/App/Base.lproj/Main.storyboard",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<document\n  type=\"com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB\"\n  version=\"3.0\"\n  toolsVersion=\"14111\"\n  targetRuntime=\"iOS.CocoaTouch\"\n  propertyAccessControl=\"none\"\n  useAutolayout=\"YES\"\n  useTraitCollections=\"YES\"\n  colorMatched=\"YES\"\n  initialViewController=\"BYZ-38-t0r\"\n>\n    <device id=\"retina4_7\" orientation=\"portrait\">\n        <adaptation id=\"fullscreen\" />\n    </device>\n    <dependencies>\n        <deployment identifier=\"iOS\" />\n        <plugIn\n      identifier=\"com.apple.InterfaceBuilder.IBCocoaTouchPlugin\"\n      version=\"14088\"\n    />\n    </dependencies>\n    <scenes>\n        <!--Bridge View Controller-->\n        <scene sceneID=\"tne-QT-ifu\">\n            <objects>\n                <viewController\n          id=\"BYZ-38-t0r\"\n          customClass=\"CAPBridgeViewController\"\n          customModule=\"Capacitor\"\n          sceneMemberID=\"viewController\"\n        />\n                <placeholder\n          placeholderIdentifier=\"IBFirstResponder\"\n          id=\"dkx-z0-nzr\"\n          sceneMemberID=\"firstResponder\"\n        />\n            </objects>\n        </scene>\n    </scenes>\n</document>\n"
  },
  {
    "path": "ios/App/App/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\"\n  \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>en</string>\n\t<key>CFBundleDisplayName</key>\n\t<string>Cashu.me</string>\n\t<key>CFBundleExecutable</key>\n\t<string>$(EXECUTABLE_NAME)</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>$(PRODUCT_NAME)</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>$(MARKETING_VERSION)</string>\n\t<key>CFBundleVersion</key>\n\t<string>$(CURRENT_PROJECT_VERSION)</string>\n\t<key>LSRequiresIPhoneOS</key>\n\t<true />\n\t<key>UILaunchStoryboardName</key>\n\t<string>LaunchScreen</string>\n\t<key>UIMainStoryboardFile</key>\n\t<string>Main</string>\n\t<key>UIRequiredDeviceCapabilities</key>\n\t<array>\n\t\t<string>armv7</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t</array>\n\t<key>UISupportedInterfaceOrientations~ipad</key>\n\t<array>\n\t\t<string>UIInterfaceOrientationPortrait</string>\n\t\t<string>UIInterfaceOrientationPortraitUpsideDown</string>\n\t\t<string>UIInterfaceOrientationLandscapeLeft</string>\n\t\t<string>UIInterfaceOrientationLandscapeRight</string>\n\t</array>\n\t<key>UIViewControllerBasedStatusBarAppearance</key>\n\t<true />\n  <key>NSCameraUsageDescription</key>\n  <string>The app uses the camera to scan QR codes.</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/App/App.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 48;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; };\n\t\t50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; };\n\t\t504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; };\n\t\t504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; };\n\t\t504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };\n\t\t504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };\n\t\t50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };\n\t\tA084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXFileReference section */\n\t\t2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = \"<group>\"; };\n\t\t50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = \"<group>\"; };\n\t\t504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = \"<group>\"; };\n\t\t504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = \"<group>\"; };\n\t\t504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = \"<group>\"; };\n\t\t504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = \"<group>\"; };\n\t\t50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = \"<group>\"; };\n\t\tAF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\tAF51FD2D460BCFE21FA515B2 /* 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>\"; };\n\t\tFC68EB0AF532CFC21C3344DD /* 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>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t504EC3011FED79650016851F /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\tA084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t27E2DDA53C4D2A4D1A88CE4A /* Frameworks */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tAF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */,\n\t\t\t);\n\t\t\tname = Frameworks;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t504EC2FB1FED79650016851F = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t504EC3061FED79650016851F /* App */,\n\t\t\t\t504EC3051FED79650016851F /* Products */,\n\t\t\t\t7F8756D8B27F46E3366F6CEA /* Pods */,\n\t\t\t\t27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t504EC3051FED79650016851F /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t504EC3041FED79650016851F /* App.app */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t504EC3061FED79650016851F /* App */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t50379B222058CBB4000EE86E /* capacitor.config.json */,\n\t\t\t\t504EC3071FED79650016851F /* AppDelegate.swift */,\n\t\t\t\t504EC30B1FED79650016851F /* Main.storyboard */,\n\t\t\t\t504EC30E1FED79650016851F /* Assets.xcassets */,\n\t\t\t\t504EC3101FED79650016851F /* LaunchScreen.storyboard */,\n\t\t\t\t504EC3131FED79650016851F /* Info.plist */,\n\t\t\t\t2FAD9762203C412B000D30F8 /* config.xml */,\n\t\t\t\t50B271D01FEDC1A000F3C39B /* public */,\n\t\t\t);\n\t\t\tpath = App;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t7F8756D8B27F46E3366F6CEA /* Pods */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\tFC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */,\n\t\t\t\tAF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */,\n\t\t\t);\n\t\t\tname = Pods;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t504EC3031FED79650016851F /* App */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget \"App\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */,\n\t\t\t\t504EC3001FED79650016851F /* Sources */,\n\t\t\t\t504EC3011FED79650016851F /* Frameworks */,\n\t\t\t\t504EC3021FED79650016851F /* Resources */,\n\t\t\t\t9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = App;\n\t\t\tproductName = App;\n\t\t\tproductReference = 504EC3041FED79650016851F /* App.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t504EC2FC1FED79650016851F /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tLastSwiftUpdateCheck = 920;\n\t\t\t\tLastUpgradeCheck = 1620;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t504EC3031FED79650016851F = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 9.2;\n\t\t\t\t\t\tLastSwiftMigration = 1100;\n\t\t\t\t\t\tProvisioningStyle = Automatic;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject \"App\" */;\n\t\t\tcompatibilityVersion = \"Xcode 8.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 504EC2FB1FED79650016851F;\n\t\t\tpackageReferences = (\n\t\t\t);\n\t\t\tproductRefGroup = 504EC3051FED79650016851F /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t504EC3031FED79650016851F /* App */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t504EC3021FED79650016851F /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */,\n\t\t\t\t50B271D11FEDC1A000F3C39B /* public in Resources */,\n\t\t\t\t504EC30F1FED79650016851F /* Assets.xcassets in Resources */,\n\t\t\t\t50379B232058CBB4000EE86E /* capacitor.config.json in Resources */,\n\t\t\t\t504EC30D1FED79650016851F /* Main.storyboard in Resources */,\n\t\t\t\t2FAD9763203C412B000D30F8 /* config.xml in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t6634F4EFEBD30273BCE97C65 /* [CP] Check Pods Manifest.lock */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t\t\"${PODS_PODFILE_DIR_PATH}/Podfile.lock\",\n\t\t\t\t\"${PODS_ROOT}/Manifest.lock\",\n\t\t\t);\n\t\t\tname = \"[CP] Check Pods Manifest.lock\";\n\t\t\toutputPaths = (\n\t\t\t\t\"$(DERIVED_FILE_DIR)/Pods-App-checkManifestLockResult.txt\",\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"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\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n\t\t9592DBEFFC6D2A0C8D5DEB22 /* [CP] Embed Pods Frameworks */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\tname = \"[CP] Embed Pods Frameworks\";\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"\\\"${PODS_ROOT}/Target Support Files/Pods-App/Pods-App-frameworks.sh\\\"\\n\";\n\t\t\tshowEnvVarsInLog = 0;\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t504EC3001FED79650016851F /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t504EC3081FED79650016851F /* AppDelegate.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXVariantGroup section */\n\t\t504EC30B1FED79650016851F /* Main.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t504EC30C1FED79650016851F /* Base */,\n\t\t\t);\n\t\t\tname = Main.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t504EC3101FED79650016851F /* LaunchScreen.storyboard */ = {\n\t\t\tisa = PBXVariantGroup;\n\t\t\tchildren = (\n\t\t\t\t504EC3111FED79650016851F /* Base */,\n\t\t\t);\n\t\t\tname = LaunchScreen.storyboard;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXVariantGroup section */\n\n/* Begin XCBuildConfiguration section */\n\t\t504EC3141FED79650016851F /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t504EC3151FED79650016851F /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++14\";\n\t\t\t\tCLANG_CXX_LIBRARY = \"libc++\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCODE_SIGN_IDENTITY = \"iPhone Developer\";\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu11;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Owholemodule\";\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t504EC3171FED79650016851F /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = 3XDA5YR8QS;\n\t\t\t\tINFOPLIST_FILE = App/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = \"$(inherited) @executable_path/Frameworks\";\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tOTHER_SWIFT_FLAGS = \"$(inherited) \\\"-D\\\" \\\"COCOAPODS\\\" \\\"-DDEBUG\\\"\";\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = me.cashu.wallet;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t504EC3181FED79650016851F /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbaseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tCODE_SIGN_IDENTITY = \"Apple Development\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_TEAM = 3XDA5YR8QS;\n\t\t\t\tINFOPLIST_FILE = App/Info.plist;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 13.0;\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = \"$(inherited) @executable_path/Frameworks\";\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = me.cashu.wallet;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tPROVISIONING_PROFILE_SPECIFIER = \"\";\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = \"\";\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t504EC2FF1FED79650016851F /* Build configuration list for PBXProject \"App\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t504EC3141FED79650016851F /* Debug */,\n\t\t\t\t504EC3151FED79650016851F /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget \"App\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t504EC3171FED79650016851F /* Debug */,\n\t\t\t\t504EC3181FED79650016851F /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 504EC2FC1FED79650016851F /* Project object */;\n}\n"
  },
  {
    "path": "ios/App/App.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"group:App.xcodeproj\">\n   </FileRef>\n   <FileRef\n      location = \"group:Pods/Pods.xcodeproj\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "ios/App/App.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "ios/App/Podfile",
    "content": "require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'\n\nplatform :ios, '13.0'\nuse_frameworks!\n\n# workaround to avoid Xcode caching of Pods that requires\n# Product -> Clean Build Folder after new Cordova plugins installed\n# Requires CocoaPods 1.6 or newer\ninstall! 'cocoapods', :disable_input_output_paths => true\n\ndef capacitor_pods\n  pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'\n  pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'\n  pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard'\n  pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'\n  pod 'CapacitorPluginSafeArea', :path => '../../node_modules/capacitor-plugin-safe-area'\nend\n\ntarget 'App' do\n  capacitor_pods\n  # Add your Pods here\nend\n\npost_install do |installer|\n  assertDeploymentTarget(installer)\nend\n"
  },
  {
    "path": "jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"paths\": {\n      \"src/*\": [\"src/*\"],\n      \"app/*\": [\"*\"],\n      \"components/*\": [\"src/components/*\"],\n      \"layouts/*\": [\"src/layouts/*\"],\n      \"pages/*\": [\"src/pages/*\"],\n      \"assets/*\": [\"src/assets/*\"],\n      \"boot/*\": [\"src/boot/*\"],\n      \"stores/*\": [\"src/stores/*\"],\n      \"vue$\": [\"node_modules/vue/dist/vue.runtime.esm-bundler.js\"]\n    }\n  },\n  \"exclude\": [\"dist\", \".quasar\", \"node_modules\", \"android\", \"ios\"]\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"cashu\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Cashu.me Wallet\",\n  \"productName\": \"Cashu.me\",\n  \"author\": \"cashu.me\",\n  \"private\": true,\n  \"scripts\": {\n    \"i18n:check\": \"node scripts/check-i18n.js\",\n    \"dev\": \"quasar dev\",\n    \"build\": \"quasar build\",\n    \"build:pwa\": \"quasar build -m pwa\",\n    \"build:electron\": \"quasar build -m electron\",\n    \"quasar\": \"quasar\",\n    \"lint\": \"eslint --ext .js,.vue,.ts ./\",\n    \"format\": \"prettier --write .\",\n    \"checkformat\": \"prettier --check .\",\n    \"test\": \"vitest\",\n    \"test:ci\": \"vitest run\"\n  },\n  \"dependencies\": {\n    \"@capacitor/clipboard\": \"^6.0.2\",\n    \"@capacitor/core\": \"^6.2.0\",\n    \"@capacitor/haptics\": \"^6.0.2\",\n    \"@cashu/cashu-ts\": \"^3.5.0\",\n    \"@chenfengyuan/vue-qrcode\": \"^2.0.0\",\n    \"@gandlaf21/bc-ur\": \"^1.1.12\",\n    \"@nostr-dev-kit/ndk\": \"^2.8.1\",\n    \"@quasar/extras\": \"^1.17.0\",\n    \"@scure/bip39\": \"^1.4.0\",\n    \"@vueuse/core\": \"^10.9.0\",\n    \"axios\": \"^1.6.8\",\n    \"bech32\": \"^2.0.0\",\n    \"capacitor-plugin-safe-area\": \"^3.0.4\",\n    \"core-js\": \"^3.37.0\",\n    \"date-fns\": \"^3.6.0\",\n    \"dexie\": \"^4.0.9\",\n    \"light-bolt11-decoder\": \"^3.1.1\",\n    \"lucide-vue-next\": \"^0.453.0\",\n    \"nostr-tools\": \"^2.5.2\",\n    \"pinia\": \"^2.1.7\",\n    \"qr-scanner\": \"^1.4.2\",\n    \"qrcode\": \"^1.5.3\",\n    \"quasar\": \"^2.18.2\",\n    \"underscore\": \"^1.13.6\",\n    \"vue\": \"^3.4.27\",\n    \"vue-i18n\": \"^11.1.3\",\n    \"vue-router\": \"^4.3.2\"\n  },\n  \"devDependencies\": {\n    \"@capacitor/android\": \"^6.2.0\",\n    \"@capacitor/assets\": \"^3.0.5\",\n    \"@capacitor/cli\": \"^6.2.0\",\n    \"@capacitor/ios\": \"^6.0.0\",\n    \"@electron/packager\": \"^18.4.4\",\n    \"@quasar/app-vite\": \"^1.11.0\",\n    \"@quasar/icongenie\": \"^4.0.0\",\n    \"@types/node\": \"^20.12.11\",\n    \"@types/underscore\": \"^1.11.15\",\n    \"@types/uuid\": \"^10.0.0\",\n    \"@typescript-eslint/eslint-plugin\": \"^5.62.0\",\n    \"@typescript-eslint/parser\": \"^5.62.0\",\n    \"@vue/test-utils\": \"^2.4.6\",\n    \"autoprefixer\": \"^10.4.19\",\n    \"electron\": \"^38.1.0\",\n    \"eslint\": \"^8.57.0\",\n    \"eslint-config-prettier\": \"^8.10.0\",\n    \"eslint-plugin-vue\": \"^9.26.0\",\n    \"happy-dom\": \"^17.4.4\",\n    \"prettier\": \"^2.8.8\",\n    \"typescript\": \"^5.4.5\",\n    \"vite-jsconfig-paths\": \"^2.0.1\",\n    \"vitest\": \"^3.1.1\",\n    \"workbox-build\": \"^6.6.1\",\n    \"workbox-cacheable-response\": \"^6.6.1\",\n    \"workbox-core\": \"^6.6.1\",\n    \"workbox-expiration\": \"^6.6.1\",\n    \"workbox-precaching\": \"^6.6.1\",\n    \"workbox-routing\": \"^6.6.1\",\n    \"workbox-strategies\": \"^6.6.1\"\n  },\n  \"engines\": {\n    \"node\": \">=16.11.0\",\n    \"npm\": \">= 6.13.4\",\n    \"yarn\": \">= 1.21.1\"\n  }\n}\n"
  },
  {
    "path": "quasar.config.js",
    "content": "/* 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 features that are supported by your Node version. https://node.green/\n */\n\n// Configuration for your app\n// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js\n\nconst { configure } = require(\"quasar/wrappers\");\nconst { execSync } = require(\"child_process\");\n\nfunction resolveGitCommit() {\n  try {\n    return execSync(\"git describe --always --dirty\", {\n      cwd: __dirname,\n      stdio: \"pipe\",\n    })\n      .toString()\n      .trim();\n  } catch (err) {\n    console.warn(\"Unable to resolve git commit via `git describe`\");\n    return \"unknown\";\n  }\n}\n\nmodule.exports = configure(function (/* ctx */) {\n  return {\n    eslint: {\n      // fix: true,\n      // include: [],\n      // exclude: [],\n      // rawOptions: {},\n      warnings: true,\n      errors: true,\n    },\n\n    // https://v2.quasar.dev/quasar-cli/prefetch-feature\n    // preFetch: true,\n\n    // app boot file (/src/boot)\n    // --> boot files are part of \"main.js\"\n    // https://v2.quasar.dev/quasar-cli/boot-files\n    boot: [\"base\", \"global-components\", \"cashu\", \"i18n\"],\n\n    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css\n    css: [\"app.scss\", \"base.scss\"],\n\n    // https://github.com/quasarframework/quasar/tree/dev/extras\n    extras: [\n      // 'ionicons-v4',\n      // 'mdi-v5',\n      // 'fontawesome-v6',\n      // 'eva-icons',\n      // 'themify',\n      // 'line-awesome',\n      // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!\n\n      \"roboto-font\", // optional, you are not bound to it\n      \"material-icons\", // optional, you are not bound to it\n    ],\n\n    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build\n    build: {\n      target: {\n        browser: [\"esnext\"],\n        node: \"node16\",\n      },\n\n      vueRouterMode: \"history\", // available values: 'hash', 'history'\n      // vueRouterBase,\n      // vueDevtools,\n      // vueOptionsAPI: false,\n\n      // rebuildCache: true, // rebuilds Vite/linter/etc cache on startup\n\n      // publicPath: '/',\n      // analyze: true,\n      // env: {},\n      // rawDefine: {}\n      // ignorePublicFolder: true,\n      // minify: false,\n      // polyfillModulePreload: true,\n      // distDir\n\n      extendViteConf(viteConf) {\n        viteConf.define = viteConf.define || {};\n        viteConf.define.GIT_COMMIT = JSON.stringify(resolveGitCommit());\n      },\n      // viteVuePluginOptions: {},\n\n      // vitePlugins: [\n      //   [ 'package-name', { ..options.. } ]\n      // ]\n    },\n\n    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer\n    devServer: {\n      https: true,\n      open: true, // opens browser window automatically\n      port: 8080,\n    },\n\n    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework\n    framework: {\n      config: {},\n\n      iconSet: \"material-icons\", // Quasar icon set\n      // lang: 'en-US', // Quasar language pack\n\n      // For special cases outside of where the auto-import strategy can have an impact\n      // (like functional components as one of the examples),\n      // you can manually specify Quasar components/directives to be available everywhere:\n      //\n      // components: [],\n      // directives: [],\n\n      // Quasar plugins\n      plugins: [\"LocalStorage\", \"Notify\"],\n    },\n\n    animations: \"all\", // --- includes all animations\n    // https://v2.quasar.dev/options/animations\n    // animations: [],\n\n    // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles\n    // sourceFiles: {\n    //   rootComponent: 'src/App.vue',\n    //   router: 'src/router/index',\n    //   store: 'src/store/index',\n    //   registerServiceWorker: 'src-pwa/register-service-worker',\n    //   serviceWorker: 'src-pwa/custom-service-worker',\n    //   pwaManifestFile: 'src-pwa/manifest.json',\n    //   electronMain: 'src-electron/electron-main',\n    //   electronPreload: 'src-electron/electron-preload'\n    // },\n\n    // https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr\n    ssr: {\n      // ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!\n      // will mess up SSR\n\n      // extendSSRWebserverConf (esbuildConf) {},\n      // extendPackageJson (json) {},\n\n      pwa: false,\n\n      // manualStoreHydration: true,\n      // manualPostHydrationTrigger: true,\n\n      prodPort: 3000, // The default port that the production server should use\n      // (gets superseded if process.env.PORT is specified at runtime)\n\n      middlewares: [\n        \"render\", // keep this as last one\n      ],\n    },\n\n    // https://v2.quasar.dev/quasar-cli/developing-pwa/configuring-pwa\n    pwa: {\n      workboxMode: \"generateSW\", // or 'injectManifest'\n      injectPwaMetaTags: true,\n      swFilename: \"sw.js\",\n      manifestFilename: \"manifest.json\",\n      useCredentialsForManifestTag: false,\n      workboxOptions: {\n        skipWaiting: true,\n        clientsClaim: true,\n      },\n      // useFilenameHashes: true,\n      // extendGenerateSWOptions (cfg) {}\n      // extendInjectManifestOptions (cfg) {},\n      // extendManifestJson (json) {}\n      // extendPWACustomSWConf (esbuildConf) {}\n    },\n\n    // Full list of options: https://v2.quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova\n    cordova: {\n      // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing\n    },\n\n    // Full list of options: https://v2.quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor\n    capacitor: {\n      hideSplashscreen: false,\n    },\n\n    // Full list of options: https://v2.quasar.dev/quasar-cli/developing-electron-apps/configuring-electron\n    electron: {\n      // extendElectronMainConf (esbuildConf)\n      // extendElectronPreloadConf (esbuildConf)\n\n      inspectPort: 5858,\n\n      bundler: \"packager\", // 'packager' or 'builder'\n\n      packager: {\n        // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options\n        // OS X / Mac App Store\n        // appBundleId: '',\n        // appCategoryType: '',\n        // osxSign: '',\n        // protocol: 'myapp://path',\n        // Windows only\n        // win32metadata: { ... }\n        asar: true,\n        prune: true,\n        ignore: [\n          /(^|[\\\\/])node_modules([\\\\/]|$)/,\n          /(^|[\\\\/])screenshots([\\\\/]|$)/,\n          /(^|[\\\\/])package-lock\\.json$/,\n          /(^|[\\\\/])yarn\\.lock$/,\n          /(^|[\\\\/])pnpm-lock\\.yaml$/,\n          /(^|[\\\\/])vitest\\.config\\.js$/,\n          /(^|[\\\\/])test([\\\\/]|$)/,\n        ],\n      },\n\n      builder: {\n        // https://www.electron.build/configuration/configuration\n\n        appId: \"me.cashu\",\n      },\n    },\n\n    // Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex\n    bex: {\n      contentScripts: [\"my-content-script\"],\n\n      // extendBexScriptsConf (esbuildConf) {}\n      // extendBexManifestJson (json) {}\n    },\n  };\n});\n"
  },
  {
    "path": "quasar.extensions.json",
    "content": "{}\n"
  },
  {
    "path": "scripts/check-i18n.js",
    "content": "#!/usr/bin/env node\n// Simple i18n keys auditor for src/i18n/*/index.ts\n// - Compares key sets across locales\n// - Reports missing/extra keys per locale\n// - Detects duplicate object paths within a single locale file by parsing once\n\nconst fs = require(\"fs\");\nconst path = require(\"path\");\n\nconst i18nDir = path.join(process.cwd(), \"src\", \"i18n\");\n\nfunction flatten(obj, prefix = \"\") {\n  const res = {};\n  for (const [k, v] of Object.entries(obj || {})) {\n    const p = prefix ? `${prefix}.${k}` : k;\n    if (v && typeof v === \"object\" && !Array.isArray(v)) {\n      Object.assign(res, flatten(v, p));\n    } else {\n      res[p] = true;\n    }\n  }\n  return res;\n}\n\nfunction loadLocale(file) {\n  // Load TypeScript via ts-node/register if available; otherwise do a crude eval\n  const src = fs.readFileSync(file, \"utf8\");\n  // Transform \"export default { ... }\" to \"module.exports = { ... }\"\n  const transformed = src.replace(/export default /, \"module.exports = \");\n  const mod = { exports: {} };\n  const fn = new Function(\"module\", \"exports\", transformed);\n  fn(mod, mod.exports);\n  return mod.exports;\n}\n\nfunction main() {\n  const entries = fs\n    .readdirSync(i18nDir, { withFileTypes: true })\n    .filter((d) => d.isDirectory())\n    .map((d) => ({\n      code: d.name,\n      file: path.join(i18nDir, d.name, \"index.ts\"),\n    }))\n    .filter((e) => fs.existsSync(e.file));\n\n  if (entries.length === 0) {\n    console.error(\"No locale files found.\");\n    process.exit(1);\n  }\n\n  const locales = entries.map((e) => ({\n    code: e.code,\n    data: loadLocale(e.file),\n  }));\n  const flatMaps = locales.map(({ code, data }) => ({\n    code,\n    flat: flatten(data),\n  }));\n\n  // Choose reference: en-US if present, else first\n  const ref = flatMaps.find((l) => l.code === \"en-US\") || flatMaps[0];\n\n  let hadError = false;\n  const diffs = {};\n  for (const { code, flat } of flatMaps) {\n    const missing = Object.keys(ref.flat).filter((k) => !(k in flat));\n    const extra = Object.keys(flat).filter((k) => !(k in ref.flat));\n    if (missing.length || extra.length) {\n      diffs[code] = { missing: missing.length, extra: extra.length };\n      hadError = true;\n      console.log(`Locale ${code}:`);\n      if (missing.length) {\n        console.log(`  Missing (${missing.length}):`);\n        missing.slice(0, 50).forEach((k) => console.log(`    - ${k}`));\n        if (missing.length > 50)\n          console.log(`    ... and ${missing.length - 50} more`);\n      }\n      if (extra.length) {\n        console.log(`  Extra (${extra.length}):`);\n        extra.slice(0, 50).forEach((k) => console.log(`    + ${k}`));\n        if (extra.length > 50)\n          console.log(`    ... and ${extra.length - 50} more`);\n      }\n    }\n  }\n\n  if (hadError) {\n    console.log(\"\\nDifferences found.\");\n    console.log(\"Number of differences per file:\");\n    for (const [code, { missing, extra }] of Object.entries(diffs)) {\n      console.log(`  ${code}: missing ${missing}, extra ${extra}`);\n    }\n    process.exit(2);\n  } else {\n    console.log(\"All locale keys are consistent.\");\n  }\n}\n\nmain();\n"
  },
  {
    "path": "src/App.vue",
    "content": "<template>\n  <router-view />\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\n\nexport default defineComponent({\n  name: \"App\",\n});\n</script>\n"
  },
  {
    "path": "src/boot/.gitkeep",
    "content": ""
  },
  {
    "path": "src/boot/axios.js",
    "content": "// src/boot/axios.js\n\nimport { boot } from \"quasar/wrappers\";\nimport axios from \"axios\";\n\n// const api = axios.create({ baseURL: 'https://api.example.com' })\n\nexport default boot(({ app }) => {\n  // for use inside Vue files (Options API) through this.$axios and this.$api\n\n  app.config.globalProperties.$axios = axios;\n  // ^ ^ ^ this will allow you to use this.$axios (for Vue Options API form)\n  //       so you won't necessarily have to import axios in each vue file\n\n  //   app.config.globalProperties.$api = api\n  // ^ ^ ^ this will allow you to use this.$api (for Vue Options API form)\n  //       so you can easily perform requests against your app's API\n});\n\n// export { axios, api }\nexport { axios };\n"
  },
  {
    "path": "src/boot/base.js",
    "content": "import { copyToClipboard } from \"quasar\";\nimport { useUiStore } from \"stores/ui\";\nimport { Clipboard } from \"@capacitor/clipboard\";\nimport { SafeArea } from \"capacitor-plugin-safe-area\";\nimport { useSettingsStore } from \"stores/settings\";\nwindow.LOCALE = \"en\";\n// window.EventHub = new Vue();\n\n// Ensure we capture the PWA install prompt as early as possible\nif (typeof window !== \"undefined\") {\n  if (!window.__deferredBeforeInstallPrompt) {\n    window.__deferredBeforeInstallPrompt = null;\n  }\n  window.addEventListener(\n    \"beforeinstallprompt\",\n    (e) => {\n      // Allow custom install UI by deferring the prompt\n      e.preventDefault();\n      window.__deferredBeforeInstallPrompt = e;\n      // Notify any listeners that install is available\n      try {\n        window.dispatchEvent(new CustomEvent(\"bip-available\"));\n      } catch (err) {\n        // noop\n      }\n    },\n    { once: false }\n  );\n  window.addEventListener(\"appinstalled\", () => {\n    window.__deferredBeforeInstallPrompt = null;\n  });\n}\n\n// ---- PWA status bar color sync helpers ----\nfunction ensureMetaTag(name, initialContent) {\n  let el = document.querySelector(`meta[name=\"${name}\"]`);\n  if (!el) {\n    el = document.createElement(\"meta\");\n    el.setAttribute(\"name\", name);\n    if (initialContent != null) {\n      el.setAttribute(\"content\", initialContent);\n    }\n    document.head.appendChild(el);\n  }\n  return el;\n}\n\nfunction resolveEffectiveTopBackgroundColor() {\n  const header = document.querySelector(\".q-header\");\n  const isTransparent = (val) =>\n    !val ||\n    val === \"transparent\" ||\n    (val.startsWith(\"rgba\") && parseFloat(val.split(\",\")[3]) === 0);\n\n  const getBg = (el) =>\n    el ? window.getComputedStyle(el).backgroundColor || \"\" : \"\";\n\n  let color = getBg(header);\n  if (isTransparent(color)) {\n    const layout = document.querySelector(\".q-layout\");\n    color = getBg(layout);\n    if (isTransparent(color)) {\n      const pageContainer = document.querySelector(\".q-page-container\");\n      color = getBg(pageContainer);\n    }\n    if (isTransparent(color)) {\n      color = getBg(document.body);\n    }\n  }\n  return color || \"#000000\";\n}\n\nfunction updateStatusBarMeta() {\n  try {\n    const iosBar = ensureMetaTag(\n      \"apple-mobile-web-app-status-bar-style\",\n      \"black-translucent\"\n    );\n    iosBar.setAttribute(\"content\", \"black-translucent\");\n\n    const themeMeta = ensureMetaTag(\"theme-color\", \"#000000\");\n    const color = resolveEffectiveTopBackgroundColor();\n    themeMeta.setAttribute(\"content\", color);\n  } catch {\n    // noop\n  }\n}\n// -------------------------------------------\n\nwindow.windowMixin = {\n  data: function () {\n    return {\n      g: {\n        offline: !navigator.onLine,\n        visibleDrawer: false,\n        extensions: [],\n        user: null,\n        wallet: null,\n        payments: [],\n        allowedThemes: null,\n      },\n    };\n  },\n  methods: {\n    changeColor: function (newValue) {\n      document.body.setAttribute(\"data-theme\", newValue);\n      this.$q.localStorage.set(\"cashu.theme\", newValue);\n      updateStatusBarMeta();\n    },\n    changeLanguage: function (e) {\n      this.$q.localStorage.set(\"cashu.language\", e.target.value);\n    },\n    toggleDarkMode: function () {\n      this.$q.dark.toggle();\n      this.$q.localStorage.set(\"cashu.darkMode\", this.$q.dark.isActive);\n      updateStatusBarMeta();\n    },\n    copyText: function (text, message, position) {\n      const notify = this.$q.notify;\n      const i18n = this.$i18n;\n      copyToClipboard(text).then(function () {\n        notify({\n          message:\n            message ||\n            (i18n && i18n.t(\"global.copy_to_clipboard.success\")) ||\n            \"Copied to clipboard!\",\n          position: position || \"bottom\",\n        });\n      });\n    },\n    pasteFromClipboard: async function () {\n      let text = \"\";\n      if (window?.Capacitor) {\n        const { value } = await Clipboard.read();\n        text = value;\n      } else {\n        text = await navigator.clipboard.readText();\n      }\n      return text;\n    },\n    formatCurrency: function (value, currency, showBalance = false) {\n      if (currency == undefined) {\n        currency = \"sat\";\n      }\n      if (useUiStore().hideBalance && !showBalance) {\n        return \"****\";\n      }\n      if (currency == \"sat\") return this.formatSat(value);\n      if (currency == \"msat\") return this.fromMsat(value);\n      if (currency == \"usd\") value = value / 100;\n      if (currency == \"eur\") value = value / 100;\n      return new Intl.NumberFormat(window.LOCALE, {\n        style: \"currency\",\n        currency: currency,\n      }).format(value);\n      // + \" \" +\n      // currency.toUpperCase()\n    },\n    formatSat: function (value) {\n      // convert value to integer\n      value = parseInt(value);\n      if (useSettingsStore().bip177BitcoinSymbol) {\n        if (value >= 0) {\n          return \"₿\" + new Intl.NumberFormat(window.LOCALE).format(value);\n        } else {\n          return (\n            \"-₿\" + new Intl.NumberFormat(window.LOCALE).format(Math.abs(value))\n          );\n        }\n      }\n      return new Intl.NumberFormat(window.LOCALE).format(value) + \" sat\";\n    },\n    fromMsat: function (value) {\n      value = parseInt(value);\n      return new Intl.NumberFormat(window.LOCALE).format(value) + \" msat\";\n    },\n    notifyApiError: function (error) {\n      const types = {\n        400: \"warning\",\n        401: \"warning\",\n        500: \"negative\",\n      };\n      this.$q.notify({\n        timeout: 5000,\n        type: types[error.response.status] || \"warning\",\n        message:\n          error.message ||\n          error.response.data.message ||\n          error.response.data.detail ||\n          null,\n        caption:\n          [error.response.status, \" \", error.response.statusText]\n            .join(\"\")\n            .toUpperCase() || null,\n        icon: null,\n      });\n    },\n    notifySuccess: async function (message, position = \"top\") {\n      this.$q.notify({\n        timeout: 5000,\n        type: \"positive\",\n        message: message,\n        position: position,\n        progress: true,\n        actions: [\n          {\n            icon: \"close\",\n            color: \"white\",\n            handler: () => {},\n          },\n        ],\n      });\n    },\n    notifyRefreshed: async function (message, position = \"top\") {\n      this.$q.notify({\n        timeout: 500,\n        type: \"positive\",\n        message: message,\n        position: position,\n        actions: [\n          {\n            color: \"white\",\n            handler: () => {},\n          },\n        ],\n      });\n    },\n    notifyError: async function (message, caption = null) {\n      this.$q.notify({\n        color: \"red\",\n        message: message,\n        caption: caption,\n        position: \"top\",\n        progress: true,\n        actions: [\n          {\n            icon: \"close\",\n            color: \"white\",\n            handler: () => {},\n          },\n        ],\n      });\n    },\n    notifyWarning: async function (message, caption = null, timeout = 5000) {\n      this.$q.notify({\n        timeout: timeout,\n        type: \"warning\",\n        message: message,\n        caption: caption,\n        position: \"top\",\n        progress: true,\n        actions: [\n          {\n            icon: \"close\",\n            color: \"black\",\n            handler: () => {},\n          },\n        ],\n      });\n    },\n    notify: async function (\n      message,\n      type = \"null\",\n      position = \"top\",\n      caption = null,\n      color = null\n    ) {\n      // failure\n      this.$q.notify({\n        timeout: 5000,\n        type: \"nuill\",\n        color: \"grey\",\n        message: message,\n        caption: null,\n        position: \"top\",\n        actions: [\n          {\n            icon: \"close\",\n            color: \"white\",\n            handler: () => {},\n          },\n        ],\n      });\n    },\n  },\n  created: function () {\n    if (\n      this.$q.localStorage.getItem(\"cashu.darkMode\") == true ||\n      this.$q.localStorage.getItem(\"cashu.darkMode\") == false\n    ) {\n      this.$q.dark.set(this.$q.localStorage.getItem(\"cashu.darkMode\"));\n    } else {\n      this.$q.dark.set(true);\n    }\n    this.g.allowedThemes = window.allowedThemes ?? [\"classic\"];\n\n    addEventListener(\"offline\", (event) => {\n      this.g.offline = true;\n    });\n\n    addEventListener(\"online\", (event) => {\n      this.g.offline = false;\n    });\n\n    // addEventListener(\"beforeunload\", (event) => {\n    //   event.preventDefault();\n    //   const dialogText = \"Are you sure about this?\";\n    //   event.returnValue = dialogText;\n    //   return dialogText;\n    // });\n\n    if (this.$q.localStorage.getItem(\"cashu.theme\")) {\n      document.body.setAttribute(\n        \"data-theme\",\n        this.$q.localStorage.getItem(\"cashu.theme\")\n      );\n    } else {\n      this.changeColor(\"monochrome\");\n    }\n\n    // Initial status bar sync and observers for changes\n    updateStatusBarMeta();\n    window.addEventListener(\"resize\", updateStatusBarMeta);\n    window.addEventListener(\"orientationchange\", updateStatusBarMeta);\n    try {\n      const header = document.querySelector(\".q-header\");\n      const observer = new MutationObserver(updateStatusBarMeta);\n      if (header) {\n        observer.observe(header, {\n          attributes: true,\n          attributeFilter: [\"class\", \"style\"],\n        });\n      }\n      observer.observe(document.body, {\n        attributes: true,\n        attributeFilter: [\"class\", \"style\", \"data-theme\"],\n      });\n    } catch {\n      // noop\n    }\n\n    const language = this.$q.localStorage.getItem(\"cashu.language\");\n    if (language) {\n      this.$i18n.locale = language;\n    }\n\n    // only for iOS\n    if (window.Capacitor && Capacitor.getPlatform() === \"ios\") {\n      SafeArea.getStatusBarHeight().then(({ statusBarHeight }) => {\n        document.documentElement.style.setProperty(\n          `--safe-area-inset-top`,\n          `${statusBarHeight}px`\n        );\n      });\n\n      SafeArea.removeAllListeners();\n\n      // when safe-area changed\n      SafeArea.addListener(\"safeAreaChanged\", (data) => {\n        const { insets } = data;\n        for (const [key, value] of Object.entries(insets)) {\n          document.documentElement.style.setProperty(\n            `--safe-area-inset-${key}`,\n            `${value}px`\n          );\n        }\n      });\n    }\n  },\n};\n"
  },
  {
    "path": "src/boot/cashu.js",
    "content": "/**\n * Configures the Cashu-ts library axios client\n */\n// import { setupAxios } from \"@cashu/cashu-ts\";\n\n// export default () => {\n//   setupAxios({\n//     // Default timeout for any interaction using the cashu-ts library to interact with a mint\n//     timeout: 15 * 1000, // 15 seconds\n//   });\n// };\n"
  },
  {
    "path": "src/boot/global-components.js",
    "content": "import { boot } from \"quasar/wrappers\";\n\nimport VueQrcode from \"@chenfengyuan/vue-qrcode\";\n\n// \"async\" is optional;\n// more info on params: https://v2.quasar.dev/quasar-cli/boot-files\nexport default boot(async ({ app }) => {\n  app.component(VueQrcode.name, VueQrcode);\n});\n"
  },
  {
    "path": "src/boot/i18n.js",
    "content": "import { boot } from \"quasar/wrappers\";\nimport { createI18n } from \"vue-i18n\";\nimport messages from \"src/i18n\";\n\n// Get stored locale from localStorage or fallback to browser language or en-US\nconst storedLocale =\n  localStorage.getItem(\"cashu.language\") || navigator.language || \"en-US\";\n\nexport const i18n = createI18n({\n  locale: storedLocale,\n  fallbackLocale: \"en-US\",\n  globalInjection: true,\n  messages,\n});\n\nexport default boot(async ({ app }) => {\n  app.use(i18n);\n});\n"
  },
  {
    "path": "src/components/ActivityOrb.vue",
    "content": "<template>\n  <div\n    class=\"row justify-center q-mb-none\"\n    style=\"position: absolute; top: 60px; left: 0; width: 100%\"\n  >\n    <div>\n      <transition\n        appear\n        enter-active-class=\"animated fadeIn\"\n        leave-active-class=\"animated fadeOut\"\n      >\n        <q-icon\n          v-if=\"enableSpinner\"\n          name=\"adjust\"\n          color=\"primary\"\n          size=\"1.5em\"\n        />\n      </transition>\n    </div>\n  </div>\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { mapState, mapWritableState } from \"pinia\";\nimport { useUiStore } from \"stores/ui\";\nimport { useWalletStore } from \"../stores/wallet\";\nimport { useReceiveTokensStore } from \"src/stores/receiveTokensStore\";\nimport { set } from \"@vueuse/core\";\n\nexport default defineComponent({\n  name: \"ActivityOrb\",\n  mixins: [windowMixin],\n  components: {},\n  props: {},\n  data: function () {\n    return {\n      enableSpinner: false,\n    };\n  },\n  mounted() {},\n  computed: {\n    ...mapWritableState(useUiStore, [\"activityOrb\"]),\n    ...mapState(useWalletStore, [\"activeWebsocketConnections\"]),\n    ...mapState(useReceiveTokensStore, [\"scanningCard\"]),\n  },\n  watch: {\n    activityOrb: function () {\n      if (this.activityOrb) {\n        this.enableSpinner = true;\n        setTimeout(() => {\n          this.activityOrb = false;\n          this.enableSpinner = false;\n        }, 2000);\n      } else {\n        this.enableSpinner = false;\n      }\n    },\n    scanningCard: function () {\n      if (this.scanningCard) {\n        this.enableSpinner = true;\n      } else {\n        this.enableSpinner = false;\n      }\n    },\n  },\n  methods: {},\n});\n</script>\n<style scoped>\n.animated.pulse {\n  animation-duration: 0.5s;\n}\n.animated.fadeInDown {\n  animation-duration: 0.3s;\n}\n.animated.fadeOut {\n  animation-duration: 1s;\n}\n.animated.fadeIn {\n  animation-duration: 1s;\n}\n</style>\n"
  },
  {
    "path": "src/components/AddMintDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showAddMintDialogLocal\"\n    @keydown.enter.prevent=\"addMintLocal\"\n    backdrop-filter=\"blur(4px) brightness(50%)\"\n    transition-show=\"fade\"\n    transition-hide=\"fade\"\n    scrollable\n  >\n    <q-card class=\"add-mint-dialog\">\n      <!-- Header Section -->\n      <q-card-section class=\"add-mint-header q-pa-md\">\n        <div class=\"add-mint-title-row\">\n          <h4 class=\"add-mint-title q-my-none\">\n            {{ $t(\"AddMintDialog.title\") }}\n          </h4>\n        </div>\n      </q-card-section>\n\n      <!-- Scrollable Content Section -->\n      <q-card-section\n        class=\"add-mint-content q-px-md scroll\"\n        style=\"max-height: 60vh\"\n      >\n        <p class=\"add-mint-description q-mb-lg\">\n          {{ $t(\"AddMintDialog.description\") }}\n        </p>\n\n        <div class=\"q-mb-lg\">\n          <label class=\"input-label\">{{\n            $t(\"AddMintDialog.inputs.mint_url.label\")\n          }}</label>\n          <q-input\n            outlined\n            readonly\n            :model-value=\"mintUrl\"\n            dense\n            class=\"mint-input\"\n            filled\n            type=\"textarea\"\n            autogrow\n            style=\"font-family: monospace; font-size: 0.9em\"\n          ></q-input>\n        </div>\n\n        <!-- Audit Info Section -->\n        <div v-if=\"mintUrl\" class=\"q-mb-lg\">\n          <div class=\"audit-info-section\">\n            <q-btn\n              flat\n              class=\"audit-info-btn\"\n              @click=\"showAuditInfo = !showAuditInfo\"\n            >\n              <info-icon size=\"16\" class=\"q-mr-xs\" />\n              {{\n                showAuditInfo ? \"Hide Mint Audit Info\" : \"View Mint Audit Info\"\n              }}\n            </q-btn>\n\n            <!-- Audit Info Component -->\n            <transition\n              enter-active-class=\"animated fadeIn\"\n              leave-active-class=\"animated fadeOut\"\n            >\n              <MintAuditInfo\n                v-if=\"showAuditInfo\"\n                :mintUrl=\"mintUrl\"\n                class=\"q-mt-md\"\n              />\n            </transition>\n          </div>\n        </div>\n      </q-card-section>\n\n      <!-- Fixed Action Buttons Section -->\n      <q-card-actions class=\"action-buttons flex q-pa-md\">\n        <q-btn flat class=\"cancel-btn\" v-close-popup>\n          {{ $t(\"AddMintDialog.actions.cancel.label\") }}\n        </q-btn>\n        <q-spacer></q-spacer>\n        <q-btn\n          color=\"primary\"\n          class=\"add-btn\"\n          @click=\"addMintLocal\"\n          v-close-popup\n          :loading=\"addMintBlocking\"\n          icon=\"check\"\n        >\n          {{ $t(\"AddMintDialog.actions.add_mint.label\") }}\n          <template v-slot:loading>\n            <q-spinner />\n            {{ $t(\"AddMintDialog.actions.add_mint.in_progress\") }}\n          </template>\n        </q-btn>\n      </q-card-actions>\n    </q-card>\n  </q-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, computed, ref } from \"vue\";\nimport { useSettingsStore } from \"src/stores/settings\";\nimport MintAuditInfo from \"./MintAuditInfo.vue\";\nimport { Info as InfoIcon } from \"lucide-vue-next\";\n\nexport default defineComponent({\n  name: \"AddMintDialog\",\n  components: {\n    MintAuditInfo,\n    InfoIcon,\n  },\n  props: {\n    addMintData: {\n      type: Object,\n      required: true,\n    },\n    showAddMintDialog: {\n      type: Boolean,\n      required: true,\n    },\n    addMintBlocking: {\n      type: Boolean,\n      required: true,\n    },\n  },\n  emits: [\"add\", \"update:showAddMintDialog\"],\n  setup(props, { emit }) {\n    const settings = useSettingsStore();\n    const showAuditInfo = ref(false);\n\n    const showAddMintDialogLocal = computed({\n      get: () => props.showAddMintDialog,\n      set: (value) => emit(\"update:showAddMintDialog\", value),\n    });\n\n    const addMintLocal = () => {\n      emit(\"add\", props.addMintData, true); // Pass verbose = true\n    };\n\n    const mintUrl = computed(() => props.addMintData.url);\n\n    return {\n      addMintLocal,\n      showAddMintDialogLocal,\n      mintUrl,\n      settings,\n      showAuditInfo,\n    };\n  },\n});\n</script>\n\n<style scoped>\n.add-mint-dialog {\n  width: 100%;\n  max-width: 450px;\n  max-height: 80vh;\n  border-radius: 16px;\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n}\n\n.add-mint-header {\n  position: relative;\n  padding-top: 20px;\n  flex-shrink: 0;\n}\n\n.add-mint-title-row {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.add-mint-title {\n  font-size: 24px;\n  font-weight: 700;\n  letter-spacing: -0.5px;\n  font-family: \"Inter\", sans-serif;\n}\n\n.add-mint-content {\n  padding-top: 0;\n  flex: 1;\n  overflow-y: auto;\n}\n\n.add-mint-description {\n  font-size: 15px;\n  line-height: 1.5;\n  font-weight: 400;\n  margin-top: 0;\n  opacity: 0.7;\n  font-family: \"Inter\", sans-serif;\n}\n\n.input-label {\n  display: block;\n  font-size: 13px;\n  font-weight: 600;\n  margin-bottom: 8px;\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n  opacity: 0.7;\n  font-family: \"Inter\", sans-serif;\n}\n\n.mint-data-display {\n  padding: 12px;\n  background-color: rgba(0, 0, 0, 0.03);\n  border-radius: 8px;\n  min-height: 48px;\n  display: flex;\n  align-items: center;\n  font-family: \"Inter\", sans-serif;\n}\n\n.body--dark .mint-data-display {\n  background-color: rgba(255, 255, 255, 0.05);\n}\n\n.mint-input {\n  border-radius: 8px;\n  height: 48px;\n  font-size: 16px;\n  font-family: \"Inter\", sans-serif;\n}\n\n/* Completely remove all input animations */\n:deep(.mint-input) {\n  /* Disable all transitions on the input and its children except for background-color */\n  * {\n    transition: none !important;\n    animation: none !important;\n  }\n\n  /* Add a smooth transition just for the background-color */\n  transition: background-color 0.2s ease-in-out !important;\n}\n\n:deep(.mint-input .q-field__focus-target) {\n  border-radius: 8px;\n}\n\n:deep(.mint-input .q-focus-helper) {\n  /* Remove animation completely */\n  opacity: 0 !important;\n  display: none !important; /* Hide it completely */\n}\n\n/* Add subtle focus/active state - theme responsive */\n:deep(.mint-input.q-field--focused) {\n  background-color: rgba(255, 255, 255, 0.1);\n}\n\n/* For dark mode, adjust the focus color */\n:deep(.body--dark .mint-input.q-field--focused) {\n  background-color: rgba(255, 255, 255, 0.07);\n}\n\n/* For light mode, use a darker shade for contrast */\n:deep(.body--light .mint-input.q-field--focused) {\n  background-color: rgba(0, 0, 0, 0.05);\n}\n\n/* Remove any ripple effects */\n:deep(.mint-input .q-ripple) {\n  display: none !important;\n}\n\n/* Remove any before/after pseudo-elements that might animate */\n:deep(.mint-input .q-field__control:before),\n:deep(.mint-input .q-field__control:after) {\n  display: none !important;\n}\n\n/* Ensure no border animations */\n:deep(.mint-input .q-field__control) {\n  height: 48px;\n  border-radius: 8px;\n  transition: none !important;\n}\n\n:deep(.mint-input .q-field__native) {\n  padding: 12px;\n  font-family: \"Inter\", sans-serif;\n}\n\n/* Make sure input placeholders use Inter font */\n:deep(.mint-input .q-field__native),\n:deep(.mint-input .q-field__input),\n:deep(.mint-input .q-placeholder) {\n  font-family: \"Inter\", sans-serif;\n}\n\n.action-buttons {\n  display: flex;\n  justify-content: space-between;\n  margin-top: 32px;\n}\n\n.cancel-btn {\n  font-weight: 600;\n  padding: 8px 16px;\n  border-radius: 8px;\n  font-family: \"Inter\", sans-serif;\n}\n\n.add-btn {\n  font-weight: 700;\n  padding: 8px 20px;\n  border-radius: 8px;\n  transition: all 0.2s ease;\n  font-family: \"Inter\", sans-serif;\n}\n\n.add-btn:hover {\n  transform: translateY(-1px);\n}\n</style>\n"
  },
  {
    "path": "src/components/AmountInputComponent.vue",
    "content": "<template>\n  <div class=\"amount-input-root\">\n    <slot name=\"overlay\" />\n    <transition name=\"swap-primary\">\n      <div\n        :key=\"`primary-${fiatMode}`\"\n        ref=\"amountDisplayRef\"\n        class=\"amount-display text-weight-bold text-center\"\n        :class=\"{ 'text-grey-6': muted }\"\n      >\n        {{ primaryAmountDisplay }}\n      </div>\n    </transition>\n    <div v-if=\"showFiatConversion\" class=\"fiat-container\">\n      <div class=\"fiat-wrapper\">\n        <transition name=\"swap-secondary\">\n          <div :key=\"`secondary-${fiatMode}`\" class=\"fiat-wrapper-content\">\n            <div\n              class=\"fiat-display text-grey-6\"\n              :class=\"{ invisible: !secondaryDisplay }\"\n              @click=\"toggleFiatMode\"\n            >\n              {{ secondaryDisplay || \" \" }}\n            </div>\n            <q-icon\n              v-if=\"showSwap\"\n              name=\"swap_vert\"\n              size=\"24px\"\n              class=\"fiat-icon text-grey-6 cursor-pointer\"\n              @click=\"toggleFiatMode\"\n              aria-label=\"Swap amount/fiat input mode\"\n              :aria-pressed=\"fiatMode ? 'true' : 'false'\"\n              role=\"button\"\n            />\n          </div>\n        </transition>\n      </div>\n    </div>\n  </div>\n  <!-- amount-input-root keeps structure minimal so parents can place it within their layout -->\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { mapState } from \"pinia\";\nimport { useMintsStore } from \"src/stores/mints\";\nimport { useSettingsStore } from \"src/stores/settings\";\nimport { usePriceStore } from \"src/stores/price\";\ndeclare const windowMixin: any;\n\nconst MAX_AMOUNT = 999_999_999;\n\nexport default defineComponent({\n  name: \"AmountInputComponent\",\n  mixins: [windowMixin],\n  props: {\n    modelValue: {\n      type: Number,\n      default: null,\n    },\n    // gray out amount (e.g. insufficient funds)\n    muted: {\n      type: Boolean,\n      default: false,\n    },\n    // whether the component should react to global keyboard input\n    enabled: {\n      type: Boolean,\n      default: true,\n    },\n    showFiatConversion: {\n      type: Boolean,\n      default: true,\n    },\n    // maximum allowed amount (in base units, before currency multiplier)\n    maxAmount: {\n      type: Number,\n      default: null,\n    },\n    // minimum allowed amount (in base units, before currency multiplier)\n    minAmount: {\n      type: Number,\n      default: null,\n    },\n  },\n  emits: [\"update:modelValue\", \"enter\", \"fiat-mode-changed\"],\n  data() {\n    return {\n      amountEditBuffer: \"\" as string,\n      fiatEditBuffer: \"\" as string,\n      fiatMode: false as boolean,\n      isFiatTyping: false as boolean,\n    };\n  },\n  computed: {\n    ...mapState(\n      useMintsStore as any,\n      [\"activeUnit\", \"activeUnitLabel\", \"activeUnitCurrencyMultiplyer\"] as any\n    ),\n    ...mapState(useSettingsStore, [\"bitcoinPriceCurrency\"]),\n    ...mapState(usePriceStore, [\"bitcoinPrice\", \"currentCurrencyPrice\"]),\n    formattedAmountDisplay(): string {\n      const amount = this.modelValue || 0;\n      return (this as any).formatCurrency(\n        amount * this.activeUnitCurrencyMultiplyer,\n        this.activeUnit,\n        true\n      );\n    },\n    primaryAmountDisplay(): string {\n      if (this.fiatMode) {\n        const fiat = this.getFiatBufferNumber();\n        return (this as any).formatCurrency(\n          fiat,\n          this.bitcoinPriceCurrency,\n          true\n        );\n      }\n      return this.formattedAmountDisplay;\n    },\n    secondaryFiatDisplay(): string {\n      if (!this.bitcoinPrice || this.activeUnit !== \"sat\") {\n        return \"\";\n      }\n      const baseAmount = this.modelValue ?? 0;\n      const fiat = (this as any).formatCurrency(\n        (this.currentCurrencyPrice / 100000000) *\n          baseAmount *\n          this.activeUnitCurrencyMultiplyer,\n        this.bitcoinPriceCurrency,\n        true\n      );\n      return fiat;\n    },\n    secondaryDisplay(): string {\n      if (this.fiatMode) {\n        // show converted sats (actual emitted amount)\n        if (!this.currentCurrencyPrice || this.activeUnit !== \"sat\") return \"\";\n        const sats = this.derivedSatsFromFiatBuffer;\n        return (this as any).formatCurrency(\n          sats * this.activeUnitCurrencyMultiplyer,\n          this.activeUnit,\n          true\n        );\n      }\n      return this.secondaryFiatDisplay;\n    },\n    showSwap(): boolean {\n      // Show when fiat pricing is available and unit is sats (or currently in fiat mode)\n      return (\n        (this.activeUnit === \"sat\" && !!this.currentCurrencyPrice) ||\n        this.fiatMode\n      );\n    },\n    derivedSatsFromFiatBuffer(): number {\n      if (!this.bitcoinPrice || !this.currentCurrencyPrice) return 0;\n      const fiat = this.getFiatBufferNumber();\n      if (!isFinite(fiat) || fiat <= 0) return 0;\n      // sats = fiat * 100_000_000 / price_per_BTC\n      let sats = Math.round((fiat * 100000000) / this.currentCurrencyPrice);\n      sats = Math.max(0, Math.min(sats, MAX_AMOUNT));\n      // Apply min/max constraints\n      if (this.minAmount != null && sats < this.minAmount) {\n        sats = this.minAmount;\n      } else if (this.maxAmount != null && sats > this.maxAmount) {\n        sats = this.maxAmount;\n      }\n      return sats;\n    },\n  },\n  watch: {\n    formattedAmountDisplay() {\n      this.$nextTick(() => this.adjustAmountFontSize());\n    },\n    modelValue(newVal: number | null) {\n      if (newVal == null) return;\n      // Apply min/max constraints\n      let clampedVal = newVal;\n      if (this.minAmount != null && clampedVal < this.minAmount) {\n        clampedVal = this.minAmount;\n      } else if (this.maxAmount != null && clampedVal > this.maxAmount) {\n        clampedVal = this.maxAmount;\n      } else if (clampedVal > MAX_AMOUNT) {\n        clampedVal = MAX_AMOUNT;\n      }\n      if (clampedVal !== newVal) {\n        this.$emit(\"update:modelValue\", clampedVal);\n        const isFiatInput =\n          this.fiatMode || this.activeUnitCurrencyMultiplyer === 100;\n        this.amountEditBuffer = isFiatInput\n          ? Number(clampedVal).toFixed(2)\n          : String(clampedVal);\n        if (this.fiatMode) {\n          // keep fiat buffer in sync with clamped sat value\n          const fiat = this.fiatFromSats(clampedVal);\n          this.fiatEditBuffer = this.numberToFiatBuffer(fiat);\n        }\n        return;\n      }\n      // Sync buffers when modelValue changes externally (e.g., from NumericKeyboard)\n      if (this.fiatMode) {\n        // Do not override user's fiat typing buffer during input\n        if (this.isFiatTyping) return;\n        const fiat = this.fiatFromSats(newVal);\n        this.fiatEditBuffer = this.numberToFiatBuffer(fiat);\n      } else {\n        const isFiatInput =\n          this.fiatMode || this.activeUnitCurrencyMultiplyer === 100;\n        this.amountEditBuffer = isFiatInput\n          ? Number(newVal).toFixed(2)\n          : String(newVal);\n      }\n    },\n    enabled(val: boolean) {\n      if (val) {\n        this.initializeKeyHandling();\n      } else {\n        this.teardownKeyHandling();\n      }\n    },\n  },\n  mounted() {\n    this.$nextTick(() => this.adjustAmountFontSize());\n    if (this.enabled) {\n      this.initializeKeyHandling();\n    }\n  },\n  beforeUnmount() {\n    this.teardownKeyHandling();\n  },\n  methods: {\n    adjustAmountFontSize(): void {\n      const element = this.$refs.amountDisplayRef as HTMLElement | undefined;\n      if (!element) return;\n      element.style.fontSize = \"\";\n      const container = element.parentElement as HTMLElement | null;\n      if (!container) return;\n      const containerWidth = container.offsetWidth;\n      const scrollWidth = element.scrollWidth;\n      if (scrollWidth > containerWidth) {\n        const baseFontSize = parseFloat(\n          window.getComputedStyle(element).fontSize\n        );\n        const scaleFactor = containerWidth / scrollWidth;\n        const newFontSize = Math.max(baseFontSize * scaleFactor * 0.95, 24);\n        element.style.fontSize = `${newFontSize}px`;\n      }\n    },\n    toggleFiatMode(): void {\n      // Only allow switching when price data is available and activeUnit is sat\n      if (!this.currentCurrencyPrice || this.activeUnit !== \"sat\") return;\n      this.fiatMode = !this.fiatMode;\n      if (this.fiatMode) {\n        // initialize fiat buffer from current model value\n        const sats = this.modelValue == null ? 0 : this.modelValue;\n        const fiat = this.fiatFromSats(sats);\n        this.fiatEditBuffer = this.numberToFiatBuffer(fiat);\n      } else {\n        // initialize sats buffer from current model value\n        this.amountEditBuffer =\n          this.modelValue == null ? \"0\" : String(this.modelValue);\n      }\n      this.$nextTick(() => this.adjustAmountFontSize());\n      this.$emit(\"fiat-mode-changed\", this.fiatMode);\n    },\n    fiatFromSats(sats: number): number {\n      // fiat = sats * price_per_BTC / 100_000_000\n      return (sats * this.currentCurrencyPrice) / 100000000;\n    },\n    satsFromFiat(fiat: number): number {\n      if (!this.currentCurrencyPrice) return 0;\n      const sats = Math.round((fiat * 100000000) / this.currentCurrencyPrice);\n      return Math.max(0, Math.min(sats, MAX_AMOUNT));\n    },\n    getFiatBufferNumber(): number {\n      const buf = this.fiatEditBuffer;\n      if (!buf || buf === \".\") return 0;\n      const num = Number(buf.replace(/,/g, \".\"));\n      return isNaN(num) ? 0 : num;\n    },\n    numberToFiatBuffer(num: number): string {\n      // keep exactly 2 decimals for fiat buffer\n      return (Math.round(num * 100) / 100).toFixed(2);\n    },\n    initializeKeyHandling(): void {\n      const isFiatInput =\n        this.fiatMode || this.activeUnitCurrencyMultiplyer === 100;\n      // initialize buffer from current value\n      if (this.modelValue == null) {\n        this.amountEditBuffer = isFiatInput ? \"0.00\" : \"0\";\n      } else {\n        this.amountEditBuffer = isFiatInput\n          ? Number(this.modelValue).toFixed(2)\n          : String(this.modelValue);\n      }\n      if (this.currentCurrencyPrice && this.activeUnit === \"sat\") {\n        const fiat = this.fiatFromSats(this.modelValue || 0);\n        this.fiatEditBuffer = this.numberToFiatBuffer(fiat);\n      } else {\n        this.fiatEditBuffer = \"\";\n      }\n      window.addEventListener(\"keydown\", this.onGlobalAmountKeydown);\n      window.addEventListener(\"resize\", this.adjustAmountFontSize);\n    },\n    teardownKeyHandling(): void {\n      window.removeEventListener(\"keydown\", this.onGlobalAmountKeydown);\n      window.removeEventListener(\"resize\", this.adjustAmountFontSize);\n      this.amountEditBuffer = \"\";\n      this.fiatEditBuffer = \"\";\n    },\n    onGlobalAmountKeydown(e: KeyboardEvent): void {\n      // ignore if an input/textarea/contenteditable is focused\n      const ae = document.activeElement as HTMLElement | null;\n      if (\n        ae &&\n        (ae.tagName === \"INPUT\" ||\n          ae.tagName === \"TEXTAREA\" ||\n          ae.getAttribute(\"contenteditable\") === \"true\")\n      ) {\n        return;\n      }\n      if ((e as any).metaKey || (e as any).ctrlKey || (e as any).altKey) return;\n      const allowDecimal = this.fiatMode\n        ? true\n        : this.activeUnit !== \"sat\" && this.activeUnit !== \"msat\";\n      const isFiatInput =\n        this.fiatMode || this.activeUnitCurrencyMultiplyer === 100;\n      const key = (e as KeyboardEvent).key;\n      let buf = this.fiatMode\n        ? this.fiatEditBuffer ||\n          this.numberToFiatBuffer(this.fiatFromSats(this.modelValue || 0))\n        : this.amountEditBuffer ||\n          (this.modelValue == null ? \"0\" : String(this.modelValue));\n      let handled = false;\n\n      if (/^[0-9]$/.test(key)) {\n        if (isFiatInput) {\n          const num = Number(buf.replace(/,/g, \".\"));\n          const cents = isNaN(num) ? 0 : Math.round(num * 100);\n          let centsStr = cents.toString();\n          if (centsStr === \"0\") centsStr = \"\";\n          centsStr += key;\n          const parsed = parseInt(centsStr, 10);\n          const newCents = isNaN(parsed) ? 0 : parsed;\n          buf = (newCents / 100).toFixed(2);\n        } else {\n          // If buffer represents zero (0, 0.0, 0.00, etc.), reset completely\n          const bufNum = allowDecimal\n            ? Number(buf.replace(/,/g, \".\"))\n            : Number(buf);\n          if (bufNum === 0 || isNaN(bufNum)) {\n            buf = key;\n          } else {\n            buf = buf + key;\n          }\n        }\n        handled = true;\n      } else if (key === \"Backspace\" || key === \"Delete\") {\n        if (isFiatInput) {\n          const num = Number(buf.replace(/,/g, \".\"));\n          const cents = isNaN(num) ? 0 : Math.round(num * 100);\n          let centsStr = cents.toString();\n          centsStr = centsStr.length > 1 ? centsStr.slice(0, -1) : \"0\";\n          const parsed = parseInt(centsStr, 10);\n          const newCents = isNaN(parsed) ? 0 : parsed;\n          buf = (newCents / 100).toFixed(2);\n        } else {\n          buf = buf.length > 1 ? buf.slice(0, -1) : \"0\";\n        }\n        handled = true;\n      } else if ((key === \".\" || key === \",\") && allowDecimal) {\n        if (!isFiatInput) {\n          if (!buf.includes(\".\")) {\n            buf = buf + \".\";\n          }\n        }\n        handled = true;\n      } else if (key === \"Enter\") {\n        if (this.modelValue != null && this.modelValue > 0) {\n          this.$emit(\"enter\");\n        }\n        handled = true;\n      }\n      if (!handled) return;\n      (e as Event).preventDefault();\n\n      // sanitize buffer\n      if (allowDecimal) {\n        if (!isFiatInput) {\n          buf = buf.replace(/,/g, \".\");\n          buf = buf.replace(/[^\\d.]/g, \"\").replace(/^(\\d*\\.\\d*).*$/, \"$1\");\n          if (buf.includes(\".\")) {\n            const parts = buf.split(\".\");\n            const decimals = parts[1] ?? \"\";\n            buf = parts[0] + \".\" + decimals.slice(0, 2);\n          }\n        }\n      } else {\n        buf = buf.replace(/[^\\d]/g, \"\");\n      }\n      if (\n        !isFiatInput &&\n        buf.startsWith(\"0\") &&\n        buf.length > 1 &&\n        buf[1] !== \".\"\n      ) {\n        buf = String(parseInt(buf, 10) || 0);\n      }\n      if (this.fiatMode) {\n        this.fiatEditBuffer = buf;\n        if (buf === \"\" || buf === \".\") {\n          this.$emit(\"update:modelValue\", null);\n        } else {\n          const fiatNum = Number(buf);\n          if (isNaN(fiatNum)) {\n            this.$emit(\"update:modelValue\", null);\n          } else {\n            let sats = this.satsFromFiat(fiatNum);\n            if (sats >= MAX_AMOUNT) {\n              sats = MAX_AMOUNT;\n              // reflect clamp back into fiat buffer\n              this.fiatEditBuffer = this.numberToFiatBuffer(\n                this.fiatFromSats(MAX_AMOUNT)\n              );\n            }\n            // Apply min/max constraints\n            if (this.minAmount != null && sats < this.minAmount) {\n              sats = this.minAmount;\n              this.fiatEditBuffer = this.numberToFiatBuffer(\n                this.fiatFromSats(this.minAmount)\n              );\n            } else if (this.maxAmount != null && sats > this.maxAmount) {\n              sats = this.maxAmount;\n              this.fiatEditBuffer = this.numberToFiatBuffer(\n                this.fiatFromSats(this.maxAmount)\n              );\n            }\n            this.isFiatTyping = true;\n            this.$emit(\"update:modelValue\", sats);\n            this.$nextTick(() => {\n              this.isFiatTyping = false;\n            });\n          }\n        }\n      } else {\n        this.amountEditBuffer = buf;\n        if (buf === \"\" || buf === \".\") {\n          this.$emit(\"update:modelValue\", null);\n        } else {\n          let num = Number(buf);\n          if (isNaN(num)) {\n            this.$emit(\"update:modelValue\", null);\n          } else {\n            // Apply min/max constraints\n            if (this.minAmount != null && num < this.minAmount) {\n              num = this.minAmount;\n              this.amountEditBuffer = isFiatInput\n                ? num.toFixed(2)\n                : String(this.minAmount);\n            } else if (this.maxAmount != null && num > this.maxAmount) {\n              num = this.maxAmount;\n              this.amountEditBuffer = isFiatInput\n                ? num.toFixed(2)\n                : String(this.maxAmount);\n            } else if (num > MAX_AMOUNT) {\n              num = MAX_AMOUNT;\n              this.amountEditBuffer = isFiatInput\n                ? num.toFixed(2)\n                : String(MAX_AMOUNT);\n            }\n            this.$emit(\"update:modelValue\", num);\n          }\n        }\n      }\n    },\n  },\n});\n</script>\n<style scoped>\n.amount-input-root {\n  position: relative;\n  width: 100%;\n  min-height: 120px; /* Ensure enough space for both displays */\n}\n.amount-display {\n  position: absolute;\n  top: 0;\n  left: 50%;\n  transform: translateX(-50%);\n  font-size: clamp(56px, 11vw, 80px);\n  line-height: 1.1;\n  white-space: nowrap;\n  max-width: 90vw;\n}\n.fiat-container {\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  height: 18px;\n}\n.fiat-wrapper {\n  position: relative;\n  width: 100%;\n  height: 18px;\n}\n.fiat-wrapper-content {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  display: inline-flex;\n  align-items: center;\n  transform: translate(-50%, -50%);\n}\n.fiat-display {\n  font-size: 20px;\n  text-align: center;\n}\n.fiat-icon {\n  position: absolute;\n  left: 100%;\n  margin-left: 4px;\n}\n.invisible {\n  visibility: hidden;\n}\n/* Primary amount swap animation (top position) - Apple-like */\n.swap-primary-enter-active {\n  transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);\n  transform-origin: center;\n}\n.swap-primary-leave-active {\n  transition: all 0.25s cubic-bezier(0.55, 0.06, 0.68, 0.19);\n  transform-origin: center;\n}\n.swap-primary-leave-to {\n  opacity: 0;\n  transform: translateX(-50%) translateY(40px) scale(0.85);\n}\n.swap-primary-enter-from {\n  opacity: 0;\n  transform: translateX(-50%) translateY(-40px) scale(0.85);\n}\n.swap-primary-enter-to,\n.swap-primary-leave-from {\n  opacity: 1;\n  transform: translateX(-50%) translateY(0) scale(1);\n}\n/* Secondary amount swap animation (bottom position) - Apple-like */\n.swap-secondary-enter-active {\n  transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);\n  transform-origin: center;\n}\n.swap-secondary-leave-active {\n  transition: all 0.25s cubic-bezier(0.55, 0.06, 0.68, 0.19);\n  transform-origin: center;\n}\n.swap-secondary-leave-to {\n  opacity: 0;\n  transform: translate(-50%, calc(-50% - 40px)) scale(0.85);\n}\n.swap-secondary-enter-from {\n  opacity: 0;\n  transform: translate(-50%, calc(-50% + 40px)) scale(0.85);\n}\n.swap-secondary-enter-to,\n.swap-secondary-leave-from {\n  opacity: 1;\n  transform: translate(-50%, -50%) scale(1);\n}\n</style>\n"
  },
  {
    "path": "src/components/AndroidPWAPrompt.vue",
    "content": "<!-- src/components/AndroidPWAPrompt.vue -->\n<template>\n  <transition appear enter-active-class=\"animated fadeInDown\">\n    <div\n      v-if=\"showAndroidPWAPrompt\"\n      class=\"pwa-prompt android-pwa-prompt q-pa-md text-center\"\n    >\n      <div class=\"pwa-prompt-content\">\n        <i18n-t keypath=\"AndroidPWAPrompt.text\" tag=\"span\">\n          <template v-slot:icon>\n            <q-icon name=\"more_vert\" size=\"sm\" />\n          </template>\n          <template v-slot:buttonText>\n            <strong>{{ $t(\"AndroidPWAPrompt.buttonText\") }}</strong>\n          </template>\n        </i18n-t>\n        <q-btn\n          flat\n          icon=\"close\"\n          @click=\"closePrompt\"\n          size=\"sm\"\n          class=\"close-btn q-px-sm\"\n        />\n      </div>\n\n      <div class=\"pwa-prompt-arrow\"></div>\n    </div>\n  </transition>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\n\nexport default defineComponent({\n  name: \"AndroidPWAPrompt\",\n  data() {\n    return {\n      showAndroidPWAPromptLocal:\n        localStorage.getItem(\"cashu.ui.showAndroidPWAPrompt\") != \"seen\",\n      showAndroidPWAPrompt: false,\n    };\n  },\n  mounted() {\n    if (\n      this.showAndroidPWAPromptLocal &&\n      this.isChromeOnAndroid() &&\n      !this.isInStandaloneMode()\n    ) {\n      this.showAndroidPWAPrompt = true;\n    }\n  },\n  methods: {\n    closePrompt() {\n      localStorage.setItem(\"cashu.ui.showAndroidPWAPrompt\", \"seen\");\n      this.showAndroidPWAPrompt = false;\n    },\n    isChromeOnAndroid() {\n      const userAgent = navigator.userAgent.toLowerCase();\n      const isAndroid = /android/.test(userAgent);\n      const isChrome =\n        /chrome/.test(userAgent) && !/edge|edg|opr|opera/.test(userAgent);\n      return isAndroid && isChrome;\n    },\n    isInStandaloneMode() {\n      return window.matchMedia(\"(display-mode: standalone)\").matches;\n    },\n  },\n});\n</script>\n\n<style scoped>\n@keyframes moveUpDown {\n  0%,\n  100% {\n    transform: translateY(0);\n  }\n  50% {\n    transform: translateY(-10px);\n  }\n}\n\n.pwa-prompt {\n  position: fixed;\n  top: 20px;\n  right: 20px;\n  margin: 0 auto;\n  z-index: 9999;\n  text-align: center;\n  display: flex;\n  flex-direction: column; /* Add this line */\n  align-items: center; /* Add this line */\n  justify-content: center;\n  animation: moveUpDown 1s infinite; /* Add this line for animation */\n}\n\n.pwa-prompt-content {\n  display: inline-flex;\n  align-items: center;\n  background-color: black;\n  padding: 10px;\n  border: 1px solid #ccc;\n  border-radius: 8px;\n}\n\n.pwa-prompt-content q-icon {\n  margin-right: 5px;\n}\n\n.pwa-prompt-arrow {\n  position: relative;\n  width: 0;\n  height: 0;\n  bottom: 60px;\n  left: 45%;\n  border-left: 10px solid transparent;\n  border-right: 10px solid transparent;\n  border-bottom: 10px solid white;\n  text-align: center;\n  margin: 0 auto;\n}\n</style>\n"
  },
  {
    "path": "src/components/AnimatedNumber.vue",
    "content": "<template>\n  <span @click=\"$emit('click')\">{{ formattedValue }}</span>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, ref, watch, computed } from \"vue\";\n\nexport default defineComponent({\n  name: \"AnimatedNumber\",\n  props: {\n    value: {\n      type: Number,\n      required: true,\n    },\n    duration: {\n      type: Number,\n      default: 1000, // Animation duration in milliseconds\n    },\n    format: {\n      type: Function,\n      required: true, // Function to format the number\n    },\n  },\n  setup(props) {\n    const displayedValue = ref(props.value);\n    // value to remember that we do not want to animate the very first update (when the component is created)\n    const initialized = ref(false);\n\n    watch(\n      () => props.value,\n      (newValue, oldValue) => {\n        if (!initialized.value) {\n          displayedValue.value = newValue;\n          if (newValue > 0) {\n            // do not animate until we set the first value\n            initialized.value = true;\n          }\n          return;\n        }\n        const startTime = performance.now();\n        const startValue = oldValue !== undefined ? oldValue : newValue;\n        const endValue = newValue;\n\n        const animate = (currentTime: number) => {\n          const elapsed = currentTime - startTime;\n          const progress = Math.max(Math.min(elapsed / props.duration, 1), 0);\n          const currentValue = startValue + (endValue - startValue) * progress;\n          displayedValue.value = currentValue;\n          if (progress < 1) {\n            requestAnimationFrame(animate);\n          }\n        };\n\n        requestAnimationFrame(animate);\n      },\n      { immediate: true }\n    );\n\n    const formattedValue = computed(() => {\n      return props.format(displayedValue.value);\n    });\n\n    return {\n      formattedValue,\n    };\n  },\n});\n</script>\n"
  },
  {
    "path": "src/components/BalanceView.vue",
    "content": "<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-xl q-pb-md\">\n    <div class=\"row justify-center q-pb-lg\" style=\"height: 80px\">\n      <div v-if=\"globalMutexLock\">\n        <transition\n          appear\n          enter-active-class=\"animated pulse\"\n          leave-active-class=\"animated fadeOut\"\n        >\n          <q-spinner class=\"q-mt-lg q-mb-none\" size=\"lg\" color=\"primary\" />\n        </transition>\n      </div>\n      <div v-else>\n        <transition\n          appear\n          enter-active-class=\"animated pulse\"\n          leave-active-class=\"animated fadeOut\"\n        >\n          <ToggleUnit class=\"q-mt-lg q-mb-none\" />\n        </transition>\n      </div>\n    </div>\n    <transition\n      appear\n      enter-active-class=\"animated fadeInDown\"\n      leave-active-class=\"animated fadeInDown\"\n      mode=\"out-in\"\n    >\n      <q-carousel\n        v-model=\"this.activeUnit\"\n        transition-prev=\"jump-up\"\n        transition-next=\"jump-up\"\n        swipeable\n        animated\n        :height=\"$q.screen.width < 390 ? '130px' : '80px'\"\n        control-color=\"primary\"\n        class=\"bg-transparent rounded-borders q-mb-xl q-mt-xl text-primary\"\n      >\n        <!-- make a q-carousel-slide with v-for for all possible units -->\n        <q-carousel-slide\n          v-for=\"unit in balancesOptions\"\n          :key=\"unit.value\"\n          :name=\"unit.value\"\n          class=\"q-pt-none\"\n        >\n          <div class=\"row\">\n            <div class=\"col-12\">\n              <h3\n                class=\"q-my-none q-py-none cursor-pointer\"\n                @click=\"toggleHideBalance\"\n              >\n                <strong>\n                  <AnimatedNumber\n                    :value=\"getTotalBalance\"\n                    :format=\"(val) => formatCurrency(val, activeUnit)\"\n                    class=\"q-my-none q-py-none cursor-pointer\"\n                  />\n                </strong>\n              </h3>\n              <div v-if=\"bitcoinPrice\" class=\"q-mt-sm\">\n                <strong v-if=\"this.activeUnit == 'sat'\">\n                  <AnimatedNumber\n                    :value=\"\n                      (currentCurrencyPrice / 100000000) * getTotalBalance\n                    \"\n                    :format=\"(val) => formatCurrency(val, bitcoinPriceCurrency)\"\n                  />\n                </strong>\n                <strong\n                  v-if=\"this.activeUnit == 'usd' || this.activeUnit == 'eur'\"\n                >\n                  <AnimatedNumber\n                    :value=\"\n                      (getTotalBalance / 100 / currentCurrencyPrice) * 100000000\n                    \"\n                    :format=\"(val) => formatCurrency(val, 'sat')\"\n                  />\n                </strong>\n                <q-tooltip>\n                  1 BTC =\n                  {{\n                    formatCurrency(currentCurrencyPrice, bitcoinPriceCurrency)\n                  }}\n                </q-tooltip>\n              </div>\n            </div>\n          </div>\n        </q-carousel-slide>\n      </q-carousel>\n    </transition>\n    <div\n      v-if=\"activeMint().mint.errored\"\n      class=\"row q-mt-md q-mb-none text-secondary\"\n    >\n      <div class=\"col-12\">\n        <q-badge outline color=\"red\" class=\"q-mr-xs q-mt-sm text-weight-bold\">\n          {{ $t(\"BalanceView.mintError.label\") }}\n          <q-icon name=\"error\" class=\"q-ml-xs\" />\n        </q-badge>\n      </div>\n    </div>\n    <!-- mint url -->\n    <div class=\"row q-mt-md q-mb-none text-secondary\" v-if=\"activeMintUrl\">\n      <div class=\"col-12 cursor-pointer\">\n        <span class=\"text-weight-light\" @click=\"setTab('mints')\">\n          {{ $t(\"BalanceView.mintUrl.label\") }}: <b>{{ activeMintLabel }}</b>\n        </span>\n      </div>\n    </div>\n    <!-- mint balance -->\n    <div class=\"row q-mb-none text-secondary\" v-if=\"mints.length > 1\">\n      <div class=\"col-12\">\n        <span class=\"q-my-none q-py-none text-weight-regular\">\n          {{ $t(\"BalanceView.mintBalance.label\") }}:\n          <b>\n            <AnimatedNumber\n              :value=\"getActiveBalance\"\n              :format=\"(val) => formatCurrency(val, activeUnit)\"\n              class=\"q-my-none q-py-none cursor-pointer\"\n            />\n          </b>\n        </span>\n      </div>\n    </div>\n  </div>\n  <!-- pending -->\n  <div class=\"row q-mt-xs q-mb-none\" v-if=\"pendingBalance > 0\">\n    <div class=\"col-12\">\n      <q-btn\n        name=\"history\"\n        size=\"sm\"\n        align=\"between\"\n        color=\"secondary\"\n        dense\n        outline\n        class=\"q-mx-none q-mt-xs q-pr-sm cursor-pointer\"\n        @click=\"checkPendingTokens()\"\n        ><q-icon name=\"history\" size=\"1rem\" class=\"q-mx-xs\" />\n        {{ $t(\"BalanceView.pending.label\") }}:\n        {{ formatCurrency(pendingBalance, this.activeUnit) }}\n        <q-tooltip>{{ $t(\"BalanceView.pending.tooltip\") }}</q-tooltip>\n      </q-btn>\n    </div>\n  </div>\n  <!-- </q-card-section>\n  </q-card> -->\n</template>\n<script lang=\"ts\">\nimport { defineComponent, ref } from \"vue\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport { mapState, mapWritableState, mapActions } from \"pinia\";\nimport { useMintsStore } from \"stores/mints\";\nimport { useSettingsStore } from \"stores/settings\";\nimport { useTokensStore } from \"stores/tokens\";\nimport { useUiStore } from \"stores/ui\";\nimport { useWalletStore } from \"stores/wallet\";\nimport { usePriceStore } from \"stores/price\";\nimport ToggleUnit from \"components/ToggleUnit.vue\";\nimport AnimatedNumber from \"components/AnimatedNumber.vue\";\nimport axios from \"axios\";\nimport { map } from \"underscore\";\n\nexport default defineComponent({\n  name: \"BalanceView\",\n  mixins: [windowMixin],\n  components: {\n    ToggleUnit,\n    AnimatedNumber,\n  },\n  props: {\n    setTab: Function,\n  },\n  computed: {\n    ...mapState(useMintsStore, [\n      \"activeMintUrl\",\n      \"activeProofs\",\n      \"activeBalance\",\n      \"mints\",\n      \"totalUnitBalance\",\n      \"activeUnit\",\n      \"activeMint\",\n    ]),\n    ...mapState(useTokensStore, [\"historyTokens\"]),\n    ...mapState(useUiStore, [\"globalMutexLock\"]),\n    ...mapState(usePriceStore, [\n      \"bitcoinPrice\",\n      \"bitcoinPrices\",\n      \"currentCurrencyPrice\",\n    ]),\n    ...mapState(useSettingsStore, [\"bitcoinPriceCurrency\"]),\n    ...mapWritableState(useMintsStore, [\"activeUnit\"]),\n    ...mapWritableState(useUiStore, [\"hideBalance\", \"lastBalanceCached\"]),\n    pendingBalance: function () {\n      return -this.historyTokens\n        .filter((t) => t.status == \"pending\")\n        .filter((t) => t.unit == this.activeUnit)\n        .reduce((sum, el) => (sum += el.amount), 0);\n    },\n    balancesOptions: function () {\n      const mint = this.activeMint();\n      return Object.entries(mint.allBalances).map(([key, value]) => ({\n        label: key,\n        value: key,\n      }));\n    },\n    allMintKeysets: function () {\n      return [].concat(...this.mints.map((m) => m.keysets));\n    },\n    getTotalBalance: function () {\n      return this.totalUnitBalance;\n    },\n    getActiveBalance: function () {\n      return this.activeBalance;\n    },\n    activeMintLabel: function () {\n      const mintClass = this.activeMint();\n\n      return (\n        mintClass.mint.nickname ||\n        mintClass.mint.info?.name ||\n        getShortUrl(this.activeMintUrl)\n      );\n    },\n    getBalance: function () {\n      const balance = this.activeProofs\n        .flat()\n        .reduce((sum, el) => (sum += el.amount), 0);\n      return balance;\n    },\n  },\n  data() {\n    return {\n      priceLabel: null,\n    };\n  },\n  mounted() {\n    this.fetchBitcoinPrice();\n  },\n  methods: {\n    ...mapActions(useWalletStore, [\"checkPendingTokens\"]),\n    ...mapActions(usePriceStore, [\"fetchBitcoinPrice\"]),\n    toggleUnit: function () {\n      const units = this.activeMint().units;\n      this.activeUnit =\n        units[(units.indexOf(this.activeUnit) + 1) % units.length];\n      return this.activeUnit;\n    },\n    toggleHideBalance() {\n      this.hideBalance = !this.hideBalance;\n    },\n  },\n});\n</script>\n<style scoped>\n.animated.pulse {\n  animation-duration: 0.5s;\n}\n.animated.fadeInDown {\n  animation-duration: 0.3s;\n}\n</style>\n"
  },
  {
    "path": "src/components/ChooseMint.vue",
    "content": "<template>\n  <div class=\"q-pb-md\">\n    <!-- Main mint selector button -->\n    <div\n      class=\"row q-mt-xs q-mb-none\"\n      v-if=\"activeMintUrl || !requireActiveMint\"\n    >\n      <div class=\"col-12\">\n        <div\n          class=\"mint-selector-btn\"\n          :class=\"{ 'mint-selector-dense': dense }\"\n          :style=\"style\"\n          @click=\"showMintSheet = true\"\n        >\n          <div class=\"row items-center full-width no-wrap\">\n            <!-- Mint Icon -->\n            <q-avatar\n              :size=\"dense ? '40px' : '48px'\"\n              class=\"q-mr-md mint-icon-avatar\"\n            >\n              <q-img\n                v-if=\"chosenMint?.iconUrl\"\n                :src=\"chosenMint.iconUrl\"\n                spinner-color=\"white\"\n                spinner-size=\"xs\"\n              >\n                <template v-slot:error>\n                  <div class=\"row items-center justify-center full-height\">\n                    <q-icon name=\"account_balance\" color=\"grey-7\" size=\"24px\" />\n                  </div>\n                </template>\n              </q-img>\n              <q-icon\n                v-else\n                name=\"account_balance\"\n                color=\"grey-7\"\n                size=\"24px\"\n              />\n            </q-avatar>\n\n            <!-- Mint Info -->\n            <div class=\"col text-left mint-info-section\">\n              <div class=\"mint-name-label\">\n                {{\n                  chosenMint?.nickname ||\n                  chosenMint?.shorturl ||\n                  placeholder ||\n                  $t(\"ChooseMint.placeholder\")\n                }}\n              </div>\n              <div v-if=\"showBalances && chosenMint\" class=\"mint-balance-label\">\n                <span v-if=\"!chosenMint.errored\" class=\"text-grey-6\">\n                  {{ formatCurrency(selectedMintBalance, activeUnit) }}\n                  {{ $t(\"ChooseMint.available_text\") }}\n                </span>\n                <span v-else class=\"text-red\">\n                  {{ $t(\"ChooseMint.badge_mint_error_text\") }}\n                </span>\n              </div>\n            </div>\n\n            <!-- Chevron -->\n            <q-icon name=\"expand_more\" color=\"grey-6\" size=\"20px\" />\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- Bottom Sheet for Mint Selection -->\n    <teleport to=\"body\">\n      <transition name=\"mint-overlay\">\n        <div\n          v-if=\"showMintSheet\"\n          class=\"mint-sheet-overlay\"\n          @click=\"showMintSheet = false\"\n        >\n          <div class=\"mint-sheet\" @click.stop>\n            <!-- Header -->\n            <div class=\"mint-sheet-header\">\n              <h3>{{ $t(\"ChooseMint.sheet_title\") }}</h3>\n              <q-btn\n                flat\n                round\n                icon=\"close\"\n                @click=\"showMintSheet = false\"\n                class=\"close-btn\"\n              />\n            </div>\n\n            <!-- Mint List -->\n            <div class=\"mint-options\">\n              <div\n                v-for=\"mint in chooseMintOptions()\"\n                :key=\"mint.url\"\n                class=\"mint-option\"\n                :class=\"{ active: chosenMint?.url === mint.url }\"\n                @click=\"selectMint(mint)\"\n              >\n                <div class=\"row items-center full-width no-wrap\">\n                  <!-- Mint Icon -->\n                  <q-avatar size=\"48px\" class=\"q-mr-md\">\n                    <q-img\n                      v-if=\"mint.iconUrl\"\n                      :src=\"mint.iconUrl\"\n                      spinner-color=\"white\"\n                      spinner-size=\"xs\"\n                    >\n                      <template v-slot:error>\n                        <div\n                          class=\"row items-center justify-center full-height\"\n                        >\n                          <q-icon\n                            name=\"account_balance\"\n                            color=\"grey-7\"\n                            size=\"24px\"\n                          />\n                        </div>\n                      </template>\n                    </q-img>\n                    <q-icon\n                      v-else\n                      name=\"account_balance\"\n                      color=\"grey-7\"\n                      size=\"24px\"\n                    />\n                  </q-avatar>\n\n                  <!-- Mint Info -->\n                  <div class=\"col text-left\">\n                    <div class=\"mint-option-name\">\n                      {{ mint.nickname || mint.shorturl }}\n                    </div>\n                    <div v-if=\"showBalances\" class=\"mint-option-balance\">\n                      <span v-if=\"!mint.errored\" class=\"text-grey-6\">\n                        <span\n                          v-for=\"unit in mint.units\"\n                          :key=\"unit\"\n                          class=\"q-mr-sm\"\n                        >\n                          {{ formatCurrency(mint.balances[unit], unit) }}\n                        </span>\n                      </span>\n                      <span v-else class=\"text-red\">\n                        {{ $t(\"ChooseMint.badge_mint_error_text\") }}\n                      </span>\n                    </div>\n                  </div>\n\n                  <!-- Selection Indicator -->\n                  <q-icon\n                    v-if=\"chosenMint?.url === mint.url\"\n                    name=\"check_circle\"\n                    color=\"primary\"\n                    size=\"24px\"\n                  />\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </transition>\n    </teleport>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport { useMintsStore } from \"stores/mints\";\nimport { MintClass } from \"stores/mints\";\nimport type { StoredMint } from \"stores/mints\";\nimport { useUiStore } from \"stores/ui\";\nimport { i18n } from \"../boot/i18n\";\n\ndeclare const windowMixin: any;\n\ntype MintOption = {\n  nickname?: string | null;\n  url: string;\n  shorturl: string | null;\n  iconUrl?: string | null;\n  balances: Record<string, number>;\n  errored?: boolean;\n  units: string[];\n};\n\nexport default defineComponent({\n  name: \"ChooseMint\",\n  mixins: [windowMixin],\n  props: {\n    rounded: {\n      type: Boolean,\n      default: false,\n    },\n    dense: {\n      type: Boolean,\n      default: false,\n    },\n    title: {\n      type: String,\n      default: i18n.global.t(\"ChooseMint.title\"),\n    },\n    style: {\n      type: String,\n      default: \"\",\n    },\n    placeholder: {\n      type: String,\n      default: \"\",\n    },\n    showBalances: {\n      type: Boolean,\n      default: true,\n    },\n    requireActiveMint: {\n      type: Boolean,\n      default: true,\n    },\n    dryRun: {\n      type: Boolean,\n      default: false,\n    },\n    excludeMint: {\n      type: String,\n      default: null,\n    },\n    // When provided, this will be used instead of activeMintUrl\n    modelValue: {\n      type: String,\n      default: null,\n    },\n  },\n  emits: [\"update:modelValue\"],\n  data: function () {\n    return {\n      chosenMint: null as MintOption | null,\n      showMintSheet: false,\n    };\n  },\n  mounted() {\n    this.initializeChosenMint();\n  },\n  watch: {\n    chosenMint: async function () {\n      const selectedUrl = this.chosenMint?.url || \"\";\n      if (this.dryRun || this.modelValue !== null) {\n        this.$emit(\"update:modelValue\", selectedUrl);\n      }\n      if (this.modelValue === null && !this.dryRun) {\n        // Use the original behavior when not using v-model\n        (this.activeMintUrl as unknown as string) = selectedUrl;\n      }\n    },\n    modelValue: {\n      handler() {\n        this.initializeChosenMint();\n      },\n      immediate: true,\n    },\n    activeMintUrl: {\n      handler() {\n        if (this.modelValue === null) {\n          this.initializeChosenMint();\n        }\n      },\n    },\n    excludeMint() {\n      this.initializeChosenMint();\n    },\n  },\n  computed: {\n    ...mapState(useMintsStore, [\"activeProofs\", \"mints\", \"activeUnit\"]),\n    ...mapWritableState(useMintsStore, [\"activeMintUrl\"]),\n    selectedMintBalance(): number {\n      const unit = this.activeUnit;\n      if (!this.chosenMint || !unit) {\n        return 0;\n      }\n      const balance = this.chosenMint.balances?.[unit];\n      return typeof balance === \"number\" ? balance : 0;\n    },\n  },\n  methods: {\n    ...mapActions(useMintsStore, [\"activateMintUrl\"]),\n    formatCurrency(value: number, currency: string) {\n      return useUiStore().formatCurrency(value, currency);\n    },\n    applyChosenMint(option: MintOption | null) {\n      if (!option) {\n        this.chosenMint = null;\n        return;\n      }\n      this.chosenMint = {\n        ...option,\n        balances: { ...option.balances },\n        units: [...option.units],\n      };\n    },\n    initializeChosenMint() {\n      const options = this.chooseMintOptions();\n      const fallbackUrl =\n        this.chosenMint?.url && this.chosenMint.url !== this.excludeMint\n          ? this.chosenMint.url\n          : \"\";\n      let targetUrl =\n        this.modelValue !== null\n          ? this.modelValue\n          : fallbackUrl || this.activeMintUrl;\n      if (targetUrl && targetUrl === this.excludeMint) {\n        targetUrl = \"\";\n      }\n      if (targetUrl) {\n        const matched = options.find(\n          (option: MintOption) => option.url === targetUrl\n        );\n        if (matched) {\n          this.applyChosenMint(matched);\n          return;\n        }\n      }\n      if (options.length) {\n        this.applyChosenMint(options[0]);\n      } else {\n        this.applyChosenMint(null);\n      }\n    },\n    selectMint(mint: MintOption) {\n      if (this.excludeMint && mint.url === this.excludeMint) {\n        return;\n      }\n      this.applyChosenMint(mint);\n      this.showMintSheet = false;\n    },\n    chooseMintOptions: function () {\n      const options: MintOption[] = [];\n      const availableMints = Array.isArray(this.mints)\n        ? (this.mints as StoredMint[])\n        : [];\n      for (const mintData of availableMints) {\n        const all_units = mintData.keysets.map((r) => r.unit);\n        const units = [...new Set(all_units)];\n        const mint = new MintClass(mintData);\n        if (!this.excludeMint || mint.mint.url !== this.excludeMint) {\n          options.push({\n            nickname: mint.mint.nickname || mint.mint.info?.name,\n            url: mint.mint.url,\n            shorturl: getShortUrl(mintData.url),\n            iconUrl: mint.mint.info?.icon_url,\n            balances: mint.allBalances,\n            errored: mint.mint.errored,\n            units: units,\n          });\n        }\n      }\n      return options;\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.mint-selector-btn {\n  width: 100%;\n  padding: 16px;\n  background: rgba(255, 255, 255, 0.05);\n  border: 1px solid rgba(255, 255, 255, 0.1);\n  border-radius: 12px;\n  cursor: pointer;\n  transition: all 0.2s ease;\n\n  &:hover {\n    background: rgba(255, 255, 255, 0.08);\n    border-color: rgba(255, 255, 255, 0.2);\n  }\n\n  &.mint-selector-dense {\n    padding: 12px;\n  }\n}\n\n.mint-icon-avatar {\n  flex-shrink: 0;\n}\n\n.mint-info-section {\n  min-width: 0;\n}\n\n.mint-name-label {\n  font-size: 16px;\n  font-weight: 500;\n  line-height: 1.3;\n  color: white;\n}\n\n.mint-balance-label {\n  font-size: 14px;\n  line-height: 1.3;\n  margin-top: 4px;\n}\n\n/* Bottom sheet overlay */\n.mint-sheet-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background: rgba(0, 0, 0, 0.5);\n  backdrop-filter: blur(4px);\n  z-index: 9999;\n  display: flex;\n  align-items: flex-end;\n}\n\n/* Bottom sheet */\n.mint-sheet {\n  width: 100%;\n  background: rgba(20, 20, 20, 0.98);\n  backdrop-filter: blur(20px);\n  border-top: 1px solid rgba(255, 255, 255, 0.1);\n  border-radius: 20px 20px 0 0;\n  max-height: 70vh;\n  overflow: hidden;\n  display: flex;\n  flex-direction: column;\n}\n\n.mint-sheet-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 20px 24px 16px 24px;\n  border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.mint-sheet-header h3 {\n  color: white;\n  font-size: 1.1rem;\n  font-weight: 600;\n  margin: 0;\n}\n\n.close-btn {\n  color: rgba(255, 255, 255, 0.7) !important;\n}\n\n.mint-options {\n  flex: 1;\n  overflow-y: auto;\n  padding-bottom: env(safe-area-inset-bottom);\n}\n\n.mint-option {\n  padding: 16px 24px;\n  cursor: pointer;\n  transition: all 0.2s ease;\n  border-bottom: 1px solid rgba(255, 255, 255, 0.05);\n\n  &:hover {\n    background: rgba(255, 255, 255, 0.08);\n  }\n\n  &.active {\n    background: rgba(var(--q-primary-rgb), 0.2);\n  }\n\n  &:last-child {\n    border-bottom: none;\n  }\n}\n\n.mint-option-name {\n  font-size: 16px;\n  font-weight: 500;\n  line-height: 1.3;\n  color: white;\n}\n\n.mint-option-balance {\n  font-size: 14px;\n  line-height: 1.3;\n  margin-top: 4px;\n}\n\n/* Vue transition for overlay fade and sheet slide */\n.mint-overlay-enter-active,\n.mint-overlay-leave-active {\n  transition: opacity 0.3s ease;\n}\n.mint-overlay-enter-from,\n.mint-overlay-leave-to {\n  opacity: 0;\n}\n/* Animate the sheet together with the overlay */\n.mint-overlay-enter-active .mint-sheet,\n.mint-overlay-leave-active .mint-sheet {\n  transition: transform 0.3s ease;\n}\n.mint-overlay-enter-from .mint-sheet,\n.mint-overlay-leave-to .mint-sheet {\n  transform: translateY(100%);\n}\n</style>\n"
  },
  {
    "path": "src/components/CreateInvoiceDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showCreateInvoiceDialog\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n    transition-show=\"fade\"\n    transition-hide=\"fade\"\n    no-backdrop-dismiss\n    @keydown.esc=\"showCreateInvoiceDialog = false\"\n  >\n    <q-card class=\"q-pa-none q-pt-none qcard\">\n      <!-- enter invoice amount (full-screen) -->\n      <div\n        class=\"column fit send-fullscreen\"\n        :class=\"$q.dark.isActive ? 'bg-dark' : 'bg-white'\"\n      >\n        <!-- Header -->\n        <div class=\"row items-center q-pa-md\" style=\"position: relative\">\n          <q-btn\n            v-close-popup\n            flat\n            round\n            icon=\"close\"\n            color=\"grey\"\n            class=\"floating-close-btn\"\n          />\n          <div class=\"col text-center fixed-title-height\">\n            <q-item-label\n              class=\"dialog-header q-mt-sm\"\n              :class=\"$q.dark.isActive ? 'text-white' : 'text-black'\"\n            >\n              {{ $t(\"InvoiceDetailDialog.title\") }}\n            </q-item-label>\n          </div>\n          <div\n            class=\"row items-center q-gutter-sm\"\n            style=\"position: absolute; right: 16px\"\n          >\n            <q-btn\n              flat\n              dense\n              size=\"lg\"\n              color=\"primary\"\n              @click=\"toggleUnit()\"\n              :label=\"activeUnitLabel\"\n            />\n          </div>\n        </div>\n\n        <!-- Mint selection -->\n        <div class=\"row justify-center\">\n          <div\n            class=\"col-12 col-sm-11 col-md-8 q-px-lg q-mb-sm\"\n            style=\"max-width: 600px\"\n          >\n            <ChooseMint />\n          </div>\n        </div>\n\n        <!-- Amount display -->\n        <div class=\"col column items-center justify-center q-px-lg amount-area\">\n          <AmountInputComponent\n            v-model=\"invoiceData.amount\"\n            :enabled=\"true\"\n            @enter=\"requestMintButton\"\n            @fiat-mode-changed=\"fiatKeyboardMode = $event\"\n          />\n        </div>\n\n        <!-- Numeric keypad -->\n        <div class=\"bottom-panel\">\n          <div class=\"keypad-wrapper\">\n            <NumericKeyboard\n              :force-visible=\"true\"\n              :hide-close=\"true\"\n              :hide-enter=\"true\"\n              :hide-comma=\"\n                (activeUnit === 'sat' || activeUnit === 'msat') &&\n                !fiatKeyboardMode\n              \"\n              :model-value=\"String(invoiceData.amount ?? 0)\"\n              @update:modelValue=\"(val: string | number) => (invoiceData.amount = Number(val))\"\n              @done=\"requestMintButton\"\n            />\n          </div>\n          <!-- Create action below keyboard -->\n          <div class=\"row justify-center q-pb-lg q-pt-sm\">\n            <div\n              class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n              style=\"max-width: 600px\"\n            >\n              <q-btn\n                class=\"full-width\"\n                unelevated\n                size=\"lg\"\n                :disable=\"\n                  invoiceData.amount == null || Number(invoiceData.amount) <= 0\n                \"\n                @click=\"requestMintButton\"\n                color=\"primary\"\n                rounded\n                type=\"submit\"\n                :loading=\"globalMutexLock || createInvoiceButtonBlocked\"\n              >\n                {{ $t(\"InvoiceDetailDialog.actions.create.label\") }}\n                <template v-slot:loading>\n                  <q-spinner />\n                </template>\n              </q-btn>\n            </div>\n          </div>\n        </div>\n      </div>\n    </q-card>\n  </q-dialog>\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport ChooseMint from \"components/ChooseMint.vue\";\nimport NumericKeyboard from \"components/NumericKeyboard.vue\";\nimport AmountInputComponent from \"components/AmountInputComponent.vue\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { useMintsStore } from \"src/stores/mints\";\nimport { useSettingsStore } from \"src/stores/settings\";\nimport { usePriceStore } from \"src/stores/price\";\ndeclare const windowMixin: any;\n\nexport default defineComponent({\n  name: \"CreateInvoiceDialog\",\n  mixins: [windowMixin],\n  components: {\n    ChooseMint,\n    NumericKeyboard,\n    AmountInputComponent,\n  },\n  props: {},\n  data: function () {\n    return {\n      createInvoiceButtonBlocked: false,\n      fiatKeyboardMode: false as boolean,\n    };\n  },\n  computed: {\n    ...mapWritableState(useUiStore, [\n      \"showNumericKeyboard\",\n      \"showInvoiceDetails\",\n    ]),\n    ...mapWritableState(useUiStore, [\"tickerShort\", \"globalMutexLock\"]),\n    ...mapWritableState(useUiStore, [\"showCreateInvoiceDialog\"]),\n    ...mapWritableState(useWalletStore, [\"invoiceData\"]),\n    ...mapState(useMintsStore, [\n      \"activeUnit\",\n      \"activeUnitLabel\",\n      \"activeUnitCurrencyMultiplyer\",\n      \"activeMintUrl\",\n    ]),\n    ...mapState(useSettingsStore, [\n      \"bitcoinPriceCurrency\",\n      \"useNumericKeyboard\",\n    ]),\n    ...mapState(usePriceStore, [\"bitcoinPrice\", \"currentCurrencyPrice\"]),\n  },\n  watch: {\n    showCreateInvoiceDialog: function (val) {\n      if (val) {\n        this.$nextTick(() => {\n          this.showNumericKeyboard = true;\n        });\n      } else {\n      }\n    },\n  },\n  methods: {\n    ...mapActions(useWalletStore, [\n      \"requestMint\",\n      \"mintOnPaid\",\n      \"activeWallet\",\n    ]),\n    ...mapActions(useMintsStore, [\"toggleUnit\"]),\n    requestMintButton: async function () {\n      if (!this.invoiceData.amount) {\n        return;\n      }\n      try {\n        this.showNumericKeyboard = false;\n        const amount = Math.floor(\n          this.invoiceData.amount * this.activeUnitCurrencyMultiplyer\n        );\n        this.createInvoiceButtonBlocked = true;\n        const wallet = await this.activeWallet(true);\n        const mintQuote = await this.requestMint(amount, wallet);\n        // Switch to QR display dialog\n        this.showCreateInvoiceDialog = false;\n        this.showInvoiceDetails = true;\n        await this.mintOnPaid(mintQuote.quote);\n      } catch (e) {\n        console.log(\"#### requestMintButton\", e);\n      } finally {\n        this.createInvoiceButtonBlocked = false;\n      }\n    },\n  },\n});\n</script>\n<style scoped>\n.send-fullscreen {\n  height: 100vh;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n.floating-close-btn {\n  position: absolute;\n  left: 16px;\n  top: 50%;\n  transform: translateY(-50%);\n  z-index: 1;\n}\n.fixed-title-height {\n  height: 24px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.amount-area {\n  flex: 1;\n}\n.amount-container {\n  position: relative;\n  display: inline-block;\n  max-width: 90vw;\n  overflow: hidden;\n}\n.amount-display {\n  font-size: clamp(56px, 11vw, 80px);\n  line-height: 1.1;\n  overflow-wrap: break-word;\n  word-break: break-all;\n  max-width: 100%;\n}\n.fiat-display {\n  font-size: 14px;\n}\n.bottom-panel {\n  margin-top: auto;\n  background: var(--q-color-grey-1);\n  padding-bottom: env(safe-area-inset-bottom, 0px);\n}\n.keypad-wrapper {\n  width: 100%;\n  display: flex;\n  justify-content: center;\n}\n</style>\n"
  },
  {
    "path": "src/components/DisplayTokenComponent.vue",
    "content": "<template>\n  <div\n    :class=\"$q.dark.isActive ? 'bg-dark' : 'bg-white'\"\n    class=\"display-token-fullscreen\"\n  >\n    <!-- Header -->\n    <div class=\"row items-center q-pa-md\" style=\"position: relative\">\n      <q-btn\n        v-close-popup\n        flat\n        round\n        icon=\"close\"\n        color=\"grey\"\n        class=\"floating-close-btn\"\n        @click=\"closeCardScanner\"\n      />\n      <div class=\"col text-center fixed-title-height\">\n        <div v-show=\"!showExpandedButtons\">\n          <q-item-label\n            overline\n            class=\"q-mt-sm text-white\"\n            style=\"font-size: 1rem\"\n          >\n            {{\n              sendData.historyToken.amount && sendData.historyToken.amount < 0\n                ? sendData.historyToken.status === \"paid\"\n                  ? \"Sent\"\n                  : \"Pending\"\n                : \"Received\"\n            }}\n            Ecash</q-item-label\n          >\n        </div>\n      </div>\n      <!-- Floating actions (expandable) -->\n      <div class=\"floating-actions\">\n        <div class=\"row no-wrap items-center\">\n          <div\n            v-if=\"showExpandedButtons\"\n            class=\"row no-wrap items-center q-gutter-xs justify-end q-mr-sm\"\n          >\n            <q-btn\n              class=\"q-mx-xs q-pb-sm\"\n              size=\"md\"\n              flat\n              dense\n              @click=\"copyUsingMixin(encodeToPeanut(sendData.tokensBase64))\"\n            >\n              <NutIcon :size=\"16\" />\n\n              <q-tooltip>{{\n                $t(\"SendTokenDialog.actions.copy_emoji.tooltip_text\")\n              }}</q-tooltip>\n            </q-btn>\n\n            <q-btn\n              v-if=\"webShareSupported\"\n              class=\"q-mx-xs q-pb-sm\"\n              size=\"md\"\n              dense\n              flat\n              @click=\"shareToken\"\n            >\n              <ShareIcon :size=\"16\" />\n              <q-tooltip>{{\n                $t(\"SendTokenDialog.actions.share.tooltip_text\")\n              }}</q-tooltip>\n            </q-btn>\n\n            <q-btn\n              class=\"q-mx-none\"\n              size=\"md\"\n              dense\n              icon=\"link\"\n              flat\n              @click=\"\n                copyUsingMixin(baseURL + '#token=' + sendData.tokensBase64)\n              \"\n              ><q-tooltip>{{\n                $t(\"SendTokenDialog.actions.copy_link.tooltip_text\")\n              }}</q-tooltip></q-btn\n            >\n            <q-btn\n              unelevated\n              dense\n              size=\"md\"\n              class=\"q-mx-xs\"\n              v-if=\"\n                hasCamera &&\n                !sendData.paymentRequest &&\n                sendData.historyAmount < 0\n              \"\n              @click=\"showCamera\"\n            >\n              <ScanIcon :size=\"16\" />\n            </q-btn>\n            <q-btn\n              unelevated\n              dense\n              v-if=\"\n                ndefSupported &&\n                !sendData.paymentRequest &&\n                sendData.historyAmount < 0\n              \"\n              :disabled=\"scanningCard\"\n              :loading=\"scanningCard\"\n              class=\"q-mx-xs\"\n              size=\"md\"\n              @click=\"writeTokensToCard\"\n              flat\n            >\n              <NfcIcon :size=\"16\" />\n              <q-tooltip>{{\n                ndefSupported\n                  ? $t(\n                      \"SendTokenDialog.actions.write_tokens_to_card.tooltips.ndef_supported_text\"\n                    )\n                  : $t(\n                      \"SendTokenDialog.actions.write_tokens_to_card.tooltips.ndef_unsupported_text\"\n                    )\n              }}</q-tooltip>\n              <template v-slot:loading>\n                <q-spinner @click=\"closeCardScanner\" />\n              </template>\n            </q-btn>\n            <q-btn\n              class=\"q-mx-none\"\n              dense\n              color=\"negative\"\n              icon=\"delete\"\n              size=\"sm\"\n              @click=\"\n                showDeleteDialog = true;\n                closeCardScanner();\n              \"\n              flat\n            >\n              <q-tooltip>{{\n                $t(\"SendTokenDialog.actions.delete.tooltip_text\")\n              }}</q-tooltip>\n            </q-btn>\n          </div>\n          <q-btn\n            class=\"q-mx-none\"\n            size=\"md\"\n            flat\n            dense\n            @click=\"toggleExpandButtons\"\n          >\n            <q-icon\n              name=\"more_horiz\"\n              :color=\"showExpandedButtons ? 'primary' : 'grey'\"\n            />\n          </q-btn>\n        </div>\n      </div>\n    </div>\n    <!-- Content area -->\n    <div class=\"content-area\">\n      <q-card-section class=\"q-pa-none\">\n        <div v-if=\"qrCodeFragment\" class=\"row justify-center q-mb-md\">\n          <div class=\"col-12 col-sm-11 col-md-8 q-px-md\">\n            <q-responsive :ratio=\"1\" class=\"q-mx-none\">\n              <vue-qrcode\n                :value=\"qrCodeFragment\"\n                :options=\"{ width: 600, height: 600 }\"\n                class=\"rounded-borders\"\n                style=\"width: 100%; height: 100%\"\n                @click=\"copyTokens\"\n              >\n              </vue-qrcode>\n            </q-responsive>\n            <div style=\"height: 2px\">\n              <q-linear-progress\n                v-if=\"runnerActive\"\n                indeterminate\n                color=\"primary\"\n              />\n            </div>\n          </div>\n        </div>\n        <div class=\"row justify-center q-pb-xs q-ba-none\">\n          <div\n            class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n            style=\"max-width: 600px; position: relative\"\n          >\n            <div class=\"row justify-center items-center no-wrap\">\n              <div class=\"q-gutter-sm\">\n                <q-btn\n                  v-if=\"showAnimatedQR\"\n                  flat\n                  style=\"font-size: 10px\"\n                  color=\"grey\"\n                  class=\"q-ma-none\"\n                  @click=\"changeSpeed\"\n                >\n                  <q-icon name=\"speed\" style=\"margin-right: 8px\"></q-icon>\n                  Speed: {{ fragmentSpeedLabel }}\n                </q-btn>\n                <q-btn\n                  v-if=\"showAnimatedQR\"\n                  flat\n                  style=\"font-size: 10px\"\n                  class=\"q-ma-none\"\n                  color=\"grey\"\n                  @click=\"changeSize\"\n                >\n                  <q-icon name=\"zoom_in\" style=\"margin-right: 8px\"></q-icon>\n                  Size: {{ fragmentLengthLabel }}\n                </q-btn>\n              </div>\n            </div>\n          </div>\n        </div>\n        <q-card-section class=\"q-pa-sm\">\n          <div class=\"row justify-center q-pt-lg\">\n            <TokenInformation\n              :encodedToken=\"sendData.tokensBase64\"\n              :payment-request-id=\"sendData.historyToken?.paymentRequestId\"\n            />\n          </div>\n          <div\n            v-if=\"sendData.paymentRequest\"\n            class=\"row justify-center q-pt-md\"\n          >\n            <div\n              class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n              style=\"max-width: 600px\"\n            >\n              <PaymentRequestInfo :request=\"sendData.paymentRequest\" />\n            </div>\n          </div>\n          <div\n            v-if=\"sendData.historyToken?.meltQuote\"\n            class=\"row justify-center q-pt-md\"\n          >\n            <div\n              class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n              style=\"max-width: 600px\"\n            >\n              <MeltQuoteInformation\n                :melt-quote=\"sendData.historyToken.meltQuote\"\n                :invoice=\"sendData.historyToken\"\n                :mint-url=\"sendData.historyToken.mint\"\n                :history-paid-at=\"sendData.historyToken.paidDate\"\n                :show-amount=\"false\"\n              />\n            </div>\n          </div>\n          <div\n            v-if=\"\n              sendData.paymentRequest &&\n              sendData.historyToken &&\n              sendData.historyToken.amount < 0 &&\n              sendData.historyToken.status === 'pending'\n            \"\n            class=\"row justify-center q-pt-sm\"\n          >\n            <SendPaymentRequest />\n          </div>\n        </q-card-section>\n      </q-card-section>\n    </div>\n    <!-- Fixed bottom panel with copy button -->\n    <div class=\"bottom-panel\">\n      <div class=\"row justify-center q-pb-md q-pt-sm\">\n        <div class=\"col-12 col-sm-11 col-md-8 q-px-md\" style=\"max-width: 600px\">\n          <q-btn\n            class=\"full-width\"\n            unelevated\n            size=\"lg\"\n            color=\"primary\"\n            rounded\n            @click=\"copyTokens\"\n          >\n            {{ copyButtonLabel }}\n          </q-btn>\n        </div>\n      </div>\n    </div>\n  </div>\n  <!-- popup dialog to confirm deletion -->\n  <q-dialog v-model=\"showDeleteDialog\">\n    <q-card class=\"q-pa-lg q-pt-md qcard\">\n      <q-card-section class=\"q-pa-none\">\n        <div class=\"row items-center no-wrap q-mb-sm\">\n          <div class=\"col-12\">\n            <span class=\"text-h6\">Delete Ecash</span>\n          </div>\n        </div>\n        <div class=\"row items-center no-wrap q-my-sm q-py-none\">\n          <div class=\"col-12\">\n            <q-item-label>\n              Are you sure you want to delete this transaction from your\n              history?\n            </q-item-label>\n            <q-item-label class=\"q-pt-md text-weight-bold\">\n              Warning: This action cannot be undone and there is no way to\n              recover the token.\n            </q-item-label>\n          </div>\n        </div>\n        <div class=\"row q-mt-lg\">\n          <q-btn\n            @click=\"deleteThisToken\"\n            color=\"negative\"\n            rounded\n            class=\"q-mr-sm\"\n            >Delete</q-btn\n          >\n          <q-btn v-close-popup rounded flat color=\"grey\" class=\"q-ml-auto\"\n            >Cancel</q-btn\n          >\n        </div>\n      </q-card-section>\n    </q-card>\n  </q-dialog>\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { Buffer } from \"buffer\";\nimport { UR, UREncoder } from \"@gandlaf21/bc-ur\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport { useSendTokensStore } from \"src/stores/sendTokensStore\";\nimport { useWorkersStore } from \"src/stores/workers\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { useCameraStore } from \"src/stores/camera\";\nimport { useSettingsStore } from \"src/stores/settings\";\nimport { useTokensStore } from \"src/stores/tokens\";\nimport { useMintsStore } from \"src/stores/mints\";\nimport TokenInformation from \"components/TokenInformation.vue\";\nimport MeltQuoteInformation from \"components/MeltQuoteInformation.vue\";\nimport SendPaymentRequest from \"./SendPaymentRequest.vue\";\nimport PaymentRequestInfo from \"./PaymentRequestInfo.vue\";\nimport {\n  getDecodedToken,\n  getEncodedTokenBinary,\n  getEncodedToken,\n  getEncodedTokenV4,\n} from \"@cashu/cashu-ts\";\nimport token from \"src/js/token\";\nimport { notifyError, notifySuccess } from \"src/js/notify\";\nimport { copyToClipboard } from \"quasar\";\nimport {\n  Scan as ScanIcon,\n  Nfc as NfcIcon,\n  Share as ShareIcon,\n  Nut as NutIcon,\n} from \"lucide-vue-next\";\n\ndeclare const windowMixin: any;\n\nexport default defineComponent({\n  name: \"DisplayTokenComponent\",\n  mixins: [windowMixin],\n  components: {\n    TokenInformation,\n    MeltQuoteInformation,\n    SendPaymentRequest,\n    PaymentRequestInfo,\n    ScanIcon,\n    NfcIcon,\n    ShareIcon,\n    NutIcon,\n  },\n  data: function () {\n    return {\n      baseURL: location.protocol + \"//\" + location.host + location.pathname,\n      showAnimatedQR: false,\n      qrCodeFragment: \"\",\n      qrInterval: null as any,\n      encoder: null as any,\n      // animated QR params\n      currentFragmentLength: 150,\n      fragmentLengthMedium: 100,\n      fragmentLengthShort: 50,\n      fragmentLengthLong: 150,\n      fragmentLengthLabel: \"L\",\n      currentFragmentInterval: 150,\n      fragmentIntervalMedium: 250,\n      fragmentIntervalFast: 150,\n      framentInervalSlow: 500,\n      fragmentSpeedLabel: \"F\",\n      isV4Token: false,\n      scanningCard: false,\n      showExpandedButtons: false,\n      showDeleteDialog: false,\n      copyButtonCopied: false,\n      copyButtonTimeout: null as any,\n    };\n  },\n  computed: {\n    ...mapWritableState(useSendTokensStore, [\"showSendTokens\", \"sendData\"]),\n    ...mapWritableState(useCameraStore, [\"hasCamera\"]),\n    ...mapState(useUiStore, [\"ndefSupported\", \"webShareSupported\"]),\n    ...mapState(useWorkersStore, [\"tokenWorkerRunning\"]),\n    ...mapState(useSettingsStore, [\"nfcEncoding\"]),\n    // display helpers\n    sumProofs: function () {\n      const proofs = token.getProofs(token.decode(this.sendData.tokensBase64));\n      return proofs.flat().reduce((sum, el) => (sum += el.amount), 0);\n    },\n    displayUnit: function () {\n      const display = this.formatCurrency(this.sumProofs, this.tokenUnit);\n      return display;\n    },\n    tokenUnit: function () {\n      const unit = token.getUnit(token.decode(this.sendData.tokensBase64));\n      return unit;\n    },\n    paidFees: function () {\n      return this.sumProofs - Math.abs(this.sendData.historyAmount);\n    },\n    runnerActive: function () {\n      return this.tokenWorkerRunning;\n    },\n    copyButtonLabel: function () {\n      if (this.copyButtonCopied) {\n        return \"Copied\";\n      }\n      return this.$t(\"SendTokenDialog.actions.copy_tokens.label\");\n    },\n  },\n  watch: {\n    \"sendData.tokensBase64\": function (val: string) {\n      this.showAnimatedQR = false;\n      if (!val?.length) {\n        return;\n      }\n      const tokenObj = token.decode(val);\n      const proofs = tokenObj.proofs || [];\n      if (!proofs.length) {\n        return;\n      } else if (proofs.length <= 2) {\n        this.qrCodeFragment = val;\n      } else {\n        this.showAnimatedQR = true;\n        this.qrCodeFragment = \"\";\n        this.startQrCodeLoop();\n      }\n      this.isV4Token = val.startsWith(\"cashuB\");\n    },\n  },\n  methods: {\n    ...mapActions(useWorkersStore, [\"clearAllWorkers\"]),\n    ...mapActions(useTokensStore, [\"deleteToken\"]),\n    ...mapActions(useCameraStore, [\"showCamera\"]),\n    copyUsingMixin(text: string) {\n      (this as any).copyText(text);\n    },\n    toggleExpandButtons() {\n      this.showExpandedButtons = !this.showExpandedButtons;\n    },\n    initQr: function () {\n      const val = this.sendData?.tokensBase64;\n      this.showAnimatedQR = false;\n      if (!val?.length) {\n        this.qrCodeFragment = \"\";\n        return;\n      }\n      const tokenObj = token.decode(val);\n      const proofs = tokenObj.proofs || [];\n      if (!proofs.length) {\n        this.qrCodeFragment = \"\";\n        return;\n      } else if (proofs.length <= 2) {\n        this.qrCodeFragment = val;\n      } else {\n        this.showAnimatedQR = true;\n        this.qrCodeFragment = \"\";\n        this.startQrCodeLoop();\n      }\n      this.isV4Token = val.startsWith(\"cashuB\");\n    },\n    startQrCodeLoop: async function () {\n      if (this.sendData.tokensBase64.length == 0) {\n        return;\n      }\n      const messageBuffer = Buffer.from(this.sendData.tokensBase64);\n      const ur = UR.fromBuffer(messageBuffer);\n      const firstSeqNum = 0;\n      this.encoder = new UREncoder(ur, this.currentFragmentLength, firstSeqNum);\n      clearInterval(this.qrInterval);\n      this.qrInterval = setInterval(() => {\n        this.qrCodeFragment = this.encoder.nextPart();\n      }, this.currentFragmentInterval);\n    },\n    updateQrCode: function () {\n      this.qrCodeFragment = this.encoder.nextPart();\n    },\n    changeSpeed: function () {\n      if (this.currentFragmentInterval == this.fragmentIntervalMedium) {\n        this.currentFragmentInterval = this.framentInervalSlow;\n        this.fragmentSpeedLabel = \"S\";\n      } else if (this.currentFragmentInterval == this.framentInervalSlow) {\n        this.currentFragmentInterval = this.fragmentIntervalFast;\n        this.fragmentSpeedLabel = \"F\";\n      } else {\n        this.currentFragmentInterval = this.fragmentIntervalMedium;\n        this.fragmentSpeedLabel = \"M\";\n      }\n      this.startQrCodeLoop();\n    },\n    changeSize: function () {\n      if (this.currentFragmentLength == this.fragmentLengthMedium) {\n        this.currentFragmentLength = this.fragmentLengthShort;\n        this.fragmentLengthLabel = \"S\";\n      } else if (this.currentFragmentLength == this.fragmentLengthShort) {\n        this.currentFragmentLength = this.fragmentLengthLong;\n        this.fragmentLengthLabel = \"L\";\n      } else {\n        this.currentFragmentLength = this.fragmentLengthMedium;\n        this.fragmentLengthLabel = \"M\";\n      }\n      this.startQrCodeLoop();\n    },\n    toggleTokenEncoding: function () {\n      const decodedToken = token.decode(this.sendData.tokensBase64);\n      if (this.sendData.tokensBase64.startsWith(\"cashuA\")) {\n        try {\n          this.sendData.tokensBase64 = getEncodedTokenV4(decodedToken);\n        } catch {\n          this.sendData.tokensBase64 = getEncodedToken(decodedToken, {\n            version: 3,\n          });\n        }\n      } else {\n        this.sendData.tokensBase64 = getEncodedToken(decodedToken, {\n          version: 3,\n        });\n      }\n    },\n    encodeToPeanut: function (tokenStr: string) {\n      return (\n        \"🥜\" +\n        Array.from(tokenStr)\n          .map((char) => {\n            const byteValue = char.charCodeAt(0);\n            if (byteValue >= 0 && byteValue <= 15) {\n              return String.fromCodePoint(0xfe00 + byteValue);\n            }\n            if (byteValue >= 16 && byteValue <= 255) {\n              return String.fromCodePoint(0xe0100 + (byteValue - 16));\n            }\n            return \"\";\n          })\n          .join(\"\")\n      );\n    },\n    shareToken: async function () {\n      if (!this.webShareSupported) {\n        return;\n      }\n      const shareData = {\n        text: `cashu:${this.sendData.tokensBase64}`,\n      };\n      try {\n        await navigator.share(shareData);\n      } catch (error: any) {\n        if (error?.name !== \"AbortError\") {\n          console.error(\"Error sharing token:\", error);\n        }\n      }\n    },\n    writeTokensToCard: function () {\n      if (!this.scanningCard) {\n        try {\n          // @ts-ignore\n          this.ndef = new NDEFReader();\n          // @ts-ignore\n          this.controller = new AbortController();\n          const signal = this.controller.signal;\n          this.ndef\n            .scan({ signal })\n            .then(() => {\n              this.ndef.onreadingerror = (error: any) => {\n                console.error(`NFC read failed: ${error}`);\n                notifyError(`${error?.message || error}`, \"NFC read failed\");\n                this.controller.abort();\n                this.scanningCard = false;\n              };\n\n              this.ndef.onreading = ({ message, serialNumber }: any) => {\n                this.controller.abort();\n                this.scanningCard = false;\n                try {\n                  let records: any[] = [];\n                  switch (this.nfcEncoding) {\n                    case \"text\":\n                      records = [\n                        {\n                          recordType: \"text\",\n                          data: `${this.sendData.tokensBase64}`,\n                          lang: \"en\",\n                        },\n                      ];\n                      break;\n                    case \"weburl\":\n                      records = [\n                        {\n                          recordType: \"url\",\n                          data: `${window.location}#token=${this.sendData.tokensBase64}`,\n                        },\n                      ];\n                      break;\n                    case \"binary\": {\n                      const decoded = getDecodedToken(\n                        this.sendData.tokensBase64,\n                        useMintsStore().allMintKeysets\n                      );\n                      const data = getEncodedTokenBinary(decoded);\n                      records = [\n                        {\n                          recordType: \"mime\",\n                          mediaType: \"application/octet-stream\",\n                          data: data,\n                        },\n                      ];\n                      break;\n                    }\n                    default:\n                      throw new Error(\n                        `Unknown NFC encoding: ${this.nfcEncoding}`\n                      );\n                  }\n                  notifySuccess(\"Writing to NFC card...\");\n                  this.ndef\n                    .write({ records: records }, { overwrite: true })\n                    .then(() => {\n                      notifySuccess(\"Successfully flashed token to card!\");\n                    })\n                    .catch((err: any) => {\n                      console.error(\n                        `NFC write failed: The card may not have enough capacity (needed ${records[0].data.length} bytes).`\n                      );\n                      notifyError(\n                        `The card may not have enough capacity (needed ${records[0].data.length} bytes).`,\n                        \"NFC write failed\"\n                      );\n                    });\n                } catch (err: any) {\n                  console.error(`NFC error: ${err?.message}`);\n                  notifyError(`${err?.message}`, \"NFC error\");\n                }\n              };\n              this.scanningCard = true;\n            })\n            .catch((error: any) => {\n              console.error(`NFC error: ${error?.message}`);\n              notifyError(`${error?.message}`, \"NFC error\");\n              this.scanningCard = false;\n            });\n        } catch (error: any) {\n          console.error(`NFC error: ${error?.message}`);\n          notifyError(`${error?.message}`, \"NFC error\");\n          this.scanningCard = false;\n        }\n      }\n    },\n    closeCardScanner: function () {\n      // @ts-ignore\n      this.controller?.abort?.();\n      this.scanningCard = false;\n    },\n    deleteThisToken: function () {\n      this.deleteToken(this.sendData.tokensBase64);\n      this.showSendTokens = false;\n      this.showDeleteDialog = false;\n      this.clearAllWorkers();\n    },\n    copyTokens: async function () {\n      try {\n        await copyToClipboard(this.sendData.tokensBase64);\n        this.copyButtonCopied = true;\n        // Clear any existing timeout\n        if (this.copyButtonTimeout) {\n          clearTimeout(this.copyButtonTimeout);\n        }\n        // Reset button label after 3 seconds\n        this.copyButtonTimeout = setTimeout(() => {\n          this.copyButtonCopied = false;\n        }, 3000);\n      } catch (error) {\n        console.error(\"Failed to copy to clipboard:\", error);\n      }\n    },\n  },\n  mounted() {\n    this.initQr();\n  },\n  beforeUnmount() {\n    clearInterval(this.qrInterval);\n    if (this.copyButtonTimeout) {\n      clearTimeout(this.copyButtonTimeout);\n    }\n  },\n});\n</script>\n<style scoped>\n.display-token-fullscreen {\n  height: 100vh; /* fallback */\n  height: 100dvh; /* account for mobile browser UI */\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n.content-area {\n  flex: 1;\n  overflow-y: auto;\n  max-width: 600px;\n  width: 100%;\n  margin: 0 auto;\n}\n.floating-close-btn {\n  position: absolute;\n  left: 16px;\n  top: 50%;\n  transform: translateY(-50%);\n  z-index: 1;\n}\n.floating-actions {\n  position: absolute;\n  right: 16px;\n  top: 50%;\n  transform: translateY(-50%);\n  z-index: 2;\n}\n.fixed-title-height {\n  height: 24px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.bottom-panel {\n  margin-top: auto;\n  background: var(--q-color-grey-1);\n  box-shadow: 0 -8px 16px rgba(0, 0, 0, 0.05);\n  padding-bottom: env(safe-area-inset-bottom, 0px);\n  position: sticky;\n  bottom: 0;\n  z-index: 2;\n}\n</style>\n"
  },
  {
    "path": "src/components/EditMintDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showEditMintDialogLocal\"\n    backdrop-filter=\"blur(4px) brightness(50%)\"\n    transition-show=\"fade\"\n    transition-hide=\"fade\"\n  >\n    <q-card class=\"edit-mint-dialog\">\n      <!-- Header Section -->\n      <div class=\"edit-mint-header q-pa-md\">\n        <div class=\"edit-mint-title-row\">\n          <h4 class=\"edit-mint-title q-my-none\">\n            {{ $t(\"EditMintDialog.title\") }}\n          </h4>\n        </div>\n      </div>\n\n      <!-- Content Section -->\n      <div class=\"edit-mint-content q-px-md q-pb-md\">\n        <div class=\"q-mb-lg\">\n          <label class=\"input-label\">{{\n            $t(\"EditMintDialog.inputs.mint_url.label\")\n          }}</label>\n          <q-input\n            v-model=\"editMintData.url\"\n            dense\n            class=\"mint-input q-mb-md\"\n            filled\n            type=\"textarea\"\n            autogrow\n            style=\"font-family: monospace; font-size: 0.9em\"\n          ></q-input>\n\n          <label class=\"input-label\">{{\n            $t(\"EditMintDialog.inputs.nickname.label\")\n          }}</label>\n          <q-input\n            v-model=\"editMintData.nickname\"\n            dense\n            class=\"mint-input\"\n            filled\n            type=\"textarea\"\n            autogrow\n            placeholder=\"e.g. Testnet\"\n          ></q-input>\n        </div>\n\n        <div class=\"action-buttons\">\n          <q-btn flat class=\"cancel-btn\" v-close-popup>\n            {{ $t(\"EditMintDialog.actions.cancel.label\") }}\n          </q-btn>\n          <q-spacer></q-spacer>\n          <q-btn\n            color=\"primary\"\n            class=\"update-btn\"\n            @click=\"updateMintLocal\"\n            v-close-popup\n          >\n            {{ $t(\"EditMintDialog.actions.update.label\") }}\n          </q-btn>\n        </div>\n      </div>\n    </q-card>\n  </q-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, ref, watch, computed } from \"vue\";\nimport { useMintsStore } from \"src/stores/mints\";\nimport { useQuasar } from \"quasar\";\n\nexport default defineComponent({\n  name: \"EditMintDialog\",\n  props: {\n    mint: {\n      type: Object,\n      required: true,\n    },\n    showEditMintDialog: {\n      type: Boolean,\n      required: true,\n    },\n  },\n  setup(props, { emit }) {\n    const $q = useQuasar();\n    const editMintData = ref({\n      url: \"\",\n      nickname: \"\",\n    });\n    const mintToEdit = ref({});\n    const showEditMintDialogLocal = computed({\n      get: () => props.showEditMintDialog,\n      set: (value) => emit(\"update:showEditMintDialog\", value),\n    });\n\n    watch(\n      () => props.mint,\n      (newMint) => {\n        mintToEdit.value = { ...newMint };\n        editMintData.value = { ...newMint };\n      },\n      { immediate: true }\n    );\n\n    const updateMintLocal = () => {\n      const mintStore = useMintsStore();\n      mintStore.updateMint(mintToEdit.value, editMintData.value);\n      showEditMintDialogLocal.value = false;\n    };\n\n    return {\n      editMintData,\n      mintToEdit,\n      updateMintLocal,\n      showEditMintDialogLocal,\n      isDark: computed(() => $q.dark.isActive),\n    };\n  },\n});\n</script>\n\n<style scoped>\n.edit-mint-dialog {\n  width: 100%;\n  max-width: 450px;\n  border-radius: 16px;\n  overflow: hidden;\n}\n\n.edit-mint-header {\n  position: relative;\n  padding-top: 20px;\n}\n\n.edit-mint-title-row {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.edit-mint-title {\n  font-size: 24px;\n  font-weight: 700;\n  letter-spacing: -0.5px;\n  font-family: \"Inter\", sans-serif;\n}\n\n.edit-mint-content {\n  padding-top: 0;\n}\n\n.edit-mint-description {\n  font-size: 15px;\n  line-height: 1.5;\n  font-weight: 400;\n  margin-top: 0;\n  opacity: 0.7;\n  font-family: \"Inter\", sans-serif;\n}\n\n.input-label {\n  display: block;\n  font-size: 13px;\n  font-weight: 600;\n  margin-bottom: 8px;\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n  opacity: 0.7;\n  font-family: \"Inter\", sans-serif;\n}\n\n.mint-input {\n  border-radius: 8px;\n  height: 48px;\n  font-size: 16px;\n  font-family: \"Inter\", sans-serif;\n}\n\n/* Completely remove all input animations */\n:deep(.mint-input) {\n  /* Disable all transitions on the input and its children except for background-color */\n  * {\n    transition: none !important;\n    animation: none !important;\n  }\n\n  /* Add a smooth transition just for the background-color */\n  transition: background-color 0.2s ease-in-out !important;\n}\n\n:deep(.mint-input .q-field__focus-target) {\n  border-radius: 8px;\n}\n\n:deep(.mint-input .q-focus-helper) {\n  /* Remove animation completely */\n  opacity: 0 !important;\n  display: none !important; /* Hide it completely */\n}\n\n/* Add subtle focus/active state - theme responsive */\n:deep(.mint-input.q-field--focused) {\n  background-color: rgba(255, 255, 255, 0.1);\n}\n\n/* For dark mode, adjust the focus color */\n:deep(.body--dark .mint-input.q-field--focused) {\n  background-color: rgba(255, 255, 255, 0.07);\n}\n\n/* For light mode, use a darker shade for contrast */\n:deep(.body--light .mint-input.q-field--focused) {\n  background-color: rgba(0, 0, 0, 0.05);\n}\n\n/* Remove any ripple effects */\n:deep(.mint-input .q-ripple) {\n  display: none !important;\n}\n\n/* Remove any before/after pseudo-elements that might animate */\n:deep(.mint-input .q-field__control:before),\n:deep(.mint-input .q-field__control:after) {\n  display: none !important;\n}\n\n/* Ensure no border animations */\n:deep(.mint-input .q-field__control) {\n  height: 48px;\n  border-radius: 8px;\n  transition: none !important;\n}\n\n:deep(.mint-input .q-field__native) {\n  padding: 12px;\n  font-family: \"Inter\", sans-serif;\n}\n\n/* Make sure input placeholders use Inter font */\n:deep(.mint-input .q-field__native),\n:deep(.mint-input .q-field__input),\n:deep(.mint-input .q-placeholder) {\n  font-family: \"Inter\", sans-serif;\n}\n\n.action-buttons {\n  display: flex;\n  justify-content: space-between;\n  margin-top: 32px;\n}\n\n.cancel-btn {\n  font-weight: 600;\n  padding: 8px 16px;\n  border-radius: 8px;\n  font-family: \"Inter\", sans-serif;\n}\n\n.update-btn {\n  font-weight: 700;\n  padding: 8px 20px;\n  border-radius: 8px;\n  transition: all 0.2s ease;\n  font-family: \"Inter\", sans-serif;\n}\n\n.update-btn:hover {\n  transform: translateY(-1px);\n}\n\n/* Subtle hover effects */\n:deep(.close-btn:hover) {\n  background: rgba(128, 128, 128, 0.2);\n}\n</style>\n"
  },
  {
    "path": "src/components/EssentialLink.vue",
    "content": "<template>\n  <q-item clickable tag=\"a\" target=\"_blank\" :href=\"link\">\n    <q-item-section v-if=\"icon\" avatar>\n      <q-icon :name=\"icon\" />\n    </q-item-section>\n\n    <q-item-section>\n      <q-item-label>{{ title }}</q-item-label>\n      <q-item-label caption>{{ caption }}</q-item-label>\n    </q-item-section>\n  </q-item>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\n\nexport default defineComponent({\n  name: \"EssentialLink\",\n  props: {\n    title: {\n      type: String,\n      required: true,\n    },\n\n    caption: {\n      type: String,\n      default: \"\",\n    },\n\n    link: {\n      type: String,\n      default: \"#\",\n    },\n\n    icon: {\n      type: String,\n      default: \"\",\n    },\n  },\n});\n</script>\n"
  },
  {
    "path": "src/components/FullscreenHeader.vue",
    "content": "<template>\n  <q-header class=\"bg-dark\">\n    <q-toolbar>\n      <q-btn\n        flat\n        dense\n        round\n        icon=\"arrow_back_ios_new\"\n        @click=\"goBack\"\n        color=\"primary\"\n        aria-label=\"Back\"\n      />\n    </q-toolbar>\n  </q-header>\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { useRouter } from \"vue-router\";\n\nexport default defineComponent({\n  name: \"FullscreenHeader\",\n  mixins: [windowMixin],\n  setup() {\n    const router = useRouter();\n\n    const goBack = () => {\n      router.back();\n    };\n\n    return {\n      goBack,\n    };\n  },\n});\n</script>\n"
  },
  {
    "path": "src/components/HistoryTable.vue",
    "content": "<template>\n  <div class=\"q-pa-xs\" style=\"max-width: 500px; margin: 0 auto\">\n    <q-list>\n      <q-item\n        v-for=\"transaction in paginatedTransactions\"\n        :key=\"getTransactionKey(transaction)\"\n        clickable\n        v-ripple\n        class=\"q-px-md q-py-md\"\n      >\n        <!-- Icon Section -->\n        <q-item-section avatar class=\"q-pr-md\" style=\"min-width: 40px\">\n          <q-avatar size=\"32px\">\n            <CoinsIcon\n              v-if=\"isEcashTransaction(transaction)\"\n              class=\"transaction-icon\"\n            />\n            <ZapIcon v-else class=\"transaction-icon\" />\n          </q-avatar>\n        </q-item-section>\n\n        <!-- Main Content Section -->\n        <q-item-section @click=\"showTransactionDialog(transaction)\">\n          <q-item-label class=\"row items-center justify-between\">\n            <!-- Transaction Label -->\n            <div class=\"col text-left\">\n              <span class=\"transaction-label text-weight-medium\">\n                {{ getTransactionLabel(transaction) }}\n              </span>\n            </div>\n\n            <!-- Amount -->\n            <div class=\"text-right\">\n              <div\n                class=\"amount-text text-weight-bold\"\n                :class=\"{\n                  'text-amount-positive':\n                    transaction.amount >= 0 && transaction.status !== 'pending',\n                  'text-grey-6': transaction.status === 'pending',\n                }\"\n              >\n                <span v-if=\"transaction.amount >= 0\">+</span\n                >{{ formatCurrency(transaction.amount, transaction.unit) }}\n              </div>\n            </div>\n          </q-item-label>\n\n          <q-item-label\n            caption\n            class=\"q-mt-none row items-center justify-between\"\n          >\n            <!-- Date -->\n            <div class=\"text-grey-6\">\n              {{\n                $t(\"HistoryTable.row.date_label\", {\n                  value: formattedDate(transaction.date),\n                })\n              }}\n            </div>\n\n            <!-- Status or empty space for consistent height -->\n            <div class=\"text-grey-6 text-caption\">\n              <span v-if=\"transaction.status === 'pending'\">Pending</span>\n              <span v-else>&nbsp;</span>\n            </div>\n          </q-item-label>\n        </q-item-section>\n\n        <!-- Actions Section -->\n        <q-item-section side top style=\"min-width: 40px\">\n          <q-btn\n            flat\n            dense\n            round\n            :icon=\"transaction.longPressActive ? 'arrow_circle_down' : 'sync'\"\n            @click=\"\n              transaction.longPressActive\n                ? handleLongPress(transaction)\n                : checkTransactionStatus(transaction)\n            \"\n            @mousedown=\"startLongPress(transaction)\"\n            @mouseup=\"endLongPress(transaction)\"\n            @touchstart=\"startLongPress(transaction)\"\n            @touchend=\"endLongTap(transaction)\"\n            class=\"cursor-pointer\"\n            v-if=\"transaction.status === 'pending'\"\n            size=\"sm\"\n          >\n            <q-tooltip>{{\n              transaction.longPressActive\n                ? $t(\"HistoryTable.actions.receive.tooltip_text\")\n                : $t(\"HistoryTable.actions.check_status.tooltip_text\")\n            }}</q-tooltip>\n          </q-btn>\n        </q-item-section>\n      </q-item>\n    </q-list>\n\n    <!-- Filter Controls -->\n    <div class=\"text-center q-mt-lg\">\n      <q-btn\n        rounded\n        outline\n        dense\n        @click=\"filterPending = !filterPending\"\n        :color=\"filterPending ? 'primary' : 'grey'\"\n        :label=\"\n          $t(\n            filterPending\n              ? 'HistoryTable.actions.show_all.label'\n              : 'HistoryTable.actions.filter_pending.label'\n          )\n        \"\n        class=\"q-ml-sm q-px-md\"\n        size=\"sm\"\n      />\n      <q-btn\n        v-if=\"filterPending\"\n        rounded\n        outline\n        dense\n        @click=\"filterPendingEcash = !filterPendingEcash\"\n        :color=\"filterPendingEcash ? 'primary' : 'grey'\"\n        label=\"Ecash Only\"\n        class=\"q-ml-sm q-px-md\"\n        size=\"sm\"\n      />\n    </div>\n\n    <!-- Empty State -->\n    <div v-if=\"paginatedTransactions.length === 0\" class=\"text-center q-mt-lg\">\n      <q-item-label caption class=\"text-primary\">{{\n        $t(\"HistoryTable.empty_text\")\n      }}</q-item-label>\n    </div>\n\n    <!-- Pagination -->\n    <div v-else-if=\"maxPages > 1\" class=\"text-center q-mt-lg\">\n      <div style=\"display: flex; justify-content: center\">\n        <q-pagination\n          v-model=\"currentPage\"\n          :max=\"maxPages\"\n          :max-pages=\"5\"\n          direction-links\n          boundary-links\n          @input=\"handlePageChange\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n<script lang=\"ts\">\nimport * as _ from \"underscore\";\nimport { defineComponent } from \"vue\";\nimport { shortenString } from \"src/js/string-utils\";\nimport { formatDistanceToNow, parseISO } from \"date-fns\";\nimport { useTokensStore } from \"src/stores/tokens\";\nimport { mapState, mapWritableState, mapActions } from \"pinia\";\nimport { useReceiveTokensStore } from \"src/stores/receiveTokensStore\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useSendTokensStore } from \"src/stores/sendTokensStore\";\nimport { useUiStore } from \"src/stores/ui\";\nimport token from \"../js/token\";\nimport { notify } from \"src/js/notify\";\nimport { Coins as CoinsIcon, Zap as ZapIcon } from \"lucide-vue-next\";\n\nexport default defineComponent({\n  name: \"HistoryTable\",\n  components: {\n    CoinsIcon,\n    ZapIcon,\n  },\n  mixins: [windowMixin],\n  props: {},\n  data: function () {\n    return {\n      currentPage: 1,\n      pageSize: 5,\n      filterPending: false,\n      filterPendingEcash: false,\n      cachedUnifiedTransactions: [],\n    };\n  },\n  watch: {\n    filterPending: function () {\n      this.currentPage = 1;\n      // Reset ecash filter when pending filter is turned off\n      if (!this.filterPending) {\n        this.filterPendingEcash = false;\n      }\n    },\n    filterPendingEcash: function () {\n      this.currentPage = 1;\n    },\n    // Watch for any changes in historyTokens (additions, updates, deletions)\n    historyTokens: {\n      handler: function () {\n        this.updateUnifiedTransactions();\n      },\n      deep: true,\n      immediate: true,\n    },\n    // Watch for any changes in invoiceHistory (additions, updates, deletions)\n    invoiceHistory: {\n      handler: function () {\n        this.updateUnifiedTransactions();\n      },\n      deep: true,\n      immediate: true,\n    },\n  },\n  computed: {\n    ...mapState(useTokensStore, [\"historyTokens\"]),\n    ...mapState(useWalletStore, [\"invoiceHistory\"]),\n    ...mapWritableState(useReceiveTokensStore, [\n      \"showReceiveTokens\",\n      \"receiveData\",\n    ]),\n    ...mapWritableState(useSendTokensStore, [\n      \"showSendTokens\",\n      \"sendData\",\n      \"showLockInput\",\n    ]),\n    ...mapWritableState(useUiStore, [\"showInvoiceDetails\"]),\n    ...mapWritableState(useWalletStore, [\"invoiceData\", \"payInvoiceData\"]),\n\n    // Use cached unified transactions for better performance\n    unifiedTransactions() {\n      return this.cachedUnifiedTransactions;\n    },\n\n    // Filter transactions first, then paginate\n    filteredTransactions() {\n      if (this.filterPendingEcash) {\n        return this.unifiedTransactions.filter(\n          (transaction) =>\n            transaction.status === \"pending\" && transaction.type === \"ecash\"\n        );\n      }\n      if (this.filterPending) {\n        return this.unifiedTransactions.filter(\n          (transaction) => transaction.status === \"pending\"\n        );\n      }\n      return this.unifiedTransactions;\n    },\n\n    maxPages() {\n      return Math.ceil(this.filteredTransactions.length / this.pageSize);\n    },\n\n    paginatedTransactions() {\n      const start = (this.currentPage - 1) * this.pageSize;\n      const end = start + this.pageSize;\n      return this.filteredTransactions.slice(start, end);\n    },\n  },\n  methods: {\n    ...mapActions(useWalletStore, [\n      \"checkTokenSpendable\",\n      \"checkInvoice\",\n      \"checkOutgoingInvoice\",\n    ]),\n\n    handleLongPress(transaction) {\n      console.debug(\"### handleLongPress called\");\n      if (this.isEcashTransaction(transaction)) {\n        transaction.longPressActive = false;\n        this.receiveToken(transaction.token);\n      }\n    },\n\n    startLongPress(transaction) {\n      if (this.isEcashTransaction(transaction)) {\n        transaction.longPressActive = false;\n        this.longPressTimeout = setTimeout(() => {\n          transaction.longPressActive = true;\n        }, 1000); // 1 second long press\n      }\n    },\n\n    endLongPress(transaction) {\n      console.debug(\"### endLongPress called\");\n      clearTimeout(this.longPressTimeout);\n    },\n\n    endLongTap(transaction) {\n      console.debug(\"### endLongTap called\");\n      clearTimeout(this.longPressTimeout);\n      if (transaction.longPressActive) {\n        this.handleLongPress(transaction);\n      }\n    },\n\n    formattedDate(date_str) {\n      const date = parseISO(date_str);\n      return formatDistanceToNow(date, { addSuffix: false });\n    },\n\n    shortenString: function (s) {\n      return shortenString(s, 20, 10);\n    },\n\n    handlePageChange(page) {\n      this.currentPage = page;\n    },\n\n    receiveToken(tokenStr) {\n      this.receiveData.tokensBase64 = tokenStr;\n      this.showReceiveTokens = true;\n    },\n\n    getTransactionKey(transaction) {\n      return transaction.id;\n    },\n\n    isEcashTransaction(transaction) {\n      return transaction.type === \"ecash\";\n    },\n\n    isLightningTransaction(transaction) {\n      return transaction.type === \"lightning\";\n    },\n\n    getTransactionIcon(transaction) {\n      return transaction.type === \"lightning\"\n        ? \"flash_on\"\n        : \"account_balance_wallet\";\n    },\n\n    getTransactionIconColor(transaction) {\n      return transaction.type === \"lightning\" ? \"orange\" : \"blue\";\n    },\n\n    getDefaultLabel(transaction) {\n      return transaction.type === \"lightning\" ? \"Lightning\" : \"Ecash\";\n    },\n\n    getTransactionLabel(transaction) {\n      return transaction.label || this.getDefaultLabel(transaction);\n    },\n\n    checkTransactionStatus(transaction) {\n      if (transaction.type === \"ecash\") {\n        // If it's an incoming ecash transaction, open receive dialog\n        if (transaction.amount > 0) {\n          this.receiveToken(transaction.token);\n        } else {\n          // For outgoing ecash transactions, check spendable status\n          this.checkTokenSpendable(transaction);\n        }\n      } else if (transaction.type === \"lightning\") {\n        if (transaction.amount > 0) {\n          this.checkInvoice(transaction.quote, true);\n        } else {\n          this.checkOutgoingInvoice(transaction.quote, true);\n        }\n      }\n    },\n\n    showTransactionDialog(transaction) {\n      if (transaction.type === \"ecash\") {\n        // For pending incoming tokens, open receive dialog instead\n        if (transaction.status === \"pending\" && transaction.amount > 0) {\n          this.receiveToken(transaction.token);\n        } else {\n          this.showTokenDialog(transaction);\n        }\n      } else if (transaction.type === \"lightning\") {\n        this.showInvoiceDialog(transaction);\n      }\n    },\n\n    showTokenDialog: function (historyToken) {\n      if (historyToken.token === undefined) {\n        notify(this.$i18n.t(\"HistoryTable.old_token_not_found_error_text\"));\n        return;\n      }\n      const tokensBase64 = historyToken.token;\n      console.log(\"##### showTokenDialog\");\n      const tokenObj = token.decode(tokensBase64);\n      this.sendData.tokens = token.getProofs(tokenObj);\n      this.sendData.tokensBase64 = _.clone(tokensBase64);\n      this.sendData.paymentRequest = historyToken.paymentRequest;\n      this.sendData.historyAmount = historyToken.amount;\n      this.sendData.historyToken = historyToken;\n      this.showSendTokens = true;\n    },\n\n    showInvoiceDialog: async function (invoice) {\n      this.invoiceData = invoice;\n      this.showInvoiceDetails = true;\n      if (invoice.status === \"pending\") {\n        if (invoice.amount > 0) {\n          try {\n            await this.checkInvoice(invoice.quote, false, false);\n          } catch (e) {\n            // Handle error\n          }\n        } else {\n          this.checkOutgoingInvoice(invoice.quote, true);\n        }\n      }\n    },\n\n    // Efficiently update and sort unified transactions only when needed\n    updateUnifiedTransactions() {\n      const transactions = [];\n\n      // Add token transactions (ecash)\n      this.historyTokens.forEach((token) => {\n        transactions.push({\n          ...token,\n          type: \"ecash\",\n          id: token.id,\n          label: token.label,\n        });\n      });\n\n      // Add invoice transactions (lightning)\n      this.invoiceHistory.forEach((invoice) => {\n        transactions.push({\n          ...invoice,\n          type: \"lightning\",\n          id: `invoice-${invoice.quote}`,\n          label: invoice.label,\n        });\n      });\n\n      // Sort by date (newest first) and cache the result\n      this.cachedUnifiedTransactions = transactions.sort(\n        (a, b) => new Date(b.date) - new Date(a.date)\n      );\n    },\n  },\n  created: function () {},\n});\n</script>\n\n<style scoped>\n.transaction-label {\n  border-radius: 4px;\n  font-size: 1rem;\n  transition: background-color 0.2s ease;\n}\n\n.transaction-label:hover {\n  background-color: rgba(0, 0, 0, 0.04);\n}\n\n.transaction-icon {\n  width: 20px;\n  height: 20px;\n  color: var(--q-primary);\n}\n\n.amount-text {\n  font-size: 1rem;\n  line-height: 1.2;\n}\n\n.text-amount-positive {\n  color: hsl(120, 88%, 58%);\n}\n</style>\n"
  },
  {
    "path": "src/components/InvoiceDetailDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showInvoiceDetails\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n    transition-show=\"fade\"\n    transition-hide=\"fade\"\n    no-backdrop-dismiss\n  >\n    <q-card class=\"q-pa-none qcard\">\n      <div\n        :class=\"$q.dark.isActive ? 'bg-dark' : 'bg-white'\"\n        class=\"display-token-fullscreen\"\n      >\n        <!-- Header -->\n        <div class=\"row items-center q-pa-md\" style=\"position: relative\">\n          <q-btn\n            v-close-popup\n            flat\n            round\n            icon=\"close\"\n            color=\"grey\"\n            class=\"floating-close-btn\"\n          />\n          <div class=\"col text-center fixed-title-height\">\n            <q-item-label\n              overline\n              class=\"q-mt-sm text-white\"\n              style=\"font-size: 1rem\"\n            >\n              {{ $t(\"InvoiceDetailDialog.invoice.caption\") }}\n            </q-item-label>\n          </div>\n        </div>\n\n        <!-- Content -->\n        <div class=\"content-area\">\n          <q-card-section class=\"q-pa-none\">\n            <div v-if=\"invoiceData.bolt11\" class=\"row justify-center q-mb-md\">\n              <div\n                class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n                style=\"max-width: 600px\"\n              >\n                <a\n                  class=\"text-secondary\"\n                  :href=\"'lightning:' + invoiceData.bolt11\"\n                >\n                  <q-responsive :ratio=\"1\" class=\"q-mx-none\">\n                    <vue-qrcode\n                      :value=\"'lightning:' + invoiceData.bolt11.toUpperCase()\"\n                      :options=\"{ width: 400 }\"\n                      class=\"rounded-borders\"\n                      style=\"width: 100%\"\n                    >\n                    </vue-qrcode>\n                  </q-responsive>\n                </a>\n                <div style=\"height: 2px\">\n                  <q-linear-progress\n                    v-if=\"runnerActive\"\n                    indeterminate\n                    color=\"primary\"\n                  />\n                </div>\n              </div>\n            </div>\n\n            <q-card-section class=\"q-pa-sm\">\n              <div class=\"row justify-center q-pt-md\">\n                <q-item-label style=\"font-size: 28px\" class=\"text-weight-bold\">\n                  <strong>{{ displayUnit }}</strong>\n                </q-item-label>\n              </div>\n              <div\n                v-if=\"invoiceData.amount > 0 && invoiceData.status === 'paid'\"\n                class=\"row justify-center\"\n              >\n                <transition appear enter-active-class=\"animated tada\">\n                  <span class=\"q-mt-md text-bold\" style=\"font-size: 28px\">\n                    <q-icon\n                      name=\"check_circle\"\n                      size=\"1.8rem\"\n                      color=\"positive\"\n                      class=\"q-mr-sm q-mb-xs\"\n                    />{{ $t(\"InvoiceDetailDialog.invoice.status_paid_text\") }}\n                  </span>\n                </transition>\n              </div>\n              <div\n                v-if=\"invoiceData?.mintQuote\"\n                class=\"row justify-center q-pt-lg\"\n              >\n                <div\n                  class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n                  style=\"max-width: 600px\"\n                >\n                  <MintQuoteInformation\n                    :mint-quote=\"invoiceData.mintQuote\"\n                    :invoice=\"invoiceData\"\n                    :mint-url=\"invoiceData.mint\"\n                    :show-amount=\"false\"\n                  />\n                </div>\n              </div>\n              <div\n                v-else-if=\"invoiceData?.meltQuote\"\n                class=\"row justify-center q-pt-lg\"\n              >\n                <div\n                  class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n                  style=\"max-width: 600px\"\n                >\n                  <MeltQuoteInformation\n                    :melt-quote=\"invoiceData.meltQuote\"\n                    :invoice=\"invoiceData\"\n                    :mint-url=\"invoiceData.mint\"\n                    :history-paid-at=\"invoiceData.paidDate\"\n                    :show-amount=\"true\"\n                  />\n                </div>\n              </div>\n            </q-card-section>\n          </q-card-section>\n        </div>\n\n        <!-- Bottom panel action -->\n        <div class=\"bottom-panel\">\n          <div class=\"row justify-center q-pb-lg q-pt-sm\">\n            <div\n              class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n              style=\"max-width: 600px\"\n            >\n              <q-btn\n                v-if=\"invoiceData.bolt11\"\n                class=\"full-width\"\n                unelevated\n                size=\"lg\"\n                color=\"primary\"\n                rounded\n                @click=\"onCopyBolt11\"\n              >\n                {{ copyButtonLabel }}\n              </q-btn>\n            </div>\n          </div>\n        </div>\n      </div>\n    </q-card>\n  </q-dialog>\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport VueQrcode from \"@chenfengyuan/vue-qrcode\";\nimport { copyToClipboard } from \"quasar\";\n\nimport { useWalletStore } from \"../stores/wallet\";\nimport { useUiStore } from \"../stores/ui\";\nimport { useWorkersStore } from \"../stores/workers\";\nimport MeltQuoteInformation from \"./MeltQuoteInformation.vue\";\nimport MintQuoteInformation from \"./MintQuoteInformation.vue\";\n// type hint for global mixin\ndeclare const windowMixin: any;\n\nexport default defineComponent({\n  name: \"InvoiceDetailDialog\",\n  mixins: [windowMixin],\n  components: {\n    VueQrcode,\n    MeltQuoteInformation,\n    MintQuoteInformation,\n  },\n  props: {},\n  data: function () {\n    return {\n      copyButtonCopied: false,\n      copyButtonTimeout: null as any,\n    };\n  },\n  computed: {\n    ...mapState(useWalletStore, [\"invoiceData\"]),\n    ...mapState(useWorkersStore, [\"invoiceWorkerRunning\"]),\n    ...mapWritableState(useUiStore, [\"showInvoiceDetails\"]),\n    displayUnit: function () {\n      const display = (this as any).formatCurrency(\n        this.invoiceData.amount,\n        this.invoiceData.unit,\n        true\n      );\n      return display;\n    },\n    runnerActive: function () {\n      return this.invoiceWorkerRunning;\n    },\n    isSmallScreen() {\n      return this.$q.screen.lt.sm;\n    },\n    copyButtonLabel: function () {\n      if (this.copyButtonCopied) {\n        return \"Copied\";\n      }\n      return this.$t(\"InvoiceDetailDialog.invoice.actions.copy.label\");\n    },\n  },\n  methods: {\n    onCopyBolt11: async function () {\n      if (this.invoiceData?.bolt11) {\n        try {\n          await copyToClipboard(this.invoiceData.bolt11);\n          this.copyButtonCopied = true;\n          // Clear any existing timeout\n          if (this.copyButtonTimeout) {\n            clearTimeout(this.copyButtonTimeout);\n          }\n          // Reset button label after 3 seconds\n          this.copyButtonTimeout = setTimeout(() => {\n            this.copyButtonCopied = false;\n          }, 3000);\n        } catch (error) {\n          console.error(\"Failed to copy to clipboard:\", error);\n        }\n      }\n    },\n  },\n  beforeUnmount() {\n    if (this.copyButtonTimeout) {\n      clearTimeout(this.copyButtonTimeout);\n    }\n  },\n});\n</script>\n\n<style scoped>\n.display-token-fullscreen {\n  height: 100vh;\n  height: 100dvh;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n.content-area {\n  flex: 1;\n  overflow-y: auto;\n}\n.floating-close-btn {\n  position: absolute;\n  left: 16px;\n  top: 50%;\n  transform: translateY(-50%);\n  z-index: 1;\n}\n.fixed-title-height {\n  height: 24px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.bottom-panel {\n  margin-top: auto;\n  background: var(--q-color-grey-1);\n  box-shadow: 0 -8px 16px rgba(0, 0, 0, 0.05);\n  padding-bottom: env(safe-area-inset-bottom, 0px);\n  position: sticky;\n  bottom: 0;\n  z-index: 2;\n}\n.qcard {\n  border-top-left-radius: 20px;\n  border-top-right-radius: 20px;\n}\n</style>\n"
  },
  {
    "path": "src/components/MainHeader.vue",
    "content": "<template>\n  <q-header class=\"bg-transparent\">\n    <q-toolbar>\n      <q-btn\n        flat\n        dense\n        round\n        icon=\"menu\"\n        color=\"primary\"\n        aria-label=\"Menu\"\n        @click=\"toggleLeftDrawer\"\n        :disable=\"uiStore.globalMutexLock\"\n      />\n      <q-toolbar-title></q-toolbar-title>\n      <transition\n        appear\n        enter-active-class=\"animated wobble\"\n        leave-active-class=\"animated fadeOut\"\n      >\n        <q-badge\n          v-if=\"g.offline\"\n          color=\"red\"\n          text-color=\"black\"\n          class=\"q-mr-sm\"\n        >\n          <span>{{ $t(\"MainHeader.offline.warning.text\") }}</span>\n        </q-badge>\n      </transition>\n      <q-badge\n        v-if=\"isStaging()\"\n        color=\"yellow\"\n        text-color=\"black\"\n        class=\"q-mr-sm\"\n      >\n        <span>{{ $t(\"MainHeader.staging.warning.text\") }}</span>\n      </q-badge>\n      <!-- <q-badge color=\"yellow\" text-color=\"black\" class=\"q-mr-sm\">\n        <span v-if=\"!isStaging()\">Beta</span>\n        <span v-else>Staging – don't use with real funds!</span>\n      </q-badge> -->\n      <transition-group appear enter-active-class=\"animated pulse\">\n        <q-badge\n          v-if=\"countdown > 0\"\n          color=\"negative\"\n          text-color=\"white\"\n          class=\"q-mr-sm\"\n          @click=\"reload\"\n        >\n          {{ $t(\"MainHeader.reload.warning.text\", { countdown }) }}\n          <q-spinner\n            v-if=\"countdown > 0\"\n            size=\"0.8em\"\n            :thickness=\"10\"\n            class=\"q-ml-sm\"\n            color=\"white\"\n          />\n        </q-badge>\n      </transition-group>\n      <q-btn\n        flat\n        dense\n        round\n        size=\"0.8em\"\n        :icon=\"countdown > 0 ? 'close' : 'refresh'\"\n        :color=\"countdown > 0 ? 'negative' : 'primary'\"\n        aria-label=\"Refresh\"\n        @click=\"reload\"\n        :disable=\"uiStore.globalMutexLock && countdown === 0\"\n      >\n      </q-btn>\n    </q-toolbar>\n  </q-header>\n\n  <q-drawer v-model=\"leftDrawerOpen\" bordered>\n    <q-list>\n      <q-item-label header>{{\n        $t(\"MainHeader.menu.settings.title\")\n      }}</q-item-label>\n      <q-item clickable to=\"/settings\">\n        <q-item-section avatar>\n          <q-icon name=\"settings\" />\n        </q-item-section>\n        <q-item-section>\n          <q-item-label>{{\n            $t(\"MainHeader.menu.settings.settings.title\")\n          }}</q-item-label>\n          <q-item-label caption>{{\n            $t(\"MainHeader.menu.settings.settings.caption\")\n          }}</q-item-label>\n        </q-item-section>\n      </q-item>\n      <q-item-label header>{{\n        $t(\"MainHeader.menu.terms.title\")\n      }}</q-item-label>\n      <q-item clickable to=\"/terms\">\n        <q-item-section avatar>\n          <q-icon name=\"gavel\" />\n        </q-item-section>\n        <q-item-section>\n          <q-item-label>{{\n            $t(\"MainHeader.menu.terms.terms.title\")\n          }}</q-item-label>\n          <q-item-label caption>{{\n            $t(\"MainHeader.menu.terms.terms.caption\")\n          }}</q-item-label>\n        </q-item-section>\n      </q-item>\n      <q-item-label header>{{\n        $t(\"MainHeader.menu.links.title\")\n      }}</q-item-label>\n      <EssentialLink\n        v-for=\"link in essentialLinks\"\n        :key=\"link.title\"\n        v-bind=\"link\"\n      />\n    </q-list>\n  </q-drawer>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, ref } from \"vue\";\nimport EssentialLink from \"components/EssentialLink.vue\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { useI18n } from \"vue-i18n\";\n\nexport default defineComponent({\n  name: \"MainHeader\",\n  mixins: [windowMixin],\n  components: {\n    EssentialLink,\n  },\n  setup() {\n    const leftDrawerOpen = ref(false);\n    const uiStore = useUiStore();\n    const { t } = useI18n();\n    const countdown = ref(0);\n    let countdownInterval;\n\n    const essentialLinks = [\n      {\n        title: t(\"MainHeader.menu.links.cashuSpace.title\"),\n        caption: t(\"MainHeader.menu.links.cashuSpace.caption\"),\n        icon: \"web\",\n        link: \"https://cashu.space\",\n      },\n      {\n        title: t(\"MainHeader.menu.links.github.title\"),\n        caption: t(\"MainHeader.menu.links.github.caption\"),\n        icon: \"code\",\n        link: \"https://github.com/cashubtc/cashu.me\",\n      },\n      {\n        title: t(\"MainHeader.menu.links.telegram.title\"),\n        caption: t(\"MainHeader.menu.links.telegram.caption\"),\n        icon: \"chat\",\n        link: \"https://t.me/CashuMe\",\n      },\n      {\n        title: t(\"MainHeader.menu.links.twitter.title\"),\n        caption: t(\"MainHeader.menu.links.twitter.caption\"),\n        icon: \"rss_feed\",\n        link: \"https://twitter.com/CashuBTC\",\n      },\n      {\n        title: t(\"MainHeader.menu.links.donate.title\"),\n        caption: t(\"MainHeader.menu.links.donate.caption\"),\n        icon: \"favorite\",\n        link: \"https://docs.cashu.space/contribute\",\n      },\n    ];\n\n    const toggleLeftDrawer = () => {\n      leftDrawerOpen.value = !leftDrawerOpen.value;\n    };\n\n    const isStaging = () => {\n      return location.host.includes(\"staging\");\n    };\n\n    const reload = () => {\n      if (countdown.value > 0) {\n        uiStore.unlockMutex();\n        clearInterval(countdownInterval);\n        countdown.value = 0;\n        return;\n      }\n      if (uiStore.globalMutexLock) return;\n      uiStore.lockMutex();\n      countdown.value = 3;\n      countdownInterval = setInterval(() => {\n        countdown.value--;\n        if (countdown.value === 0) {\n          clearInterval(countdownInterval);\n          uiStore.unlockMutex();\n          location.reload();\n        }\n      }, 1000);\n    };\n\n    return {\n      essentialLinks,\n      leftDrawerOpen,\n      toggleLeftDrawer,\n      isStaging,\n      reload,\n      countdown,\n      uiStore,\n    };\n  },\n});\n</script>\n<style scoped>\n.q-header {\n  position: relative;\n  z-index: auto;\n  overflow-x: hidden;\n}\n\n.q-toolbar {\n  flex-wrap: nowrap;\n  min-height: 50px; /* Ensure consistent height */\n}\n\n.q-toolbar-title {\n  flex: 0 1 auto; /* Allow title to shrink */\n}\n\n/* Make badges container handle overflow properly */\n.q-toolbar > .q-badge {\n  flex-shrink: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/MeltQuoteInformation.vue",
    "content": "<template>\n  <div\n    v-if=\"hasQuote\"\n    class=\"melt-quote-information q-mt-md\"\n    :class=\"containerTextClass\"\n  >\n    <div v-if=\"showAmount\" class=\"detail-item q-mb-md\">\n      <div class=\"detail-label\">\n        <ZapIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">Amount</div>\n      </div>\n      <div class=\"detail-value\">{{ amountDisplay }}</div>\n    </div>\n\n    <div v-if=\"hasFeeReserve && !hasFeePaid\" class=\"detail-item q-mb-md\">\n      <div class=\"detail-label\">\n        <ArrowDownUpIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">Fee Reserve</div>\n      </div>\n      <div class=\"detail-value\">{{ feeReserveDisplay }}</div>\n    </div>\n\n    <div v-if=\"hasFeePaid\" class=\"detail-item q-mb-md\">\n      <div class=\"detail-label\">\n        <BadgeCheckIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">Fee Paid</div>\n      </div>\n      <div class=\"detail-value\">{{ feePaidDisplay }}</div>\n    </div>\n\n    <div class=\"detail-item q-mb-md\">\n      <div class=\"detail-label\">\n        <BanknoteIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">Unit</div>\n      </div>\n      <div class=\"detail-value\">{{ unitDisplay }}</div>\n    </div>\n\n    <div v-if=\"stateDisplay\" class=\"detail-item q-mb-md\">\n      <div class=\"detail-label\">\n        <InfoIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">State</div>\n      </div>\n      <div class=\"detail-value\" :class=\"{ 'text-positive': isPaidState }\">\n        {{ stateDisplay }}\n      </div>\n    </div>\n\n    <div v-if=\"paidAtDisplay\" class=\"detail-item q-mb-md\">\n      <div class=\"detail-label\">\n        <ClockIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">Time Paid</div>\n      </div>\n      <div class=\"detail-value\">{{ paidAtDisplay }}</div>\n    </div>\n\n    <div v-if=\"hasPreimage\" class=\"detail-item q-mb-md\">\n      <div class=\"detail-label\">\n        <FingerprintIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">Preimage</div>\n      </div>\n      <div class=\"detail-value\" @click=\"copyPreimage\" style=\"cursor: pointer\">\n        {{ preimageValue }}\n      </div>\n    </div>\n\n    <div v-if=\"mintDisplay\" class=\"detail-item\">\n      <div class=\"detail-label\">\n        <BuildingIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">Mint</div>\n      </div>\n      <div class=\"detail-value\">{{ mintDisplay }}</div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from \"vue\";\nimport { mapState } from \"pinia\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport { useMintsStore } from \"stores/mints\";\nimport { MeltQuoteBolt11Response } from \"@cashu/cashu-ts\";\nimport {\n  Zap as ZapIcon,\n  ArrowDownUp as ArrowDownUpIcon,\n  BadgeCheck as BadgeCheckIcon,\n  Clock as ClockIcon,\n  Building as BuildingIcon,\n  Banknote as BanknoteIcon,\n  Info as InfoIcon,\n  Fingerprint as FingerprintIcon,\n} from \"lucide-vue-next\";\n\ndeclare const windowMixin: any;\ndeclare const formatCurrency: any;\n\ntype ExtendedMeltQuote = MeltQuoteBolt11Response & {\n  fee_paid?: number | null;\n  paid?: number | string | boolean | null;\n  paid_at?: number | string | null;\n  paid_timestamp?: number | string | null;\n  paid_time?: number | string | null;\n};\n\nexport default defineComponent({\n  name: \"MeltQuoteInformation\",\n  mixins: [windowMixin],\n  components: {\n    ZapIcon,\n    ArrowDownUpIcon,\n    BadgeCheckIcon,\n    ClockIcon,\n    BuildingIcon,\n    BanknoteIcon,\n    InfoIcon,\n    FingerprintIcon,\n  },\n  props: {\n    meltQuote: {\n      type: Object as PropType<ExtendedMeltQuote | null>,\n      default: null,\n    },\n    mintQuote: {\n      type: Object as PropType<any | null>,\n      default: null,\n    },\n    invoice: {\n      type: Object as PropType<any | null>,\n      default: null,\n    },\n    mintUrl: {\n      type: String,\n      default: \"\",\n    },\n    showAmount: {\n      type: Boolean,\n      default: true,\n    },\n    historyPaidAt: {\n      type: [String, Number, Date] as PropType<\n        string | number | Date | null | undefined\n      >,\n      default: null,\n    },\n  },\n  computed: {\n    ...mapState(useMintsStore, [\"activeMintUrl\", \"activeUnit\"]),\n    hasQuote(): boolean {\n      return Boolean(this.meltQuote && this.meltQuote.quote);\n    },\n    iconColor(): string {\n      return this.$q.dark.isActive ? \"#9E9E9E\" : \"#616161\";\n    },\n    containerTextClass(): string {\n      return this.$q.dark.isActive ? \"text-white\" : \"text-dark\";\n    },\n    quoteUnit(): string {\n      if (this.meltQuote?.unit) {\n        return this.meltQuote.unit;\n      }\n      if (this.mintQuote?.unit) {\n        return this.mintQuote.unit;\n      }\n      if (this.activeUnit) {\n        return this.activeUnit as string;\n      }\n      return \"sat\";\n    },\n    unitDisplay(): string {\n      return this.quoteUnit?.toUpperCase?.() || \"\";\n    },\n    amountDisplay(): string {\n      const amount =\n        (this.meltQuote?.amount as number | undefined) ??\n        (this.mintQuote?.amount as number | undefined) ??\n        (typeof this.invoice?.amount === \"number\"\n          ? Math.abs(this.invoice.amount)\n          : undefined);\n      if (typeof amount !== \"number\") return \"\";\n      return (this as any).formatCurrency(amount, this.quoteUnit);\n    },\n    hasFeeReserve(): boolean {\n      return Boolean(\n        this.meltQuote && typeof this.meltQuote.fee_reserve === \"number\"\n      );\n    },\n    feeReserveDisplay(): string {\n      const value = this.meltQuote?.fee_reserve ?? 0;\n      return (this as any).formatCurrency(value, this.quoteUnit);\n    },\n    feePaidValue(): number | null {\n      if (\n        !this.meltQuote ||\n        this.meltQuote.fee_paid === undefined ||\n        this.meltQuote.fee_paid === null\n      ) {\n        return null;\n      }\n      return typeof this.meltQuote.fee_paid === \"number\"\n        ? this.meltQuote.fee_paid\n        : Number(this.meltQuote.fee_paid);\n    },\n    feePaidActual(): number | null {\n      if (\n        this.invoice &&\n        this.meltQuote &&\n        typeof this.meltQuote.amount === \"number\"\n      ) {\n        const totalPaidAbs = Math.abs(Number(this.invoice.amount) || 0);\n        if (Number.isFinite(totalPaidAbs) && totalPaidAbs >= 0) {\n          const value = totalPaidAbs - this.meltQuote.amount;\n          return Math.max(0, value);\n        }\n      }\n      return this.feePaidValue;\n    },\n    feePaidDisplay(): string {\n      const v = this.feePaidActual;\n      if (v === null || Number.isNaN(v)) {\n        return \"\";\n      }\n      return (this as any).formatCurrency(v, this.quoteUnit);\n    },\n    hasFeePaid(): boolean {\n      const v = this.feePaidActual;\n      return v !== null && Number.isFinite(v) && v >= 0;\n    },\n    stateDisplay(): string {\n      const state = (this.invoice?.status as string) || this.meltQuote?.state;\n      if (!state) {\n        return \"\";\n      }\n      const lower = state.toLowerCase();\n      return lower.charAt(0).toUpperCase() + lower.slice(1);\n    },\n    isPaidState(): boolean {\n      const state = (this.invoice?.status as string) || this.meltQuote?.state;\n      return state?.toLowerCase() === \"paid\";\n    },\n    paidAtTimestamp(): number | null {\n      if (this.invoice?.paidDate) {\n        return this.normalizeToTimestamp(this.invoice.paidDate);\n      }\n      if (this.historyPaidAt) {\n        return this.normalizeToTimestamp(this.historyPaidAt);\n      }\n      if (!this.meltQuote) {\n        return null;\n      }\n      const raw =\n        (this.meltQuote as ExtendedMeltQuote).paid_at ??\n        (this.meltQuote as ExtendedMeltQuote).paid_timestamp ??\n        (this.meltQuote as ExtendedMeltQuote).paid_time ??\n        (this.meltQuote as ExtendedMeltQuote).paid;\n      if (raw === undefined || raw === null) {\n        return null;\n      }\n      if (typeof raw === \"boolean\") {\n        return null;\n      }\n      if (typeof raw === \"number\") {\n        return this.normalizeNumberTimestamp(raw);\n      }\n      if (typeof raw === \"string\") {\n        return this.normalizeStringTimestamp(raw);\n      }\n      return null;\n    },\n    paidAtDisplay(): string {\n      if (!this.paidAtTimestamp) {\n        return \"\";\n      }\n      return new Intl.DateTimeFormat(undefined, {\n        dateStyle: \"medium\",\n        timeStyle: \"short\",\n      }).format(new Date(this.paidAtTimestamp));\n    },\n    mintDisplay(): string {\n      const url = this.mintUrl || (this.activeMintUrl as string) || \"\";\n      return url ? getShortUrl(url) : \"\";\n    },\n    hasPreimage(): boolean {\n      const pre = (this.meltQuote as any)?.payment_preimage;\n      return typeof pre === \"string\" && pre.length > 0;\n    },\n    preimageValue(): string {\n      return ((this.meltQuote as any)?.payment_preimage as string) || \"\";\n    },\n  },\n  methods: {\n    async copyPreimage() {\n      const pre = this.preimageValue;\n      if (!pre) return;\n      try {\n        if (navigator?.clipboard?.writeText) {\n          await navigator.clipboard.writeText(pre);\n        } else {\n          (this as any).copyText?.(pre);\n        }\n        this.$q.notify({\n          message: \"Preimage copied\",\n          color: \"positive\",\n          position: \"top\",\n          timeout: 1000,\n        });\n      } catch (e) {\n        console.error(\"Failed to copy preimage\", e);\n      }\n    },\n    normalizeToTimestamp(\n      value: string | number | Date | null | undefined\n    ): number | null {\n      if (value === null || value === undefined) {\n        return null;\n      }\n      if (value instanceof Date) {\n        return value.getTime();\n      }\n      if (typeof value === \"number\") {\n        return this.normalizeNumberTimestamp(value);\n      }\n      if (typeof value === \"string\") {\n        return this.normalizeStringTimestamp(value);\n      }\n      return null;\n    },\n    normalizeNumberTimestamp(value: number): number | null {\n      if (!Number.isFinite(value) || value <= 0) {\n        return null;\n      }\n      return value > 1e12 ? value : value * 1000;\n    },\n    normalizeStringTimestamp(value: string): number | null {\n      if (!value) {\n        return null;\n      }\n      const numeric = Number(value);\n      if (!Number.isNaN(numeric) && numeric > 0) {\n        return this.normalizeNumberTimestamp(numeric);\n      }\n      const parsed = Date.parse(value);\n      if (!Number.isNaN(parsed)) {\n        return parsed;\n      }\n      return null;\n    },\n  },\n});\n</script>\n\n<style scoped>\n.melt-quote-information {\n  width: 100%;\n}\n\n.detail-item {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  width: 100%;\n}\n\n.detail-label {\n  display: flex;\n  align-items: center;\n}\n\n.detail-icon {\n  margin-right: 10px;\n}\n\n.detail-name {\n  font-size: 14px;\n  font-weight: 600;\n  color: var(--q-color-grey-6);\n}\n\n.detail-value {\n  font-size: 16px;\n  font-weight: 600;\n  color: inherit;\n  text-align: right;\n  max-width: 60%;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n</style>\n"
  },
  {
    "path": "src/components/MintAuditInfo.vue",
    "content": "<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=\"grey\" size=\"20px\" class=\"q-mr-sm\" />\n      Loading audit info...\n    </div>\n    <div v-else-if=\"mintNotAudited\" class=\"q-mx-sm\">\n      <q-icon\n        name=\"info_outline\"\n        color=\"grey\"\n        size=\"20px\"\n        class=\"q-mr-sm\"\n        style=\"padding-bottom: 2px; margin-bottom: 2px\"\n      />\n      <span class=\"text-bold\">This mint has not been audited yet.</span>\n      <br />\n      <span class=\"text-caption\">\n        To learn more about the auditor or support a future audit, visit\n        <a :href=\"auditUrl\">audit.8333.space</a> or click\n        <span\n          class=\"text-bold cursor-pointer text-primary\"\n          @click=\"getAuditorPaymentRequestsAndHandle\"\n        >\n          here\n        </span>\n        to donate ecash and help initiate an audit of this mint.\n      </span>\n    </div>\n    <div v-else-if=\"error\" class=\"q-mx-sm text-bold\">Error: {{ error }}</div>\n    <div v-else-if=\"mintInfo\" class=\"mint-info\">\n      <!-- Warning Box -->\n      <div style=\"margin-bottom: 32px\">\n        <transition\n          appear\n          enter-active-class=\"animated pulse\"\n          name=\"smooth-slide\"\n        >\n          <MintAuditWarningBox :mint=\"mintInfo\" :swaps=\"mintSwaps\" />\n        </transition>\n      </div>\n      <!-- statistics -->\n      <div class=\"q-pb-none\">\n        <div class=\"row q-col-gutter-md q-px-md\" style=\"flex-wrap: nowrap\">\n          <!-- Success Rate Card -->\n          <div class=\"col-6 stat-card\">\n            <div class=\"q-pa-md\">\n              <div class=\"text-subtitle1\">Success Rate</div>\n              <div class=\"text-h5\">{{ successRate }}%</div>\n              <div class=\"text-caption\">\n                {{ successfulSwaps }} of {{ mintSwaps.length }} swaps\n              </div>\n            </div>\n          </div>\n          <!-- Average Time Card -->\n          <div class=\"col-6 stat-card q-ml-md\">\n            <div class=\"q-pa-md\">\n              <div class=\"text-subtitle1\">Average Time</div>\n              <div class=\"text-h5\">{{ formatTime(averageTime) }}</div>\n              <div class=\"text-caption\">For successful swaps</div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- Bar Chart -->\n      <div v-if=\"mintSwaps.length > 0\" class=\"q-py-md\">\n        <MintAuditSwapsBarChart :swaps=\"mintSwaps\" />\n      </div>\n\n      <!-- Disclaimer -->\n      <div class=\"q-mt-md text-grey-6 text-caption\">\n        Audit data is provided by independent third parties to help assess a\n        mint’s reliability over time. However, these results are informational\n        only and do not guarantee the safety, solvency, or trustworthiness of\n        any mint. Always conduct your own research and ensure you trust the mint\n        operator before using their services.\n      </div>\n      <!-- Donate ecash to the auditor -->\n      <div class=\"q-mt-md text-grey-6 text-caption\">\n        Audit information is made available by\n        <a :href=\"auditUrl\">{{ auditUrlShort }}</a\n        >. The current balance held by the auditor for this mint is\n        <span class=\"text-bold\">{{ mintInfo.balance }} sats</span>, with a total\n        of\n        <span class=\"text-bold\">{{ mintInfo.sum_donations }} sats</span>\n        donated so far. To support continued auditing, you can\n        <span\n          class=\"text-bold cursor-pointer text-primary\"\n          @click=\"getAuditorPaymentRequestsAndHandle\"\n        >\n          press here\n        </span>\n        to donate ecash to the auditor.\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\ninterface MintRead {\n  id: number;\n  url: string;\n  info?: string;\n  name?: string;\n  balance: number;\n  sum_donations?: number;\n  updated_at: string;\n  next_update?: string;\n  state: string;\n  n_errors: number;\n  n_mints: number;\n  n_melts: number;\n}\n\ninterface SwapEventRead {\n  id: number;\n  from_id: number;\n  to_id: number;\n  from_url: string;\n  to_url: string;\n  amount: number;\n  fee: number;\n  created_at: string;\n  time_taken: number;\n  state: string;\n  error: string;\n}\n\nimport MintAuditWarningBox from \"./MintAuditWarningBox.vue\";\nimport MintAuditSwapsBarChart from \"./MintAuditSwapsBarChart.vue\";\nimport { useWalletStore } from \"../stores/wallet\";\nimport { useMintsStore } from \"../stores/mints\";\nimport { useSettingsStore } from \"../stores/settings\";\nimport { getShortUrl } from \"../js/wallet-helpers\";\n\nexport default {\n  name: \"MintAuditInfo\",\n  components: {\n    MintAuditWarningBox,\n    MintAuditSwapsBarChart,\n  },\n  props: {\n    mintUrl: {\n      type: String,\n      required: true,\n    },\n  },\n  data() {\n    const settingsStore = useSettingsStore();\n    return {\n      auditorApiUrl: settingsStore.auditorApiUrl,\n      auditUrl: settingsStore.auditorUrl,\n      auditUrlShort: getShortUrl(settingsStore.auditorUrl),\n      mintNotAudited: false,\n      loading: true,\n      error: null,\n      couldFetchInfo: true,\n      mints: [] as MintRead[],\n      mintInfo: null as MintRead | null,\n      mintSwaps: [] as SwapEventRead[],\n      limit: 100,\n      skip: 0,\n      allLoaded: false,\n      loadingMore: false,\n    };\n  },\n  computed: {\n    successfulSwaps() {\n      return this.mintSwaps.filter((swap) => swap.state === \"OK\").length;\n    },\n    successRate() {\n      if (this.mintSwaps.length === 0) return 0;\n      return Math.round((this.successfulSwaps / this.mintSwaps.length) * 100);\n    },\n    averageTime() {\n      const successfulSwapsWithTime = this.mintSwaps.filter(\n        (swap) => swap.state === \"OK\" && swap.time_taken\n      );\n      if (successfulSwapsWithTime.length === 0) return 0;\n      const totalTime = successfulSwapsWithTime.reduce(\n        (sum, swap) => sum + (swap.time_taken || 0),\n        0\n      );\n      return totalTime / successfulSwapsWithTime.length;\n    },\n  },\n  async mounted() {\n    try {\n      this.loading = true;\n      await this.getMintInfo();\n      if (this.mintInfo && this.mintInfo.id) {\n        await this.getMintSwaps(this.mintInfo.id);\n      }\n    } catch (err: any) {\n      this.error = err.message || \"Failed to load mint data\";\n    } finally {\n      this.loading = false;\n    }\n  },\n  methods: {\n    async getMintInfo() {\n      try {\n        const response = await fetch(\n          `${this.auditorApiUrl}/mints/url?url=${this.mintUrl}`\n        );\n        if (!response.ok) {\n          if (response.status === 404) {\n            this.mintNotAudited = true;\n            throw new Error(`This mint is not being audited yet.`);\n          }\n          throw new Error(`API error: ${response.status}`);\n        }\n        this.mintInfo = (await response.json()) as MintRead;\n        console.log(\"# MintAuditInfo\", this.mintInfo);\n        if (!this.mintInfo) {\n          this.mintNotAudited = true;\n          throw new Error(`This mint is not being audited yet.`);\n        }\n      } catch (err) {\n        console.error(\"Error fetching mint info:\", err);\n        throw err;\n      }\n    },\n    async getMintSwaps(mintId: number, skip = 0, limit = 100) {\n      try {\n        const response = await fetch(\n          `${this.auditorApiUrl}/swaps/mint/${mintId}?skip=${skip}&limit=${limit}`\n        );\n        if (!response.ok) {\n          throw new Error(`API error: ${response.status}`);\n        }\n        const fetchedSwaps = (await response.json()) as SwapEventRead[];\n\n        if (skip === 0) {\n          this.mintSwaps = fetchedSwaps;\n        } else {\n          this.mintSwaps = [...this.mintSwaps, ...fetchedSwaps];\n        }\n\n        this.skip += limit;\n        this.allLoaded = fetchedSwaps.length < limit;\n      } catch (err) {\n        console.error(\"Error fetching mint swaps:\", err);\n        throw err;\n      }\n    },\n    async getAuditorPaymentRequestsAndHandle() {\n      const walletStore = useWalletStore();\n      const mintStore = useMintsStore();\n      try {\n        const response = await fetch(\n          `${this.auditorApiUrl}/pr?url=${this.mintUrl}`\n        );\n        const paymentRequestResponse = await response.json();\n        const paymentRequestString = paymentRequestResponse.pr;\n        const paymentRequest = paymentRequestString.replace(/\"/g, \"\");\n        console.log(\"# AuditorPaymentRequests\", paymentRequest);\n        await mintStore.activateMintUrl(this.mintUrl);\n        await mintStore.activateUnit(\"sat\");\n        await walletStore.decodeRequest(paymentRequest);\n        // close the mint info dialog\n        this.$emit(\"close\");\n      } catch (err) {\n        console.error(\"Error fetching auditor payment requests:\", err);\n      }\n    },\n    async loadMoreSwaps() {\n      if (this.allLoaded || this.loadingMore || !this.mintInfo) return;\n\n      this.loadingMore = true;\n      try {\n        await this.getMintSwaps(this.mintInfo.id, this.skip, this.limit);\n      } finally {\n        this.loadingMore = false;\n      }\n    },\n    formatTime(milliseconds: number) {\n      if (milliseconds === 0) return \"N/A\";\n      const seconds = milliseconds / 1000;\n      if (seconds < 10) {\n        return `${milliseconds.toFixed(0)} ms`;\n      } else {\n        return `${seconds.toFixed(2)} s`;\n      }\n    },\n  },\n};\n</script>\n\n<style scoped>\n.mint-info {\n  text-align: left;\n  margin-top: 20px;\n}\n\n.stat-card {\n  background-color: #1e1e1e;\n  color: white;\n  min-height: 100px;\n  border-radius: 8px;\n  padding-top: 0px;\n  padding-bottom: 0px;\n  padding-left: 15px;\n  padding-right: 15px;\n}\n\n.text-subtitle1 {\n  font-size: 14px;\n  color: #9e9e9e;\n  margin-bottom: 8px;\n}\n\n.text-h5 {\n  font-size: 24px;\n  font-weight: 600;\n  margin-bottom: 8px;\n}\n\n.text-caption {\n  font-size: 12px;\n  color: #9e9e9e;\n}\n\n.row {\n  display: flex;\n  flex-direction: row;\n}\n\n.col-6 {\n  flex: 0 0 50%;\n  max-width: 50%;\n}\n\n.q-mb-md {\n  margin-bottom: 16px;\n}\n\n.q-ml-md {\n  margin-left: 16px;\n}\n\n.q-pa-md {\n  padding: 16px;\n}\n\n.q-py-md {\n  padding-top: 16px;\n  padding-bottom: 16px;\n}\n\n.q-py-none {\n  padding-top: 0;\n  padding-bottom: 0;\n}\n\n.q-px-md {\n  padding-left: 16px;\n  padding-right: 16px;\n}\n</style>\n"
  },
  {
    "path": "src/components/MintAuditSwapsBarChart.vue",
    "content": "<template>\n  <div class=\"bar-chart-container\" ref=\"chartContainer\">\n    <div class=\"chart-wrapper\">\n      <div class=\"bars-container\">\n        <div\n          v-for=\"(bucket, index) in displayBuckets\"\n          :key=\"`bucket-${index}`\"\n          class=\"bar\"\n          :class=\"{\n            placeholder: bucket.count === 0,\n          }\"\n          :style=\"{\n            width: `${barWidth}px`,\n            left: `${index * (barWidth + spacing)}px`,\n            backgroundColor:\n              bucket.count > 0\n                ? getSuccessColor(bucket.successRate)\n                : 'transparent',\n          }\"\n          @mouseover=\"showBucketTooltip($event, bucket)\"\n          @mouseleave=\"hideTooltip\"\n        ></div>\n      </div>\n      <div class=\"time-axis\">\n        <div\n          v-for=\"(date, index) in timeTicks\"\n          :key=\"index\"\n          class=\"time-tick\"\n          :style=\"{\n            left: `${(index / (timeTicks.length - 1)) * 100}%`,\n            display: index === timeTicks.length - 1 ? 'none' : 'block',\n          }\"\n        >\n          {{ formatTime(date) }}\n        </div>\n      </div>\n    </div>\n    <div\n      v-if=\"tooltip.show\"\n      class=\"tooltip\"\n      :style=\"{ left: tooltip.x + 'px', top: tooltip.y + 'px' }\"\n    >\n      <div v-html=\"tooltip.content\"></div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, ref, computed, onMounted, watch } from \"vue\";\n\ninterface SwapEventRead {\n  id: number;\n  from_id: number;\n  to_id: number;\n  from_url: string;\n  to_url: string;\n  amount: number;\n  fee: number;\n  created_at: string;\n  time_taken: number;\n  state: string;\n  error: string;\n}\n\ninterface SwapBucket {\n  swaps: SwapEventRead[];\n  count: number;\n  successCount: number;\n  successRate: number;\n  startTime: Date;\n  endTime: Date;\n}\n\nexport default defineComponent({\n  name: \"MintAuditSwapsBarChart\",\n  props: {\n    swaps: {\n      type: Array as () => SwapEventRead[],\n      required: true,\n    },\n    minBars: {\n      type: Number,\n      default: 16,\n    },\n    maxPossibleBars: {\n      type: Number,\n      default: 200,\n    },\n    barSpacing: {\n      type: Number,\n      default: 2,\n    },\n    minBarWidth: {\n      type: Number,\n      default: 2,\n    },\n  },\n  setup(props) {\n    const tooltip = ref({\n      show: false,\n      x: 0,\n      y: 0,\n      content: \"\",\n    });\n\n    const chartContainer = ref<HTMLElement | null>(null);\n    const containerWidth = ref(0);\n    const spacing = computed(() => props.barSpacing);\n    const minBarWidth = computed(() => props.minBarWidth);\n    const sortedSwaps = computed(() =>\n      [...props.swaps].sort(\n        (a, b) =>\n          new Date(a.created_at).getTime() - new Date(b.created_at).getTime()\n      )\n    );\n\n    // Calculate responsive maxBars based on container width\n    const maxBars = computed(() => {\n      if (containerWidth.value <= 0) return props.minBars;\n\n      // Scale maxBars based on container width\n      // For small screens (e.g., width < 600px), use minBars\n      // For large screens, scale up to maxPossibleBars\n      const minWidth = 200; // Width for minimum bars\n      const maxWidth = 2600; // Width for maximum bars\n\n      if (containerWidth.value <= minWidth) {\n        return props.minBars;\n      } else if (containerWidth.value >= maxWidth) {\n        return props.maxPossibleBars;\n      } else {\n        // Linear interpolation between minBars and maxPossibleBars\n        const widthRatio =\n          (containerWidth.value - minWidth) / (maxWidth - minWidth);\n        return Math.floor(\n          props.minBars + widthRatio * (props.maxPossibleBars - props.minBars)\n        );\n      }\n    });\n\n    const barsCount = computed(() => {\n      if (containerWidth.value <= 0) return maxBars.value;\n\n      // Calculate how many bars can fit with the given spacing\n      // Formula: (containerWidth + barSpacing) / (minBarWidth + barSpacing)\n      const maxPossibleBars = Math.floor(\n        (containerWidth.value + spacing.value) /\n          (minBarWidth.value + spacing.value)\n      );\n\n      // Limit to maxBars computed value\n      return Math.min(maxPossibleBars, maxBars.value);\n    });\n\n    const barWidth = computed(() => {\n      if (containerWidth.value <= 0 || barsCount.value <= 0)\n        return minBarWidth.value;\n\n      // Calculate the width that would fill the container with the given number of bars and spacing\n      // Formula: (containerWidth - (barSpacing * (barsCount - 1))) / barsCount\n      return Math.max(\n        minBarWidth.value,\n        (containerWidth.value - spacing.value * (barsCount.value - 1)) /\n          barsCount.value\n      );\n    });\n\n    const updateContainerWidth = () => {\n      if (chartContainer.value) {\n        const barsContainer =\n          chartContainer.value.querySelector(\".bars-container\");\n        if (barsContainer) {\n          containerWidth.value = barsContainer.clientWidth;\n        }\n      }\n    };\n\n    onMounted(() => {\n      updateContainerWidth();\n\n      const resizeObserver = new ResizeObserver(() => {\n        updateContainerWidth();\n      });\n\n      if (chartContainer.value) {\n        resizeObserver.observe(chartContainer.value);\n      }\n\n      window.addEventListener(\"resize\", updateContainerWidth);\n\n      return () => {\n        resizeObserver.disconnect();\n        window.removeEventListener(\"resize\", updateContainerWidth);\n      };\n    });\n\n    const timeTicks = computed(() => {\n      if (sortedSwaps.value.length === 0) return [];\n\n      // Use the same time range logic as in displayBuckets\n      const startTime = new Date(sortedSwaps.value[0].created_at).getTime(); // Oldest swap\n      const endTime = new Date(\n        sortedSwaps.value[sortedSwaps.value.length - 1].created_at\n      ).getTime(); // Latest swap\n\n      const interval = (startTime - endTime) / 3; // 4 ticks (now, 2 middle, end)\n\n      // Generate time ticks from oldest to newest to match bar positions\n      return Array.from(\n        { length: 4 },\n        (_, i) => new Date(endTime + interval * i)\n      );\n    });\n\n    const displayBuckets = computed(() => {\n      if (sortedSwaps.value.length === 0) {\n        return Array(barsCount.value).fill({\n          swaps: [],\n          count: 0,\n          successCount: 0,\n          successRate: 0,\n          startTime: new Date(),\n          endTime: new Date(),\n        });\n      }\n\n      // Create buckets based on time\n      const endTime = new Date(\n        sortedSwaps.value[sortedSwaps.value.length - 1].created_at\n      ).getTime(); // Latest swap\n      const startTime = new Date(sortedSwaps.value[0].created_at).getTime(); // Oldest swap\n      const timeRange = endTime - startTime;\n\n      const buckets: SwapBucket[] = Array(barsCount.value)\n        .fill(null)\n        .map(() => ({\n          swaps: [],\n          count: 0,\n          successCount: 0,\n          successRate: 0,\n          startTime: new Date(),\n          endTime: new Date(),\n        }));\n\n      if (timeRange === 0) {\n        // If all swaps happened at the same time, place them in the first bucket\n        buckets[0].swaps = [...sortedSwaps.value];\n        buckets[0].count = sortedSwaps.value.length;\n        buckets[0].successCount = sortedSwaps.value.filter(\n          (swap) => swap.state === \"OK\"\n        ).length;\n        buckets[0].successRate =\n          buckets[0].count > 0 ? buckets[0].successCount / buckets[0].count : 0;\n        buckets[0].startTime = new Date(startTime);\n        buckets[0].endTime = new Date(endTime);\n        return buckets;\n      }\n\n      // Calculate bucket time ranges\n      const bucketTimeSpan = timeRange / barsCount.value;\n\n      for (let i = 0; i < barsCount.value; i++) {\n        // Reverse the order: newest (latest) on the left, oldest on the right\n        const reversedIndex = barsCount.value - 1 - i;\n        const bucketStartTime = startTime + reversedIndex * bucketTimeSpan;\n        const bucketEndTime = startTime + (reversedIndex + 1) * bucketTimeSpan;\n\n        buckets[i].startTime = new Date(bucketStartTime);\n        buckets[i].endTime = new Date(bucketEndTime);\n      }\n\n      // Assign swaps to buckets\n      sortedSwaps.value.forEach((swap) => {\n        const swapTime = new Date(swap.created_at).getTime();\n\n        // Find which bucket this swap belongs to (reversing order so newest is on left)\n        const normalizedPosition = (swapTime - startTime) / timeRange; // 0 = oldest, 1 = newest\n        const reversedPosition = 1 - normalizedPosition; // 0 = newest, 1 = oldest\n        const bucketIndex = Math.min(\n          Math.floor(reversedPosition * barsCount.value),\n          barsCount.value - 1\n        );\n\n        if (bucketIndex >= 0) {\n          buckets[bucketIndex].swaps.push(swap);\n          buckets[bucketIndex].count++;\n          if (swap.state === \"OK\") {\n            buckets[bucketIndex].successCount++;\n          }\n        }\n      });\n\n      // Calculate success rates for each bucket\n      buckets.forEach((bucket) => {\n        bucket.successRate =\n          bucket.count > 0 ? bucket.successCount / bucket.count : 0;\n      });\n\n      return buckets;\n    });\n\n    const getSuccessColor = (successRate: number) => {\n      // Convert success rate to a color from red (0%) to orange (50%) to green (100%)\n      if (successRate === 1) return \"#4CAF50\"; // Pure green for 100%\n      if (successRate === 0) return \"#f44336\"; // Pure red for 0%\n\n      if (successRate < 0.5) {\n        // Red to orange gradient (0% to 50%)\n        const r = 244;\n        const g = Math.floor(67 + successRate * 2 * (165 - 67));\n        const b = 54;\n        return `rgb(${r}, ${g}, ${b})`;\n      } else {\n        // Orange to green gradient (50% to 100%)\n        const r = Math.floor(244 - (successRate - 0.5) * 2 * (244 - 76));\n        const g = Math.floor(165 + (successRate - 0.5) * 2 * (175 - 165));\n        const b = Math.floor(54 - (successRate - 0.5) * 2 * (54 - 50));\n        return `rgb(${r}, ${g}, ${b})`;\n      }\n    };\n\n    const formatTime = (date: Date) => {\n      // Special case for the most recent time (leftmost tick)\n      if (sortedSwaps.value.length > 0) {\n        const latestSwapTime = new Date(\n          sortedSwaps.value[sortedSwaps.value.length - 1].created_at\n        ).getTime();\n        if (Math.abs(date.getTime() - latestSwapTime) < 1000) {\n          // Within 1 second\n          return \"Now\";\n        }\n      }\n\n      const today = new Date();\n      const isToday = date.toDateString() === today.toDateString();\n      const isYesterday =\n        new Date(today.setDate(today.getDate() - 1)).toDateString() ===\n        date.toDateString();\n\n      let dateStr = \"\";\n      if (isToday) {\n        dateStr = \"Today\";\n      } else if (isYesterday) {\n        dateStr = \"Yesterday\";\n      } else {\n        dateStr = date.toLocaleDateString(undefined, {\n          month: \"short\",\n          day: \"numeric\",\n        });\n      }\n\n      return `${dateStr} ${date.toLocaleTimeString(undefined, {\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        hour12: false,\n      })}`;\n    };\n\n    const formatDate = (dateStr: string) => {\n      const hasTimezone = /([Zz]|[+-]\\d{2}:\\d{2})$/.test(dateStr);\n      let utcDateStr = dateStr;\n\n      if (!hasTimezone) {\n        utcDateStr += \"Z\";\n      }\n\n      const dateObj = new Date(utcDateStr);\n      if (isNaN(dateObj.getTime())) {\n        return \"Invalid Date\";\n      }\n\n      const today = new Date();\n      const isToday = dateObj.toDateString() === today.toDateString();\n      const isYesterday =\n        new Date(today.setDate(today.getDate() - 1)).toDateString() ===\n        dateObj.toDateString();\n\n      let formattedDate = \"\";\n      if (isToday) {\n        formattedDate = \"Today\";\n      } else if (isYesterday) {\n        formattedDate = \"Yesterday\";\n      } else {\n        formattedDate = dateObj.toLocaleDateString(undefined, {\n          month: \"short\",\n          day: \"numeric\",\n        });\n      }\n\n      return `${formattedDate} ${dateObj.toLocaleTimeString(undefined, {\n        hour: \"2-digit\",\n        minute: \"2-digit\",\n        hour12: false,\n      })}`;\n    };\n\n    const showBucketTooltip = (event: MouseEvent, bucket: SwapBucket) => {\n      if (bucket.count === 0) {\n        hideTooltip();\n        return;\n      }\n\n      const successRate = Math.round(bucket.successRate * 100);\n      let timeRange = \"\";\n\n      if (bucket.startTime && bucket.endTime) {\n        timeRange = `${formatDate(\n          bucket.startTime.toISOString()\n        )} - ${formatDate(bucket.endTime.toISOString())}`;\n      }\n\n      tooltip.value = {\n        show: true,\n        x: event.clientX + 10,\n        y: event.clientY + 10,\n        content: `\n          <div><b>Swaps:</b> ${bucket.count}</div>\n          <div><b>Success rate:</b> ${successRate}% (${bucket.successCount}/${bucket.count})</div>\n          <div><b>Time range:</b> ${timeRange}</div>\n        `,\n      };\n    };\n\n    const hideTooltip = () => {\n      tooltip.value.show = false;\n    };\n\n    return {\n      tooltip,\n      timeTicks,\n      displayBuckets,\n      chartContainer,\n      barsCount,\n      barWidth,\n      spacing,\n      maxBars,\n      formatTime,\n      formatDate,\n      showBucketTooltip,\n      hideTooltip,\n      getSuccessColor,\n    };\n  },\n});\n</script>\n\n<style scoped>\n.bar-chart-container {\n  background-color: #1e1e1e;\n  border-radius: 8px;\n  padding-bottom: 6px;\n  padding-top: 20px;\n  padding-left: 20px;\n  padding-right: 20px;\n  position: relative;\n  width: 100%;\n}\n\n.chart-title {\n  color: white;\n  text-align: center;\n}\n\n.chart-wrapper {\n  position: relative;\n  height: 65px;\n  width: 100%;\n}\n\n.bars-container {\n  display: flex;\n  align-items: flex-end;\n  height: 40px;\n  background-color: #2d2d2d;\n  border-radius: 4px;\n  padding: 4px;\n  position: relative;\n  width: 100%;\n}\n\n.bar {\n  height: 100%;\n  transition: height 0.3s ease;\n  border-radius: 2px;\n  position: absolute;\n  top: 0;\n}\n\n.bar:hover {\n  filter: brightness(1.2);\n}\n\n.time-axis {\n  position: absolute;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  height: 20px;\n  display: flex;\n  justify-content: space-between;\n  padding: 0 4px;\n}\n\n.time-tick {\n  position: absolute;\n  transform: translateX(-50%);\n  color: #888;\n  font-size: 10px;\n  white-space: nowrap;\n}\n\n.tooltip {\n  position: fixed;\n  background-color: rgba(0, 0, 0, 0.8);\n  color: white;\n  padding: 4px 8px;\n  border-radius: 4px;\n  font-size: 12px;\n  pointer-events: none;\n  z-index: 1000;\n  white-space: nowrap;\n}\n\n.bar.placeholder {\n  background-color: transparent;\n  border: 1px solid #444;\n}\n\n.bar.placeholder:hover {\n  filter: none;\n}\n</style>\n"
  },
  {
    "path": "src/components/MintAuditWarningBox.vue",
    "content": "<template>\n  <div class=\"warning-container\" v-if=\"hasWarnings\">\n    <div class=\"warning-content\">\n      <q-icon name=\"warning\" size=\"24px\" class=\"warning-icon\" />\n      <div class=\"warning-text-container\">\n        <div class=\"warning-header\">\n          <div class=\"warning-title\">Auditor Warnings</div>\n        </div>\n        <div class=\"warning-messages\">\n          <ul class=\"warning-list\">\n            <li\n              v-for=\"(message, index) in warningMessages\"\n              :key=\"index\"\n              class=\"warning-message\"\n            >\n              {{ message }}\n            </li>\n          </ul>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, computed } from \"vue\";\n\ninterface MintRead {\n  id: number;\n  url: string;\n  info?: string;\n  name?: string;\n  balance: number;\n  sum_donations?: number;\n  updated_at: string;\n  next_update?: string;\n  state: string;\n  n_errors: number;\n  n_mints: number;\n  n_melts: number;\n}\n\ninterface SwapEventRead {\n  id: number;\n  from_id: number;\n  to_id: number;\n  from_url: string;\n  to_url: string;\n  amount: number;\n  fee: number;\n  created_at: string;\n  time_taken: number;\n  state: string;\n  error: string;\n}\n\nexport default defineComponent({\n  name: \"MintAuditWarningBox\",\n  props: {\n    mint: {\n      type: Object as () => MintRead,\n      required: true,\n    },\n    swaps: {\n      type: Array as () => SwapEventRead[],\n      required: true,\n    },\n    inactiveThresholdDays: {\n      type: Number,\n      default: 2,\n    },\n    recentDaysThreshold: {\n      type: Number,\n      default: 7,\n    },\n    successRateThreshold: {\n      type: Number,\n      default: 85,\n    },\n    slowMintThresholdMs: {\n      type: Number,\n      default: 5000, // 5 seconds in milliseconds\n    },\n    requiredSuccessfulSwaps: {\n      type: Number,\n      default: 7,\n    },\n  },\n  setup(props) {\n    const warningMessages = computed(() => {\n      const messages: string[] = [];\n\n      // Get recent swaps for reuse in multiple conditions\n      const now = new Date();\n      const recentSwaps = props.swaps.filter((swap) => {\n        const swapDate = new Date(swap.created_at);\n        const daysDifference =\n          (now.getTime() - swapDate.getTime()) / (1000 * 60 * 60 * 24);\n        return daysDifference <= props.recentDaysThreshold;\n      });\n\n      const successfulRecentSwaps = recentSwaps.filter(\n        (swap) => swap.state === \"OK\"\n      );\n      const recentSuccessRate =\n        recentSwaps.length > 0\n          ? (successfulRecentSwaps.length / recentSwaps.length) * 100\n          : 0;\n\n      // Check mint state\n      if (props.mint.state === \"WARN\" || props.mint.state === \"ERROR\") {\n        const baseMessage = \"The last swap attempt has failed.\";\n\n        if (recentSwaps.length > 0) {\n          const successMessage = `${successfulRecentSwaps.length} of ${recentSwaps.length} swaps in the last ${props.recentDaysThreshold} days succeeded.`;\n\n          // Add \"However, \" prefix if success rate is above threshold\n          if (recentSuccessRate >= props.successRateThreshold) {\n            messages.push(\n              `${baseMessage} However, ${successMessage} This mint seems reliable, failures might be due to the receiving mint.`\n            );\n          } else {\n            messages.push(\n              `${baseMessage} Only ${successMessage} This mint might be unreliable.`\n            );\n          }\n        } else {\n          messages.push(baseMessage);\n        }\n      } else if (props.mint.state === \"UNKNOWN\") {\n        messages.push(\n          \"The auditor was not able to determine the quality of this mint yet.\"\n        );\n      } else if (props.mint.state === \"OK\") {\n        // Add warnings for OK state mints with not enough successful swaps or low success rate\n        const warningsNeeded = [];\n\n        // Check for not enough successful swaps\n        if (successfulRecentSwaps.length < props.requiredSuccessfulSwaps) {\n          warningsNeeded.push(\n            `The auditor does not have enough recent data on this mint. There were only ${\n              successfulRecentSwaps.length\n            } successful ${\n              successfulRecentSwaps.length === 1 ? \"swap\" : \"swaps\"\n            } in the last ${props.recentDaysThreshold} days.`\n          );\n        }\n\n        // Check for low success rate\n        if (\n          recentSwaps.length > 0 &&\n          recentSuccessRate < props.successRateThreshold\n        ) {\n          warningsNeeded.push(\n            `In the last ${\n              props.recentDaysThreshold\n            } days, this mint had a success rate of ${Math.round(\n              recentSuccessRate\n            )}%, below the recommended ${props.successRateThreshold}%.`\n          );\n        }\n\n        // Add combined message if both issues exist\n        if (warningsNeeded.length > 0) {\n          messages.push(...warningsNeeded);\n        }\n      }\n\n      // Check last successful swap\n      const successfulSwaps = props.swaps.filter((swap) => swap.state === \"OK\");\n      if (successfulSwaps.length > 0) {\n        const lastSuccessfulSwap = new Date(successfulSwaps[0].created_at);\n        const now = new Date();\n        const daysDifference =\n          (now.getTime() - lastSuccessfulSwap.getTime()) /\n          (1000 * 60 * 60 * 24);\n\n        if (daysDifference > props.inactiveThresholdDays) {\n          const days = Math.floor(daysDifference);\n\n          const baseMessage = `This mint had no swaps for ${days} ${\n            days === 1 ? \"day\" : \"days\"\n          }. It might be unreachable.`;\n          messages.push(baseMessage);\n        }\n\n        // Check if mint is slow\n        const successfulSwapsWithTime = successfulSwaps.filter(\n          (swap) => swap.time_taken\n        );\n        if (successfulSwapsWithTime.length > 0) {\n          const totalTime = successfulSwapsWithTime.reduce(\n            (sum, swap) => sum + (swap.time_taken || 0),\n            0\n          );\n          const averageTimeMs = totalTime / successfulSwapsWithTime.length;\n\n          if (averageTimeMs > props.slowMintThresholdMs) {\n            const averageTimeSeconds = (averageTimeMs / 1000).toFixed(1);\n            messages.push(\n              `This mint is slow. Payments from this mint took ${averageTimeSeconds} seconds on average.`\n            );\n          }\n        }\n      } else if (props.swaps.length > 0) {\n        // If there are swaps but none are successful\n        messages.push(\n          \"No successful swaps recorded for this mint. It might be unreachable.\"\n        );\n      }\n\n      return messages;\n    });\n\n    const hasWarnings = computed(() => warningMessages.value.length > 0);\n\n    return {\n      warningMessages,\n      hasWarnings,\n    };\n  },\n});\n</script>\n\n<style scoped>\n.warning-container {\n  width: 100%;\n  position: relative;\n  border-radius: 8px;\n  border: 1px solid #f18408;\n  box-sizing: border-box;\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  justify-content: flex-start;\n  padding: 16px;\n  text-align: left;\n  font-size: 14px;\n  color: #f18408;\n  margin-bottom: 16px;\n  margin-top: 8px;\n}\n\n.warning-content {\n  width: 100%;\n  display: flex;\n  flex-direction: row;\n  align-items: flex-start;\n  justify-content: flex-start;\n  gap: 12px;\n}\n\n.warning-icon {\n  flex-shrink: 0;\n  color: #f18408;\n}\n\n.warning-text-container {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  justify-content: center;\n  gap: 2px;\n}\n\n.warning-header {\n  align-self: stretch;\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.warning-title {\n  position: relative;\n  line-height: 16px;\n  font-weight: 500;\n}\n\n.warning-messages {\n  align-self: stretch;\n}\n\n.warning-list {\n  margin: 4px 0 0 0;\n  padding-left: 20px;\n}\n\n.warning-message {\n  align-self: stretch;\n  position: relative;\n  line-height: 16px;\n  margin-bottom: 4px;\n}\n\n.warning-message:last-child {\n  margin-bottom: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/MintDiscovery.vue",
    "content": "<template>\n  <div class=\"discover-section\">\n    <div class=\"discover-card\">\n      <!-- <div class=\"discover-header\">\n        <p class=\"discover-subtitle\">\n          {{ $t(\"MintSettings.discover.caption\") }}\n        </p>\n      </div> -->\n      <div class=\"discover-button\">\n        <q-btn\n          color=\"primary\"\n          size=\"lg\"\n          rounded\n          :loading=\"discovering\"\n          @click=\"discover\"\n          class=\"discover-btn full-width\"\n          style=\"\n            min-height: 48px;\n            font-weight: 500;\n            text-transform: none;\n            font-size: 0.95rem;\n          \"\n        >\n          <q-icon name=\"search\" size=\"20px\" class=\"q-mr-sm\" />\n          <span>{{ $t(\"MintSettings.discover.actions.discover.label\") }}</span>\n          <template v-slot:loading>\n            <q-spinner class=\"on-left\" />\n            {{ $t(\"MintSettings.discover.actions.discover.in_progress\") }}\n          </template>\n        </q-btn>\n      </div>\n    </div>\n\n    <div v-if=\"discoverList.length > 0\" class=\"text-left\" on-left>\n      <q-list padding>\n        <q-item class=\"q-px-none\">\n          <q-item-section>\n            <q-item-label overline>\n              {{\n                $t(\"MintSettings.discover.recommendations.overline\", {\n                  length: discoverList.length,\n                })\n              }}\n            </q-item-label>\n            <q-item-label caption>\n              {{ $t(\"MintSettings.discover.recommendations.caption\") }}\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n        <TransitionGroup name=\"mint\" tag=\"div\" class=\"q-pt-sm\">\n          <div\n            v-for=\"rec in discoverList\"\n            :key=\"rec.url\"\n            class=\"q-px-none q-mb-md\"\n          >\n            <q-item\n              class=\"mint-card\"\n              :class=\"{ dimmed: isFetchingMintInfo(rec.url) }\"\n              :style=\"{\n                'border-radius': '10px',\n                border: '1px solid rgba(128,128,128,0.2)',\n                padding: '0px',\n                position: 'relative',\n              }\"\n            >\n              <div class=\"full-width\" style=\"position: relative\">\n                <!-- Centered spinner overlay -->\n                <div v-if=\"isFetchingMintInfo(rec.url)\" class=\"spinner-overlay\">\n                  <q-spinner-dots size=\"34px\" color=\"grey-5\" />\n                </div>\n\n                <div class=\"row items-center q-pa-md\">\n                  <div class=\"col\">\n                    <div class=\"row items-center\">\n                      <MintInfoContainer\n                        :iconUrl=\"getMintIconUrlUrl(rec.url) || undefined\"\n                        :name=\"getMintDisplayName(rec.url)\"\n                        :url=\"rec.url\"\n                      />\n                    </div>\n                    <div class=\"row\">\n                      <div\n                        class=\"text-grey-5 q-mt-xs\"\n                        v-if=\"avgFor(rec.url) !== null && countFor(rec.url) > 0\"\n                      >\n                        <span>\n                          ⭐ {{ (avgFor(rec.url) ?? 0).toFixed(1) }} ·\n                          {{ countFor(rec.url) }}\n                          <span\n                            class=\"text-primary cursor-pointer\"\n                            style=\"text-decoration: underline\"\n                            @click.stop=\"openReviews(rec.url)\"\n                            >{{ $t(\"MintSettings.reviews_text\") }}</span\n                          >\n                        </span>\n                      </div>\n                      <div class=\"text-grey-7 q-mt-xs\" v-else>\n                        <span>{{ $t(\"MintRatings.no_reviews\") }}</span>\n                      </div>\n                    </div>\n                  </div>\n                  <div class=\"col-auto\">\n                    <q-btn\n                      dense\n                      round\n                      flat\n                      icon=\"add\"\n                      @click=\"addDiscovered(rec.url)\"\n                      :disable=\"\n                        isFetchingMintInfo(rec.url) || isExistingMint(rec.url)\n                      \"\n                    />\n                  </div>\n                </div>\n              </div>\n            </q-item>\n          </div>\n        </TransitionGroup>\n      </q-list>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, ref, computed, watch, onMounted } from \"vue\";\nimport { useRouter } from \"vue-router\";\nimport { useMintRecommendationsStore } from \"src/stores/mintRecommendations\";\nimport { useMintsStore, MintClass } from \"src/stores/mints\";\nimport MintInfoContainer from \"./MintInfoContainer.vue\";\nimport { notifyError, notifySuccess } from \"src/js/notify\";\n\nexport default defineComponent({\n  name: \"MintDiscovery\",\n  components: { MintInfoContainer },\n  props: {\n    infoTimeoutMs: { type: Number, default: 5000 },\n    autoDiscover: { type: Boolean, default: false },\n    allowCreateReview: { type: Boolean, default: true },\n  },\n  setup(props) {\n    const router = useRouter();\n    const recsStore = useMintRecommendationsStore();\n    const mints = useMintsStore();\n\n    const discovering = ref(false);\n\n    const recommendations = computed(() => recsStore.recommendations);\n    const isExistingMint = (url: string) =>\n      mints.mints.some((m) => m.url === url);\n    const discoverList = computed(() =>\n      recommendations.value\n        .filter((r) => !isExistingMint(r.url) && !r.error)\n        .sort((a, b) => {\n          const aFetching = isFetchingMintInfo(a.url);\n          const bFetching = isFetchingMintInfo(b.url);\n\n          // If one is fetching and the other isn't, put the fetching one at the bottom\n          if (aFetching && !bFetching) return 1;\n          if (!aFetching && bFetching) return -1;\n\n          // If both are in the same state, maintain original order\n          return 0;\n        })\n    );\n\n    // Use store-managed HTTP info (Dexie + in-memory), persist only error in localStorage\n    const fetchingMintInfo = ref(new Set<string>());\n    const getRec = (url: string) =>\n      recommendations.value.find((r) => r.url === url);\n    const getMintIconUrlUrl = (url: string) =>\n      recsStore.getHttpInfoForUrl(url)?.icon_url || null;\n    const getMintDisplayName = (url: string) =>\n      recsStore.getHttpInfoForUrl(url)?.name || url.replace(/^https?:\\/\\//, \"\");\n    const isFetchingMintInfo = (url: string) => {\n      const rec = getRec(url);\n      return rec\n        ? !recsStore.hasHttpInfo(url) && !rec.error\n        : fetchingMintInfo.value.has(url);\n    };\n\n    const fetchMintInfoForDiscovered = async () => {\n      const targets = discoverList.value\n        .filter((rec) => !fetchingMintInfo.value.has(rec.url))\n        .map((rec) => rec.url);\n      targets.forEach((u) => fetchingMintInfo.value.add(u));\n      await recsStore.scheduleHttpInfoFetches(\n        targets,\n        10,\n        100,\n        (defineComponent as any).props?.infoTimeoutMs?.default ?? 5000\n      );\n    };\n    watch(discoverList, () => fetchMintInfoForDiscovered(), {\n      immediate: true,\n    });\n\n    const discover = async () => {\n      discovering.value = true;\n      try {\n        // ensure cached reviews from IndexedDB are visible immediately\n        await (recsStore as any).hydrateFromDb?.();\n        recsStore.clearDiscoveryCaches();\n        // recsStore.clearRecommendations();\n        // Start live updates immediately\n        recsStore.startSubscriptions();\n        // Kick off initial fetches and keep spinner until both complete\n        const pInfos = recsStore.fetchMintInfos();\n        const pReviews = recsStore.fetchReviews();\n        await Promise.allSettled([pInfos, pReviews]);\n      } finally {\n        discovering.value = false;\n      }\n    };\n\n    onMounted(() => {\n      if (props.autoDiscover) {\n        // Start discovery right away when mounted if enabled\n        discover();\n      }\n    });\n    const addDiscovered = async (url: string) => {\n      await mints.addMint({ url }, true);\n    };\n    const openReviews = (url: string) => {\n      // Navigate to ratings page\n      router.push({\n        path: \"/mintratings\",\n        query: {\n          mintUrl: url,\n          allowCreateReview: \"false\",\n        },\n      });\n    };\n\n    return {\n      discovering,\n      discover,\n      discoverList,\n      avgFor: (url: string) => recsStore.getAverageForUrl(url),\n      countFor: (url: string) => recsStore.getCountForUrl(url),\n      getMintIconUrlUrl,\n      isFetchingMintInfo,\n      getMintDisplayName,\n      isExistingMint,\n      addDiscovered,\n      openReviews,\n    };\n  },\n});\n</script>\n\n<style scoped>\n@import \"src/css/mintlist.css\";\n\n.discover-section {\n  width: 100%;\n  margin-bottom: 24px;\n}\n\n.discover-card {\n  padding: 0;\n  margin-bottom: 24px;\n}\n\n.discover-header {\n  margin-bottom: 20px;\n}\n\n.discover-title {\n  font-size: 15.2px;\n  font-family: Inter, -apple-system, \"system-ui\", \"Segoe UI\", Roboto,\n    \"Helvetica Neue\", Arial, sans-serif;\n  font-weight: 600;\n  color: #ffffff;\n  margin: 0 0 8px 0;\n  text-transform: none;\n}\n\n.discover-subtitle {\n  font-size: 0.9rem;\n  color: rgba(255, 255, 255, 0.8);\n  margin: 0;\n  line-height: 1.4;\n}\n\n.discover-button {\n  margin-top: 16px;\n}\n\n.discover-btn {\n  width: 100%;\n}\n\n.mint-card {\n  border: 1px solid rgba(128, 128, 128, 0.2);\n  border-radius: 10px;\n}\n\n.mint-card.dimmed {\n  opacity: 0.5;\n}\n\n.spinner-overlay {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n  z-index: 10;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n/* Smooth list animations for discovered mints */\n.mint-move {\n  transition: transform 500ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease;\n}\n.mint-enter-active,\n.mint-leave-active {\n  transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease;\n}\n.mint-enter-from,\n.mint-leave-to {\n  opacity: 0;\n  transform: translateY(8px) scale(0.98);\n}\n/* Prevent layout jump during leave */\n.mint-leave-active {\n  position: absolute;\n  width: 100%;\n}\n</style>\n"
  },
  {
    "path": "src/components/MintInfoContainer.vue",
    "content": "<template>\n  <div class=\"row items-center\">\n    <q-avatar v-if=\"iconUrl\" :size=\"avatarSize\" class=\"q-mr-sm\">\n      <q-img\n        :src=\"iconUrl\"\n        spinner-color=\"white\"\n        spinner-size=\"xs\"\n        :style=\"`height: ${avatarSize}; max-width: ${avatarSize}; font-size: 12px;`\"\n      >\n        <template v-slot:error>\n          <div\n            class=\"row items-center justify-center\"\n            style=\"height: 100%; width: 100%; padding: 0px\"\n          >\n            <q-icon\n              name=\"account_balance\"\n              color=\"grey-7\"\n              :size=\"fallbackIconSize\"\n            />\n          </div>\n        </template>\n      </q-img>\n    </q-avatar>\n\n    <div class=\"mint-info-container\">\n      <div v-if=\"name\" class=\"mint-name\">{{ name }}</div>\n      <div class=\"text-grey-6 mint-url\">{{ url }}</div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\n\nexport default defineComponent({\n  name: \"MintInfoContainer\",\n  props: {\n    url: { type: String, required: true },\n    name: { type: String, required: false },\n    iconUrl: { type: String, required: false },\n    avatarSize: { type: String, default: \"34px\" },\n  },\n  computed: {\n    fallbackIconSize(): string {\n      // pick a slightly smaller icon inside the avatar\n      const n = parseInt(String(this.avatarSize).replace(/px$/, \"\")) || 34;\n      return Math.max(16, Math.floor(n * 0.6)) + \"px\";\n    },\n  },\n});\n</script>\n\n<style scoped></style>\n"
  },
  {
    "path": "src/components/MintMotdMessage.vue",
    "content": "<template>\n  <div class=\"motd-container\" v-if=\"message && !dismissed\">\n    <div class=\"motd-content\">\n      <info-icon size=\"24\" class=\"motd-icon\" />\n      <div class=\"motd-text-container\">\n        <div class=\"motd-header\">\n          <div class=\"motd-title\">{{ $t(\"MintMotdMessage.title\") }}</div>\n          <x-icon\n            size=\"18\"\n            class=\"motd-close-icon cursor-pointer\"\n            @click=\"dismissMessage\"\n          />\n        </div>\n        <div class=\"motd-message\">{{ message }}</div>\n      </div>\n    </div>\n  </div>\n  <div class=\"motd-dismissed q-mt-md\" v-else-if=\"message && dismissed\">\n    <div class=\"motd-dismissed-message\">\n      <info-icon size=\"24\" class=\"motd-dismissed-icon\" />\n      <div class=\"motd-text-container\">\n        <div class=\"motd-title\">{{ $t(\"MintMotdMessage.title\") }}</div>\n        <div class=\"motd-message\">{{ message }}</div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { X as XIcon, Info as InfoIcon } from \"lucide-vue-next\";\nimport { useMintsStore } from \"src/stores/mints\";\n\nexport default defineComponent({\n  name: \"MintMotdMessage\",\n  components: {\n    XIcon,\n    InfoIcon,\n  },\n  props: {\n    message: {\n      type: String,\n      default: \"\",\n    },\n    mintUrl: {\n      type: String,\n      required: true,\n    },\n    dismissed: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: [\"dismiss\"],\n  setup(props, { emit }) {\n    const mintsStore = useMintsStore();\n\n    const dismissMessage = () => {\n      mintsStore.mints.filter((m) => m.url === props.mintUrl)[0].motdDismissed =\n        true;\n      emit(\"dismiss\");\n    };\n\n    return {\n      dismissMessage,\n    };\n  },\n});\n</script>\n\n<style scoped>\n.motd-container {\n  width: 100%;\n  position: relative;\n  border-radius: 8px;\n  border: 1px solid #f18408;\n  box-sizing: border-box;\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  justify-content: flex-start;\n  padding: 8px;\n  text-align: left;\n  font-size: 14px;\n  color: #f18408;\n  margin-bottom: 16px;\n}\n\n.motd-content {\n  width: 100%;\n  display: flex;\n  flex-direction: row;\n  align-items: flex-start;\n  justify-content: flex-start;\n  gap: 12px;\n}\n\n.motd-icon {\n  flex-shrink: 0;\n  color: #f18408;\n}\n\n.motd-text-container {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  justify-content: center;\n  gap: 2px;\n}\n\n.motd-header {\n  align-self: stretch;\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: space-between;\n}\n\n.motd-title {\n  position: relative;\n  line-height: 16px;\n  font-weight: 500;\n}\n\n.motd-close-icon {\n  overflow: hidden;\n  flex-shrink: 0;\n  color: #f18408;\n}\n\n.motd-message {\n  align-self: stretch;\n  position: relative;\n  line-height: 16px;\n}\n\n.motd-dismissed {\n  width: 100%;\n}\n\n.motd-dismissed-message {\n  font-size: 14px;\n  color: #9e9e9e;\n  line-height: 16px;\n  display: flex;\n  align-items: flex-start;\n  gap: 12px;\n}\n\n.motd-dismissed-icon {\n  flex-shrink: 0;\n  color: #9e9e9e;\n}\n</style>\n"
  },
  {
    "path": "src/components/MintQuoteInformation.vue",
    "content": "<template>\n  <div\n    v-if=\"hasQuote\"\n    class=\"mint-quote-information q-mt-md\"\n    :class=\"containerTextClass\"\n  >\n    <div v-if=\"showAmount\" class=\"detail-item q-mb-md\">\n      <div class=\"detail-label\">\n        <ZapIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">Amount</div>\n      </div>\n      <div class=\"detail-value\">{{ amountDisplay }}</div>\n    </div>\n\n    <div class=\"detail-item q-mb-md\">\n      <div class=\"detail-label\">\n        <BanknoteIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">Unit</div>\n      </div>\n      <div class=\"detail-value\">{{ unitDisplay }}</div>\n    </div>\n\n    <div v-if=\"stateDisplay\" class=\"detail-item q-mb-md\">\n      <div class=\"detail-label\">\n        <InfoIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">State</div>\n      </div>\n      <div class=\"detail-value\" :class=\"{ 'text-positive': isPaidState }\">\n        {{ stateDisplay }}\n      </div>\n    </div>\n\n    <div v-if=\"paidAtDisplay\" class=\"detail-item q-mb-md\">\n      <div class=\"detail-label\">\n        <ClockIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">Time Paid</div>\n      </div>\n      <div class=\"detail-value\">{{ paidAtDisplay }}</div>\n    </div>\n\n    <div v-if=\"mintDisplay\" class=\"detail-item\">\n      <div class=\"detail-label\">\n        <BuildingIcon :size=\"20\" :color=\"iconColor\" class=\"detail-icon\" />\n        <div class=\"detail-name\">Mint</div>\n      </div>\n      <div class=\"detail-value\">{{ mintDisplay }}</div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from \"vue\";\nimport { mapState } from \"pinia\";\nimport { useMintsStore } from \"stores/mints\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport {\n  Zap as ZapIcon,\n  Banknote as BanknoteIcon,\n  Info as InfoIcon,\n  Clock as ClockIcon,\n  Building as BuildingIcon,\n} from \"lucide-vue-next\";\n\ndeclare const windowMixin: any;\ndeclare const formatCurrency: any;\n\ntype MintQuote = {\n  request: string;\n  quote: string;\n  state?: string;\n  expiry?: number;\n  unit?: string;\n  amount?: number;\n};\n\nexport default defineComponent({\n  name: \"MintQuoteInformation\",\n  mixins: [windowMixin],\n  components: {\n    ZapIcon,\n    BanknoteIcon,\n    InfoIcon,\n    ClockIcon,\n    BuildingIcon,\n  },\n  props: {\n    mintQuote: {\n      type: Object as PropType<MintQuote | null>,\n      default: null,\n    },\n    invoice: {\n      type: Object as PropType<any | null>,\n      default: null,\n    },\n    mintUrl: {\n      type: String,\n      default: \"\",\n    },\n    showAmount: {\n      type: Boolean,\n      default: true,\n    },\n  },\n  computed: {\n    ...mapState(useMintsStore, [\"activeMintUrl\", \"activeUnit\"]),\n    hasQuote(): boolean {\n      return Boolean(this.mintQuote && this.mintQuote.quote);\n    },\n    iconColor(): string {\n      return this.$q.dark.isActive ? \"#9E9E9E\" : \"#616161\";\n    },\n    containerTextClass(): string {\n      return this.$q.dark.isActive ? \"text-white\" : \"text-dark\";\n    },\n    unit(): string {\n      if (this.mintQuote?.unit) return this.mintQuote.unit;\n      if (this.activeUnit) return this.activeUnit as string;\n      return \"sat\";\n    },\n    unitDisplay(): string {\n      return this.unit?.toUpperCase?.() || \"\";\n    },\n    amountDisplay(): string {\n      const amount =\n        (this.mintQuote?.amount as number | undefined) ??\n        (typeof this.invoice?.amount === \"number\"\n          ? Math.abs(this.invoice.amount)\n          : undefined);\n      if (typeof amount !== \"number\") return \"\";\n      return (this as any).formatCurrency(amount, this.unit);\n    },\n    stateDisplay(): string {\n      const state = (this.invoice?.status as string) || this.mintQuote?.state;\n      if (!state) return \"\";\n      const lower = state.toLowerCase();\n      return lower.charAt(0).toUpperCase() + lower.slice(1);\n    },\n    isPaidState(): boolean {\n      const state = (this.invoice?.status as string) || this.mintQuote?.state;\n      return state?.toLowerCase() === \"paid\";\n    },\n    paidAtTimestamp(): number | null {\n      if (this.invoice?.paidDate) {\n        return this.normalizeToTimestamp(this.invoice.paidDate);\n      }\n      return null;\n    },\n    paidAtDisplay(): string {\n      if (!this.paidAtTimestamp) return \"\";\n      return new Intl.DateTimeFormat(undefined, {\n        dateStyle: \"medium\",\n        timeStyle: \"short\",\n      }).format(new Date(this.paidAtTimestamp));\n    },\n    mintDisplay(): string {\n      const url = this.mintUrl || (this.activeMintUrl as string) || \"\";\n      return url ? getShortUrl(url) : \"\";\n    },\n  },\n  methods: {\n    normalizeToTimestamp(value: string | number | Date | null | undefined) {\n      if (value === null || value === undefined) return null;\n      if (typeof value === \"number\") {\n        if (!Number.isFinite(value) || value <= 0) return null;\n        return value > 1e12 ? value : value * 1000;\n      }\n      if (typeof value === \"string\") {\n        const numeric = Number(value);\n        if (!Number.isNaN(numeric) && numeric > 0) {\n          return numeric > 1e12 ? numeric : numeric * 1000;\n        }\n        const parsed = Date.parse(value);\n        if (!Number.isNaN(parsed)) return parsed;\n      }\n      if (value instanceof Date) return value.getTime();\n      return null;\n    },\n  },\n});\n</script>\n\n<style scoped>\n.mint-quote-information {\n  width: 100%;\n}\n\n.detail-item {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  width: 100%;\n}\n\n.detail-label {\n  display: flex;\n  align-items: center;\n}\n\n.detail-icon {\n  margin-right: 10px;\n}\n\n.detail-name {\n  font-size: 14px;\n  font-weight: 600;\n  color: var(--q-color-grey-6);\n}\n\n.detail-value {\n  font-size: 16px;\n  font-weight: 600;\n  color: inherit;\n  text-align: right;\n  max-width: 60%;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n</style>\n"
  },
  {
    "path": "src/components/MintRatingsComponent.vue",
    "content": "<template>\n  <q-card\n    class=\"bg-dark\"\n    style=\"min-width: 360px; max-width: 820px; width: 100%\"\n  >\n    <q-card-section class=\"q-pt-md q-pb-md\">\n      <!-- Mint Header - Matching MintDetailsPage style -->\n      <div class=\"mint-header-container q-mb-lg\">\n        <div class=\"mint-header q-pa-md\">\n          <q-avatar size=\"56px\" class=\"q-mb-sm\">\n            <img\n              v-if=\"mintInfo?.icon_url\"\n              :src=\"mintInfo.icon_url\"\n              alt=\"Mint Profile\"\n            />\n            <q-icon v-else name=\"account_balance\" size=\"36px\" color=\"grey-7\" />\n          </q-avatar>\n          <div class=\"mint-name q-mb-xs\">\n            {{ mintInfo?.name || \"Mint\" }}\n          </div>\n          <div class=\"mint-url text-grey-6\">\n            {{ url }}\n          </div>\n        </div>\n      </div>\n\n      <!-- Rating Summary Section - Apple Podcasts Style -->\n      <div v-if=\"hasAnyReviews\" class=\"q-mb-lg\">\n        <div class=\"row items-start q-mb-sm\" style=\"gap: 24px\">\n          <!-- Large Rating Display on Left -->\n          <div class=\"column items-center\" style=\"min-width: 100px\">\n            <div class=\"text-h2\" style=\"font-weight: 600; line-height: 1\">\n              {{ averageDisplay }}\n            </div>\n            <div\n              class=\"text-caption text-grey-5\"\n              style=\"font-weight: 400; margin-top: 4px\"\n            >\n              {{ $t(\"MintRatings.out_of\") }} 5\n            </div>\n          </div>\n\n          <!-- Star Distribution Bars on Right -->\n          <div class=\"col column\" style=\"gap: 6px\">\n            <div\n              v-for=\"star in [5, 4, 3, 2, 1]\"\n              :key=\"star\"\n              class=\"row items-center no-wrap\"\n              style=\"gap: 8px\"\n            >\n              <!-- Star Icons (show only the number of stars for this rating level) -->\n              <div class=\"row items-center\" style=\"gap: 1px\">\n                <q-icon\n                  v-for=\"i in star\"\n                  :key=\"i\"\n                  name=\"star\"\n                  size=\"12px\"\n                  class=\"text-grey-6\"\n                />\n              </div>\n              <!-- Distribution Bar -->\n              <div\n                class=\"col\"\n                style=\"\n                  height: 4px;\n                  background: rgba(128, 128, 128, 0.2);\n                  border-radius: 2px;\n                  overflow: hidden;\n                \"\n              >\n                <div\n                  :style=\"{\n                    width: ratingPercentage(star) + '%',\n                    height: '100%',\n                    background: 'var(--q-primary)',\n                    transition: 'width 0.3s ease',\n                  }\"\n                />\n              </div>\n            </div>\n          </div>\n        </div>\n        <!-- WoT Filter and Total Ratings Count -->\n        <div class=\"col-12 row items-center justify-end\" style=\"gap: 6px\">\n          <div class=\"row items-center\" style=\"gap: 6px\">\n            <span class=\"text-caption text-grey-6\" style=\"font-size: 0.7rem\">\n              {{ $t(\"Settings.web_of_trust.title\") }}\n            </span>\n            <q-toggle v-model=\"filterByWoT\" size=\"xs\" color=\"primary\" dense />\n          </div>\n          <div class=\"text-body2 text-grey-5\" style=\"font-weight: 500\">\n            {{ totalReviews }} {{ $t(\"MintRatings.ratings\") }}\n          </div>\n        </div>\n      </div>\n\n      <!-- No Reviews State -->\n      <div v-else class=\"q-mb-lg\">\n        <div\n          class=\"empty-state section-card q-pa-xl column items-center text-center\"\n        >\n          <q-icon\n            name=\"chat_bubble_outline\"\n            size=\"26px\"\n            class=\"empty-icon q-mb-sm\"\n          />\n          <div class=\"empty-title q-mb-sm\">\n            {{ $t(\"MintRatings.no_reviews\") }}\n          </div>\n          <div class=\"empty-subtitle q-mb-lg\">\n            {{ $t(\"MintRatings.empty_state_subtitle\") }}\n          </div>\n          <q-btn\n            v-if=\"allowCreateReview\"\n            color=\"primary\"\n            unelevated\n            rounded\n            class=\"q-px-xl q-mt-md\"\n            style=\"font-weight: 600\"\n            @click=\"openCreateReview\"\n          >\n            {{ $t(\"MintRatings.actions.write_review\") }}\n          </q-btn>\n        </div>\n      </div>\n\n      <!-- Action Buttons Row -->\n      <div\n        v-if=\"allowCreateReview && hasAnyReviews\"\n        class=\"row q-mt-md justify-center\"\n        style=\"gap: 12px\"\n      >\n        <q-btn\n          color=\"primary\"\n          unelevated\n          rounded\n          class=\"q-px-lg\"\n          style=\"font-weight: 500; width: 100%\"\n          @click=\"openCreateReview\"\n        >\n          {{ $t(\"MintRatings.actions.write_review\") }}\n        </q-btn>\n      </div>\n    </q-card-section>\n\n    <q-separator v-if=\"hasAnyReviews\" />\n\n    <!-- Sort Section -->\n    <q-card-section v-if=\"hasAnyReviews\" class=\"q-py-sm\">\n      <div\n        class=\"sort-trigger row items-center justify-between\"\n        @click=\"showSortSheet = true\"\n      >\n        <div class=\"row items-center\" style=\"gap: 8px\">\n          <q-icon name=\"sort\" size=\"14px\" class=\"text-grey-7\" />\n          <span class=\"text-body2\" style=\"font-weight: 600\">{{\n            $t(\"MintRatings.sort\")\n          }}</span>\n          <span class=\"text-body2 text-grey-5\">{{ currentSortLabel }}</span>\n        </div>\n        <q-icon name=\"keyboard_arrow_down\" size=\"20px\" class=\"text-grey-5\" />\n      </div>\n    </q-card-section>\n\n    <q-separator v-if=\"hasAnyReviews\" />\n\n    <q-card-section v-if=\"hasAnyReviews\">\n      <div class=\"column\">\n        <div\n          v-for=\"r in paged\"\n          :key=\"r.eventId\"\n          :class=\"['review-item', { 'own-review': r.pubkey === myPubkey }]\"\n        >\n          <div\n            v-if=\"r.pubkey === myPubkey\"\n            class=\"own-review-badge currency-unit-badge\"\n          >\n            <span class=\"currency-unit-text\">{{\n              $t(\"MintRatings.your_review\")\n            }}</span>\n          </div>\n          <div class=\"row items-start\" style=\"gap: 12px\">\n            <!-- Avatar on the left -->\n            <q-avatar size=\"40px\" class=\"review-avatar\">\n              <q-img\n                v-if=\"profiles[r.pubkey]?.picture\"\n                :src=\"profiles[r.pubkey].picture\"\n                spinner-color=\"white\"\n                spinner-size=\"xs\"\n              />\n              <q-icon v-else name=\"account_circle\" size=\"40px\" />\n            </q-avatar>\n\n            <!-- Content on the right -->\n            <div class=\"col\">\n              <!-- Name and verified badge -->\n              <div\n                class=\"row items-center\"\n                style=\"gap: 6px; margin-bottom: 2px\"\n              >\n                <span\n                  v-if=\"hasProfileName(r.pubkey)\"\n                  class=\"text-body1\"\n                  style=\"font-weight: 600\"\n                  >{{ displayName(r.pubkey) }}</span\n                >\n                <span\n                  v-else\n                  class=\"text-body1 monospace\"\n                  style=\"font-weight: 600\"\n                  >{{ shortNpub(r.pubkey) }}</span\n                >\n                <q-icon\n                  v-if=\"wotHop(r.pubkey)\"\n                  name=\"verified\"\n                  size=\"14px\"\n                  :color=\"wotColor(wotHop(r.pubkey))\"\n                >\n                  <q-tooltip\n                    >In your web of trust (hop\n                    {{ wotHop(r.pubkey) }})</q-tooltip\n                  >\n                </q-icon>\n                <q-icon\n                  name=\"content_copy\"\n                  size=\"14px\"\n                  class=\"cursor-pointer text-grey-7\"\n                  @click=\"copyNpub(r.pubkey)\"\n                  style=\"margin-left: 4px\"\n                >\n                  <q-tooltip>Copy npub</q-tooltip>\n                </q-icon>\n              </div>\n\n              <!-- Date and Star Rating on same line -->\n              <div\n                class=\"row items-center\"\n                style=\"gap: 8px; margin-bottom: 8px\"\n              >\n                <span class=\"text-caption text-grey-6\">\n                  {{ formatDateOnly(r.created_at) }}\n                </span>\n                <q-rating\n                  v-if=\"r.rating !== null\"\n                  :model-value=\"r.rating\"\n                  readonly\n                  size=\"12px\"\n                  color=\"amber\"\n                  icon=\"star\"\n                  icon-selected=\"star\"\n                  icon-half=\"star_half\"\n                />\n                <span v-else class=\"text-caption text-grey-6\">{{\n                  $t(\"MintRatings.no_rating\")\n                }}</span>\n              </div>\n\n              <!-- Review Comment -->\n              <div\n                v-if=\"r.comment\"\n                class=\"text-body2\"\n                style=\"\n                  white-space: pre-wrap;\n                  line-height: 1.5;\n                  color: rgba(255, 255, 255, 0.87);\n                \"\n              >\n                {{ r.comment }}\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <div class=\"row justify-between items-center q-mt-sm\">\n          <q-select\n            dense\n            class=\"q-ml-sm\"\n            color=\"primary\"\n            v-model=\"rowsPerPage\"\n            :options=\"rowsPerPageOptions\"\n            emit-value\n            map-options\n            :label=\"$t('MintRatings.rows')\"\n            style=\"width: 100px\"\n          />\n          <q-pagination\n            v-model=\"page\"\n            :max=\"totalPages\"\n            color=\"primary\"\n            max-pages=\"6\"\n            boundary-numbers\n            direction-links\n            rounded\n          />\n        </div>\n      </div>\n    </q-card-section>\n  </q-card>\n\n  <!-- Sort Bottom Sheet - Outside card, positioned to viewport -->\n  <teleport to=\"body\">\n    <div\n      v-if=\"showSortSheet\"\n      class=\"sort-sheet-overlay\"\n      @click=\"closeSortSheet\"\n    >\n      <div class=\"sort-sheet\" @click.stop>\n        <div class=\"sort-sheet-header\">\n          <h3>{{ $t(\"MintRatings.sort\") }}</h3>\n          <q-btn\n            flat\n            round\n            icon=\"close\"\n            @click=\"closeSortSheet\"\n            class=\"close-btn\"\n          />\n        </div>\n        <div class=\"sort-options\">\n          <div\n            v-for=\"option in sortOptions\"\n            :key=\"option.value\"\n            class=\"sort-option\"\n            :class=\"{ active: sortMode === option.value }\"\n            @click=\"selectSort(option.value)\"\n          >\n            {{ option.label }}\n          </div>\n        </div>\n      </div>\n    </div>\n  </teleport>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { useNostrStore } from \"src/stores/nostr\";\nimport NDK from \"@nostr-dev-kit/ndk\";\nimport { nip19 } from \"nostr-tools\";\nimport { useMintRecommendationsStore } from \"src/stores/mintRecommendations\";\nimport { useNostrUserStore } from \"src/stores/nostrUser\";\n\nexport default defineComponent({\n  name: \"MintRatingsComponent\",\n  props: {\n    url: { type: String, required: true },\n    reviews: { type: Array, required: true },\n    allowCreateReview: { type: Boolean, default: false },\n    mintInfo: { type: Object, required: false },\n  },\n  emits: [\"close\"],\n  components: {},\n  methods: {\n    // Calculate percentage for star rating bars\n    ratingPercentage(star: number): number {\n      const total = this.ratingsOnly.length;\n      if (total === 0) return 0;\n      const count = this.ratingDistribution[star] || 0;\n      return (count / total) * 100;\n    },\n    formatDate(ts: number) {\n      try {\n        return new Date(ts * 1000).toLocaleString();\n      } catch {\n        return \"\";\n      }\n    },\n    formatDateOnly(ts: number) {\n      try {\n        const date = new Date(ts * 1000);\n        return date.toLocaleDateString(undefined, {\n          year: \"numeric\",\n          month: \"short\",\n          day: \"numeric\",\n        });\n      } catch {\n        return \"\";\n      }\n    },\n    shortPubkey(pk: string) {\n      if (!pk) return \"\";\n      return `${pk.slice(0, 8)}…${pk.slice(-4)}`;\n    },\n    npub(pk: string) {\n      try {\n        return nip19.npubEncode(pk);\n      } catch {\n        return this.shortPubkey(pk);\n      }\n    },\n    shortNpub(pk: string) {\n      const v = this.npub(pk);\n      if (!v) return \"\";\n      return `${v.slice(0, 6)}…${v.slice(-6)}`;\n    },\n    copyNpub(pk: string) {\n      try {\n        const v = this.npub(pk);\n        navigator.clipboard.writeText(v);\n        // @ts-ignore\n        this.$q?.notify &&\n          this.$q.notify({\n            message: \"Copied\",\n            color: \"positive\",\n            position: \"top\",\n            timeout: 800,\n          });\n      } catch {}\n    },\n    displayName(pk: string) {\n      const p = (this as any).profiles[pk];\n      return p?.name || this.shortPubkey(pk);\n    },\n    hasProfileName(pk: string) {\n      const p = (this as any).profiles[pk];\n      return !!(p && p.name && String(p.name).trim().length > 0);\n    },\n    wotHop(pk: string): number | null {\n      try {\n        const store = useNostrUserStore();\n        return store.getHop(pk);\n      } catch {\n        return null;\n      }\n    },\n    wotColor(hop: number | null): string {\n      if (!hop) return \"\";\n      if (hop === 1) return \"light-green-5\";\n      if (hop === 2) return \"amber-5\";\n      return \"\";\n    },\n    async ensureNdk() {\n      const nostr = useNostrStore();\n      if (!nostr.connected || !nostr.ndk) nostr.initNdkReadOnly();\n      return nostr.ndk as unknown as NDK;\n    },\n    async fetchProfileFor(pk: string) {\n      // Skip if already loaded or currently loading\n      if (this.profiles[pk] || this.loadingProfiles.has(pk)) return;\n\n      this.loadingProfiles.add(pk);\n      try {\n        const ndk = await this.ensureNdk();\n        // @ts-ignore\n        const user = ndk.getUser({ pubkey: pk });\n        // @ts-ignore\n        await user.fetchProfile();\n        const name =\n          (user.profile as any)?.name ||\n          (user.profile as any)?.display_name ||\n          \"\";\n        const picture =\n          (user.profile as any)?.image || (user.profile as any)?.picture || \"\";\n        this.profiles = { ...this.profiles, [pk]: { name, picture } };\n      } catch (error) {\n        console.warn(`Failed to fetch profile for ${pk}:`, error);\n      } finally {\n        this.loadingProfiles.delete(pk);\n      }\n    },\n    loadProfilesForPagedReviews() {\n      // Load profiles only for users currently shown in pagination\n      const uniquePks = Array.from(\n        new Set((this.paged || []).map((r: any) => r.pubkey))\n      );\n      uniquePks.forEach((pk) => {\n        if (pk && !this.profiles[pk] && !this.loadingProfiles.has(pk)) {\n          this.fetchProfileFor(pk);\n        }\n      });\n    },\n    closeSortSheet() {\n      this.showSortSheet = false;\n    },\n    selectSort(value: string) {\n      this.sortMode = value as \"newest\" | \"oldest\" | \"highest\" | \"lowest\";\n      this.closeSortSheet();\n    },\n    openCreateReview() {\n      // Navigate to create review page\n      this.$router.push({\n        path: \"/createreview\",\n        query: {\n          mintUrl: this.url,\n        },\n      });\n    },\n  },\n  data() {\n    return {\n      sortMode: \"newest\" as \"newest\" | \"oldest\" | \"highest\" | \"lowest\",\n      sortOptions: [\n        { label: this.$t(\"MintRatings.sort_options.newest\"), value: \"newest\" },\n        { label: this.$t(\"MintRatings.sort_options.oldest\"), value: \"oldest\" },\n        {\n          label: this.$t(\"MintRatings.sort_options.highest\"),\n          value: \"highest\",\n        },\n        { label: this.$t(\"MintRatings.sort_options.lowest\"), value: \"lowest\" },\n      ],\n      onlyWithComment: false,\n      filterByWoT: false, // Default: show all reviews\n      rowsPerPage: 10,\n      rowsPerPageOptions: [5, 10, 20, 50].map((v) => ({\n        label: String(v),\n        value: v,\n      })),\n      page: 1,\n      profiles: {} as Record<string, { name?: string; picture?: string }>,\n      loadingProfiles: new Set<string>(),\n      showSortSheet: false,\n    };\n  },\n  computed: {\n    myPubkey(): string {\n      try {\n        const nostr = useNostrStore();\n        return nostr.pubkey || nostr.seedSignerPublicKey || \"\";\n      } catch {\n        return \"\";\n      }\n    },\n    storeReviews(): any[] {\n      try {\n        const store = useMintRecommendationsStore();\n        const list = store.urlReviews?.get?.(this.url) || [];\n        return Array.isArray(list) ? list : [];\n      } catch {\n        return [];\n      }\n    },\n    allReviews(): any[] {\n      const a = Array.isArray(this.reviews) ? this.reviews : [];\n      const b = Array.isArray(this.storeReviews) ? this.storeReviews : [];\n      // merge and de-duplicate by eventId\n      const map = new Map<string, any>();\n      [...a, ...b].forEach((r: any) => {\n        if (r && r.eventId) map.set(r.eventId, r);\n      });\n      return Array.from(map.values());\n    },\n    hasAnyReviews(): boolean {\n      return Array.isArray(this.allReviews) && this.allReviews.length > 0;\n    },\n    totalReviews(): number {\n      // Show count based on current filter state\n      return this.filtered?.length || 0;\n    },\n    ratingsOnly(): any[] {\n      // Use filtered reviews instead of all reviews\n      return (this.filtered || []).filter(\n        (r: any) => typeof r.rating === \"number\"\n      );\n    },\n    average(): number | null {\n      const rs = this.ratingsOnly;\n      if (rs.length === 0) return null;\n      const sum = rs.reduce((acc: number, r: any) => acc + (r.rating || 0), 0);\n      return sum / rs.length;\n    },\n    averageDisplay(): string {\n      return this.average !== null ? this.average.toFixed(1) : \"n/a\";\n    },\n    currentSortLabel(): string {\n      const option = this.sortOptions.find(\n        (opt) => opt.value === this.sortMode\n      );\n      return option ? option.label : \"\";\n    },\n    // Rating distribution for star bars (count of each rating 1-5)\n    ratingDistribution(): Record<number, number> {\n      const dist: Record<number, number> = { 1: 0, 2: 0, 3: 0, 4: 0, 5: 0 };\n      this.ratingsOnly.forEach((r: any) => {\n        const rating = Math.floor(r.rating || 0);\n        if (rating >= 1 && rating <= 5) {\n          dist[rating]++;\n        }\n      });\n      return dist;\n    },\n    filtered(): any[] {\n      let list = (this.allReviews || []) as any[];\n      if (this.onlyWithComment)\n        list = list.filter((r) => (r.comment || \"\").trim().length > 0);\n      // Filter by Web of Trust if enabled\n      if (this.filterByWoT) {\n        list = list.filter((r) => {\n          // Always include the user's own review\n          if (r.pubkey === this.myPubkey) return true;\n          // Include if in Web of Trust\n          const hop = this.wotHop(r.pubkey);\n          return typeof hop === \"number\";\n        });\n      }\n      return list;\n    },\n    sorted(): any[] {\n      const list = [...this.filtered];\n      // Sort based on selected sort mode only\n      list.sort((a, b) => {\n        switch (this.sortMode) {\n          case \"oldest\":\n            return (a.created_at || 0) - (b.created_at || 0);\n          case \"highest\":\n            return (b.rating || 0) - (a.rating || 0);\n          case \"lowest\":\n            return (a.rating || 0) - (b.rating || 0);\n          default: // \"newest\"\n            return (b.created_at || 0) - (a.created_at || 0);\n        }\n      });\n      // Pin our own review to the top if present in the filtered list\n      if (this.myPubkey) {\n        const idx = list.findIndex((r) => r.pubkey === this.myPubkey);\n        if (idx > 0) {\n          const [mine] = list.splice(idx, 1);\n          list.unshift(mine);\n        }\n      }\n      return list;\n    },\n    totalPages(): number {\n      return Math.max(1, Math.ceil(this.sorted.length / this.rowsPerPage));\n    },\n    paged(): any[] {\n      const start = (this.page - 1) * this.rowsPerPage;\n      return this.sorted.slice(start, start + this.rowsPerPage);\n    },\n  },\n  async mounted() {\n    try {\n      // Ensure WoT data is hydrated from IndexedDB on first load\n      const nostrUser = useNostrUserStore();\n      await nostrUser.ensureDbInitialized();\n      // If no WoT data yet, proactively load first-hop follows so review sorting has minimal context\n      if (!nostrUser.wotCount && !nostrUser.wotLoading) {\n        // Let the store decide the source based on signerType:\n        // - SEED -> defaultWoTSeedPubkey (ODELL)\n        // - otherwise -> user's pubkey\n        void nostrUser.shallowCrawlWebOfTrust();\n      }\n    } catch {}\n\n    // Load local reviews immediately to show them instantly\n    try {\n      const recs = useMintRecommendationsStore();\n      await recs.getReviewsForUrl(this.url);\n    } catch {}\n\n    // Load profiles for currently visible reviews (non-blocking)\n    this.$nextTick(() => {\n      this.loadProfilesForPagedReviews();\n    });\n\n    // Fetch latest reviews from Nostr in background (non-blocking)\n    try {\n      const recs = useMintRecommendationsStore();\n      recs.fetchReviewsForUrl(this.url);\n    } catch {}\n  },\n\n  watch: {\n    filterByWoT() {\n      // Reset to first page when filter changes\n      this.page = 1;\n    },\n    paged: {\n      handler() {\n        // Load profiles when pagination changes (new page, different reviews shown)\n        this.$nextTick(() => {\n          this.loadProfilesForPagedReviews();\n        });\n      },\n      deep: true,\n      immediate: false,\n    },\n    allReviews: {\n      handler() {\n        // Load profiles when reviews change (including from store)\n        this.$nextTick(() => {\n          this.loadProfilesForPagedReviews();\n        });\n      },\n      deep: true,\n      immediate: false,\n    },\n    url: {\n      async handler(newUrl: string) {\n        try {\n          if (newUrl) {\n            const recs = useMintRecommendationsStore();\n            // Load local reviews immediately\n            await recs.getReviewsForUrl(newUrl);\n            // Load profiles for new reviews\n            this.$nextTick(() => {\n              this.loadProfilesForPagedReviews();\n            });\n            // Fetch latest from Nostr in background\n            recs.fetchReviewsForUrl(newUrl);\n          }\n        } catch {}\n      },\n      immediate: true,\n    },\n  },\n});\n</script>\n\n<style scoped>\n/* Mint Header - Matching MintDetailsPage style */\n.mint-header-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  width: 100%;\n  margin-top: 50px;\n}\n\n.mint-header {\n  width: 100%;\n  border-radius: 12px;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  background-color: rgba(255, 255, 255, 0.05);\n}\n\n.mint-name {\n  font-size: 24px;\n  font-weight: 600;\n  text-align: center;\n  color: white;\n}\n\n.mint-url {\n  font-size: 14px;\n  font-weight: 500;\n  text-align: center;\n  word-break: break-all;\n}\n\n.section-card {\n  background-color: rgba(255, 255, 255, 0.03);\n  border-radius: 12px;\n  border: 1px solid rgba(255, 255, 255, 0.08);\n}\n\n.empty-state {\n  width: 100%;\n  background-color: rgba(0, 0, 0, 0.25);\n}\n\n.empty-title {\n  font-size: 18px;\n  font-weight: 600;\n  color: white;\n}\n\n.empty-subtitle {\n  font-size: 14px;\n  line-height: 1.5;\n  color: rgba(255, 255, 255, 0.7);\n  max-width: 320px;\n}\n\n.empty-icon {\n  color: rgba(255, 255, 255, 0.6);\n}\n\n.review-item {\n  padding: 20px 0;\n  border-bottom: 1px solid rgba(128, 128, 128, 0.1);\n}\n\n.review-item:last-child {\n  border-bottom: none;\n}\n\n.review-item:first-child {\n  padding-top: 0;\n}\n\n.own-review {\n  background: rgba(255, 255, 255, 0.02);\n  padding: 20px 16px;\n  margin: 0 -16px;\n  border-radius: 8px;\n  border-bottom: none;\n  position: relative;\n}\n\n.review-avatar {\n  flex-shrink: 0;\n}\n\n.monospace {\n  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,\n    \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n/* Sort trigger */\n.sort-trigger {\n  cursor: pointer;\n  transition: background 0.2s ease;\n  padding: 8px 0;\n}\n\n.sort-trigger:hover {\n  opacity: 0.8;\n}\n\n/* Sort bottom sheet */\n.sort-sheet-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background: rgba(0, 0, 0, 0.5);\n  backdrop-filter: blur(4px);\n  z-index: 9999;\n  display: flex;\n  align-items: flex-end;\n  animation: fadeIn 0.3s ease;\n}\n\n.sort-sheet {\n  width: 100%;\n  background: rgba(20, 20, 20, 0.98);\n  backdrop-filter: blur(20px);\n  border-top: 1px solid rgba(255, 255, 255, 0.1);\n  border-radius: 20px 20px 0 0;\n  max-height: 50vh;\n  overflow: hidden;\n  animation: slideUp 0.3s ease;\n  display: flex;\n  flex-direction: column;\n}\n\n.sort-sheet-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 20px 24px 16px 24px;\n  border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.sort-sheet-header h3 {\n  color: white;\n  font-size: 1.1rem;\n  font-weight: 600;\n  margin: 0;\n}\n\n.close-btn {\n  color: rgba(255, 255, 255, 0.7) !important;\n}\n\n.sort-options {\n  flex: 1;\n  overflow-y: auto;\n  padding-bottom: env(safe-area-inset-bottom);\n}\n\n.sort-option {\n  padding: 16px 24px;\n  color: rgba(255, 255, 255, 0.9);\n  font-size: 1rem;\n  font-weight: 500;\n  cursor: pointer;\n  transition: all 0.2s ease;\n  border-bottom: 1px solid rgba(255, 255, 255, 0.05);\n  text-align: center;\n}\n\n.sort-option:hover {\n  background: rgba(255, 255, 255, 0.08);\n  color: white;\n}\n\n.sort-option.active {\n  background: rgba(var(--q-primary-rgb), 0.2);\n  color: var(--q-primary);\n}\n\n.sort-option:last-child {\n  border-bottom: none;\n}\n\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes slideUp {\n  from {\n    transform: translateY(100%);\n  }\n  to {\n    transform: translateY(0);\n  }\n}\n\n/* Match balance badges style from MintSettings.vue */\n.currency-unit-badge {\n  border-radius: 4px;\n  background-color: #1d1d1d;\n  display: inline-block;\n  padding: 4px 8px;\n  margin: 4px 4px 4px 0;\n}\n\n.currency-unit-text {\n  color: white;\n  font-size: 14px;\n  font-weight: 500;\n}\n\n.own-review-badge {\n  position: absolute;\n  top: -2px;\n  right: 12px;\n  z-index: 1;\n}\n</style>\n"
  },
  {
    "path": "src/components/MintSettings.vue",
    "content": "<template>\n  <AddMintDialog\n    :addMintData=\"addMintData\"\n    :showAddMintDialog=\"showAddMintDialog\"\n    @update:showAddMintDialog=\"showAddMintDialog = $event\"\n    :addMintBlocking=\"addMintBlocking\"\n    @add=\"addMintInternal\"\n  />\n\n  <div style=\"max-width: 800px; margin: 0 auto\">\n    <!-- ////////////////////// SETTINGS ////////////////// -->\n    <div class=\"q-py-md q-px-xs text-left\" on-left>\n      <q-list padding>\n        <!-- <q-item-label header>Your mints</q-item-label> -->\n        <!-- MINT CARDS -->\n        <div v-for=\"mint in mints\" :key=\"mint.url\" class=\"q-px-md\">\n          <q-item\n            :active=\"mint.url == activeMintUrl\"\n            active-class=\"text-weight-bold text-primary\"\n            clickable\n            @click=\"activateMintUrlInternal(mint.url)\"\n            class=\"mint-card q-mb-md cursor-pointer\"\n            :style=\"{\n              'border-radius': '10px',\n              border:\n                mint.url == activeMintUrl\n                  ? '1px solid var(--q-primary) !important'\n                  : '1px solid rgba(128, 128, 128, 0.2) !important',\n              padding: '0px',\n              position: 'relative',\n            }\"\n            :loading=\"mint.url == activatingMintUrl\"\n          >\n            <!-- hourglass spinner if mint is being activated -->\n            <transition\n              appear\n              enter-active-class=\"animated fadeIn\"\n              leave-active-class=\"animated fadeOut\"\n              name=\"fade\"\n            >\n              <q-spinner\n                v-if=\"mint.url == activatingMintUrl\"\n                color=\"white\"\n                size=\"1.3rem\"\n                class=\"mint-loading-spinner\"\n              />\n            </transition>\n            <transition\n              appear\n              enter-active-class=\"animated fadeIn\"\n              leave-active-class=\"animated fadeOut\"\n              name=\"fade\"\n            >\n              <div\n                v-if=\"mint.url != activatingMintUrl && mint.errored\"\n                class=\"error-badge\"\n              >\n                <q-badge\n                  color=\"red\"\n                  outline\n                  class=\"q-mr-xs q-mt-sm text-weight-bold\"\n                >\n                  {{ $t(\"MintSettings.error_badge\") }}\n                  <q-icon name=\"error\" class=\"q-ml-xs\" size=\"xs\" />\n                </q-badge>\n              </div>\n            </transition>\n\n            <!-- Top right more_vert icon -->\n            <div class=\"more-vert-icon\">\n              <q-icon\n                name=\"more_vert\"\n                @click.stop=\"showMintInfo(mint)\"\n                color=\"white\"\n                class=\"cursor-pointer\"\n                size=\"1.3rem\"\n              />\n            </div>\n            <div class=\"full-width\" style=\"position: relative\">\n              <div class=\"row items-center q-pa-md\">\n                <div class=\"col\">\n                  <div class=\"row items-center\">\n                    <MintInfoContainer\n                      :iconUrl=\"getMintIconUrl(mint)\"\n                      :name=\"mint.nickname || mint.info?.name\"\n                      :url=\"mint.url\"\n                    />\n                  </div>\n                </div>\n              </div>\n\n              <div\n                class=\"row items-center justify-between q-pb-md q-pl-lg q-pr-md\"\n              >\n                <div class=\"col\">\n                  <!-- Currency units with regular text styling -->\n                  <div class=\"row q-gutter-x-sm\">\n                    <div\n                      v-for=\"unit in mintClass(mint).units\"\n                      :key=\"unit\"\n                      class=\"currency-unit-badge\"\n                    >\n                      <span class=\"currency-unit-text\">\n                        {{\n                          formatCurrency(\n                            mintClass(mint).unitBalance(unit),\n                            unit\n                          )\n                        }}\n                      </span>\n                    </div>\n                  </div>\n                </div>\n                <div\n                  class=\"text-grey-5\"\n                  v-if=\"!isMintExcludedFromReviews(mint.url)\"\n                >\n                  <template\n                    v-if=\"\n                      getRecommendation(mint.url) &&\n                      getRecommendation(mint.url).averageRating !== null\n                    \"\n                  >\n                    <span>\n                      ⭐\n                      {{ getRecommendation(mint.url).averageRating.toFixed(1) }}\n                      ·\n                      {{ getRecommendation(mint.url).reviewsCount }}\n                      <span\n                        class=\"text-primary cursor-pointer\"\n                        style=\"text-decoration: underline\"\n                        @click.stop=\"openReviews(mint.url, mint)\"\n                        >{{ $t(\"MintSettings.reviews_text\") }}</span\n                      >\n                    </span>\n                  </template>\n                  <template v-else>\n                    <span\n                      class=\"text-primary cursor-pointer\"\n                      style=\"text-decoration: underline\"\n                      @click.stop=\"openReviews(mint.url, mint)\"\n                    >\n                      {{ $t(\"MintSettings.no_reviews_yet\") }}\n                    </span>\n                  </template>\n                </div>\n              </div>\n            </div>\n          </q-item>\n        </div>\n      </q-list>\n\n      <!-- Mint discovery -->\n      <div class=\"q-px-md q-mb-md row justify-center\">\n        <q-btn\n          color=\"primary\"\n          rounded\n          @click=\"$router.push('/discoverMints')\"\n          style=\"width: 100%\"\n        >\n          <q-icon name=\"search\" size=\"20px\" class=\"q-mr-sm\" />\n          <span>{{ $t(\"MintSettings.discover_mints_button\") }}</span>\n        </q-btn>\n      </div>\n    </div>\n\n    <!-- Add mint section -->\n    <div class=\"q-pt-xs q-px-md\" ref=\"addMintDiv\">\n      <div class=\"add-mint-container\">\n        <div class=\"section-divider q-mb-md\">\n          <div class=\"divider-line\"></div>\n          <div class=\"divider-text\">\n            {{ $t(\"MintSettings.add.title\") }}\n          </div>\n          <div class=\"divider-line\"></div>\n        </div>\n\n        <div\n          class=\"add-mint-description q-mb-lg text-left\"\n          style=\"color: rgba(255, 255, 255, 0.7)\"\n        >\n          {{ $t(\"MintSettings.add.description\") }}\n        </div>\n\n        <div class=\"add-mint-inputs\">\n          <q-input\n            rounded\n            outlined\n            v-model=\"addMintData.url\"\n            placeholder=\"https://\"\n            @keydown.enter.prevent=\"sanitizeMintUrlAndShowAddDialog\"\n            ref=\"mintInput\"\n            class=\"q-mb-md mint-input url-input\"\n          />\n\n          <q-input\n            rounded\n            outlined\n            v-model=\"addMintData.nickname\"\n            :placeholder=\"$t('MintSettings.add.inputs.nickname.placeholder')\"\n            @keydown.enter.prevent=\"sanitizeMintUrlAndShowAddDialog\"\n            ref=\"mintNicknameInput\"\n            class=\"mint-input\"\n          />\n        </div>\n\n        <div class=\"add-mint-actions\">\n          <div class=\"row justify-between items-center q-mt-xs\">\n            <q-btn\n              flat\n              :disable=\"addMintData.url.length === 0\"\n              @click=\"\n                addMintData.url.length > 0\n                  ? sanitizeMintUrlAndShowAddDialog()\n                  : null\n              \"\n              class=\"text-white\"\n              :class=\"{ 'text-grey-7': addMintData.url.length === 0 }\"\n            >\n              <q-icon name=\"add\" size=\"20px\" class=\"q-mr-sm\" />\n              <span>{{ $t(\"MintSettings.add.actions.add_mint.label\") }}</span>\n            </q-btn>\n\n            <q-btn flat @click=\"showCamera\" class=\"text-white\">\n              <q-icon name=\"qr_code\" size=\"20px\" class=\"q-mr-sm\" />\n              <span>{{ $t(\"MintSettings.add.actions.scan.label\") }}</span>\n            </q-btn>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- Swap section -->\n\n    <div class=\"section-divider q-mb-md q-px-md\">\n      <div class=\"divider-line\"></div>\n      <div class=\"divider-text\">\n        {{ $t(\"MintSettings.swap.title\") }}\n      </div>\n      <div class=\"divider-line\"></div>\n    </div>\n    <div class=\"q-px-xs text-left\" on-left>\n      <q-list padding>\n        <q-item>\n          <q-item-section>\n            <q-item-label overline>\n              {{ $t(\"MintSettings.swap.overline\") }}</q-item-label\n            >\n            <q-item-label caption>\n              {{ $t(\"MintSettings.swap.caption\") }}\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n        <q-item class=\"q-pt-sm\">\n          <q-select\n            clearable\n            rounded\n            outlined\n            dense\n            color=\"primary\"\n            v-model=\"swapData.fromUrl\"\n            :options=\"swapAmountDataOptions()\"\n            option-value=\"url\"\n            option-label=\"optionLabel\"\n            :label=\"$t('MintSettings.swap.inputs.from.label')\"\n            style=\"\n              min-width: 200px;\n              width: 100%;\n              font-family: monospace;\n              font-size: 0.9em;\n            \"\n            :disable=\"swapBlocking\"\n          />\n        </q-item>\n        <q-item>\n          <q-select\n            clearable\n            rounded\n            outlined\n            dense\n            color=\"primary\"\n            v-model=\"swapData.toUrl\"\n            :options=\"swapAmountDataOptions()\"\n            option-value=\"url\"\n            option-label=\"optionLabel\"\n            :label=\"$t('MintSettings.swap.inputs.to.label')\"\n            style=\"\n              min-width: 200px;\n              width: 100%;\n              font-family: monospace;\n              font-size: 0.9em;\n            \"\n            :disable=\"swapBlocking\"\n          />\n        </q-item>\n        <q-item>\n          <q-input\n            rounded\n            outlined\n            dense\n            v-model.number=\"swapData.amount\"\n            type=\"number\"\n            :label=\"\n              $t('MintSettings.swap.inputs.amount.label', {\n                ticker: tickerShort,\n              })\n            \"\n            style=\"min-width: 200px\"\n            @keydown.enter.prevent=\"extractAndMintAmountSwap(swapAmountData)\"\n            :disable=\"\n              !swapData.fromUrl ||\n              !swapData.toUrl ||\n              swapData.fromUrl == swapData.toUrl ||\n              swapBlocking\n            \"\n          ></q-input>\n          <q-btn\n            class=\"q-ml-sm q-px-md\"\n            color=\"primary\"\n            rounded\n            @click=\"extractAndMintAmountSwap(swapAmountData)\"\n            :disable=\"\n              !swapData.fromUrl ||\n              !swapData.toUrl ||\n              !(swapData.amount > 0) ||\n              swapData.fromUrl == swapData.toUrl\n            \"\n            :loading=\"swapBlocking\"\n          >\n            <q-icon size=\"xs\" name=\"swap_horiz\" class=\"q-pr-xs\" />\n            {{ $t(\"MintSettings.swap.actions.swap.label\") }}\n            <template v-slot:loading>\n              <q-spinner size=\"xs\" />\n              {{ $t(\"MintSettings.swap.actions.swap.in_progress\") }}\n            </template>\n          </q-btn>\n        </q-item>\n      </q-list>\n    </div>\n  </div>\n</template>\n<script lang=\"ts\">\nimport { ref, defineComponent, onMounted, onBeforeUnmount } from \"vue\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport { useMintsStore, MintClass } from \"src/stores/mints\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useCameraStore } from \"src/stores/camera\";\nimport { map } from \"underscore\";\nimport { currentDateStr } from \"src/js/utils\";\nimport { useSettingsStore } from \"src/stores/settings\";\nimport { useNostrStore } from \"src/stores/nostr\";\nimport { useP2PKStore } from \"src/stores/p2pk\";\nimport { useWorkersStore } from \"src/stores/workers\";\nimport { useSwapStore } from \"src/stores/swap\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { notifyError, notifyWarning } from \"src/js/notify\";\nimport { EventBus } from \"../js/eventBus\";\nimport AddMintDialog from \"src/components/AddMintDialog.vue\";\nimport { useMintRecommendationsStore } from \"src/stores/mintRecommendations\";\nimport MintInfoContainer from \"./MintInfoContainer.vue\";\n\n// Mints that should not show reviews\nconst EXCLUDED_FROM_REVIEWS = [\n  \"http://localhost*\", // localhost with any port\n  \"https://*cashu.space\", // exact match and subdomains\n];\n\nexport default defineComponent({\n  name: \"MintSettings\",\n  mixins: [windowMixin],\n  components: {\n    AddMintDialog,\n    MintInfoContainer,\n  },\n  props: {},\n  setup() {\n    const addMintDiv = ref(null);\n\n    const scrollToAddMintDiv = () => {\n      if (addMintDiv.value) {\n        // addMintDiv.value.scrollIntoView({ behavior: \"smooth\" });\n        // const top = addMintDiv.value.offsetTop;\n        const rect = addMintDiv.value.getBoundingClientRect();\n        window.scrollTo({\n          top: window.scrollY + rect.top,\n          behavior: \"smooth\",\n        });\n      }\n    };\n\n    onMounted(() => {\n      EventBus.on(\"scrollToAddMintDiv\", scrollToAddMintDiv);\n    });\n\n    onBeforeUnmount(() => {\n      EventBus.off(\"scrollToAddMintDiv\", scrollToAddMintDiv);\n    });\n    return {\n      addMintDiv,\n    };\n  },\n  data: function () {\n    return {\n      discoveringMints: false,\n      addingMint: false,\n      swapData: {\n        fromUrl: {\n          url: \"\",\n          optionLabel: \"\",\n        },\n        toUrl: {\n          url: \"\",\n          optionLabel: \"\",\n        },\n        amount: undefined,\n      },\n      activatingMintUrl: \"\",\n      mintInfoCache: new Map(),\n      fetchingMintInfo: new Set(),\n    };\n  },\n  computed: {\n    ...mapWritableState(useSettingsStore, [\"getBitcoinPrice\"]),\n    ...mapState(useP2PKStore, [\"p2pkKeys\"]),\n    ...mapState(useMintsStore, [\n      \"activeMintUrl\",\n      \"activeUnit\",\n      \"mints\",\n      \"activeProofs\",\n      \"addMintBlocking\",\n    ]),\n    ...mapState(useNostrStore, [\"pubkey\"]),\n    ...mapState(useMintRecommendationsStore, [\"recommendations\"]),\n    ...mapState(useWorkersStore, [\"invoiceWorkerRunning\"]),\n    ...mapWritableState(useMintsStore, [\"addMintData\", \"showAddMintDialog\"]),\n    ...mapState(useUiStore, [\"tickerShort\"]),\n    ...mapState(useSwapStore, [\"swapAmountData\"]),\n    ...mapWritableState(useSwapStore, [\"swapBlocking\"]),\n    discoverList() {\n      return this.recommendations.filter((r) => !this.isExistingMint(r.url));\n    },\n    getRecommendation() {\n      return (url) => this.recommendations.find((r) => r.url === url);\n    },\n  },\n  watch: {\n    // if swapBlocking is true and invoiceWorkerRunning changes to false, then swapBlocking should be set to false\n    invoiceWorkerRunning: function (val) {\n      if (this.swapBlocking && !val) {\n        this.swapBlocking = false;\n      }\n    },\n  },\n  methods: {\n    ...mapActions(useNostrStore, [\n      \"init\",\n      \"initNdkReadOnly\",\n      \"getUserPubkey\",\n      \"fetchEventsFromUser\",\n    ]),\n    ...mapActions(useMintRecommendationsStore, [\n      \"discover\",\n      \"startSubscriptions\",\n    ]),\n    ...mapActions(useP2PKStore, [\"generateKeypair\", \"showKeyDetails\"]),\n    ...mapActions(useMintsStore, [\n      \"addMint\",\n      \"removeMint\",\n      \"activateMintUrl\",\n      \"updateMint\",\n      \"triggerMintInfoMotdChanged\",\n      \"fetchMintInfo\",\n    ]),\n    ...mapActions(useWalletStore, [\"decodeRequest\", \"mintOnPaid\"]),\n    ...mapActions(useWorkersStore, [\"clearAllWorkers\"]),\n    ...mapActions(useCameraStore, [\"closeCamera\", \"showCamera\"]),\n    ...mapActions(useSwapStore, [\"mintAmountSwap\"]),\n    isMintExcludedFromReviews: function (mintUrl) {\n      // Check if mint URL should be excluded from reviews using regex matching\n      return EXCLUDED_FROM_REVIEWS.some((pattern) => {\n        // Convert asterisk pattern to regex\n        const regexPattern = pattern.replace(/\\*/g, \".*\");\n        const regex = new RegExp(`^${regexPattern}$`);\n        return regex.test(mintUrl);\n      });\n    },\n    activateMintUrlInternal: async function (mintUrl) {\n      this.activatingMintUrl = mintUrl;\n      console.log(`Activating mint ${this.activatingMintUrl}`);\n      try {\n        await this.activateMintUrl(mintUrl, false, true);\n      } catch (e) {\n        console.log(\"#### Error activating mint:\", e);\n      } finally {\n        this.activatingMintUrl = \"\";\n      }\n    },\n    validateMintUrl: function (url) {\n      try {\n        new URL(url);\n        return true;\n      } catch (e) {\n        return false;\n      }\n    },\n    sanitizeMintUrlAndShowAddDialog: function () {\n      // if no protocol is given, add https\n      if (!this.addMintData.url.match(/^[a-zA-Z]+:\\/\\//)) {\n        this.addMintData.url = \"https://\" + this.addMintData.url;\n      }\n      if (!this.validateMintUrl(this.addMintData.url)) {\n        notifyError(\n          this.$i18n.t(\"MintSettings.add.actions.add_mint.error_invalid_url\")\n        );\n        return;\n      }\n      const urlObj = new URL(this.addMintData.url);\n      urlObj.hostname = urlObj.hostname.toLowerCase();\n      this.addMintData.url = urlObj.toString();\n      this.addMintData.url = this.addMintData.url.replace(/\\/$/, \"\");\n      this.showAddMintDialog = true;\n    },\n    addMintInternal: function (mintToAdd, verbose) {\n      this.addingMint = true;\n      try {\n        this.addMint(mintToAdd, verbose);\n        this.addMintData = { url: \"\", nickname: \"\" };\n      } finally {\n        this.addingMint = false;\n      }\n    },\n    mintClass(mint) {\n      return new MintClass(mint);\n    },\n    swapAmountDataOptions: function () {\n      const options = [];\n      for (const [i, m] of Object.entries(this.mints)) {\n        const unitStr = \"sat\";\n        const unitBalance = this.mintClass(m).unitBalance(unitStr);\n        const balanceStr = useUiStore().formatCurrency(unitBalance, unitStr);\n        options.push({\n          url: m.url,\n          optionLabel:\n            (m.nickname || getShortUrl(m.url)) + \" (\" + balanceStr + \")\",\n        });\n      }\n      return options;\n    },\n    clearSwapData: function () {\n      this.swapData.fromUrl = \"\";\n      this.swapData.toUrl = \"\";\n      this.swapData.amount = undefined;\n    },\n    extractAndMintAmountSwap: async function (swapAmountData) {\n      swapAmountData.fromUrl = this.swapData.fromUrl.url;\n      swapAmountData.toUrl = this.swapData.toUrl.url;\n      swapAmountData.amount = this.swapData.amount;\n      await this.mintAmountSwap(swapAmountData);\n      this.clearSwapData();\n    },\n    fetchMintsFromNdk: async function () {\n      this.discoveringMints = true;\n      try {\n        const recs = await this.discover();\n        if (!recs || recs.length === 0) {\n          this.notifyError(\n            this.$i18n.t(\n              \"MintSettings.discover.actions.discover.error_no_mints\"\n            )\n          );\n        } else {\n          this.notifySuccess(\n            this.$i18n.t(\"MintSettings.discover.actions.discover.success\", {\n              length: recs.length,\n            })\n          );\n        }\n        this.startSubscriptions();\n      } finally {\n        this.discoveringMints = false;\n      }\n    },\n    showMintInfo: async function (mint) {\n      // Navigate to mint details page with mint URL as query parameter\n      this.$router.push({\n        path: \"/mintdetails\",\n        query: { mintUrl: mint.url },\n      });\n    },\n    getMintIconUrl: function (mint) {\n      if (mint.info) {\n        if (mint.info.icon_url) {\n          return mint.info.icon_url;\n        } else {\n          return undefined;\n        }\n      } else {\n        return undefined;\n      }\n    },\n    // Discovery helpers\n    isExistingMint(url) {\n      return this.mints.some((m) => m.url === url);\n    },\n    async addDiscoveredMint(url) {\n      try {\n        await this.addMint({ url }, true);\n      } catch (e) {\n        console.error(e);\n      }\n    },\n    openReviews(url, mint) {\n      // Navigate to ratings page\n      this.$router.push({\n        path: \"/mintratings\",\n        query: {\n          mintUrl: url,\n          allowCreateReview: \"true\",\n        },\n      });\n    },\n    getMintInfoFromCache(url) {\n      return this.mintInfoCache.get(url);\n    },\n    getMintIconUrlUrl(url) {\n      const info = this.getMintInfoFromCache(url);\n      return info && info.icon_url ? info.icon_url : null;\n    },\n    getMintDisplayName(url) {\n      const info = this.getMintInfoFromCache(url);\n      return info && info.name ? info.name : url.replace(/^https?:\\/\\//, \"\");\n    },\n    isFetchingMintInfo(url) {\n      return this.fetchingMintInfo.has(url);\n    },\n    async fetchMintInfoForUrl(url) {\n      try {\n        const tempMint = { url, keys: [], keysets: [] };\n        const info = await new MintClass(tempMint).api.getInfo();\n        this.mintInfoCache.set(url, info);\n      } catch (e) {\n        this.mintInfoCache.set(url, null);\n      } finally {\n        this.fetchingMintInfo.delete(url);\n      }\n    },\n    fetchMintInfoForDiscoverList() {\n      this.discoverList.forEach((rec) => {\n        if (\n          !this.mintInfoCache.has(rec.url) &&\n          !this.fetchingMintInfo.has(rec.url)\n        ) {\n          this.fetchingMintInfo.add(rec.url);\n          this.fetchMintInfoForUrl(rec.url);\n        }\n      });\n    },\n  },\n  created: function () {},\n});\n</script>\n\n<style>\n@import \"src/css/mintlist.css\";\n\n/* Add Mint Section Styles */\n.add-mint-container {\n  width: 100%;\n  margin: 0 auto;\n  display: flex;\n  flex-direction: column;\n  margin-bottom: 32px;\n}\n\n.add-mint-description {\n  font-size: 14px;\n  line-height: 24px;\n  font-weight: 500;\n  text-align: left;\n  margin-bottom: 24px;\n}\n\n.add-mint-inputs {\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n}\n\n.mint-input {\n  width: 100%;\n  font-family: \"Inter\", sans-serif;\n}\n\n.mint-input .q-field__control {\n  height: 54px;\n  border-radius: 100px;\n}\n\n.mint-input .q-field__native,\n.mint-input .q-field__input,\n.mint-input .q-placeholder {\n  font-family: \"Inter\", sans-serif;\n}\n\n.add-mint-actions {\n  width: 100%;\n  margin-top: 16px;\n}\n\n/* Section Divider */\n.section-divider {\n  display: flex;\n  align-items: center;\n  width: 100%;\n  margin-bottom: 24px;\n}\n\n.divider-line {\n  flex: 1;\n  height: 1px;\n  background-color: #48484a;\n}\n\n.divider-text {\n  padding: 0 10px;\n  font-size: 14px;\n  font-weight: 600;\n  color: #ffffff;\n  text-transform: uppercase;\n}\n\n/* More vert icon positioning */\n.more-vert-icon {\n  position: absolute;\n  top: 12px;\n  right: 12px;\n  z-index: 10;\n}\n</style>\n"
  },
  {
    "path": "src/components/MultinutPaymentDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showMultinutPaymentDialog\"\n    position=\"top\"\n    :maximized=\"true\"\n    transition-show=\"fade\"\n    transition-hide=\"fade\"\n    full-width\n    full-height\n    seamless\n    no-backdrop-dismiss\n    @keyup.esc=\"closeMultinutDialog\"\n  >\n    <div class=\"fullscreen bg-dark\">\n      <div class=\"multinut-content-container q-pa-md\">\n        <!-- Top Icons -->\n        <div class=\"top-icons q-pt-md q-mb-lg\">\n          <q-icon\n            name=\"close\"\n            size=\"24px\"\n            class=\"close-icon cursor-pointer text-white\"\n            @click=\"closeMultinutDialog\"\n          />\n        </div>\n\n        <!-- Header Section -->\n        <div class=\"multinut-header-container q-mb-lg\">\n          <div class=\"multinut-header q-pa-md q-py-lg\">\n            <div class=\"multinut-title q-mb-xs\">Multinut Payment</div>\n            <div class=\"multinut-amount\">\n              {{\n                formatCurrency(\n                  payInvoiceData.meltQuote.response.amount,\n                  activeUnit\n                )\n              }}\n            </div>\n            <div\n              v-if=\"!isPaymentInProgress\"\n              class=\"multinut-description q-mt-sm\"\n            >\n              Pay using multiple mints\n            </div>\n          </div>\n\n          <!-- Experimental Warning -->\n          <transition\n            appear\n            enter-active-class=\"animated pulse\"\n            name=\"smooth-slide\"\n          >\n            <div\n              v-if=\"\n                !multinutExperimentalWarningDismissed && !isPaymentInProgress\n              \"\n              class=\"experimental-warning q-mt-lg\"\n            >\n              <div class=\"warning-content q-pa-md\">\n                <div class=\"warning-title q-mb-xs\">Experimental Feature</div>\n                <div class=\"warning-text q-mb-md\">\n                  This feature is highly experimental and may not work as\n                  expected. Make sure you don't try to pay an invoice that has\n                  already been paid before. You could lose funds.\n                </div>\n                <q-btn\n                  flat\n                  color=\"warning\"\n                  @click=\"dismissExperimentalWarning\"\n                  class=\"warning-dismiss-btn\"\n                >\n                  I understand\n                </q-btn>\n              </div>\n            </div>\n          </transition>\n        </div>\n\n        <!-- Mint Selection Section -->\n        <div class=\"mint-selection-section q-mb-lg\">\n          <div\n            class=\"mint-selection-description q-mb-md\"\n            v-if=\"multinutExperimentalWarningDismissed && !isPaymentInProgress\"\n          >\n            Select funds from multiple mints to execute the payment.\n          </div>\n\n          <div class=\"mints-container\">\n            <div\n              v-for=\"mint in multiMints\"\n              :key=\"mint.url\"\n              v-show=\"!isPaymentInProgress || isSelected(mint)\"\n              class=\"mint-item q-mb-md\"\n            >\n              <div\n                class=\"mint-card cursor-pointer\"\n                @click=\"\n                  !isPaymentInProgress && !isSelected(mint) && toggleMint(mint)\n                \"\n                :class=\"{\n                  'mint-card-selected': isSelected(mint),\n                  'cursor-not-allowed': isPaymentInProgress,\n                  'mint-card-success':\n                    isPaymentInProgress &&\n                    isSelected(mint) &&\n                    mintStates[mint.url] === 'success',\n                  'mint-card-error':\n                    isPaymentInProgress &&\n                    isSelected(mint) &&\n                    mintStates[mint.url] === 'error',\n                }\"\n              >\n                <div class=\"mint-card-content q-pa-md\">\n                  <div class=\"row items-center\">\n                    <div class=\"col-auto q-mr-md\">\n                      <q-checkbox\n                        v-if=\"!isPaymentInProgress\"\n                        :model-value=\"isSelected(mint)\"\n                        @update:model-value=\"toggleMint(mint)\"\n                        :color=\"isSelected(mint) ? 'primary' : 'grey'\"\n                        class=\"cursor-pointer\"\n                      />\n                    </div>\n\n                    <div class=\"col\">\n                      <div class=\"row items-center\">\n                        <q-avatar\n                          v-if=\"getMintIconUrl(mint)\"\n                          size=\"34px\"\n                          class=\"q-mr-sm\"\n                        >\n                          <q-img\n                            spinner-color=\"white\"\n                            spinner-size=\"xs\"\n                            :src=\"getMintIconUrl(mint)\"\n                            alt=\"Mint Icon\"\n                            style=\"\n                              height: 34px;\n                              max-width: 34px;\n                              font-size: 12px;\n                            \"\n                          />\n                        </q-avatar>\n\n                        <div class=\"mint-info-container\">\n                          <div\n                            v-if=\"mint.nickname || mint.info?.name\"\n                            class=\"mint-name\"\n                          >\n                            {{ mint.nickname || mint.info?.name }}\n                          </div>\n                          <div class=\"text-grey-6 mint-url\">\n                            {{ getShortUrl(mint.url) }}\n                          </div>\n                        </div>\n                      </div>\n                    </div>\n                    <!-- Show state indicator during payment, checkbox otherwise -->\n                    <div\n                      v-if=\"isPaymentInProgress && isSelected(mint)\"\n                      class=\"col-auto state-indicator\"\n                    >\n                      <q-spinner\n                        v-if=\"\n                          mintStates[mint.url] === 'requesting' ||\n                          mintStates[mint.url] === 'paying'\n                        \"\n                        color=\"primary\"\n                        size=\"24px\"\n                      />\n                      <q-icon\n                        v-else-if=\"mintStates[mint.url] === 'success'\"\n                        name=\"check_circle\"\n                        color=\"positive\"\n                        size=\"24px\"\n                      />\n                      <q-icon\n                        v-else-if=\"mintStates[mint.url] === 'error'\"\n                        name=\"error\"\n                        color=\"negative\"\n                        size=\"24px\"\n                      />\n                    </div>\n                  </div>\n\n                  <!-- Payment State Progress Bar - Full Width -->\n                  <div\n                    v-if=\"\n                      isPaymentInProgress &&\n                      isSelected(mint) &&\n                      mintStates[mint.url]\n                    \"\n                    class=\"payment-progress-section q-mt-md q-px-md\"\n                  >\n                    <q-linear-progress\n                      :value=\"getStateProgress(mintStates[mint.url])\"\n                      :color=\"getStateColor(mintStates[mint.url])\"\n                      size=\"4px\"\n                      rounded\n                      class=\"payment-progress\"\n                    />\n                    <div class=\"payment-state-text q-mt-xs\">\n                      {{ getStateText(mintStates[mint.url]) }}\n                    </div>\n                  </div>\n\n                  <!-- Balance Display for Unselected Mints -->\n                  <div\n                    v-if=\"!isSelected(mint)\"\n                    class=\"mint-balance-section q-mt-md\"\n                  >\n                    <div class=\"currency-unit-badge\">\n                      <span class=\"currency-unit-text\">\n                        {{\n                          formatCurrency(\n                            mintClass(mint).unitBalance(this.activeUnit),\n                            this.activeUnit\n                          )\n                        }}\n                      </span>\n                    </div>\n                  </div>\n\n                  <!-- Payment Distribution Slider for Selected Mints -->\n                  <div\n                    v-if=\"isSelected(mint) && !isPaymentInProgress\"\n                    class=\"mint-slider-section q-mt-md\"\n                  >\n                    <div class=\"row items-center q-gutter-sm no-wrap\">\n                      <!-- Allocation Badge -->\n                      <div class=\"col-auto\">\n                        <div class=\"allocation-badge\">\n                          {{\n                            formatCurrency(getPartialAmount(mint), activeUnit)\n                          }}\n                        </div>\n                      </div>\n\n                      <!-- Slider -->\n                      <div class=\"col slider-container\">\n                        <q-slider\n                          :model-value=\"getMintProportion(mint)\"\n                          @update:model-value=\"\n                            (value) => updateMintProportion(mint, value)\n                          \"\n                          :min=\"0\"\n                          :max=\"getMaxPercentageForMint(mint)\"\n                          :step=\"1\"\n                          color=\"primary\"\n                          :disable=\"selectedMints.length <= 1\"\n                          class=\"mint-slider q-px-md\"\n                        />\n                      </div>\n\n                      <!-- Total Balance Badge -->\n                      <div class=\"col-auto\">\n                        <div class=\"total-balance-badge\">\n                          {{\n                            formatCurrency(\n                              mintClass(mint).unitBalance(this.activeUnit),\n                              activeUnit\n                            )\n                          }}\n                        </div>\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <!-- Action Buttons Section -->\n        <div class=\"action-buttons-section\">\n          <q-btn\n            unelevated\n            rounded\n            color=\"primary\"\n            :disabled=\"!canExecutePayment\"\n            @click=\"executeMultinutPayment\"\n            :loading=\"multiMeltButtonLoading\"\n            class=\"pay-button-fixed\"\n            size=\"lg\"\n          >\n            <template v-slot:loading>\n              <q-spinner />\n            </template>\n            Pay Invoice\n          </q-btn>\n        </div>\n      </div>\n    </div>\n  </q-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport { useMintsStore, MintClass } from \"src/stores/mints\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { notifyError, notifySuccess } from \"src/js/notify\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\n\nexport default defineComponent({\n  name: \"MultinutPaymentDialog\",\n  mixins: [windowMixin],\n  components: {},\n  data() {\n    return {\n      multiMeltButtonLoading: false,\n      selectedMints: [],\n      showMultinutPaymentDialog: false,\n      // State tracking for each mint during payment\n      mintStates: {}, // { mintUrl: 'requesting' | 'paying' | 'success' | 'error' }\n      isPaymentInProgress: false,\n      // Custom proportions for each mint (percentage 0-100)\n      mintProportions: {}, // { mintUrl: percentage }\n    };\n  },\n  computed: {\n    ...mapWritableState(useWalletStore, [\"payInvoiceData\"]),\n    ...mapWritableState(useUiStore, [\"multinutExperimentalWarningDismissed\"]),\n    ...mapState(useMintsStore, [\"mints\", \"activeUnit\", \"multiMints\"]),\n    totalSelectedBalance() {\n      return this.selectedMints.reduce((total, mint) => {\n        const mintInstance = this.mintClass(mint);\n        return total + mintInstance.unitBalance(this.activeUnit);\n      }, 0);\n    },\n    canExecutePayment() {\n      return (\n        this.selectedMints.length > 0 &&\n        this.totalSelectedBalance >=\n          this.payInvoiceData.meltQuote.response.amount &&\n        this.totalDistributedAmount ===\n          this.payInvoiceData.meltQuote.response.amount &&\n        !this.multiMeltButtonLoading\n      );\n    },\n    totalDistributedAmount() {\n      if (this.selectedMints.length === 0) return 0;\n      const partialAmounts = this.getPartialAmounts();\n      return Object.values(partialAmounts).reduce(\n        (sum, amount) => sum + amount,\n        0\n      );\n    },\n  },\n  methods: {\n    ...mapActions(useWalletStore, [\"meltQuote\", \"melt\"]),\n    ...mapActions(useMintsStore, [\n      \"activateMintUrl\",\n      \"updateMintMultinutSelection\",\n    ]),\n    mintClass(mint) {\n      return new MintClass(mint);\n    },\n    getShortUrl(url) {\n      return getShortUrl(url);\n    },\n    isSelected(mint) {\n      return this.selectedMints.some((selected) => selected.url === mint.url);\n    },\n    toggleMint(mint) {\n      const index = this.selectedMints.findIndex(\n        (selected) => selected.url === mint.url\n      );\n      if (index >= 0) {\n        this.selectedMints.splice(index, 1);\n        this.updateMintMultinutSelection(mint.url, false);\n        // Remove from proportions\n        delete this.mintProportions[mint.url];\n      } else {\n        this.selectedMints.push(mint);\n        this.updateMintMultinutSelection(mint.url, true);\n      }\n      // Recalculate proportions when selection changes\n      this.initializeMintProportions();\n    },\n    initializeMintProportions() {\n      if (this.selectedMints.length === 0) {\n        this.mintProportions = {};\n        return;\n      }\n\n      const totalAmount = this.payInvoiceData.meltQuote.response.amount;\n\n      // Calculate default proportions based on balance weights, but respect capacity constraints\n      const { weights } = this.multiMintBalance(\n        this.selectedMints,\n        this.activeUnit\n      );\n      const newProportions = {};\n\n      // First pass: calculate ideal proportions\n      this.selectedMints.forEach((mint, index) => {\n        const idealPercentage = weights[index] * 100;\n        const maxPercentage = this.getMaxPercentageForMint(mint);\n        newProportions[mint.url] = Math.min(idealPercentage, maxPercentage);\n      });\n\n      // Check if we need to redistribute due to capacity constraints\n      const totalAllocated = Object.values(newProportions).reduce(\n        (sum, percentage) => sum + percentage,\n        0\n      );\n\n      if (totalAllocated < 100) {\n        // We have remaining capacity to distribute\n        const remaining = 100 - totalAllocated;\n        const mintsWithCapacity = this.selectedMints.filter((mint) => {\n          const maxPercentage = this.getMaxPercentageForMint(mint);\n          return newProportions[mint.url] < maxPercentage;\n        });\n\n        if (mintsWithCapacity.length > 0) {\n          // Distribute remaining proportionally among mints with available capacity\n          const totalAvailableCapacity = mintsWithCapacity.reduce(\n            (sum, mint) => {\n              const maxPercentage = this.getMaxPercentageForMint(mint);\n              return sum + (maxPercentage - newProportions[mint.url]);\n            },\n            0\n          );\n\n          if (totalAvailableCapacity > 0) {\n            mintsWithCapacity.forEach((mint) => {\n              const maxPercentage = this.getMaxPercentageForMint(mint);\n              const availableCapacity =\n                maxPercentage - newProportions[mint.url];\n              const proportion = availableCapacity / totalAvailableCapacity;\n              const additional = Math.min(\n                availableCapacity,\n                remaining * proportion\n              );\n              newProportions[mint.url] += additional;\n            });\n          }\n        }\n      }\n\n      this.mintProportions = { ...newProportions };\n    },\n    getMintProportion(mint) {\n      return this.mintProportions[mint.url] || 0;\n    },\n    getMaxPercentageForMint(mint) {\n      const totalAmount = this.payInvoiceData.meltQuote.response.amount;\n      const mintBalance = this.mintClass(mint).unitBalance(this.activeUnit);\n      return Math.min(100, (mintBalance / totalAmount) * 100);\n    },\n    getMintIconUrl(mint) {\n      if (mint.info) {\n        if (mint.info.icon_url) {\n          return mint.info.icon_url;\n        } else {\n          return undefined;\n        }\n      } else {\n        return undefined;\n      }\n    },\n    updateMintProportion(mint, newPercentage) {\n      if (this.selectedMints.length <= 1) return;\n\n      const totalAmount = this.payInvoiceData.meltQuote.response.amount;\n      const mintBalance = this.mintClass(mint).unitBalance(this.activeUnit);\n\n      // Calculate the maximum percentage this mint can handle based on its balance\n      const maxPercentageForMint = Math.min(\n        100,\n        (mintBalance / totalAmount) * 100\n      );\n\n      // Constrain the new percentage to not exceed the mint's capacity\n      const constrainedPercentage = Math.min(\n        newPercentage,\n        maxPercentageForMint\n      );\n\n      const oldPercentage = this.mintProportions[mint.url] || 0;\n      const difference = constrainedPercentage - oldPercentage;\n\n      // Update the changed mint\n      this.mintProportions = {\n        ...this.mintProportions,\n        [mint.url]: constrainedPercentage,\n      };\n\n      // Redistribute the difference among other selected mints\n      const otherMints = this.selectedMints.filter((m) => m.url !== mint.url);\n      const totalOtherPercentage = otherMints.reduce(\n        (sum, m) => sum + (this.mintProportions[m.url] || 0),\n        0\n      );\n\n      if (totalOtherPercentage > 0 && otherMints.length > 0) {\n        // Proportionally adjust other mints, respecting their balance constraints\n        let remainingDifference = difference;\n\n        // Sort other mints by their available capacity (descending)\n        const mintsWithCapacity = otherMints\n          .map((otherMint) => {\n            const balance = this.mintClass(otherMint).unitBalance(\n              this.activeUnit\n            );\n            const maxPercentage = Math.min(100, (balance / totalAmount) * 100);\n            const currentPercentage = this.mintProportions[otherMint.url] || 0;\n            const availableCapacity = maxPercentage - currentPercentage;\n            return {\n              mint: otherMint,\n              currentPercentage,\n              maxPercentage,\n              availableCapacity,\n              balance,\n            };\n          })\n          .sort((a, b) => b.availableCapacity - a.availableCapacity);\n\n        // Distribute the difference, respecting capacity constraints\n        for (const mintInfo of mintsWithCapacity) {\n          if (Math.abs(remainingDifference) < 0.01) break; // Stop if difference is negligible\n\n          const weight = mintInfo.currentPercentage / totalOtherPercentage;\n          let adjustment = difference * weight;\n\n          // If reducing (difference is positive), we need to reduce others\n          if (difference > 0) {\n            // Can't reduce below 0\n            adjustment = Math.min(adjustment, mintInfo.currentPercentage);\n          } else {\n            // If increasing others (difference is negative), respect capacity\n            adjustment = Math.max(adjustment, -mintInfo.availableCapacity);\n          }\n\n          const newValue = Math.max(\n            0,\n            Math.min(\n              mintInfo.maxPercentage,\n              mintInfo.currentPercentage - adjustment\n            )\n          );\n\n          this.mintProportions = {\n            ...this.mintProportions,\n            [mintInfo.mint.url]: newValue,\n          };\n\n          remainingDifference -= mintInfo.currentPercentage - newValue;\n        }\n      } else if (otherMints.length > 0) {\n        // If other mints have 0%, distribute evenly respecting capacity\n        const remainingPercentage = 100 - constrainedPercentage;\n        const mintsWithCapacity = otherMints.map((otherMint) => {\n          const balance = this.mintClass(otherMint).unitBalance(\n            this.activeUnit\n          );\n          const maxPercentage = Math.min(100, (balance / totalAmount) * 100);\n          return { mint: otherMint, maxPercentage, balance };\n        });\n\n        const totalMaxCapacity = mintsWithCapacity.reduce(\n          (sum, m) => sum + m.maxPercentage,\n          0\n        );\n\n        if (totalMaxCapacity > 0) {\n          mintsWithCapacity.forEach((mintInfo) => {\n            const proportion = mintInfo.maxPercentage / totalMaxCapacity;\n            const allocatedPercentage = Math.min(\n              mintInfo.maxPercentage,\n              remainingPercentage * proportion\n            );\n\n            this.mintProportions = {\n              ...this.mintProportions,\n              [mintInfo.mint.url]: allocatedPercentage,\n            };\n          });\n        }\n      }\n\n      // Ensure total is exactly 100% (with capacity constraints)\n      this.normalizeMintProportions();\n    },\n    normalizeMintProportions() {\n      const totalAmount = this.payInvoiceData.meltQuote.response.amount;\n      const total = this.selectedMints.reduce(\n        (sum, mint) => sum + (this.mintProportions[mint.url] || 0),\n        0\n      );\n\n      if (total > 0 && Math.abs(total - 100) > 0.01) {\n        // Check if we can scale proportionally while respecting constraints\n        const factor = 100 / total;\n        const normalizedProportions = {};\n        let canScale = true;\n\n        // First check if scaling would violate any constraints\n        for (const mint of this.selectedMints) {\n          const currentPercentage = this.mintProportions[mint.url] || 0;\n          const scaledPercentage = currentPercentage * factor;\n          const mintBalance = this.mintClass(mint).unitBalance(this.activeUnit);\n          const maxPercentageForMint = Math.min(\n            100,\n            (mintBalance / totalAmount) * 100\n          );\n\n          if (scaledPercentage > maxPercentageForMint) {\n            canScale = false;\n            break;\n          }\n        }\n\n        if (canScale) {\n          // Safe to scale proportionally\n          this.selectedMints.forEach((mint) => {\n            normalizedProportions[mint.url] =\n              (this.mintProportions[mint.url] || 0) * factor;\n          });\n          this.mintProportions = { ...normalizedProportions };\n        } else {\n          // Need to redistribute respecting constraints\n          this.redistributeWithConstraints();\n        }\n      }\n    },\n    redistributeWithConstraints() {\n      const totalAmount = this.payInvoiceData.meltQuote.response.amount;\n      const newProportions = {};\n\n      // Calculate available capacity for each mint\n      const mintsWithCapacity = this.selectedMints.map((mint) => {\n        const balance = this.mintClass(mint).unitBalance(this.activeUnit);\n        const maxPercentage = Math.min(100, (balance / totalAmount) * 100);\n        const currentPercentage = this.mintProportions[mint.url] || 0;\n        return {\n          mint,\n          balance,\n          maxPercentage,\n          currentPercentage,\n        };\n      });\n\n      // Start with current proportions, then adjust\n      let totalAllocated = 0;\n      const finalProportions = {};\n\n      // First pass: allocate up to max capacity or current percentage, whichever is lower\n      mintsWithCapacity.forEach((mintInfo) => {\n        const allocated = Math.min(\n          mintInfo.maxPercentage,\n          mintInfo.currentPercentage\n        );\n        finalProportions[mintInfo.mint.url] = allocated;\n        totalAllocated += allocated;\n      });\n\n      // Second pass: distribute remaining percentage to mints with available capacity\n      let remaining = 100 - totalAllocated;\n      while (remaining > 0.01) {\n        const availableMints = mintsWithCapacity.filter(\n          (mintInfo) =>\n            finalProportions[mintInfo.mint.url] < mintInfo.maxPercentage\n        );\n\n        if (availableMints.length === 0) break;\n\n        const totalAvailableCapacity = availableMints.reduce(\n          (sum, mintInfo) =>\n            sum +\n            (mintInfo.maxPercentage - finalProportions[mintInfo.mint.url]),\n          0\n        );\n\n        if (totalAvailableCapacity <= 0) break;\n\n        availableMints.forEach((mintInfo) => {\n          const availableCapacity =\n            mintInfo.maxPercentage - finalProportions[mintInfo.mint.url];\n          const proportion = availableCapacity / totalAvailableCapacity;\n          const toAllocate = Math.min(\n            availableCapacity,\n            remaining * proportion\n          );\n          finalProportions[mintInfo.mint.url] += toAllocate;\n          remaining -= toAllocate;\n        });\n      }\n\n      this.mintProportions = { ...finalProportions };\n    },\n    getPartialAmount(mint) {\n      const partialAmounts = this.getPartialAmounts();\n      return partialAmounts[mint.url] || 0;\n    },\n    getPartialAmounts() {\n      const totalAmount = this.payInvoiceData.meltQuote.response.amount;\n      const partialAmounts = {};\n      let calculatedTotal = 0;\n\n      // First pass: calculate amounts based on percentages\n      this.selectedMints.forEach((mint) => {\n        const percentage = this.mintProportions[mint.url] || 0;\n        const amount = Math.round(totalAmount * (percentage / 100));\n        partialAmounts[mint.url] = amount;\n        calculatedTotal += amount;\n      });\n\n      // Fix rounding errors by adjusting the largest allocation\n      const difference = totalAmount - calculatedTotal;\n      if (difference !== 0 && this.selectedMints.length > 0) {\n        // Find the mint with the largest allocation to adjust\n        let largestMint = null;\n        let largestAmount = 0;\n\n        this.selectedMints.forEach((mint) => {\n          if (partialAmounts[mint.url] > largestAmount) {\n            largestAmount = partialAmounts[mint.url];\n            largestMint = mint;\n          }\n        });\n\n        if (largestMint) {\n          partialAmounts[largestMint.url] += difference;\n\n          // Ensure we don't exceed the mint's balance\n          const mintBalance = this.mintClass(largestMint).unitBalance(\n            this.activeUnit\n          );\n          if (partialAmounts[largestMint.url] > mintBalance) {\n            // If the largest mint can't handle the adjustment, distribute the difference\n            partialAmounts[largestMint.url] = mintBalance;\n            let remainingDifference =\n              difference - (mintBalance - largestAmount);\n\n            // Find other mints that can handle the remaining difference\n            const otherMints = this.selectedMints.filter(\n              (m) => m.url !== largestMint.url\n            );\n            for (const mint of otherMints) {\n              const mintBalance = this.mintClass(mint).unitBalance(\n                this.activeUnit\n              );\n              const currentAmount = partialAmounts[mint.url];\n              const canTake = Math.min(\n                remainingDifference,\n                mintBalance - currentAmount\n              );\n\n              if (canTake > 0) {\n                partialAmounts[mint.url] += canTake;\n                remainingDifference -= canTake;\n                if (remainingDifference === 0) break;\n              }\n            }\n          }\n        }\n      }\n\n      return partialAmounts;\n    },\n    getStateText(state) {\n      switch (state) {\n        case \"requesting\":\n          return \"Requesting...\";\n        case \"paying\":\n          return \"Paying...\";\n        case \"success\":\n          return \"Success\";\n        case \"error\":\n          return \"Failed\";\n        default:\n          return \"\";\n      }\n    },\n    getStateColor(state) {\n      switch (state) {\n        case \"requesting\":\n          return \"warning\";\n        case \"paying\":\n          return \"primary\";\n        case \"success\":\n          return \"positive\";\n        case \"error\":\n          return \"negative\";\n        default:\n          return \"grey\";\n      }\n    },\n    getStateProgress(state) {\n      switch (state) {\n        case \"requesting\":\n          return 0.3; // Low progress - just started\n        case \"paying\":\n          return 0.7; // Middle progress - actively paying\n        case \"success\":\n          return 1.0; // Full progress - completed\n        case \"error\":\n          return 0.1; // Minimal progress - failed\n        default:\n          return 0;\n      }\n    },\n    setMintState(mintUrl, state) {\n      // Use Vue 3 compatible reactivity\n      this.mintStates = { ...this.mintStates, [mintUrl]: state };\n    },\n    clearMintStates() {\n      this.mintStates = {};\n      this.isPaymentInProgress = false;\n    },\n    openMultinutDialog() {\n      const mintsStore = useMintsStore();\n      const previouslySelectedMints = mintsStore.multiMints.filter(\n        (mint) => mint.multinutSelected === true\n      );\n\n      // If no mints were previously selected, default to selecting all available mints\n      this.selectedMints =\n        previouslySelectedMints.length > 0\n          ? [...previouslySelectedMints]\n          : [...mintsStore.multiMints];\n\n      this.showMultinutPaymentDialog = true;\n      this.clearMintStates(); // Clear any previous states\n      this.initializeMintProportions(); // Initialize proportions based on balance\n    },\n    closeMultinutDialog() {\n      this.showMultinutPaymentDialog = false;\n      this.clearMintStates(); // Clear states when dialog closes\n      // Re-open the PayInvoiceDialog\n      this.$emit(\"return-to-pay-dialog\");\n    },\n    multiMintBalance: function (selectedMints, unit) {\n      const multiMints = selectedMints;\n      const mintBalances = [];\n      const overallBalance = multiMints.reduce((sum, m) => {\n        const mint = new MintClass(m);\n        const mintBalance = mint.unitBalance(unit);\n        mintBalances.push(mintBalance);\n        return sum + mintBalance;\n      }, 0);\n      const weights = mintBalances.map((b) => b / overallBalance);\n      return { overallBalance: overallBalance, weights: weights };\n    },\n    executeMultinutPayment: async function () {\n      const uiStore = useUiStore();\n      //\n      const totalQuoteAmount = this.payInvoiceData.meltQuote.response.amount;\n      const activeUnit = useMintsStore().activeUnit;\n      // update selected mints\n      this.selectedMints.forEach((mint) => {\n        this.updateMintMultinutSelection(mint.url, true);\n      });\n\n      // Clear previous states\n      this.clearMintStates();\n\n      // Start payment process\n      this.isPaymentInProgress = true;\n      this.multiMeltButtonLoading = true;\n\n      let mintsToQuotes = [];\n      const mintsToAmounts = [];\n      let data;\n\n      try {\n        // Phase 1: Calculate amount for each Mint using custom proportions\n        for (const mint of this.selectedMints) {\n          const partialAmount = this.getPartialAmount(mint);\n          console.log(`partialAmount for mint ${mint.url}: ${partialAmount}`);\n\n          if (partialAmount > 0) {\n            mintsToAmounts.push([mint, partialAmount]);\n          } else {\n            // remove from selectedMints so we don't show progress in the UI\n            this.selectedMints = this.selectedMints.filter(\n              (m) => m.url !== mint.url\n            );\n          }\n        }\n\n        // Verify total amounts match exactly\n        const totalCalculated = mintsToAmounts.reduce(\n          (sum, [mint, amount]) => sum + amount,\n          0\n        );\n        console.log(\n          `Total calculated: ${totalCalculated}, Expected: ${totalQuoteAmount}`\n        );\n\n        if (totalCalculated !== totalQuoteAmount) {\n          console.error(\n            `Amount mismatch: calculated ${totalCalculated}, expected ${totalQuoteAmount}`\n          );\n          notifyError(\n            `Amount calculation error: ${totalCalculated} vs ${totalQuoteAmount}`\n          );\n          return;\n        }\n\n        // Phase 2: Request quotes from all selected mints\n        mintsToQuotes = await Promise.all(\n          mintsToAmounts.map(async ([mint, partialAmount], i) => {\n            if (partialAmount <= 0) {\n              return null;\n            }\n            console.log(`Quoting mint: ${mint.url}`);\n            const mintWallet = await useWalletStore().mintWallet(\n              mint.url,\n              useMintsStore().activeUnit,\n              true\n            );\n            try {\n              this.setMintState(mint.url, \"requesting\");\n              const quote = await this.meltQuote(\n                mintWallet,\n                this.payInvoiceData.input.request,\n                partialAmount\n              );\n              console.log(quote);\n              return [mint, quote];\n            } catch (error) {\n              console.error(`Quote failed for mint ${mint.url}:`, error);\n              this.setMintState(mint.url, \"error\");\n              throw error;\n            }\n          })\n        );\n\n        // Filter out null values (for mints with partialAmount <= 0)\n        mintsToQuotes = mintsToQuotes.filter((item) => item !== null);\n\n        // Phase 3: Execute payments\n        data = await Promise.all(\n          mintsToQuotes.map(async ([mint, quote]) => {\n            try {\n              // Move to paying state\n              this.setMintState(mint.url, \"paying\");\n              const mintWallet = await useWalletStore().mintWallet(\n                mint.url,\n                activeUnit,\n                true\n              );\n              const mintClass = new MintClass(mint);\n              const proofs = mintClass.unitProofs(activeUnit);\n              const result = await this.melt(\n                proofs,\n                quote,\n                mintWallet,\n                true,\n                true // ! RELEASE THE MUTEX !\n              );\n\n              // Mark as success\n              this.setMintState(mint.url, \"success\");\n              return result;\n            } catch (error) {\n              console.error(`Payment failed for mint ${mint.url}:`, error);\n              this.setMintState(mint.url, \"error\");\n              throw error;\n            }\n          })\n        );\n      } catch (error) {\n        // notifyError(`Multi-nut payment failed: ${error}`);\n        console.error(`${error}`);\n\n        // Reset states on error so user can try again\n        setTimeout(() => {\n          this.clearMintStates();\n        }, 3000); // Show error states for 3 seconds before clearing\n\n        throw error;\n      } finally {\n        this.multiMeltButtonLoading = false;\n      }\n\n      uiStore.vibrate();\n      const amountPaid =\n        mintsToQuotes.reduce(\n          (acc, q) => acc + q[1].amount + q[1].fee_reserve,\n          0\n        ) -\n        data.reduce(\n          (acc, d) => acc + d.change.reduce((acc1, p) => acc1 + p.amount, 0),\n          0\n        );\n\n      notifySuccess(\n        \"Paid \" +\n          uiStore.formatCurrency(amountPaid, activeUnit) +\n          \" via Lightning\"\n      );\n\n      // Close the dialog after successful payment\n      setTimeout(() => {\n        this.showMultinutPaymentDialog = false;\n        this.payInvoiceData.show = false;\n        this.clearMintStates();\n      }, 2000); // Show success states for 2 seconds before closing\n    },\n    dismissExperimentalWarning() {\n      this.multinutExperimentalWarningDismissed = true;\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n@import \"src/css/mintlist.css\";\n\n.fullscreen {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  z-index: 6000;\n}\n\n.multinut-content-container {\n  max-width: 600px;\n  margin: 0 auto;\n  color: white;\n  height: 100%;\n  overflow-y: auto;\n  position: relative;\n  padding-bottom: 100px;\n}\n\n/* Top Icons */\n.top-icons {\n  width: 100%;\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: flex-start;\n}\n\n.close-icon {\n  transition: opacity 0.2s ease;\n}\n\n.close-icon:hover {\n  opacity: 0.7;\n}\n\n/* Header Section */\n.multinut-header-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  width: 100%;\n}\n\n.multinut-header {\n  width: 100%;\n  border-radius: 12px;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  background-color: rgba(255, 255, 255, 0.05);\n}\n\n.multinut-title {\n  font-size: 20px;\n  font-weight: 600;\n  text-align: center;\n  color: white;\n}\n\n.multinut-amount {\n  font-size: 32px;\n  font-weight: 600;\n  text-align: center;\n  color: white;\n}\n\n.multinut-description {\n  font-size: 14px;\n  font-weight: 500;\n  text-align: center;\n  color: #9e9e9e;\n}\n\n/* Section Divider */\n.section-divider {\n  display: flex;\n  align-items: center;\n  width: 100%;\n}\n\n.divider-line {\n  flex: 1;\n  height: 1px;\n  background-color: #333;\n}\n\n.divider-text {\n  padding: 0 10px;\n  font-size: 14px;\n  font-weight: 600;\n  color: #ffffff;\n  text-transform: uppercase;\n}\n\n/* Mint Selection Section */\n.mint-selection-section {\n  width: 100%;\n}\n\n.mint-selection-description {\n  font-size: 14px;\n  line-height: 20px;\n  color: #9e9e9e;\n  font-weight: 500;\n  text-align: left;\n}\n\n.mints-container {\n  width: 100%;\n}\n\n.mint-item {\n  width: 100%;\n}\n\n/* MultinutPaymentDialog specific mint card styles */\n.mint-card {\n  width: 100%;\n  border-radius: 12px;\n  background-color: rgba(255, 255, 255, 0.02);\n  transition: all 0.2s ease;\n}\n\n.mint-card:hover:not(.cursor-not-allowed) {\n  transform: translateY(-1px);\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n}\n\n.mint-card-selected {\n  border-color: var(--q-primary) !important;\n  background-color: rgba(25, 118, 210, 0.1) !important;\n}\n\n.mint-card-success {\n  border-color: var(--q-positive) !important;\n  background-color: rgba(76, 175, 80, 0.1) !important;\n}\n\n.mint-card-error {\n  border-color: var(--q-negative) !important;\n  background-color: rgba(244, 67, 54, 0.1) !important;\n}\n\n.mint-card-content {\n  width: 100%;\n}\n\n.state-indicator {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  width: 24px;\n  height: 24px;\n}\n\n.cursor-not-allowed {\n  cursor: not-allowed !important;\n  opacity: 0.7;\n}\n\n/* Balance and Slider Sections */\n.mint-balance-section {\n  display: flex;\n  justify-content: flex-end;\n  width: 100%;\n}\n\n.mint-slider-section {\n  width: 100%;\n}\n\n.allocation-badge {\n  background-color: var(--q-primary);\n  border-radius: 4px;\n  padding: 4px 8px;\n  font-size: 14px;\n  font-weight: 500;\n  color: var(--q-dark);\n  display: inline-block;\n  min-width: 50px;\n  text-align: center;\n}\n\n.slider-container {\n  min-width: 0;\n  overflow: hidden;\n  flex: 1;\n}\n\n.mint-slider {\n  margin: 8px 0;\n}\n\n.total-balance-badge {\n  background-color: #1d1d1d;\n  border-radius: 4px;\n  padding: 4px 8px;\n  font-size: 14px;\n  font-weight: 500;\n  color: white;\n  display: inline-block;\n}\n\n/* Payment Progress */\n.payment-progress-section {\n  width: 100%;\n}\n\n.payment-progress {\n  border-radius: 2px;\n  overflow: hidden;\n  width: 100%;\n}\n\n.payment-progress .q-linear-progress__track {\n  background-color: rgba(255, 255, 255, 0.1);\n}\n\n.payment-state-text {\n  font-size: 10px;\n  color: #9e9e9e;\n  font-weight: 500;\n  text-align: left;\n}\n\n/* Action Buttons Section */\n.action-buttons-section {\n  position: fixed;\n  margin: 0 auto;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  background-color: var(--q-dark);\n  padding: 16px;\n  z-index: 1000;\n}\n\n.pay-button-fixed {\n  width: 100%;\n  max-width: 600px;\n  margin: 0 auto;\n  display: block;\n  height: 48px;\n  font-size: 16px;\n  font-weight: 600;\n}\n\n/* Responsive adjustments for smaller screens */\n@media (max-width: 600px) {\n  .mint-slider {\n    margin: 4px 0;\n  }\n\n  .multinut-content-container {\n    padding: 12px;\n    padding-bottom: 100px;\n  }\n\n  .mint-card-content {\n    padding: 12px;\n  }\n}\n\n/* Ensure no horizontal overflow */\n.no-wrap {\n  flex-wrap: nowrap !important;\n}\n\n.slider-container {\n  flex-shrink: 1;\n  min-width: 0 !important;\n}\n\n/* Remove old styles that are no longer needed */\n.qcard {\n  display: none;\n}\n\n/* Experimental Warning */\n.experimental-warning {\n  width: 100%;\n}\n\n.warning-content {\n  background-color: rgba(255, 193, 7, 0.1);\n  border: 1px solid rgba(255, 193, 7, 0.3);\n  border-radius: 12px;\n  text-align: center;\n}\n\n.warning-icon {\n  display: flex;\n  justify-content: center;\n}\n\n.warning-title {\n  font-size: 16px;\n  font-weight: 600;\n  color: #ffc107;\n}\n\n.warning-text {\n  font-size: 14px;\n  line-height: 20px;\n  color: #ffffff;\n  font-weight: 500;\n}\n\n.warning-dismiss-btn {\n  font-weight: 600;\n}\n\n/* Smooth slide animation for warning */\n.smooth-slide-enter-active {\n  transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);\n  max-height: 200px;\n  margin-bottom: 16px;\n  opacity: 1;\n  pointer-events: auto;\n}\n\n.smooth-slide-leave-active {\n  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);\n  max-height: 200px;\n  margin-bottom: 16px;\n  opacity: 1;\n}\n\n.smooth-slide-enter-from,\n.smooth-slide-leave-to {\n  max-height: 0;\n  margin-bottom: 0;\n  opacity: 0;\n  transform: translateY(-10px);\n  pointer-events: none;\n}\n</style>\n"
  },
  {
    "path": "src/components/NWCDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showNWCDialog\"\n    position=\"top\"\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n  >\n    <q-card\n      v-if=\"showNWCData.connectionString\"\n      class=\"q-px-lg q-pt-md q-pb-md qcard\"\n    >\n      <div class=\"text-center q-mb-md q-mt-none q-pt-none\">\n        <a :href=\"showNWCData.connectionString\">\n          <q-responsive :ratio=\"1\" class=\"q-mx-md q-mt-none q-pt-none\">\n            <vue-qrcode\n              :value=\"showNWCData.connectionString\"\n              :options=\"{ width: 340 }\"\n              class=\"rounded-borders\"\n            >\n            </vue-qrcode>\n          </q-responsive>\n        </a>\n        <div class=\"row justify-center\">\n          <q-card-section class=\"q-pa-sm\">\n            <div class=\"row justify-center\">\n              <q-item-label\n                overline\n                class=\"q-mb-sm q-pt-md text-white\"\n                style=\"font-size: 14px\"\n                >{{ $t(\"NWCDialog.nwc.caption\") }}</q-item-label\n              >\n            </div>\n            <div class=\"row justify-center q-pt-md\">\n              <q-item-label\n                caption\n                class=\"text-weight-light text-white\"\n                style=\"font-size: 12px\"\n              >\n                {{ $t(\"NWCDialog.nwc.description\") }}\n              </q-item-label>\n              <q-item-label caption class=\"text-weight-bold text-white q-pt-md\">\n                {{ $t(\"NWCDialog.nwc.warning_text\") }}\n              </q-item-label>\n            </div>\n          </q-card-section>\n        </div>\n        <div class=\"row q-mt-lg\">\n          <q-btn\n            class=\"q-mx-xs\"\n            size=\"md\"\n            flat\n            @click=\"copyText(showNWCData.connectionString)\"\n            >{{ $t(\"NWCDialog.actions.copy.label\") }}</q-btn\n          >\n          <q-btn v-close-popup flat color=\"grey\" class=\"q-ml-auto\">{{\n            $t(\"NWCDialog.actions.close.label\")\n          }}</q-btn>\n        </div>\n      </div>\n    </q-card>\n  </q-dialog>\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport VueQrcode from \"@chenfengyuan/vue-qrcode\";\n\nimport { useNWCStore } from \"src/stores/nwc\";\n\nexport default defineComponent({\n  name: \"NWCDialog\",\n  mixins: [windowMixin],\n  components: {\n    VueQrcode,\n  },\n  data: function () {\n    return {};\n  },\n  computed: {\n    ...mapState(useNWCStore, [\"showNWCData\"]),\n    ...mapWritableState(useNWCStore, [\"showNWCDialog\"]),\n  },\n  methods: {},\n});\n</script>\n"
  },
  {
    "path": "src/components/NoMintWarnBanner.vue",
    "content": "<template>\n  <q-card class=\"q-ma-lg bg-dark q-pa-md\">\n    <q-card-section>\n      <div class=\"row items-center justify-center\">\n        <q-icon\n          color=\"primary\"\n          class=\"q-pb-md\"\n          name=\"account_balance\"\n          size=\"50px\"\n        />\n      </div>\n      <div class=\"row items-center justify-center\">\n        <div class=\"text-h6\">{{ $t(\"NoMintWarnBanner.title\") }}</div>\n      </div>\n      <div class=\"row items-center justify-center\">\n        <div class=\"text-subtitle2\">\n          {{ $t(\"NoMintWarnBanner.subtitle\") }}\n        </div>\n      </div>\n      <div class=\"row items-center justify-center q-pt-lg\">\n        <q-btn\n          rounded\n          color=\"primary\"\n          class=\"q-px-md\"\n          :label=\"$t('NoMintWarnBanner.actions.add_mint.label')\"\n          @click=\"handleAddMintClick\"\n        />\n      </div>\n      <div class=\"row items-center justify-center q-pt-md\">\n        <q-btn\n          outline\n          rounded\n          color=\"primary\"\n          class=\"q-px-md\"\n          :label=\"$t('NoMintWarnBanner.actions.receive.label')\"\n          @click=\"handleReceiveEcash\"\n        />\n      </div>\n    </q-card-section>\n  </q-card>\n</template>\n<script lang=\"ts\">\nimport { defineComponent, ref } from \"vue\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { mapWritableState } from \"pinia\";\nimport { useReceiveTokensStore } from \"src/stores/receiveTokensStore\";\nimport { EventBus } from \"../js/eventBus\";\n\nexport default defineComponent({\n  name: \"NoMintWarnBanner\",\n  mixins: [windowMixin],\n  props: {\n    proofs: Array,\n    activeProofs: Array,\n    mints: Array,\n    tickerShort: String,\n    activeMintUrl: String,\n  },\n  computed: {\n    ...mapWritableState(useUiStore, [\n      \"tab\",\n      \"expandHistory\",\n      \"showReceiveEcashDrawer\",\n    ]),\n    ...mapWritableState(useReceiveTokensStore, [\n      \"showReceiveTokens\",\n      \"receiveData\",\n    ]),\n    balance: function () {\n      return this.activeProofs\n        .map((t) => t)\n        .flat()\n        .reduce((sum, el) => (sum += el.amount), 0);\n    },\n    getActiveMintUrlShort: function () {\n      return getShortUrl(this.activeMintUrl);\n    },\n    getBalance: function () {\n      const balance = this.activeProofs\n        .map((t) => t)\n        .flat()\n        .reduce((sum, el) => (sum += el.amount), 0);\n      return balance;\n    },\n  },\n  methods: {\n    // showReceiveTokensDialog: function () {\n    //   this.receiveData.tokensBase64 = \"\";\n    //   this.showReceiveTokens = true;\n    // },\n    handleReceiveEcash: function () {\n      this.showReceiveEcashDrawer = true;\n    },\n    handleAddMintClick: function () {\n      this.expandHistory = true;\n      this.tab = \"mints\";\n      EventBus.emit(\"scrollToAddMintDiv\");\n    },\n  },\n});\n</script>\n<style scoped>\n.q-dialog__inner {\n  height: 100%;\n  width: 100%;\n  margin: 0; /* Align dialog to cover the entire viewport */\n}\n\n.q-card {\n  border-radius: 20px;\n  display: flex;\n  flex-direction: column;\n  border: 2px solid var(--q-primary);\n}\n</style>\n"
  },
  {
    "path": "src/components/NostrMintRestore.vue",
    "content": "<template>\n  <div v-if=\"!autoAdd\" class=\"nostr-mint-restore\">\n    <!-- Header -->\n    <div class=\"q-px-xs text-left q-mt-lg\">\n      <q-list padding>\n        <q-item>\n          <q-item-section>\n            <q-item-label\n              overline\n              class=\"text-weight-bold\"\n              style=\"\n                font-size: 15.2px;\n                font-family: Inter, -apple-system, 'system-ui', 'Segoe UI',\n                  Roboto, 'Helvetica Neue', Arial, sans-serif;\n                font-weight: 600;\n                color: #ffffff;\n                text-transform: none;\n                margin-bottom: 8px;\n              \"\n            >\n              {{ $t(\"RestoreView.nostr_mints.label\") }}\n            </q-item-label>\n            <q-item-label\n              caption\n              style=\"\n                font-size: 0.9rem;\n                color: rgba(255, 255, 255, 0.8);\n                line-height: 1.4;\n              \"\n            >\n              {{ $t(\"RestoreView.nostr_mints.caption\") }}\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n      </q-list>\n    </div>\n\n    <!-- Search Button -->\n    <div class=\"q-pb-lg q-pt-md q-px-xs text-left\">\n      <q-btn\n        class=\"full-width\"\n        color=\"primary\"\n        size=\"lg\"\n        rounded\n        @click=\"searchForMints\"\n        :disabled=\"!isMnemonicValid || searchInProgress\"\n        :loading=\"searchInProgress\"\n        style=\"\n          min-height: 48px;\n          font-weight: 500;\n          text-transform: none;\n          font-size: 0.95rem;\n        \"\n      >\n        <q-icon name=\"search\" size=\"20px\" class=\"q-mr-sm\" />\n        {{ $t(\"RestoreView.nostr_mints.search_button\") }}\n      </q-btn>\n    </div>\n\n    <!-- Discovered Mints List -->\n    <div v-if=\"availableMints.length > 0\" class=\"q-px-xs\">\n      <!-- Add Selected Button (Primary Action) -->\n      <div class=\"primary-action-section q-pb-lg q-pt-md\">\n        <q-btn\n          color=\"primary\"\n          size=\"lg\"\n          rounded\n          @click=\"addSelectedMints\"\n          :loading=\"addingMints\"\n          :disabled=\"addingMints || selectedMints.length === 0\"\n          class=\"full-width\"\n          style=\"\n            min-height: 48px;\n            font-weight: 500;\n            text-transform: none;\n            font-size: 0.95rem;\n          \"\n        >\n          <q-icon name=\"add\" size=\"20px\" class=\"q-mr-sm\" />\n          <span\n            style=\"\n              white-space: nowrap;\n              overflow: hidden;\n              text-overflow: ellipsis;\n            \"\n          >\n            {{\n              $t(\"RestoreView.nostr_mints.add_selected\", {\n                count: selectedMints.length,\n              })\n            }}\n          </span>\n        </q-btn>\n      </div>\n\n      <!-- Select All/Deselect All -->\n      <div class=\"selection-buttons q-pb-lg\" style=\"display: flex; gap: 16px\">\n        <q-btn\n          flat\n          size=\"md\"\n          color=\"primary\"\n          @click=\"selectAllMints\"\n          :disabled=\"allSelected\"\n          style=\"\n            flex: 1;\n            min-height: 40px;\n            font-weight: 500;\n            text-transform: none;\n          \"\n        >\n          {{ $t(\"RestoreView.nostr_mints.select_all\") }}\n        </q-btn>\n        <q-btn\n          flat\n          size=\"md\"\n          color=\"grey\"\n          @click=\"deselectAllMints\"\n          :disabled=\"!anySelected\"\n          style=\"\n            flex: 1;\n            min-height: 40px;\n            font-weight: 500;\n            text-transform: none;\n          \"\n        >\n          {{ $t(\"RestoreView.nostr_mints.deselect_all\") }}\n        </q-btn>\n      </div>\n\n      <!-- Mints List -->\n      <q-list padding class=\"discovered-mints-list\">\n        <q-item\n          v-for=\"mint in availableMints\"\n          :key=\"mint.url\"\n          clickable\n          @click=\"toggleMintSelection(mint.url)\"\n          class=\"mint-item\"\n          :class=\"{ 'mint-item-selected': mint.selected }\"\n          :style=\"{\n            'border-radius': '12px',\n            border: mint.selected\n              ? '2px solid var(--q-primary)'\n              : '1px solid rgba(128, 128, 128, 0.2)',\n            'background-color': mint.selected\n              ? 'rgba(var(--q-primary-rgb), 0.1)'\n              : 'transparent',\n            padding: '16px',\n            'margin-bottom': '12px',\n            transition: 'all 0.2s ease',\n          }\"\n        >\n          <q-item-section avatar>\n            <q-checkbox\n              :model-value=\"mint.selected\"\n              @update:model-value=\"toggleMintSelection(mint.url)\"\n              @click.stop\n              color=\"primary\"\n              class=\"clickable-checkbox\"\n            />\n          </q-item-section>\n          <q-item-section class=\"text-left\">\n            <div class=\"row items-center\">\n              <!-- Mint Avatar -->\n              <q-avatar\n                v-if=\"getMintIconUrl(mint.url)\"\n                size=\"34px\"\n                class=\"q-mr-sm\"\n              >\n                <q-img\n                  spinner-color=\"white\"\n                  spinner-size=\"xs\"\n                  :src=\"getMintIconUrl(mint.url)\"\n                  alt=\"Mint Icon\"\n                  style=\"height: 34px; max-width: 34px; font-size: 12px\"\n                />\n              </q-avatar>\n\n              <!-- Loading spinner for mint info -->\n              <q-spinner-dots\n                v-else-if=\"isFetchingMintInfo(mint.url)\"\n                size=\"34px\"\n                color=\"grey-5\"\n                class=\"q-mr-sm\"\n              />\n\n              <div class=\"mint-info-container\">\n                <!-- Mint Name -->\n                <q-item-label\n                  lines=\"1\"\n                  class=\"text-weight-medium text-left mint-name\"\n                >\n                  {{ getMintDisplayName(mint.url) }}\n                </q-item-label>\n\n                <!-- Mint URL -->\n                <q-item-label\n                  caption\n                  lines=\"2\"\n                  class=\"text-grey-6 text-left mint-url\"\n                >\n                  {{ mint.url }}\n                </q-item-label>\n\n                <!-- Backup timestamp -->\n                <q-item-label caption class=\"text-grey-5 q-pt-xs text-left\">\n                  {{ $t(\"RestoreView.nostr_mints.backed_up\") }}:\n                  {{ formatDate(mint.timestamp) }}\n                </q-item-label>\n              </div>\n            </div>\n          </q-item-section>\n        </q-item>\n      </q-list>\n    </div>\n\n    <!-- Empty State -->\n    <div\n      v-else-if=\"hasSearched && !searchInProgress\"\n      class=\"q-px-xs text-center q-py-lg\"\n    >\n      <q-icon name=\"cloud_off\" size=\"lg\" color=\"grey-5\" />\n      <div class=\"text-grey-6 q-mt-sm\">\n        {{ $t(\"RestoreView.nostr_mints.no_backups_found\") }}\n      </div>\n      <div class=\"text-grey-5 caption q-mt-xs\">\n        {{ $t(\"RestoreView.nostr_mints.no_backups_hint\") }}\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { mapState, mapActions } from \"pinia\";\nimport { useNostrMintBackupStore } from \"src/stores/nostrMintBackup\";\nimport { useMintsStore, MintClass } from \"src/stores/mints\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport { notifyError } from \"src/js/notify\";\nimport { date } from \"quasar\";\n\nexport default defineComponent({\n  name: \"NostrMintRestore\",\n  props: {\n    mnemonic: {\n      type: String,\n      required: true,\n    },\n    isMnemonicValid: {\n      type: Boolean,\n      required: true,\n    },\n    // If true, component renders no UI, starts searching immediately\n    // and auto-adds discovered mints to the wallet.\n    autoAdd: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  data() {\n    return {\n      hasSearched: false,\n      addingMints: false,\n      mintInfoCache: new Map(), // Cache for mint info to avoid duplicate requests\n      fetchingMintInfo: new Set(), // Track which mints are currently being fetched\n    };\n  },\n  computed: {\n    ...mapState(useNostrMintBackupStore, [\n      \"discoveredMints\",\n      \"searchInProgress\",\n    ]),\n    ...mapState(useMintsStore, [\"mints\"]),\n\n    // Filter out mints that already exist\n    availableMints() {\n      return this.discoveredMints.filter((mint) => !this.mintExists(mint.url));\n    },\n\n    selectedMints() {\n      return this.availableMints.filter((mint) => mint.selected);\n    },\n\n    allSelected() {\n      return (\n        this.availableMints.length > 0 &&\n        this.availableMints.every((mint) => mint.selected)\n      );\n    },\n\n    anySelected() {\n      return this.availableMints.some((mint) => mint.selected);\n    },\n  },\n  watch: {\n    // Watch for changes in discovered mints and fetch their info\n    discoveredMints: {\n      handler(newMints) {\n        if (newMints && newMints.length > 0) {\n          this.fetchMintInfoForDiscoveredMints();\n          // In autoAdd mode, add newly discovered mints immediately\n          if (this.autoAdd) {\n            this.autoAddNewDiscoveredMints(newMints);\n          }\n        }\n      },\n      immediate: true,\n    },\n    // In autoAdd mode, when mnemonic becomes valid, start searching immediately\n    isMnemonicValid: {\n      handler(v) {\n        if (this.autoAdd && v && !this.hasSearched) {\n          this.searchForMints();\n        }\n      },\n      immediate: true,\n    },\n  },\n  methods: {\n    ...mapActions(useNostrMintBackupStore, [\n      \"searchMintsOnNostr\",\n      \"toggleMintSelection\",\n      \"selectAllMints\",\n      \"deselectAllMints\",\n      \"addSelectedMintsToWallet\",\n      \"clearDiscoveredMints\",\n    ]),\n\n    async searchForMints() {\n      if (!this.isMnemonicValid) {\n        notifyError(this.$t(\"RestoreView.nostr_mints.invalid_mnemonic\"));\n        return;\n      }\n\n      try {\n        await this.searchMintsOnNostr(this.mnemonic);\n        this.hasSearched = true;\n      } catch (error) {\n        console.error(\"Failed to search for mints:\", error);\n        notifyError(this.$t(\"RestoreView.nostr_mints.search_error\"));\n      }\n    },\n\n    async addSelectedMints() {\n      if (this.selectedMints.length === 0) {\n        return;\n      }\n\n      this.addingMints = true;\n\n      try {\n        await this.addSelectedMintsToWallet(this.selectedMints);\n        // Clear selections after successful addition\n        this.deselectAllMints();\n      } catch (error) {\n        console.error(\"Failed to add selected mints:\", error);\n        notifyError(this.$t(\"RestoreView.nostr_mints.add_error\"));\n      } finally {\n        this.addingMints = false;\n      }\n    },\n\n    async autoAddNewDiscoveredMints(mints) {\n      try {\n        const mintsStore = useMintsStore();\n        for (const m of mints) {\n          const url = m.url;\n          if (!this.mintExists(url)) {\n            try {\n              await mintsStore.addMint({ url }, false);\n            } catch (e) {\n              console.error(`Auto-add mint failed for ${url}:`, e);\n            }\n          }\n        }\n      } catch (e) {\n        console.error(\"Auto-add discovered mints failed:\", e);\n      }\n    },\n\n    mintExists(url) {\n      return this.mints.some((mint) => mint.url === url);\n    },\n\n    getShortUrl(url) {\n      return getShortUrl(url);\n    },\n\n    formatDate(timestamp) {\n      return date.formatDate(timestamp * 1000, \"MMM DD, YYYY\");\n    },\n\n    // Fetch mint information for all discovered mints in the background\n    async fetchMintInfoForDiscoveredMints() {\n      const availableMints = this.availableMints;\n\n      // Fetch mint info for each mint in parallel, but don't block the UI\n      availableMints.forEach(async (mint) => {\n        if (\n          !this.mintInfoCache.has(mint.url) &&\n          !this.fetchingMintInfo.has(mint.url)\n        ) {\n          this.fetchingMintInfo.add(mint.url);\n\n          try {\n            const mintInfo = await this.fetchMintInfo(mint.url);\n            this.mintInfoCache.set(mint.url, mintInfo);\n\n            // Force reactivity by triggering a re-render\n            this.$forceUpdate();\n          } catch (error) {\n            console.error(`Failed to fetch mint info for ${mint.url}:`, error);\n            // Set empty info to avoid retrying\n            this.mintInfoCache.set(mint.url, null);\n          } finally {\n            this.fetchingMintInfo.delete(mint.url);\n          }\n        }\n      });\n    },\n\n    // Fetch mint information using cashu-ts\n    async fetchMintInfo(mintUrl) {\n      try {\n        // Create a temporary mint object for the MintClass\n        const tempMint = {\n          url: mintUrl,\n          keys: [],\n          keysets: [],\n        };\n\n        const mintClass = new MintClass(tempMint);\n        const mintInfo = await mintClass.api.getInfo();\n        return mintInfo;\n      } catch (error) {\n        console.error(`Error fetching mint info for ${mintUrl}:`, error);\n        throw error;\n      }\n    },\n\n    // Get mint info from cache\n    getMintInfo(mintUrl) {\n      return this.mintInfoCache.get(mintUrl);\n    },\n\n    // Get mint icon URL\n    getMintIconUrl(mintUrl) {\n      const mintInfo = this.getMintInfo(mintUrl);\n      if (mintInfo && mintInfo.icon_url) {\n        return mintInfo.icon_url;\n      }\n      return null;\n    },\n\n    // Get mint display name\n    getMintDisplayName(mintUrl) {\n      const mintInfo = this.getMintInfo(mintUrl);\n      if (mintInfo && mintInfo.name) {\n        return mintInfo.name;\n      }\n      return this.getShortUrl(mintUrl);\n    },\n\n    // Check if mint info is being fetched\n    isFetchingMintInfo(mintUrl) {\n      return this.fetchingMintInfo.has(mintUrl);\n    },\n  },\n\n  mounted() {\n    // In autoAdd mode, start searching right away if mnemonic is valid\n    if (this.autoAdd && this.isMnemonicValid && !this.hasSearched) {\n      this.searchForMints();\n    }\n  },\n\n  beforeUnmount() {\n    // Clear discovered mints when component is destroyed\n    this.clearDiscoveredMints();\n    // Clear cache\n    this.mintInfoCache.clear();\n    this.fetchingMintInfo.clear();\n  },\n});\n</script>\n\n<style scoped>\n@import \"src/css/mintlist.css\";\n\n.discovered-mints-list {\n  max-height: 500px;\n  overflow-y: auto;\n  padding: 0 !important;\n}\n\n.nostr-mint-restore {\n  border-top: 1px solid rgba(255, 255, 255, 0.1);\n  margin-top: 2rem;\n  padding-top: 1rem;\n}\n\n/* Mint item styling - specific to NostrMintRestore */\n.mint-item {\n  text-align: left;\n}\n\n.mint-item:hover {\n  transform: translateY(-2px);\n}\n\n/* Primary action section */\n.primary-action-section {\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n}\n\n/* Clickable checkbox */\n.clickable-checkbox {\n  cursor: pointer;\n}\n\n/* Ensure all text aligns to the left */\n.text-left {\n  text-align: left !important;\n}\n</style>\n"
  },
  {
    "path": "src/components/NumericKeyboard.vue",
    "content": "<!-- NumericKeyboard.vue -->\n<template>\n  <transition name=\"slide-up-fade\">\n    <div\n      class=\"numeric-keyboard q-pa-md q-pb-lg q-pt-lg\"\n      v-if=\"forceVisible || showNumericKeyboard\"\n    >\n      <div class=\"keyboard-grid\">\n        <q-btn flat class=\"text-h5\" @click=\"addDigit('1')\">1</q-btn>\n        <q-btn flat class=\"text-h5\" @click=\"addDigit('2')\">2</q-btn>\n        <q-btn flat class=\"text-h5\" @click=\"addDigit('3')\">3</q-btn>\n\n        <q-btn flat :ripple=\"true\" class=\"text-h5\" @click=\"addDigit('4')\"\n          >4</q-btn\n        >\n        <q-btn flat class=\"text-h5\" @click=\"addDigit('5')\">5</q-btn>\n        <q-btn flat class=\"text-h5\" @click=\"addDigit('6')\">6</q-btn>\n\n        <q-btn flat class=\"text-h5\" @click=\"addDigit('7')\">7</q-btn>\n        <q-btn flat class=\"text-h5\" @click=\"addDigit('8')\">8</q-btn>\n        <q-btn flat class=\"text-h5\" @click=\"addDigit('9')\">9</q-btn>\n\n        <q-btn flat class=\"text-h5 invisible\" disable>•</q-btn>\n        <q-btn flat class=\"text-h5\" @click=\"addDigit('0')\">0</q-btn>\n        <q-btn flat class=\"text-h5\" @click=\"backspace\">\n          <q-icon name=\"chevron_left\" size=\"md\" />\n        </q-btn>\n        <q-btn v-if=\"!hideClose\" flat @click=\"closeKeyboard\">{{\n          $t(\"NumericKeyboard.actions.close.label\")\n        }}</q-btn>\n        <br v-if=\"!hideClose || !hideEnter\" />\n        <q-btn v-if=\"!hideEnter\" flat @click=\"emitDone\">{{\n          $t(\"NumericKeyboard.actions.enter.label\")\n        }}</q-btn>\n      </div>\n    </div>\n  </transition>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { useUiStore } from \"../stores/ui\";\nimport { mapWritableState } from \"pinia\";\nimport { useSettingsStore } from \"../stores/settings\";\nimport { notify } from \"src/js/notify\";\n\nexport default defineComponent({\n  name: \"NumericKeyboard\",\n  props: {\n    modelValue: {\n      type: String,\n      default: \"0\",\n    },\n    forceVisible: {\n      type: Boolean,\n      default: false,\n    },\n    hideClose: {\n      type: Boolean,\n      default: false,\n    },\n    hideEnter: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: [\"update:modelValue\", \"done\"],\n  data() {\n    return {\n      // Decimal handling for fiat-style input\n    };\n  },\n  computed: {\n    ...mapWritableState(useUiStore, [\"showNumericKeyboard\"]),\n    ...mapWritableState(useSettingsStore, [\"useNumericKeyboard\"]),\n  },\n  methods: {\n    sendKey(key: string) {\n      // Delegate input handling to the global keyboard listener in AmountInputComponent\n      window.dispatchEvent(new KeyboardEvent(\"keydown\", { key }));\n    },\n    addDigit(digit: string) {\n      this.sendKey(digit);\n    },\n    backspace() {\n      this.sendKey(\"Backspace\");\n    },\n    closeKeyboard() {\n      (this as any).useNumericKeyboard = false;\n      (this as any).showNumericKeyboard = false;\n      notify(\n        this.$t(\"NumericKeyboard.actions.close.closed_info_text\") as any,\n        \"bottom\"\n      );\n    },\n    emitDone() {\n      this.sendKey(\"Enter\");\n    },\n  },\n});\n</script>\n\n<style scoped lang=\"scss\">\n.numeric-keyboard {\n  border-top-left-radius: 20px !important;\n  border-top-right-radius: 20px !important;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  background: inherit;\n  width: 100%;\n  max-width: 650px;\n}\n\n/* Responsive adjustment: Full width on small screens */\n@media (max-width: 600px) {\n  .numeric-keyboard {\n    left: 0;\n    transform: none;\n    max-width: 100%;\n  }\n}\n\n.keyboard-grid {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  gap: 16px;\n  width: 100%;\n  max-width: 420px;\n  margin: 0 auto;\n}\n\n.q-btn {\n  border-radius: 8px;\n  font-weight: 600;\n  text-transform: capitalize;\n  transition: none;\n  background-color: var(--q-color-grey-2);\n  color: var(--q-color-grey-10);\n  min-height: 56px;\n  padding: 12px 0;\n}\n\n// on not tall screens, make the keyboard smaller\n@media (max-height: 700px) {\n  .numeric-keyboard {\n    max-height: 300px;\n  }\n  .keyboard-grid {\n    gap: 6px;\n  }\n}\n\n// on even smaller screens, make the keyboard even smaller\n@media (max-height: 600px) {\n  .numeric-keyboard {\n    max-height: 260px;\n  }\n  .keyboard-grid {\n    gap: 0px;\n  }\n}\n\n.q-btn:hover {\n  background-color: var(--q-color-grey-3);\n}\n\n/* Remove click animations */\n\n.invisible {\n  visibility: hidden;\n}\n\n/* Transition styles */\n.slide-up-fade-enter-active,\n.slide-up-fade-leave-active {\n  transition: transform 0.3s ease, opacity 0.3s ease;\n}\n.slide-up-fade-enter,\n.slide-up-fade-leave-to {\n  transform: translateY(100%);\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/P2PKDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showP2PKDialog\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n    transition-show=\"fade\"\n    transition-hide=\"fade\"\n    no-backdrop-dismiss\n  >\n    <q-card v-if=\"p2pkData.publicKey\" class=\"q-pa-none qcard\">\n      <div\n        :class=\"$q.dark.isActive ? 'bg-dark' : 'bg-white'\"\n        class=\"display-token-fullscreen\"\n      >\n        <!-- Header -->\n        <div class=\"row items-center q-pa-md\" style=\"position: relative\">\n          <q-btn\n            v-close-popup\n            flat\n            round\n            icon=\"close\"\n            color=\"grey\"\n            class=\"floating-close-btn\"\n          />\n          <div class=\"col text-center fixed-title-height\">\n            <q-item-label\n              overline\n              class=\"q-mt-sm text-white\"\n              style=\"font-size: 1rem\"\n            >\n              {{ $t(\"P2PKDialog.p2pk.caption\") }}\n            </q-item-label>\n          </div>\n        </div>\n\n        <!-- Content -->\n        <div class=\"content-area\">\n          <q-card-section class=\"q-pa-none\">\n            <div v-if=\"p2pkData.publicKey\" class=\"row justify-center q-mb-md\">\n              <div\n                class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n                style=\"max-width: 600px\"\n              >\n                <q-responsive :ratio=\"1\" class=\"q-mx-none\">\n                  <vue-qrcode\n                    :value=\"p2pkData.publicKey\"\n                    :options=\"{ width: 400 }\"\n                    class=\"rounded-borders\"\n                    style=\"width: 100%\"\n                  >\n                  </vue-qrcode>\n                </q-responsive>\n              </div>\n            </div>\n\n            <q-card-section class=\"q-pa-sm\">\n              <div v-if=\"p2pkData.used\" class=\"row justify-center q-pt-md\">\n                <q-item-label\n                  caption\n                  class=\"text-weight-bold text-warning\"\n                  style=\"font-size: 14px\"\n                >\n                  {{ $t(\"P2PKDialog.p2pk.used_warning_text\") }}\n                </q-item-label>\n              </div>\n              <div v-else class=\"row justify-center q-pt-md\">\n                <q-item-label\n                  caption\n                  class=\"text-weight-light text-white\"\n                  style=\"font-size: 14px\"\n                >\n                  {{ $t(\"P2PKDialog.p2pk.description\") }}\n                </q-item-label>\n              </div>\n\n              <!-- New key button -->\n              <div class=\"row justify-center q-pt-lg q-pb-md\">\n                <q-btn flat size=\"md\" rounded @click=\"newKeys\">\n                  <q-icon name=\"refresh\" class=\"q-pr-sm\" size=\"xs\" />\n                  {{ $t(\"P2PKDialog.actions.new_key.label\") }}\n                </q-btn>\n              </div>\n            </q-card-section>\n          </q-card-section>\n        </div>\n\n        <!-- Bottom panel action -->\n        <div class=\"bottom-panel\">\n          <div class=\"row justify-center q-pb-lg q-pt-sm\">\n            <div\n              class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n              style=\"max-width: 600px\"\n            >\n              <q-btn\n                class=\"full-width\"\n                unelevated\n                size=\"lg\"\n                color=\"primary\"\n                rounded\n                @click=\"onCopyPublicKey\"\n              >\n                {{ $t(\"P2PKDialog.actions.copy.label\") }}\n              </q-btn>\n            </div>\n          </div>\n        </div>\n      </div>\n    </q-card>\n  </q-dialog>\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport VueQrcode from \"@chenfengyuan/vue-qrcode\";\n\nimport { useP2PKStore } from \"src/stores/p2pk\";\n// type hint for global mixin\ndeclare const windowMixin: any;\n\nexport default defineComponent({\n  name: \"P2PKDialog\",\n  mixins: [windowMixin],\n  components: {\n    VueQrcode,\n  },\n  data: function () {\n    return {};\n  },\n  computed: {\n    ...mapState(useP2PKStore, [\"p2pkKeys\", \"showP2PKData\"]),\n    ...mapWritableState(useP2PKStore, [\"showP2PKDialog\"]),\n    p2pkData() {\n      return this.showP2PKData as any;\n    },\n  },\n  methods: {\n    ...mapActions(useP2PKStore, [\n      \"generateKeypair\",\n      \"showKeyDetails\",\n      \"showLastKey\",\n    ]),\n    newKeys: function () {\n      this.generateKeypair();\n      this.showLastKey();\n    },\n    onCopyPublicKey() {\n      const data = this.p2pkData;\n      if (data?.publicKey) {\n        (this as any).copyText(data.publicKey);\n      }\n    },\n  },\n});\n</script>\n\n<style scoped>\n.display-token-fullscreen {\n  height: 100vh;\n  height: 100dvh;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n.content-area {\n  flex: 1;\n  overflow-y: auto;\n}\n.floating-close-btn {\n  position: absolute;\n  left: 16px;\n  top: 50%;\n  transform: translateY(-50%);\n  z-index: 1;\n}\n.fixed-title-height {\n  height: 24px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.bottom-panel {\n  margin-top: auto;\n  background: var(--q-color-grey-1);\n  box-shadow: 0 -8px 16px rgba(0, 0, 0, 0.05);\n  padding-bottom: env(safe-area-inset-bottom, 0px);\n  position: sticky;\n  bottom: 0;\n  z-index: 2;\n}\n.qcard {\n  border-top-left-radius: 20px;\n  border-top-right-radius: 20px;\n}\n</style>\n"
  },
  {
    "path": "src/components/ParseInputComponent.vue",
    "content": "<template>\n  <div class=\"column q-mt-md\">\n    <!-- Input field with paste button -->\n    <div class=\"input-with-paste-wrapper\">\n      <q-input\n        ref=\"inputRef\"\n        filled\n        borderless\n        :class=\"[\n          'parse-input',\n          { 'has-paste-button': canPasteFromClipboard && !modelValue },\n        ]\"\n        spellcheck=\"false\"\n        autocorrect=\"off\"\n        autocapitalize=\"off\"\n        :model-value=\"modelValue\"\n        @update:model-value=\"$emit('update:modelValue', $event)\"\n        type=\"textarea\"\n        :placeholder=\"computedPlaceholder\"\n        :autofocus=\"autofocus\"\n        @keyup.enter=\"$emit('enter')\"\n      />\n      <div\n        v-if=\"canPasteFromClipboard && !modelValue\"\n        class=\"paste-text-btn\"\n        @click=\"$emit('paste')\"\n      >\n        {{ $t(\"ParseInputComponent.paste_button.label\") }}\n      </div>\n    </div>\n\n    <!-- QR Scanner row -->\n    <div v-if=\"hasCamera\" class=\"qr-scanner-row q-mt-md\" @click=\"$emit('scan')\">\n      <div class=\"row items-center no-wrap\">\n        <div class=\"qr-icon-circle\">\n          <ScanIcon :size=\"24\" />\n        </div>\n        <div class=\"col q-ml-md\">\n          <div class=\"text-body1 text-weight-medium\">\n            {{ $t(\"ParseInputComponent.qr_scanner.title\") }}\n          </div>\n          <div class=\"text-caption text-grey-6\">\n            {{ $t(\"ParseInputComponent.qr_scanner.description\") }}\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- NFC button (if supported) -->\n    <q-btn\n      v-if=\"ndefSupported\"\n      flat\n      class=\"q-mt-md\"\n      :loading=\"scanningCard\"\n      :disabled=\"scanningCard\"\n      @click=\"$emit('nfc')\"\n    >\n      <NfcIcon class=\"q-mr-sm\" :size=\"20\" />\n      {{ nfcLabel }}\n      <q-tooltip>{{ nfcTooltip }}</q-tooltip>\n      <template v-slot:loading>\n        <q-spinner />\n      </template>\n    </q-btn>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { Scan as ScanIcon, Nfc as NfcIcon } from \"lucide-vue-next\";\n\nexport default defineComponent({\n  name: \"ParseInputComponent\",\n  components: {\n    ScanIcon,\n    NfcIcon,\n  },\n  props: {\n    modelValue: {\n      type: String,\n      default: \"\",\n    },\n    placeholder: {\n      type: String,\n      default: \"\",\n    },\n    autofocus: {\n      type: Boolean,\n      default: true,\n    },\n    hasCamera: {\n      type: Boolean,\n      default: false,\n    },\n    ndefSupported: {\n      type: Boolean,\n      default: false,\n    },\n    scanningCard: {\n      type: Boolean,\n      default: false,\n    },\n    nfcLabel: {\n      type: String,\n      default: \"Scan NFC\",\n    },\n    nfcTooltip: {\n      type: String,\n      default: \"Tap to scan NFC card\",\n    },\n  },\n  emits: [\"update:modelValue\", \"enter\", \"paste\", \"scan\", \"nfc\"],\n  computed: {\n    canPasteFromClipboard() {\n      return (\n        window.isSecureContext &&\n        navigator.clipboard &&\n        navigator.clipboard.readText\n      );\n    },\n    computedPlaceholder() {\n      return (\n        this.placeholder || this.$t(\"ParseInputComponent.placeholder.default\")\n      );\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n/* Input area styles */\n.input-with-paste-wrapper {\n  position: relative;\n}\n\n.parse-input {\n  ::v-deep .q-field__control {\n    border-radius: 12px;\n    background: rgba(255, 255, 255, 0.06);\n    min-height: 120px;\n    max-height: 120px;\n    padding: 16px;\n\n    &:before,\n    &:after {\n      border: none !important;\n    }\n  }\n\n  ::v-deep .q-field__native {\n    padding: 0;\n    font-size: 16px;\n    color: white;\n    min-height: 88px;\n  }\n\n  &.has-paste-button ::v-deep .q-field__native {\n    padding: 0 70px 0 0;\n  }\n\n  ::v-deep .q-placeholder {\n    color: rgba(255, 255, 255, 0.5);\n    font-size: 16px;\n  }\n\n  ::v-deep .q-field__control:before,\n  ::v-deep .q-field__control:after {\n    display: none !important;\n  }\n\n  ::v-deep .q-field__bottom {\n    display: none;\n  }\n}\n\n.paste-text-btn {\n  position: absolute;\n  right: 20px;\n  top: 16px;\n  font-size: 16px;\n  font-weight: 500;\n  color: var(--q-primary);\n  cursor: pointer;\n  user-select: none;\n  line-height: 1.5;\n\n  &:active {\n    opacity: 0.7;\n  }\n}\n\n.qr-scanner-row {\n  background: rgba(255, 255, 255, 0.06);\n  border-radius: 12px;\n  padding: 12px 16px;\n  cursor: pointer;\n  transition: background 0.2s ease;\n\n  &:active {\n    background: rgba(255, 255, 255, 0.1);\n  }\n}\n\n.qr-icon-circle {\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n  background: rgba(255, 255, 255, 0.1);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/PayInvoiceDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"payInvoiceData.show\"\n    @hide=\"closeParseDialog\"\n    v-if=\"!camera.show\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n    transition-show=\"fade\"\n    transition-hide=\"fade\"\n    no-backdrop-dismiss\n    @keydown.esc=\"payInvoiceData.show = false\"\n  >\n    <q-card class=\"q-pa-none q-pt-none qcard\">\n      <div\n        class=\"column fit pay-fullscreen\"\n        :class=\"$q.dark.isActive ? 'bg-dark' : 'bg-white'\"\n      >\n        <!-- Header with centered title and unit toggle on right -->\n        <div class=\"row items-center q-pa-md\" style=\"position: relative\">\n          <q-btn\n            v-close-popup\n            flat\n            round\n            icon=\"close\"\n            color=\"grey\"\n            class=\"floating-close-btn\"\n          />\n          <div class=\"col text-center fixed-title-height\">\n            <div>\n              <q-item-label\n                class=\"dialog-header q-mt-sm\"\n                :class=\"$q.dark.isActive ? 'text-white' : 'text-black'\"\n                style=\"\n                  display: inline-block;\n                  white-space: normal;\n                  word-break: break-word;\n                \"\n              >\n                {{ $t(\"PayInvoiceDialog.input_data.title\") || \"Pay Lightning\" }}\n              </q-item-label>\n            </div>\n          </div>\n          <div\n            class=\"row items-center q-gutter-sm\"\n            style=\"position: absolute; right: 16px\"\n          >\n            <q-btn\n              flat\n              dense\n              size=\"lg\"\n              color=\"primary\"\n              @click=\"toggleUnit()\"\n              :label=\"activeUnitLabel\"\n            />\n          </div>\n        </div>\n\n        <!-- Mint selection (match SendTokenDialog layout) -->\n        <div class=\"row justify-center\">\n          <div\n            class=\"col-12 col-sm-11 col-md-8 q-px-lg q-mb-sm\"\n            style=\"max-width: 600px\"\n          >\n            <ChooseMint />\n          </div>\n        </div>\n\n        <!-- Content area -->\n        <div\n          class=\"col column items-center justify-start q-px-lg scroll-container\"\n        >\n          <div class=\"row justify-center full-width\">\n            <div\n              class=\"col-12 col-sm-11 col-md-8 q-px-sm q-mb-sm\"\n              style=\"max-width: 600px\"\n            >\n              <!-- INVOICE CONTENT -->\n              <div v-if=\"payInvoiceData.invoice\">\n                <div class=\"invoice-state-container\">\n                  <transition name=\"slide-down\">\n                    <div :key=\"invoiceStateKey\" class=\"invoice-state-content\">\n                      <div v-if=\"isPaid\" class=\"q-mb-md\">\n                        <div class=\"row\">\n                          <div class=\"col-12 text-h4 text-weight-bold\">\n                            {{ $t(\"PayInvoiceDialog.invoice.paid\") }}\n                            {{\n                              payInvoiceData.meltQuote.response &&\n                              payInvoiceData.meltQuote.response.amount > 0\n                                ? formatCurrency(\n                                    payInvoiceData.meltQuote.response.amount,\n                                    activeUnit,\n                                    true\n                                  )\n                                : \"\"\n                            }}\n                            <q-icon\n                              color=\"positive\"\n                              name=\"check_circle\"\n                              size=\"md\"\n                              class=\"q-mb-sm\"\n                            />\n                          </div>\n                        </div>\n                        <div\n                          v-if=\"payInvoiceData.fee_paid != null\"\n                          class=\"text-subtitle2 text-grey-6\"\n                        >\n                          {{ $t(\"PayInvoiceDialog.invoice.fee\") }}:\n                          {{\n                            formatCurrency(\n                              payInvoiceData.fee_paid,\n                              activeUnit,\n                              true\n                            )\n                          }}\n                        </div>\n                      </div>\n                      <div v-else-if=\"isPaying\" class=\"q-mb-md\">\n                        <div class=\"row\">\n                          <div class=\"col-12 text-h4 text-weight-bold q-mb-xs\">\n                            {{ $t(\"PayInvoiceDialog.invoice.paying\") }}\n                            {{\n                              payInvoiceData.meltQuote.response &&\n                              payInvoiceData.meltQuote.response.amount > 0\n                                ? formatCurrency(\n                                    payInvoiceData.meltQuote.response.amount,\n                                    activeUnit,\n                                    true\n                                  )\n                                : \"\"\n                            }}\n                            <q-spinner class=\"q-mb-sm\" />\n                          </div>\n                        </div>\n                      </div>\n                      <div\n                        v-else-if=\"\n                          payInvoiceData.meltQuote.response &&\n                          payInvoiceData.meltQuote.response.amount > 0\n                        \"\n                        class=\"q-mb-md\"\n                      >\n                        <div class=\"text-h4 text-weight-bold q-mb-xs\">\n                          <i18n-t keypath=\"PayInvoiceDialog.invoice.title\">\n                            <template v-slot:value>\n                              {{\n                                formatCurrency(\n                                  payInvoiceData.meltQuote.response.amount,\n                                  activeUnit,\n                                  true\n                                )\n                              }}\n                            </template>\n                          </i18n-t>\n                        </div>\n                        <div\n                          v-if=\"bitcoinPrice && activeUnit == 'sat'\"\n                          class=\"text-subtitle2 text-grey-6 q-ml-xs\"\n                        >\n                          {{\n                            formatCurrency(\n                              (currentCurrencyPrice / 100000000) *\n                                payInvoiceData.meltQuote.response.amount,\n                              bitcoinPriceCurrency,\n                              true\n                            )\n                          }}\n                        </div>\n                        <MeltQuoteInformation\n                          v-if=\"showMeltQuoteInformation\"\n                          class=\"q-mt-sm\"\n                          :melt-quote=\"payInvoiceData.meltQuote.response\"\n                          :mint-url=\"activeMintUrl\"\n                        />\n                        <p\n                          class=\"text-wrap q-mt-xl\"\n                          style=\"max-width: 600px; font-size: 1.1rem\"\n                          v-if=\"\n                            payInvoiceData.invoice.description &&\n                            payInvoiceData.meltQuote.response\n                          \"\n                        >\n                          <strong\n                            >{{\n                              $t(\"PayInvoiceDialog.invoice.memo.label\")\n                            }}:</strong\n                          >\n                          {{ payInvoiceData.invoice.description }}<br />\n                        </p>\n                      </div>\n                      <div v-else-if=\"payInvoiceData.meltQuote.error != ''\">\n                        <div class=\"text-h6 q-my-none\">\n                          Error: {{ payInvoiceData.meltQuote.error }}\n                        </div>\n                      </div>\n                      <div v-else>\n                        <div class=\"row\">\n                          <div class=\"col-12 text-h4 text-weight-bold q-mb-xs\">\n                            {{\n                              $t(\n                                \"PayInvoiceDialog.invoice.processing_info_text\"\n                              )\n                            }}\n                            <q-spinner />\n                          </div>\n                        </div>\n                      </div>\n                    </div>\n                  </transition>\n                </div>\n              </div>\n\n              <!-- LNURL PAY CONTENT -->\n              <div v-else-if=\"payInvoiceData.lnurlpay\">\n                <!-- Fixed amount display (when maxSendable == minSendable) -->\n                <div\n                  v-if=\"\n                    payInvoiceData.lnurlpay.maxSendable ==\n                    payInvoiceData.lnurlpay.minSendable\n                  \"\n                >\n                  <p\n                    v-if=\"payInvoiceData.lnurlpay.lightningAddress\"\n                    class=\"q-my-none text-h6 text-center\"\n                    style=\"word-break: break-all\"\n                  >\n                    <i18n-t\n                      keypath=\"PayInvoiceDialog.lnurlpay.sending_to_lightning_address\"\n                    >\n                      <template v-slot:address>\n                        <b>{{ payInvoiceData.lnurlpay.lightningAddress }}</b>\n                      </template>\n                    </i18n-t>\n                  </p>\n                  <p\n                    v-else\n                    class=\"q-my-none text-h6 text-center\"\n                    style=\"word-break: break-all\"\n                  >\n                    <i18n-t\n                      keypath=\"PayInvoiceDialog.lnurlpay.amount_exact_label\"\n                    >\n                      <template v-slot:payee>\n                        <b>{{ payInvoiceData.lnurlpay.domain }}</b>\n                      </template>\n                      <template v-slot:value>\n                        {{ payInvoiceData.lnurlpay.maxSendable / 1000 }}\n                      </template>\n                      <template v-slot:ticker>\n                        {{ tickerShort }}\n                      </template>\n                    </i18n-t>\n                  </p>\n                  <q-separator class=\"q-my-sm\"></q-separator>\n                  <div class=\"row\" v-if=\"payInvoiceData.lnurlpay.description\">\n                    <p class=\"col text-justify text-italic\">\n                      {{ payInvoiceData.lnurlpay.description }}\n                    </p>\n                    <p\n                      class=\"col-4 q-pl-md\"\n                      v-if=\"payInvoiceData.lnurlpay.image\"\n                    >\n                      <q-img :src=\"payInvoiceData.lnurlpay.image\" />\n                    </p>\n                  </div>\n                  <!-- Comment input for fixed amount -->\n                  <div\n                    class=\"row justify-center q-mt-md\"\n                    v-if=\"payInvoiceData.lnurlpay.commentAllowed > 0\"\n                  >\n                    <div\n                      class=\"col-12 col-sm-11 col-md-8 q-px-sm\"\n                      style=\"max-width: 600px\"\n                    >\n                      <q-input\n                        round\n                        outlined\n                        v-model=\"payInvoiceData.input.comment\"\n                        :type=\"\n                          payInvoiceData.lnurlpay.commentAllowed > 64\n                            ? 'textarea'\n                            : 'text'\n                        \"\n                        :label=\"\n                          $t('PayInvoiceDialog.lnurlpay.inputs.comment.label')\n                        \"\n                        :maxlength=\"payInvoiceData.lnurlpay.commentAllowed\"\n                      ></q-input>\n                    </div>\n                  </div>\n                </div>\n\n                <!-- Variable amount with AmountInputComponent -->\n                <div v-else>\n                  <p\n                    v-if=\"payInvoiceData.lnurlpay.lightningAddress\"\n                    class=\"q-my-none text-h6 text-center\"\n                    style=\"word-break: break-all\"\n                  >\n                    <i18n-t\n                      keypath=\"PayInvoiceDialog.lnurlpay.sending_to_lightning_address\"\n                    >\n                      <template v-slot:address>\n                        <b>{{ payInvoiceData.lnurlpay.lightningAddress }}</b>\n                      </template>\n                    </i18n-t>\n                  </p>\n                  <p\n                    v-else\n                    class=\"q-my-none text-h6 text-center\"\n                    style=\"word-break: break-all\"\n                  >\n                    <i18n-t\n                      keypath=\"PayInvoiceDialog.lnurlpay.amount_range_label\"\n                    >\n                      <template v-slot:payee>\n                        <b>{{\n                          payInvoiceData.lnurlpay.targetUser ||\n                          payInvoiceData.lnurlpay.domain\n                        }}</b>\n                      </template>\n                      <template v-slot:br>\n                        <br />\n                      </template>\n                      <template v-slot:min>\n                        <b>{{ payInvoiceData.lnurlpay.minSendable / 1000 }}</b>\n                      </template>\n                      <template v-slot:max>\n                        <b>{{ payInvoiceData.lnurlpay.maxSendable / 1000 }}</b>\n                      </template>\n                      <template v-slot:ticker>\n                        {{ tickerShort }}\n                      </template>\n                    </i18n-t>\n                  </p>\n\n                  <div class=\"row\" v-if=\"payInvoiceData.lnurlpay.description\">\n                    <p class=\"col text-justify text-italic\">\n                      {{ payInvoiceData.lnurlpay.description }}\n                    </p>\n                    <p\n                      class=\"col-4 q-pl-md\"\n                      v-if=\"payInvoiceData.lnurlpay.image\"\n                    >\n                      <q-img :src=\"payInvoiceData.lnurlpay.image\" />\n                    </p>\n                  </div>\n\n                  <!-- Amount display area -->\n                  <div\n                    class=\"column items-center justify-center q-px-lg q-py-lg amount-area\"\n                  >\n                    <AmountInputComponent\n                      v-model=\"payInvoiceData.input.amount\"\n                      :enabled=\"true\"\n                      :muted=\"\n                        insufficientFunds ||\n                        (payInvoiceData.lnurlpay.minSendable &&\n                          payInvoiceData.input.amount <\n                            payInvoiceData.lnurlpay.minSendable / 1000) ||\n                        (payInvoiceData.lnurlpay.maxSendable &&\n                          payInvoiceData.input.amount >\n                            payInvoiceData.lnurlpay.maxSendable / 1000)\n                      \"\n                      :max-amount=\"\n                        Math.min(\n                          payInvoiceData.lnurlpay.maxSendable / 1000,\n                          maxAmountFromBalance\n                        )\n                      \"\n                      @enter=\"handleLnurlPaySecond\"\n                      @fiat-mode-changed=\"fiatKeyboardMode = $event\"\n                    />\n                  </div>\n\n                  <!-- Comment input below amount -->\n                  <div\n                    class=\"row justify-center q-mt-md\"\n                    v-if=\"payInvoiceData.lnurlpay.commentAllowed > 0\"\n                  >\n                    <div\n                      class=\"col-12 col-sm-11 col-md-8 q-px-sm\"\n                      style=\"max-width: 600px\"\n                    >\n                      <q-input\n                        round\n                        outlined\n                        v-model=\"payInvoiceData.input.comment\"\n                        type=\"text\"\n                        :label=\"\n                          $t('PayInvoiceDialog.lnurlpay.inputs.comment.label')\n                        \"\n                        :maxlength=\"payInvoiceData.lnurlpay.commentAllowed\"\n                      ></q-input>\n                    </div>\n                  </div>\n                </div>\n              </div>\n\n              <!-- INPUT CONTENT -->\n              <div v-else>\n                <ParseInputComponent\n                  v-if=\"!camera.show\"\n                  v-model=\"payInvoiceData.input.request\"\n                  :placeholder=\"$t('ParseInputComponent.placeholder.pay')\"\n                  :has-camera=\"hasCameraAvailable\"\n                  :ndef-supported=\"false\"\n                  @update:model-value=\"decodeAndQuote($event)\"\n                  @paste=\"pasteToParseDialog\"\n                  @scan=\"showCamera\"\n                />\n              </div>\n            </div>\n          </div>\n        </div>\n\n        <!-- Bottom fixed pay action -->\n        <div class=\"bottom-panel\" v-if=\"payInvoiceData.invoice\">\n          <div class=\"row justify-center q-pb-lg q-pt-sm\">\n            <div\n              class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n              style=\"max-width: 600px\"\n            >\n              <!-- Close button when paid or error -->\n              <template v-if=\"isPaid || payInvoiceData.meltQuote.error != ''\">\n                <q-btn\n                  class=\"full-width\"\n                  unelevated\n                  size=\"lg\"\n                  color=\"primary\"\n                  rounded\n                  @click=\"closeDialog\"\n                  :label=\"$t('PayInvoiceDialog.invoice.actions.close.label')\"\n                />\n              </template>\n              <!-- Pay button when ready to pay -->\n              <template\n                v-else-if=\"\n                  enoughtotalUnitBalance ||\n                  (hasMultinutSupport && multinutEnabled) ||\n                  globalMutexLock\n                \"\n              >\n                <q-btn\n                  class=\"full-width\"\n                  unelevated\n                  size=\"lg\"\n                  color=\"primary\"\n                  rounded\n                  :disabled=\"payInvoiceData.blocking\"\n                  @click=\"handleMeltButton\"\n                  :label=\"\n                    !payInvoiceData.blocking\n                      ? $t('PayInvoiceDialog.invoice.actions.pay.label')\n                      : $t('PayInvoiceDialog.invoice.actions.pay.in_progress')\n                  \"\n                  :loading=\"globalMutexLock && !payInvoiceData.blocking\"\n                >\n                  <template v-slot:loading>\n                    <q-spinner />\n                  </template>\n                </q-btn>\n                <q-btn\n                  v-if=\"hasMultinutSupport && multinutEnabled\"\n                  class=\"full-width q-mt-sm\"\n                  flat\n                  size=\"md\"\n                  color=\"primary\"\n                  rounded\n                  @click=\"openMultinutDialog\"\n                  label=\"Pay with multiple mints\"\n                />\n              </template>\n              <!-- Balance too low -->\n              <template v-else>\n                <q-btn\n                  class=\"full-width\"\n                  unelevated\n                  size=\"lg\"\n                  color=\"yellow\"\n                  text-color=\"black\"\n                  rounded\n                  disabled\n                >\n                  {{\n                    $t(\"PayInvoiceDialog.invoice.balance_too_low_warning_text\")\n                  }}\n                </q-btn>\n              </template>\n            </div>\n          </div>\n        </div>\n\n        <!-- Bottom fixed LNURL pay action -->\n        <div\n          class=\"bottom-panel\"\n          v-if=\"\n            payInvoiceData.lnurlpay &&\n            !payInvoiceData.invoice &&\n            payInvoiceData.lnurlpay.maxSendable !=\n              payInvoiceData.lnurlpay.minSendable\n          \"\n        >\n          <div class=\"keypad-wrapper\" v-if=\"showNumericKeyboard\">\n            <NumericKeyboard\n              :force-visible=\"true\"\n              :hide-close=\"true\"\n              :hide-enter=\"true\"\n              :hide-comma=\"\n                (activeUnit === 'sat' || activeUnit === 'msat') &&\n                !fiatKeyboardMode\n              \"\n              :model-value=\"String(payInvoiceData.input.amount ?? 0)\"\n              @update:modelValue=\"\n                (val: string | number) =>\n                  (payInvoiceData.input.amount = Number(val))\n              \"\n              @done=\"handleLnurlPaySecond\"\n            />\n          </div>\n          <!-- LNURL pay action below keyboard -->\n          <div class=\"row justify-center q-pb-lg q-pt-sm\">\n            <div\n              class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n              style=\"max-width: 600px\"\n            >\n              <q-btn\n                class=\"full-width\"\n                unelevated\n                size=\"lg\"\n                color=\"primary\"\n                rounded\n                @click=\"handleLnurlPaySecond\"\n                :disabled=\"\n                  payInvoiceData.blocking ||\n                  payInvoiceData.input.amount == null ||\n                  payInvoiceData.input.amount <= 0 ||\n                  payInvoiceData.input.amount <\n                    payInvoiceData.lnurlpay.minSendable / 1000 ||\n                  payInvoiceData.input.amount >\n                    payInvoiceData.lnurlpay.maxSendable / 1000 ||\n                  insufficientFunds\n                \"\n                :loading=\"payInvoiceData.blocking\"\n              >\n                {{\n                  payInvoiceData.blocking\n                    ? $t(\"PayInvoiceDialog.lnurlpay.actions.send.in_progress\")\n                    : $t(\"PayInvoiceDialog.lnurlpay.actions.send.label\")\n                }}\n                <template v-slot:loading>\n                  <q-spinner />\n                </template>\n              </q-btn>\n            </div>\n          </div>\n        </div>\n\n        <!-- Bottom fixed LNURL pay action (fixed amount, no keyboard) -->\n        <div\n          class=\"bottom-panel\"\n          v-if=\"\n            payInvoiceData.lnurlpay &&\n            !payInvoiceData.invoice &&\n            payInvoiceData.lnurlpay.maxSendable ==\n              payInvoiceData.lnurlpay.minSendable\n          \"\n        >\n          <div class=\"row justify-center q-pb-lg q-pt-sm\">\n            <div\n              class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n              style=\"max-width: 600px\"\n            >\n              <q-btn\n                class=\"full-width\"\n                unelevated\n                size=\"lg\"\n                color=\"primary\"\n                rounded\n                @click=\"handleLnurlPaySecond\"\n                :disabled=\"payInvoiceData.blocking\"\n                :loading=\"payInvoiceData.blocking\"\n              >\n                {{\n                  payInvoiceData.blocking\n                    ? $t(\"PayInvoiceDialog.lnurlpay.actions.send.in_progress\")\n                    : $t(\"PayInvoiceDialog.lnurlpay.actions.send.label\")\n                }}\n                <template v-slot:loading>\n                  <q-spinner />\n                </template>\n              </q-btn>\n            </div>\n          </div>\n        </div>\n      </div>\n    </q-card>\n  </q-dialog>\n\n  <!-- Multinut Payment Dialog -->\n  <MultinutPaymentDialog\n    ref=\"multinutDialog\"\n    @return-to-pay-dialog=\"handleReturnToPayDialog\"\n  />\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { useCameraStore } from \"src/stores/camera\";\nimport { useMintsStore, MintClass } from \"src/stores/mints\";\nimport { useSettingsStore } from \"src/stores/settings\";\nimport { usePriceStore } from \"src/stores/price\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport ChooseMint from \"components/ChooseMint.vue\";\nimport MultinutPaymentDialog from \"./MultinutPaymentDialog.vue\";\nimport MeltQuoteInformation from \"components/MeltQuoteInformation.vue\";\nimport NumericKeyboard from \"components/NumericKeyboard.vue\";\nimport AmountInputComponent from \"components/AmountInputComponent.vue\";\nimport ParseInputComponent from \"components/ParseInputComponent.vue\";\n\nimport * as _ from \"underscore\";\nimport { Proof } from \"@cashu/cashu-ts\";\n\ndeclare const windowMixin: any;\n\nexport default defineComponent({\n  name: \"PayInvoiceDialog\",\n  mixins: [windowMixin],\n  components: {\n    ChooseMint,\n    MultinutPaymentDialog,\n    MeltQuoteInformation,\n    NumericKeyboard,\n    AmountInputComponent,\n    ParseInputComponent,\n  },\n  props: {},\n  data: function () {\n    return {\n      fiatKeyboardMode: false as boolean,\n      isPaying: false as boolean,\n      isPaid: false as boolean,\n      autoCloseTimeout: null as ReturnType<typeof setTimeout> | null,\n    };\n  },\n  watch: {\n    activeMintUrl: async function () {\n      if (this.payInvoiceData.show && this.payInvoiceData.invoice) {\n        await this.meltQuoteInvoiceData();\n      }\n    },\n    activeUnit: async function () {\n      if (this.payInvoiceData.show && this.payInvoiceData.invoice) {\n        await this.meltQuoteInvoiceData();\n      }\n    },\n    \"payInvoiceData.lnurlpay\": {\n      handler: function (newVal) {\n        if (newVal && newVal.maxSendable == newVal.minSendable) {\n          // Set fixed amount\n          this.payInvoiceData.input.amount = newVal.minSendable / 1000;\n          this.showNumericKeyboard = false;\n        } else {\n          this.showNumericKeyboard = true;\n        }\n      },\n      immediate: true,\n    },\n    \"payInvoiceData.show\": {\n      handler: async function (val, oldVal) {\n        // Intercept automatic close after successful payment\n        if (\n          !val &&\n          oldVal &&\n          this.isPaying &&\n          !this.payInvoiceData.meltQuote.error\n        ) {\n          // Payment just succeeded and store is trying to close dialog\n          // Re-open it to show success state\n          this.isPaying = false;\n          this.isPaid = true;\n          this.payInvoiceData.show = true; // Prevent close\n\n          // Wait 2 seconds then allow close\n          this.autoCloseTimeout = setTimeout(() => {\n            if (this.isPaid) {\n              // Only auto-close if still in paid state (user hasn't manually closed)\n              this.isPaid = false;\n              this.payInvoiceData.show = false;\n            }\n            this.autoCloseTimeout = null;\n          }, 10000);\n        } else if (!val) {\n          // Normal close - reset states and clear any pending timeout\n          if (this.autoCloseTimeout) {\n            clearTimeout(this.autoCloseTimeout);\n            this.autoCloseTimeout = null;\n          }\n          this.showNumericKeyboard = false;\n          this.isPaying = false;\n          this.isPaid = false;\n        }\n      },\n    },\n  },\n  computed: {\n    ...mapState(useUiStore, [\"tickerShort\", \"globalMutexLock\"]),\n    ...mapState(useSettingsStore, [\"multinutEnabled\"]),\n    ...mapWritableState(useCameraStore, [\"camera\", \"hasCamera\"]),\n    ...mapWritableState(useUiStore, [\"showNumericKeyboard\"]),\n    // mints store via direct getters to avoid strict typing issues\n    activeMintUrl: function (): any {\n      return (useMintsStore() as any).activeMintUrl;\n    },\n    activeProofs: function (): any {\n      return (useMintsStore() as any).activeProofs;\n    },\n    mints: function (): any {\n      return (useMintsStore() as any).mints;\n    },\n    activeUnit: function (): any {\n      return (useMintsStore() as any).activeUnit;\n    },\n    activeUnitCurrencyMultiplyer: function (): any {\n      return (useMintsStore() as any).activeUnitCurrencyMultiplyer;\n    },\n    totalUnitBalance: function (): any {\n      return (useMintsStore() as any).totalUnitBalance;\n    },\n    activeBalance: function (): any {\n      return (useMintsStore() as any).activeBalance;\n    },\n    multiMints: function (): any {\n      return (useMintsStore() as any).multiMints;\n    },\n    ...mapState(usePriceStore, [\n      \"bitcoinPrice\",\n      \"bitcoinPrices\",\n      \"currentCurrencyPrice\",\n    ]),\n    ...mapState(useSettingsStore, [\"bitcoinPriceCurrency\"]),\n    payInvoiceData: function (): any {\n      return (useWalletStore() as any).payInvoiceData;\n    },\n    hasCameraAvailable: function (): boolean {\n      return Boolean((useCameraStore() as any).hasCamera);\n    },\n    showMeltQuoteInformation: function (): boolean {\n      const quote = this.payInvoiceData?.meltQuote?.response;\n      if (!quote) {\n        return false;\n      }\n      if (!quote.quote) {\n        return false;\n      }\n      const hasAmount = typeof quote.amount === \"number\" && quote.amount > 0;\n      const hasFeeReserve =\n        typeof quote.fee_reserve === \"number\" && quote.fee_reserve > 0;\n      const feePaidRaw = (quote as any).fee_paid;\n      const hasFeePaid =\n        (typeof feePaidRaw === \"number\" && Number.isFinite(feePaidRaw)) ||\n        (typeof feePaidRaw === \"string\" && feePaidRaw.trim() !== \"\");\n      const paidRaw =\n        (quote as any).paid_at ??\n        (quote as any).paid_timestamp ??\n        (quote as any).paid_time ??\n        (quote as any).paid;\n      const hasPaidTimestamp =\n        paidRaw !== undefined &&\n        paidRaw !== null &&\n        typeof paidRaw !== \"boolean\";\n      return hasAmount || hasFeeReserve || hasFeePaid || hasPaidTimestamp;\n    },\n    enoughtotalUnitBalance: function () {\n      return (\n        this.activeBalance >= this.payInvoiceData.meltQuote.response.amount\n      );\n    },\n    hasMultinutSupport: function (): boolean {\n      const totalMultinutBalance = this.multiMints.reduce(\n        (acc: number, mint: any) =>\n          acc + new MintClass(mint).unitBalance(this.activeUnit),\n        0\n      );\n      return (\n        this.multiMints.length > 1 &&\n        this.payInvoiceData.meltQuote.response.amount > 0 &&\n        totalMultinutBalance >= this.payInvoiceData.meltQuote.response.amount\n      );\n    },\n    activeUnitLabel: function (): string {\n      // Access directly from store to avoid typing friction in mapState\n      return (useMintsStore() as any).activeUnitLabel;\n    },\n    insufficientFunds: function (): boolean {\n      if (\n        !this.payInvoiceData.lnurlpay ||\n        this.payInvoiceData.input.amount == null\n      ) {\n        return false;\n      }\n      return (\n        this.activeBalance <\n        this.payInvoiceData.input.amount * this.activeUnitCurrencyMultiplyer\n      );\n    },\n    maxAmountFromBalance: function (): number {\n      if (!this.payInvoiceData.lnurlpay) return Infinity;\n      // Convert balance to the unit being displayed (e.g., sats -> BTC)\n      const balanceInDisplayUnit =\n        this.activeBalance / this.activeUnitCurrencyMultiplyer;\n      return balanceInDisplayUnit;\n    },\n    invoiceStateKey: function (): string {\n      if (this.isPaid) {\n        return \"paid\";\n      } else if (this.isPaying) {\n        return \"paying\";\n      } else if (\n        this.payInvoiceData.meltQuote.response &&\n        this.payInvoiceData.meltQuote.response.amount > 0\n      ) {\n        return \"success\";\n      } else if (this.payInvoiceData.meltQuote.error != \"\") {\n        return \"error\";\n      } else {\n        return \"processing\";\n      }\n    },\n  },\n  methods: {\n    ...mapActions(useWalletStore, [\n      \"meltInvoiceData\",\n      \"meltQuoteInvoiceData\",\n      \"decodeRequest\",\n      \"lnurlPaySecond\",\n    ]),\n    ...mapActions(useMintsStore, [\"toggleUnit\"]),\n    ...mapActions(useCameraStore, [\"closeCamera\", \"showCamera\"]),\n    formatCurrency(value: number, currency: string, showBalance = false) {\n      return useUiStore().formatCurrency(value, currency, showBalance);\n    },\n    canPay: function () {\n      if (!this.payInvoiceData.invoice) return false;\n      return (\n        this.payInvoiceData.meltQuote.response.amount +\n          this.payInvoiceData.meltQuote.response.fee_reserve <=\n        this.totalUnitBalance\n      );\n    },\n    closeParseDialog: function () {},\n    closeDialog: function () {\n      // Clear any pending auto-close timeout\n      if (this.autoCloseTimeout) {\n        clearTimeout(this.autoCloseTimeout);\n        this.autoCloseTimeout = null;\n      }\n      this.payInvoiceData.show = false;\n      this.isPaying = false;\n      this.isPaid = false;\n    },\n    decodeAndQuote: async function (request: string) {\n      await this.decodeRequest(request);\n    },\n    pasteToParseDialog: async function () {\n      console.log(\"pasteToParseDialog\");\n      const text = await useUiStore().pasteFromClipboard();\n      if (text) {\n        this.payInvoiceData.input.request = text.trim();\n        await this.decodeAndQuote(text.trim());\n      }\n    },\n    handleMeltButton: async function () {\n      if (this.payInvoiceData.blocking) {\n        throw new Error(\"already processing an invoice.\");\n      }\n      if (\n        !this.enoughtotalUnitBalance &&\n        this.hasMultinutSupport &&\n        this.multinutEnabled\n      ) {\n        this.openMultinutDialog();\n        return;\n      }\n      this.isPaying = true;\n      try {\n        const result = await this.meltInvoiceData(true);\n        const returnedChange = result.change.reduce(\n          (acc: number, p: Proof) => acc + p.amount,\n          0\n        );\n        this.payInvoiceData.fee_paid =\n          this.payInvoiceData.meltQuote.response.fee_reserve - returnedChange;\n        console.log(\"### fee_paid\", this.payInvoiceData.fee_paid);\n        // Success state and closing is handled by the watcher on payInvoiceData.show\n      } catch (error) {\n        // Error handling is done in the store, but we ensure isPaying is reset\n        console.error(\"Payment error:\", error);\n        this.isPaying = false;\n      }\n    },\n    openMultinutDialog: function () {\n      this.payInvoiceData.show = false;\n      (this.$refs as any).multinutDialog.openMultinutDialog();\n    },\n    handleReturnToPayDialog: function () {\n      this.payInvoiceData.show = true;\n    },\n    handleLnurlPaySecond: async function () {\n      // Hide keyboard before sending payment\n      this.showNumericKeyboard = false;\n      await this.lnurlPaySecond();\n    },\n  },\n  created: function () {},\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.fixed-title-height {\n  height: 24px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.pay-fullscreen {\n  height: 100vh;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n\n.bottom-panel {\n  margin-top: auto;\n  background: var(--q-color-grey-1);\n  padding-bottom: env(safe-area-inset-bottom, 0px);\n}\n\n.q-dialog__inner > div {\n  border-top-left-radius: 0px;\n  border-top-right-radius: 0px;\n}\n\n.qcard {\n  border-top-left-radius: 20px;\n  border-top-right-radius: 20px;\n}\n\n.cashub-nowrap {\n  word-break: break-all;\n  -webkit-hyphens: none;\n  -moz-hyphens: none;\n  hyphens: none;\n  font-size: 0.9em;\n  font-family: monospace;\n}\n\n.token-input {\n  // rounded input border\n  height: 180px;\n  border-radius: 10px;\n  border: 1px solid $grey-2;\n  padding: 10px 20px;\n  font-size: 14px;\n  font-family: monospace;\n  background-color: var(--q-background);\n  color: var(--q-text);\n  text-decoration: none !important;\n  &:focus {\n    border-color: $primary;\n  }\n  // hide inner border from q-input\n  &::before {\n    display: none;\n  }\n  &::after {\n    display: none;\n  }\n  &:disabled {\n    background-color: var(--q-background-disabled);\n    color: var(--q-text-disabled);\n  }\n  &:hover {\n    background-color: var(--q-background-hover);\n    color: var(--q-text-hover);\n  }\n}\n\n.scroll-container {\n  overflow-y: auto;\n  overflow-x: hidden;\n}\n\n.relative-container {\n  position: relative;\n}\n\n.floating-button {\n  position: absolute;\n  top: 10px;\n  right: 0px;\n  z-index: 100;\n  padding: 1px;\n  background-color: var(--q-primary);\n  border-radius: 50%;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n}\n\n.floating-close-btn {\n  position: absolute;\n  left: 16px;\n  top: 50%;\n  transform: translateY(-50%);\n  z-index: 1;\n}\n\n.amount-area {\n  flex: 1;\n  position: relative;\n}\n\n.keypad-wrapper {\n  width: 100%;\n  display: flex;\n  justify-content: center;\n}\n\n.invoice-state-container {\n  position: relative;\n  min-height: 100px;\n}\n\n.invoice-state-content {\n  width: 100%;\n}\n\n.slide-down-enter-active {\n  transition: all 0.4s ease;\n  z-index: 2;\n}\n\n.slide-down-leave-active {\n  transition: all 0.4s ease;\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  z-index: 1;\n}\n\n.slide-down-enter-from {\n  opacity: 0;\n  transform: translateY(-20px);\n}\n\n.slide-down-leave-to {\n  opacity: 0;\n  transform: translateY(20px);\n}\n</style>\n"
  },
  {
    "path": "src/components/PaymentRequestDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showPRDialog\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n    transition-show=\"fade\"\n    transition-hide=\"fade\"\n    no-backdrop-dismiss\n  >\n    <q-card v-if=\"showPRKData\" class=\"q-pa-none qcard\">\n      <div\n        :class=\"$q.dark.isActive ? 'bg-dark' : 'bg-white'\"\n        class=\"display-token-fullscreen\"\n      >\n        <!-- Header -->\n        <div class=\"row items-center q-pa-md\" style=\"position: relative\">\n          <q-btn\n            v-close-popup\n            flat\n            round\n            icon=\"close\"\n            color=\"grey\"\n            class=\"floating-close-btn\"\n          />\n          <div class=\"col text-center fixed-title-height\">\n            <q-item-label class=\"dialog-header q-mt-sm text-white\">\n              {{ $t(\"PaymentRequestDialog.payment_request.caption\") }}\n            </q-item-label>\n          </div>\n          <div\n            class=\"row items-center no-wrap\"\n            style=\"position: absolute; right: 12px\"\n          >\n            <q-btn\n              flat\n              dense\n              round\n              icon=\"chevron_left\"\n              color=\"grey\"\n              @click=\"selectPrevRequest\"\n              :disable=\"ourPaymentRequests.length <= 1\"\n            />\n            <div class=\"q-mx-sm text-caption text-grey-6\">\n              {{ currentIndexDisplay }}\n            </div>\n            <q-btn\n              flat\n              dense\n              round\n              icon=\"chevron_right\"\n              color=\"grey\"\n              @click=\"selectNextRequest\"\n              :disable=\"ourPaymentRequests.length <= 1\"\n            />\n          </div>\n        </div>\n\n        <!-- Content -->\n        <div class=\"content-area\">\n          <q-card-section class=\"q-pa-none\">\n            <div v-if=\"showPRKData\" class=\"row justify-center q-mb-md\">\n              <div\n                class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n                style=\"max-width: 600px\"\n              >\n                <q-responsive :ratio=\"1\" class=\"q-mx-none\">\n                  <vue-qrcode\n                    :value=\"showPRKData\"\n                    :options=\"{ width: 400 }\"\n                    class=\"rounded-borders\"\n                    style=\"width: 100%\"\n                  >\n                  </vue-qrcode>\n                </q-responsive>\n              </div>\n            </div>\n\n            <q-card-section class=\"q-pa-sm\">\n              <!-- Amount display/edit -->\n              <div class=\"row justify-center q-pt-md\">\n                <div v-if=\"!isEditingAmount\">\n                  <q-btn\n                    color=\"primary\"\n                    rounded\n                    size=\"md\"\n                    @click=\"startEditingAmount\"\n                  >\n                    <q-icon name=\"edit_note\" size=\"xs\" class=\"q-mr-sm\" />\n                    {{ amountLabel }}\n                  </q-btn>\n                </div>\n                <div v-else class=\"col-12 col-sm-8 col-md-6\">\n                  <q-input\n                    ref=\"amountInput\"\n                    v-model=\"amountInputValue\"\n                    type=\"number\"\n                    :placeholder=\"\n                      $t('PaymentRequestDialog.inputs.amount.placeholder')\n                    \"\n                    @blur=\"finishEditingAmount\"\n                    @keyup.enter=\"finishEditingAmount\"\n                  ></q-input>\n                </div>\n              </div>\n\n              <!-- Mint selection and unit toggle -->\n              <div class=\"row justify-center q-pt-md\">\n                <div class=\"row no-wrap items-center q-gutter-sm\">\n                  <q-chip\n                    outline\n                    clickable\n                    class=\"q-pa-md\"\n                    :style=\"\n                      chosenMintUrl == undefined\n                        ? ''\n                        : 'height: 36px; font-family: monospace'\n                    \"\n                    @click=\"setActiveMintUrl\"\n                  >\n                    <q-icon name=\"account_balance\" size=\"xs\" class=\"q-mr-sm\" />\n                    {{ getShortUrl(chosenMintUrl) }}\n                  </q-chip>\n                  <div @click=\"toggleUnit\">\n                    <ToggleUnit class=\"q-py-none\" color=\"white\" />\n                  </div>\n                </div>\n              </div>\n\n              <!-- Received payments summary and list -->\n              <div\n                class=\"row justify-center q-pt-md q-pb-md\"\n                v-if=\"currentPaymentRequest\"\n              >\n                <div\n                  class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n                  style=\"max-width: 600px\"\n                >\n                  <div class=\"row q-gutter-sm items-center q-mb-sm\">\n                    <q-chip\n                      outline\n                      v-for=\"(total, unit) in totalsByUnit\"\n                      :key=\"unit\"\n                      color=\"primary\"\n                      text-color=\"primary\"\n                    >\n                      {{ $t(\"PaymentRequestDialog.received_total\") }}:\n                      <span class=\"q-ml-xs text-weight-medium\">{{\n                        total\n                      }}</span>\n                      <span class=\"q-ml-xs\">{{ unit }}</span>\n                    </q-chip>\n                  </div>\n                  <PaymentRequestPayments\n                    :payments=\"currentPayments\"\n                    :page-size=\"3\"\n                  />\n                </div>\n              </div>\n\n              <!-- New request button -->\n              <div class=\"row justify-center q-pt-lg q-pb-md\">\n                <q-btn flat size=\"md\" rounded @click=\"newRequest\">\n                  <q-icon name=\"refresh\" class=\"q-pr-sm\" size=\"xs\" />\n                  {{ $t(\"PaymentRequestDialog.actions.new_request.label\") }}\n                </q-btn>\n              </div>\n            </q-card-section>\n          </q-card-section>\n        </div>\n\n        <!-- Bottom panel action -->\n        <div class=\"bottom-panel\">\n          <div class=\"row justify-center q-pb-lg q-pt-sm\">\n            <div\n              class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n              style=\"max-width: 600px\"\n            >\n              <q-btn\n                class=\"full-width\"\n                unelevated\n                size=\"lg\"\n                color=\"primary\"\n                rounded\n                @click=\"onCopyPRKData\"\n              >\n                {{ $t(\"PaymentRequestDialog.actions.copy.label\") }}\n              </q-btn>\n            </div>\n          </div>\n        </div>\n      </div>\n    </q-card>\n  </q-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport VueQrcode from \"@chenfengyuan/vue-qrcode\";\n\nimport { usePRStore } from \"src/stores/payment-request\";\nimport { useMintsStore } from \"../stores/mints\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport { useUiStore } from \"../stores/ui\";\nimport ToggleUnit from \"./ToggleUnit.vue\";\nimport PaymentRequestPayments from \"./PaymentRequestPayments.vue\";\n// type hint for global mixin\ndeclare const windowMixin: any;\n\nexport default defineComponent({\n  name: \"PRDialog\",\n  mixins: [windowMixin],\n  components: {\n    VueQrcode,\n    ToggleUnit,\n    PaymentRequestPayments,\n  },\n  data() {\n    const amountLabelDefault = (this as any).$i18n.t(\n      \"PaymentRequestDialog.actions.add_amount.label\"\n    );\n    return {\n      paymentRequestAmount: undefined as number | undefined,\n      isEditingAmount: false,\n      amountInputValue: \"\",\n      amountLabelDefault,\n      amountLabel: amountLabelDefault,\n      defaultAnyMint: (this as any).$i18n.t(\n        \"PaymentRequestDialog.actions.use_active_mint.label\"\n      ),\n      chosenMintUrl: undefined as string | undefined,\n      memo: \"\",\n    };\n  },\n  computed: {\n    ...mapState(usePRStore, [\n      \"showPRKData\",\n      \"ourPaymentRequests\",\n      \"selectedPRIndex\",\n      \"currentPaymentRequest\",\n    ]),\n    ...mapState(useMintsStore, [\"activeMintUrl\", \"activeUnit\"]),\n    activeUnitCurrencyMultiplyer() {\n      return (useMintsStore() as any).activeUnitCurrencyMultiplyer;\n    },\n    ...mapWritableState(usePRStore, [\"showPRDialog\"]),\n    currentIndexDisplay(): string {\n      const total = this.ourPaymentRequests.length || 0;\n      if (!total) return \"0/0\";\n      return `${(this.selectedPRIndex as number) + 1}/${total}`;\n    },\n    currentPayments(): any[] {\n      const prStore = usePRStore();\n      if (!this.currentPaymentRequest) return [];\n      return prStore.getPaymentsForRequest(this.currentPaymentRequest.id);\n    },\n    totalsByUnit(): Record<string, number> {\n      const totals: Record<string, number> = {};\n      for (const p of this.currentPayments) {\n        if (p.amount > 0) {\n          totals[p.unit] = (totals[p.unit] ?? 0) + p.amount;\n        }\n      }\n      return totals;\n    },\n  },\n  methods: {\n    ...mapActions(usePRStore, [\n      \"newPaymentRequest\",\n      \"selectPrevRequest\",\n      \"selectNextRequest\",\n    ]),\n    toggleUnit() {\n      this.paymentRequestAmount = undefined;\n      this.amountLabel = this.amountLabelDefault;\n      this.newPaymentRequest(\n        this.paymentRequestAmount,\n        this.memo,\n        this.chosenMintUrl\n      );\n    },\n    newRequest() {\n      this.newPaymentRequest(\n        this.paymentRequestAmount,\n        this.memo,\n        this.chosenMintUrl,\n        true\n      );\n    },\n    getShortUrl(url: string | undefined) {\n      if (!url) {\n        return this.defaultAnyMint;\n      }\n      return getShortUrl(url);\n    },\n    setActiveMintUrl() {\n      if (this.activeMintUrl == this.chosenMintUrl) {\n        return;\n      }\n      this.chosenMintUrl = this.activeMintUrl;\n      this.newPaymentRequest(\n        this.paymentRequestAmount,\n        this.memo,\n        this.chosenMintUrl\n      );\n    },\n    startEditingAmount() {\n      this.isEditingAmount = true;\n      this.$nextTick(() => {\n        const input = this.$refs.amountInput as any;\n        if (input) {\n          input.focus();\n        }\n      });\n    },\n    onCopyPRKData() {\n      if (this.showPRKData) {\n        (this as any).copyText(this.showPRKData);\n      }\n    },\n    finishEditingAmount() {\n      const amount = parseFloat(this.amountInputValue);\n      if (isNaN(amount) || amount <= 0 || this.amountInputValue == \"\") {\n        this.paymentRequestAmount = undefined;\n        this.amountLabel = this.amountLabelDefault;\n      } else {\n        this.paymentRequestAmount = amount * this.activeUnitCurrencyMultiplyer;\n        this.amountLabel = useUiStore().formatCurrency(\n          amount * this.activeUnitCurrencyMultiplyer,\n          this.activeUnit\n        );\n      }\n      this.newPaymentRequest(\n        this.paymentRequestAmount,\n        this.memo,\n        this.chosenMintUrl\n      );\n      this.isEditingAmount = false;\n      this.amountInputValue = \"\";\n    },\n  },\n});\n</script>\n\n<style scoped>\n.display-token-fullscreen {\n  height: 100vh;\n  height: 100dvh;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n.content-area {\n  flex: 1;\n  overflow-y: auto;\n}\n.floating-close-btn {\n  position: absolute;\n  left: 16px;\n  top: 50%;\n  transform: translateY(-50%);\n  z-index: 1;\n}\n.fixed-title-height {\n  height: 24px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.bottom-panel {\n  margin-top: auto;\n  background: var(--q-color-grey-1);\n  box-shadow: 0 -8px 16px rgba(0, 0, 0, 0.05);\n  padding-bottom: env(safe-area-inset-bottom, 0px);\n  position: sticky;\n  bottom: 0;\n  z-index: 2;\n}\n.qcard {\n  border-top-left-radius: 20px;\n  border-top-right-radius: 20px;\n}\n</style>\n"
  },
  {
    "path": "src/components/PaymentRequestInfo.vue",
    "content": "<template>\n  <div class=\"payment-request-info\">\n    <div class=\"row items-center no-wrap\">\n      <div class=\"icon-circle\">\n        <SendIcon :size=\"20\" />\n      </div>\n      <div class=\"col q-ml-md\">\n        <div class=\"text-body1 text-weight-medium\">\n          {{ infoTitle }}\n        </div>\n        <div class=\"text-caption text-grey-6\">\n          {{ infoSubtitle }}\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from \"vue\";\nimport { PaymentRequest, PaymentRequestTransportType } from \"@cashu/cashu-ts\";\nimport { Send as SendIcon } from \"lucide-vue-next\";\n\nexport default defineComponent({\n  name: \"PaymentRequestInfo\",\n  components: {\n    SendIcon,\n  },\n  props: {\n    request: {\n      type: Object as PropType<PaymentRequest | undefined>,\n      required: false,\n      default: undefined,\n    },\n  },\n  computed: {\n    infoTitle(): string {\n      const transport = this.transportLabel;\n      if (transport) {\n        return this.$t(\"PaymentRequestInfo.title_with_transport\", {\n          transport,\n        }) as string;\n      }\n      return this.$t(\"PaymentRequestInfo.title\") as string;\n    },\n    infoSubtitle(): string {\n      const target = this.request\n        ? this.getPaymentRequestTarget(this.request)\n        : \"\";\n      if (!target) {\n        return this.$t(\"PaymentRequestInfo.subtitle_fallback\") as string;\n      }\n      return this.$t(\"PaymentRequestInfo.subtitle\", {\n        target,\n      }) as string;\n    },\n    transportLabel(): string {\n      if (!this.request || !this.request.transport) {\n        return \"\";\n      }\n      for (const transport of this.request.transport) {\n        if (transport.type === PaymentRequestTransportType.NOSTR) {\n          return \"Nostr\";\n        }\n        if (transport.type === PaymentRequestTransportType.POST) {\n          return \"HTTP\";\n        }\n      }\n      return \"\";\n    },\n  },\n  methods: {\n    getPaymentRequestTarget(request: PaymentRequest): string {\n      for (const transport of request.transport) {\n        if (transport.type === PaymentRequestTransportType.NOSTR) {\n          if (!transport.target) {\n            return \"\";\n          }\n          return `${transport.target.slice(0, 20)}..${transport.target.slice(\n            -10\n          )}`;\n        }\n        if (transport.type === PaymentRequestTransportType.POST) {\n          try {\n            const url = new URL(transport.target);\n            return url.hostname;\n          } catch (error) {\n            console.error(\n              `Invalid URL in transport.target: ${transport.target}`,\n              error\n            );\n            return this.$t(\"PaymentRequestInfo.invalid_url\") as string;\n          }\n        }\n      }\n      return \"\";\n    },\n  },\n});\n</script>\n\n<style scoped>\n.payment-request-info {\n  background: rgba(255, 255, 255, 0.06);\n  border-radius: 12px;\n  padding: 12px 16px;\n  margin-top: 12px;\n  transition: background 0.2s ease;\n}\n\n.payment-request-info:active {\n  background: rgba(255, 255, 255, 0.1);\n}\n\n.icon-circle {\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n  background: rgba(255, 255, 255, 0.1);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/PaymentRequestPayments.vue",
    "content": "<template>\n  <div class=\"q-pa-none\">\n    <div v-if=\"payments.length === 0\" class=\"text-center q-mt-sm\">\n      <q-item-label caption class=\"text-grey-6\">\n        {{ $t(\"PaymentRequestDialog.no_payments_yet\") }}\n      </q-item-label>\n    </div>\n    <q-list v-else>\n      <q-item\n        v-for=\"payment in paginatedPayments\"\n        :key=\"payment.id\"\n        clickable\n        v-ripple\n        class=\"q-px-md q-py-md\"\n        @click=\"showTokenDialog(payment)\"\n      >\n        <q-item-section avatar class=\"q-pr-md\" style=\"min-width: 40px\">\n          <q-avatar size=\"28px\">\n            <CoinsIcon class=\"transaction-icon\" />\n          </q-avatar>\n        </q-item-section>\n        <q-item-section>\n          <q-item-label class=\"row items-center justify-between\">\n            <div class=\"col text-left\">\n              <span class=\"transaction-label text-weight-medium\">\n                {{ payment.label || \"Ecash\" }}\n              </span>\n            </div>\n            <div class=\"text-right\">\n              <div class=\"amount-text text-weight-bold\">\n                +{{ formatCurrency(payment.amount, payment.unit) }}\n              </div>\n            </div>\n          </q-item-label>\n          <q-item-label caption class=\"text-grey-6\">\n            {{ formattedDate(payment.date) }}\n          </q-item-label>\n        </q-item-section>\n      </q-item>\n    </q-list>\n    <div v-if=\"maxPages > 1\" class=\"text-center q-mt-sm\">\n      <q-pagination\n        v-model=\"currentPage\"\n        :max=\"maxPages\"\n        :max-pages=\"5\"\n        direction-links\n        boundary-links\n      />\n    </div>\n  </div>\n</template>\n<script lang=\"ts\">\nimport { defineComponent, PropType } from \"vue\";\nimport { mapWritableState } from \"pinia\";\nimport { HistoryToken } from \"src/stores/tokens\";\nimport { formatDistanceToNow, parseISO } from \"date-fns\";\nimport token from \"src/js/token\";\nimport { useSendTokensStore } from \"src/stores/sendTokensStore\";\nimport { Coins as CoinsIcon } from \"lucide-vue-next\";\n\nexport default defineComponent({\n  name: \"PaymentRequestPayments\",\n  components: { CoinsIcon },\n  props: {\n    payments: {\n      type: Array as PropType<HistoryToken[]>,\n      required: true,\n      default: () => [],\n    },\n    pageSize: {\n      type: Number,\n      required: false,\n      default: 3,\n    },\n  },\n  data() {\n    return {\n      currentPage: 1,\n    };\n  },\n  computed: {\n    ...mapWritableState(useSendTokensStore, [\"showSendTokens\", \"sendData\"]),\n    maxPages(): number {\n      return Math.ceil(this.payments.length / this.pageSize) || 1;\n    },\n    paginatedPayments(): HistoryToken[] {\n      const start = (this.currentPage - 1) * this.pageSize;\n      const end = start + this.pageSize;\n      return this.payments.slice(start, end);\n    },\n  },\n  methods: {\n    formattedDate(date_str: string) {\n      const date = parseISO(date_str);\n      return formatDistanceToNow(date, { addSuffix: false });\n    },\n    formatCurrency(amount: number, unit: string) {\n      // delegate to global mixin formatter if available\n      try {\n        // @ts-ignore\n        return this.$root?.$?.appContext.config.globalProperties.formatCurrency\n          ? // @ts-ignore\n            this.$root.$.appContext.config.globalProperties.formatCurrency(\n              amount,\n              unit\n            )\n          : `${amount} ${unit}`;\n      } catch {\n        return `${amount} ${unit}`;\n      }\n    },\n    showTokenDialog(historyToken: HistoryToken) {\n      if (historyToken.token === undefined) {\n        return;\n      }\n      const tokensBase64 = historyToken.token;\n      const tokenObj = token.decode(tokensBase64);\n      this.sendData.tokens = token.getProofs(tokenObj) as any;\n      this.sendData.tokensBase64 = tokensBase64;\n      this.sendData.paymentRequest = historyToken.paymentRequest;\n      this.sendData.historyAmount = historyToken.amount;\n      this.sendData.historyToken = historyToken as any;\n      this.showSendTokens = true;\n    },\n  },\n});\n</script>\n<style scoped>\n.transaction-label {\n  border-radius: 4px;\n  font-size: 0.95rem;\n}\n.transaction-icon {\n  width: 18px;\n  height: 18px;\n  color: var(--q-primary);\n}\n.amount-text {\n  font-size: 0.95rem;\n  line-height: 1.2;\n}\n</style>\n"
  },
  {
    "path": "src/components/QrcodeReader.vue",
    "content": "<script lang=\"ts\">\nimport QrScanner from \"qr-scanner\";\nimport { URDecoder } from \"@gandlaf21/bc-ur\";\nimport { useCameraStore } from \"src/stores/camera\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport { useUiStore } from \"src/stores/ui\";\n\nexport default {\n  emits: [\"decode\"],\n  data(): {\n    qrScanner: QrScanner | null;\n    urDecoder: URDecoder | null;\n    urDecoderProgress: number;\n  } {\n    return {\n      qrScanner: null,\n      urDecoder: null,\n      urDecoderProgress: 0,\n    };\n  },\n  mounted() {\n    this.qrScanner = new QrScanner(\n      this.$refs.cameraEl as HTMLVideoElement,\n      (result: QrScanner.ScanResult) => {\n        this.handleResult(result);\n      },\n      {\n        returnDetailedScanResult: true,\n        highlightScanRegion: true,\n        highlightCodeOutline: true,\n        onDecodeError: () => {},\n      }\n    );\n    this.qrScanner.start();\n    this.urDecoder = new URDecoder();\n  },\n  computed: {\n    ...mapState(useCameraStore, [\"camera\", \"hasCamera\"]),\n    canPasteFromClipboard: function () {\n      return (\n        window.isSecureContext &&\n        navigator.clipboard &&\n        navigator.clipboard.readText\n      );\n    },\n  },\n  methods: {\n    ...mapActions(useCameraStore, [\"closeCamera\", \"showCamera\"]),\n    handleResult(result: QrScanner.ScanResult) {\n      // if this is a multipart-qr code, do not yet emit\n      if (result.data.toLowerCase().startsWith(\"ur:\")) {\n        this.urDecoder?.receivePart(result.data);\n        this.urDecoderProgress =\n          this.urDecoder?.estimatedPercentComplete() || 0;\n        if (this.urDecoder?.isComplete() && this.urDecoder?.isSuccess()) {\n          const ur = this.urDecoder?.resultUR();\n          const decoded = ur.decodeCBOR();\n          this.$emit(\"decode\", decoded.toString());\n          this.qrScanner?.stop();\n          this.urDecoderProgress = 0;\n        }\n      } else {\n        this.$emit(\"decode\", result.data);\n        this.qrScanner?.stop();\n      }\n    },\n    pasteToParseDialog: async function () {\n      const text = await useUiStore().pasteFromClipboard();\n      if (text) {\n        this.$emit(\"decode\", text);\n      }\n    },\n  },\n  unmounted() {\n    this.qrScanner?.destroy();\n  },\n};\n</script>\n<template>\n  <q-card>\n    <div class=\"text-center\">\n      <div>\n        <video ref=\"cameraEl\" style=\"width: 100%\"></video>\n      </div>\n      <div>\n        <div class=\"row q-justify-center\">\n          <q-linear-progress\n            rounded\n            size=\"30px\"\n            v-if=\"urDecoderProgress > 0\"\n            :value=\"urDecoderProgress\"\n            :indeterminate=\"urDecoderProgress === 0\"\n            class=\"q-mt-none\"\n            color=\"secondary\"\n          >\n            <div class=\"absolute-full flex flex-center\">\n              <q-badge\n                color=\"white\"\n                text-color=\"secondary\"\n                style=\"font-size: 1rem; padding: 5px\"\n                class=\"text-weight-bold\"\n                :label=\"\n                  $t('QrcodeReader.progress.text', {\n                    percentage: $t('QrcodeReader.progress.percentage', {\n                      percentage: Math.round(urDecoderProgress * 100),\n                    }),\n                    addon:\n                      urDecoderProgress > 0.9\n                        ? $t('QrcodeReader.progress.keep_scanning_text')\n                        : '',\n                  })\n                \"\n              />\n            </div>\n          </q-linear-progress>\n        </div>\n      </div>\n    </div>\n    <div class=\"row q-my-sm\">\n      <q-btn\n        unelevated\n        v-if=\"canPasteFromClipboard\"\n        @click=\"pasteToParseDialog\"\n      >\n        <q-icon name=\"content_paste\" class=\"q-mr-sm\" />\n        {{ $t(\"QrcodeReader.actions.paste.label\") }}</q-btn\n      >\n      <q-btn @click=\"closeCamera\" flat color=\"grey\" class=\"q-ml-auto\">{{\n        $t(\"QrcodeReader.actions.close.label\")\n      }}</q-btn>\n    </div>\n  </q-card>\n</template>\n"
  },
  {
    "path": "src/components/ReceiveDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showReceiveDialog\"\n    position=\"bottom\"\n    :maximized=\"$q.screen.lt.sm\"\n    transition-show=\"slide-up\"\n    transition-hide=\"slide-down\"\n  >\n    <q-card class=\"drawer-card text-white full-width-card q-pb-lg\">\n      <q-card-section class=\"row items-center q-pb-sm\">\n        <q-btn flat round dense v-close-popup class=\"q-ml-sm\" color=\"primary\">\n          <XIcon />\n        </q-btn>\n        <div class=\"col text-center\">\n          <span class=\"text-h6\">{{ $t(\"ReceiveDialog.title\") }}</span>\n        </div>\n        <q-btn\n          flat\n          round\n          dense\n          class=\"q-mr-sm\"\n          @click=\"showCamera\"\n          color=\"primary\"\n        >\n          <ScanIcon />\n        </q-btn>\n      </q-card-section>\n\n      <q-card-section class=\"q-pa-md\">\n        <div class=\"q-gutter-y-md\">\n          <div class=\"action-row\" @click=\"toggleReceiveEcashDrawer\">\n            <div class=\"row items-center no-wrap\">\n              <div class=\"icon-circle\">\n                <CoinsIcon :size=\"24\" />\n              </div>\n              <div class=\"col q-ml-md\">\n                <div class=\"text-body1 text-weight-medium\">\n                  {{ $t(\"ReceiveDialog.actions.ecash.label\") }}\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div class=\"action-row\" @click=\"showInvoiceCreateDialog\">\n            <div class=\"row items-center no-wrap\">\n              <div class=\"icon-circle\">\n                <ZapIcon :size=\"24\" />\n              </div>\n              <div class=\"col q-ml-md\">\n                <div class=\"text-body1 text-weight-medium\">\n                  {{ $t(\"ReceiveDialog.actions.lightning.label\") }}\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </q-card-section>\n    </q-card>\n  </q-dialog>\n  <ReceiveEcashDrawer />\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { useReceiveTokensStore } from \"src/stores/receiveTokensStore\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useCameraStore } from \"src/stores/camera\";\nimport ReceiveEcashDrawer from \"src/components/ReceiveEcashDrawer.vue\";\nimport { useMintsStore } from \"src/stores/mints\";\nimport {\n  notifyError,\n  notifySuccess,\n  notify,\n  notifyWarning,\n} from \"src/js/notify.ts\";\nimport {\n  X as XIcon,\n  Coins as CoinsIcon,\n  Zap as ZapIcon,\n  Scan as ScanIcon,\n} from \"lucide-vue-next\";\n\nexport default defineComponent({\n  name: \"ReceiveDialog\",\n  components: {\n    XIcon,\n    CoinsIcon,\n    ZapIcon,\n    ScanIcon,\n    ReceiveEcashDrawer,\n  },\n  mixins: [windowMixin],\n  props: {},\n  data: function () {\n    return {\n      currentPage: 1,\n      pageSize: 5,\n    };\n  },\n  computed: {\n    ...mapWritableState(useUiStore, [\n      \"showInvoiceDetails\",\n      \"showReceiveDialog\",\n      \"showReceiveEcashDrawer\",\n      \"showCreateInvoiceDialog\",\n    ]),\n    ...mapWritableState(useReceiveTokensStore, [\n      \"showReceiveTokens\",\n      \"receiveData\",\n    ]),\n    ...mapWritableState(useWalletStore, [\"invoiceData\"]),\n    ...mapState(useMintsStore, [\"mints\"]),\n    canReceivePayments: function () {\n      if (!this.mints.length) {\n        return false;\n      } else {\n        return true;\n      }\n    },\n  },\n  methods: {\n    toggleReceiveEcashDrawer: function () {\n      this.showReceiveDialog = false;\n      this.showReceiveTokens = false;\n      this.showReceiveEcashDrawer = true;\n    },\n    showReceiveTokensDialog: function () {\n      this.receiveData.tokensBase64 = \"\";\n      this.showReceiveTokens = true;\n      this.showReceiveDialog = false;\n    },\n    showInvoiceCreateDialog: async function () {\n      if (!this.canReceivePayments) {\n        notifyWarning(\n          this.$i18n.t(\"ReceiveDialog.actions.lightning.error_no_mints\")\n        );\n        this.showReceiveDialog = false;\n        return;\n      }\n      console.log(\"##### showInvoiceCreateDialog\");\n      this.invoiceData.amount = \"\";\n      this.invoiceData.bolt11 = \"\";\n      this.invoiceData.hash = \"\";\n      this.invoiceData.memo = \"\";\n      this.showCreateInvoiceDialog = true;\n      this.showReceiveDialog = false;\n    },\n    ...mapActions(useCameraStore, [\"closeCamera\", \"showCamera\"]),\n  },\n  created: function () {},\n});\n</script>\n\n<style lang=\"scss\" scoped>\n::v-deep .q-dialog__backdrop {\n  backdrop-filter: blur(8px);\n  background: rgba(0, 0, 0, 0.4) !important;\n}\n\n.q-dialog__inner > div {\n  border-top-left-radius: 20px !important;\n  border-top-right-radius: 20px !important;\n  border-bottom-left-radius: 0px !important;\n  border-bottom-right-radius: 0px !important;\n}\n\n.drawer-card {\n  background: #1a1a1a;\n}\n\n.action-row {\n  background: rgba(255, 255, 255, 0.06);\n  border-radius: 12px;\n  padding: 12px 16px;\n  cursor: pointer;\n  transition: background 0.2s ease;\n\n  &:active {\n    background: rgba(255, 255, 255, 0.1);\n  }\n}\n\n.icon-circle {\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n  background: rgba(255, 255, 255, 0.1);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n}\n\n.lucide {\n  width: 24px;\n  height: 24px;\n  color: white;\n}\n\n.qcard {\n  border-top-left-radius: 20px;\n  border-top-right-radius: 20px;\n}\n</style>\n"
  },
  {
    "path": "src/components/ReceiveEcashDrawer.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showReceiveEcashDrawer\"\n    position=\"bottom\"\n    :maximized=\"$q.screen.lt.sm\"\n    transition-show=\"slide-up\"\n    transition-hide=\"slide-down\"\n  >\n    <q-card class=\"drawer-card text-white full-width-card q-pb-lg\">\n      <q-card-section class=\"row items-center q-pb-sm\">\n        <q-btn flat round dense @click=\"goBack\" class=\"q-ml-sm\" color=\"primary\">\n          <ChevronLeftIcon />\n        </q-btn>\n        <div class=\"col text-center\">\n          <span class=\"text-h6\">{{ $t(\"ReceiveEcashDrawer.title\") }}</span>\n        </div>\n        <q-btn\n          flat\n          round\n          dense\n          @click=\"showCamera\"\n          class=\"q-mr-sm\"\n          color=\"primary\"\n        >\n          <ScanIcon />\n        </q-btn>\n      </q-card-section>\n\n      <q-card-section class=\"q-pa-md\">\n        <div class=\"q-gutter-y-md\">\n          <div class=\"action-row\" @click=\"handlePasteBtn\">\n            <div class=\"row items-center no-wrap\">\n              <div class=\"icon-circle\">\n                <ClipboardIcon :size=\"24\" />\n              </div>\n              <div class=\"col q-ml-md\">\n                <div class=\"text-body1 text-weight-medium\">\n                  {{ $t(\"ReceiveEcashDrawer.actions.paste.label\") }}\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div class=\"action-row\" @click=\"showCamera\">\n            <div class=\"row items-center no-wrap\">\n              <div class=\"icon-circle\">\n                <ScanIcon :size=\"24\" />\n              </div>\n              <div class=\"col q-ml-md\">\n                <div class=\"text-body1 text-weight-medium\">\n                  {{ $t(\"ReceiveEcashDrawer.actions.scan.label\") }}\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div\n            v-if=\"enablePaymentRequest\"\n            class=\"action-row\"\n            @click=\"handlePaymentRequestBtn\"\n          >\n            <div class=\"row items-center no-wrap\">\n              <div class=\"icon-circle\">\n                <FileTextIcon :size=\"24\" />\n              </div>\n              <div class=\"col q-ml-md\">\n                <div class=\"text-body1 text-weight-medium\">\n                  {{ $t(\"ReceiveEcashDrawer.actions.request.label\") }}\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div\n            v-if=\"showP2PkButtonInDrawer\"\n            class=\"action-row\"\n            @click=\"handleLockBtn\"\n          >\n            <div class=\"row items-center no-wrap\">\n              <div class=\"icon-circle\">\n                <LockIcon :size=\"24\" />\n              </div>\n              <div class=\"col q-ml-md\">\n                <div class=\"text-body1 text-weight-medium\">\n                  {{ $t(\"ReceiveEcashDrawer.actions.lock.label\") }}\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div\n            v-if=\"ndefSupported && showNfcButtonInDrawer\"\n            class=\"action-row\"\n            @click=\"handleNFCBtn\"\n          >\n            <div class=\"row items-center no-wrap\">\n              <div class=\"icon-circle\">\n                <q-spinner v-if=\"scanningCard\" size=\"sm\" color=\"white\" />\n                <NfcIcon v-else :size=\"24\" />\n              </div>\n              <div class=\"col q-ml-md\">\n                <div class=\"text-body1 text-weight-medium\">\n                  {{ $t(\"ReceiveEcashDrawer.actions.nfc.label\") }}\n                  {{\n                    scanningCard\n                      ? $t(\"ReceiveEcashDrawer.actions.nfc.scanning_text\")\n                      : \"\"\n                  }}\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </q-card-section>\n    </q-card>\n  </q-dialog>\n  <P2PKDialog v-model=\"showP2PKDialog\" />\n  <PRDialog v-model=\"showPRDialog\" />\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { useReceiveTokensStore } from \"src/stores/receiveTokensStore\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { useMintsStore } from \"src/stores/mints\";\nimport { useTokensStore } from \"src/stores/tokens\";\nimport { useCameraStore } from \"src/stores/camera\";\nimport { useP2PKStore } from \"src/stores/p2pk\";\nimport { usePRStore } from \"src/stores/payment-request\";\nimport { useSettingsStore } from \"../stores/settings\";\nimport token from \"src/js/token\";\nimport P2PKDialog from \"./P2PKDialog.vue\";\nimport PRDialog from \"./PaymentRequestDialog.vue\";\n\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport {\n  ChevronLeft as ChevronLeftIcon,\n  Clipboard as ClipboardIcon,\n  FileText as FileTextIcon,\n  Lock as LockIcon,\n  Nfc as NfcIcon,\n  Scan as ScanIcon,\n} from \"lucide-vue-next\";\n// import ChooseMint from \"components/ChooseMint.vue\";\nimport TokenInformation from \"components/TokenInformation.vue\";\nimport { map } from \"underscore\";\nimport { notifyError, notifySuccess, notify } from \"../js/notify\";\n\nexport default defineComponent({\n  name: \"ReceiveTokenDialog\",\n  mixins: [windowMixin],\n  components: {\n    P2PKDialog,\n    PRDialog,\n    ChevronLeftIcon,\n    ClipboardIcon,\n    FileTextIcon,\n    LockIcon,\n    ScanIcon,\n    NfcIcon,\n  },\n  data: function () {\n    return {\n      showP2PKDialog: false,\n    };\n  },\n  computed: {\n    ...mapWritableState(useReceiveTokensStore, [\n      \"showReceiveTokens\",\n      \"receiveData\",\n      \"scanningCard\",\n      \"watchClipboardPaste\",\n    ]),\n    ...mapState(useUiStore, [\"tickerShort\", \"ndefSupported\"]),\n    ...mapState(useMintsStore, [\n      \"activeProofs\",\n      \"activeUnit\",\n      \"addMintBlocking\",\n    ]),\n    ...mapState(useSettingsStore, [\n      \"showNfcButtonInDrawer\",\n      \"autoPasteEcashReceive\",\n    ]),\n    ...mapWritableState(useUiStore, [\"showReceiveEcashDrawer\"]),\n    ...mapWritableState(useMintsStore, [\"addMintData\", \"showAddMintDialog\"]),\n    ...mapWritableState(usePRStore, [\"showPRDialog\"]),\n    ...mapState(useCameraStore, [\"hasCamera\"]),\n    ...mapState(useP2PKStore, [\"p2pkKeys\", \"showP2PkButtonInDrawer\"]),\n    ...mapState(usePRStore, [\"enablePaymentRequest\"]),\n    ...mapWritableState(useUiStore, [\"showReceiveDialog\"]),\n    ...mapState(useCameraStore, [\"lastScannedResult\"]),\n  },\n  methods: {\n    ...mapActions(useWalletStore, [\"redeem\"]),\n    ...mapActions(useCameraStore, [\"closeCamera\", \"showCamera\"]),\n    ...mapActions(useTokensStore, [\"addPendingToken\"]),\n    ...mapActions(useP2PKStore, [\n      \"getPrivateKeyForP2PKEncodedToken\",\n      \"generateKeypair\",\n      \"showLastKey\",\n    ]),\n    ...mapActions(useMintsStore, [\"addMint\"]),\n    ...mapActions(useReceiveTokensStore, [\n      \"receiveIfDecodes\",\n      \"decodeToken\",\n      \"knowThisMintOfTokenJson\",\n      \"toggleScanner\",\n      \"pasteToParseDialog\",\n    ]),\n    ...mapWritableState(useReceiveTokensStore, [\n      \"showReceiveTokens\",\n      \"receiveData\",\n    ]),\n    isiOsSafari() {\n      const userAgent = window.navigator.userAgent.toLowerCase();\n      const match =\n        /iphone|ipad|ipod/.test(userAgent) && /safari/.test(userAgent);\n      console.log(`User agent: ${userAgent}, is iOS Safari: ${match}`);\n      return match;\n    },\n    handlePasteBtn: function () {\n      this.receiveData.tokensBase64 = \"\";\n      this.showReceiveTokens = true;\n      this.showReceiveEcashDrawer = false;\n      // if (!this.isiOsSafari()\n      if (this.autoPasteEcashReceive) {\n        this.watchClipboardPaste = true;\n      }\n    },\n    handleLockBtn: function () {\n      this.showP2PKDialog = !this.showP2PKDialog;\n      if (!this.p2pkKeys.length || !this.showP2PKDialog) {\n        this.generateKeypair();\n      }\n      this.showLastKey();\n      this.showReceiveEcashDrawer = false;\n    },\n    handlePaymentRequestBtn: function () {\n      const prStore = usePRStore();\n      this.showPRDialog = !this.showPRDialog;\n      if (this.showPRDialog) {\n        prStore.newPaymentRequest();\n      }\n      this.showReceiveEcashDrawer = false;\n    },\n    handleNFCBtn: function () {\n      this.toggleScanner();\n    },\n    handleQrCodeDecode(result) {\n      console.log(\"QR code decoded:\", result);\n      // Handle the decoded QR code result here\n      this.closeCamera();\n      // You might want to process the result here\n    },\n    goBack: function () {\n      this.showReceiveEcashDrawer = false;\n      this.showReceiveDialog = true;\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n::v-deep .q-dialog__backdrop {\n  backdrop-filter: blur(8px);\n  background: rgba(0, 0, 0, 0.4) !important;\n}\n\n.q-dialog__inner > div {\n  border-top-left-radius: 20px !important;\n  border-top-right-radius: 20px !important;\n  border-bottom-left-radius: 0px !important;\n  border-bottom-right-radius: 0px !important;\n}\n\n.drawer-card {\n  background: #1a1a1a;\n}\n\n.action-row {\n  background: rgba(255, 255, 255, 0.06);\n  border-radius: 12px;\n  padding: 12px 16px;\n  cursor: pointer;\n  transition: background 0.2s ease;\n\n  &:active {\n    background: rgba(255, 255, 255, 0.1);\n  }\n}\n\n.icon-circle {\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n  background: rgba(255, 255, 255, 0.1);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n}\n\n.lucide {\n  width: 24px;\n  height: 24px;\n  color: white;\n}\n</style>\n"
  },
  {
    "path": "src/components/ReceiveTokenDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showReceiveTokens\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n    transition-show=\"fade\"\n    transition-hide=\"fade\"\n    no-backdrop-dismiss\n    @keydown.esc=\"showReceiveTokens = false\"\n  >\n    <q-card class=\"q-pa-none q-pt-none qcard\">\n      <!-- full-screen receive flow -->\n      <div\n        class=\"column fit receive-fullscreen\"\n        :class=\"$q.dark.isActive ? 'bg-dark' : 'bg-white'\"\n      >\n        <!-- Header -->\n        <div class=\"row items-center q-pa-md\" style=\"position: relative\">\n          <q-btn\n            v-close-popup\n            flat\n            round\n            icon=\"close\"\n            color=\"grey\"\n            class=\"floating-close-btn\"\n          />\n          <div class=\"col text-center fixed-title-height\">\n            <q-item-label\n              class=\"dialog-header q-mt-sm\"\n              :class=\"$q.dark.isActive ? 'text-white' : 'text-black'\"\n            >\n              {{ $t(\"ReceiveTokenDialog.title\") }}\n            </q-item-label>\n          </div>\n        </div>\n\n        <!-- Content area -->\n        <div\n          class=\"col column items-center justify-start q-px-lg scroll-container\"\n        >\n          <div class=\"row justify-center full-width\">\n            <div\n              class=\"col-12 col-sm-11 col-md-8 q-px-sm q-mb-sm\"\n              style=\"max-width: 600px\"\n            >\n              <!-- <div class=\"row items-center no-wrap q-pb-sm\">\n                <div class=\"col-12\">\n                  <span class=\"text-h6\">{{\n                    $t(\"ReceiveTokenDialog.title\", {\n                      value:\n                        tokenAmount && tokenUnit\n                          ? formatCurrency(tokenAmount - receiveFee, tokenUnit)\n                          : $t(\"ReceiveTokenDialog.title_ecash_text\"),\n                    })\n                  }}</span>\n                  <span\n                    v-if=\"\n                      tokenAmount &&\n                      tokenUnit &&\n                      tokenUnit == 'sat' &&\n                      bitcoinPrice\n                    \"\n                    class=\"q-ml-xs text-subtitle2 text-grey-6\"\n                  >\n                    ({{\n                      formatCurrency(\n                        (currentCurrencyPrice / 100000000) * tokenAmount,\n                        bitcoinPriceCurrency,\n                        true\n                      )\n                    }})\n                  </span>\n                </div>\n              </div> -->\n\n              <transition appear enter-active-class=\"animated fadeIn\">\n                <div v-if=\"tokenDecodesCorrectly\" key=\"token-valid\">\n                  <!-- VALID TOKEN content -->\n                  <!-- print token in fixed width font -->\n                  <div class=\"row q-pt-md\">\n                    <div class=\"col-12\">\n                      <TokenStringRender\n                        :token-string=\"receiveData.tokensBase64\"\n                        :max-length=\"maxLengthForTokenString\"\n                      />\n                    </div>\n                  </div>\n\n                  <div class=\"row q-pt-md\">\n                    <div class=\"col-12\">\n                      <TokenInformation\n                        :encodedToken=\"receiveData.tokensBase64\"\n                        :hide-amount=\"true\"\n                        :hide-unit=\"true\"\n                        @notLockedToUs=\"handleNotLockedToUs\"\n                      />\n                    </div>\n                  </div>\n                  <div\n                    class=\"row q-pt-sm\"\n                    v-if=\"!knowThisMint && !isNotLockedToUs\"\n                  >\n                    <div class=\"col-12\">\n                      <ToolTipInfo\n                        :text=\"$t('ReceiveTokenDialog.unknown_mint_info_text')\"\n                      />\n                    </div>\n                  </div>\n                  <transition name=\"fade\">\n                    <div v-if=\"isReceiving\" class=\"receiving-state q-mb-md\">\n                      <div class=\"row items-center no-wrap\">\n                        <div class=\"col-auto text-h5 text-weight-bold\">\n                          Receiving\n                          {{\n                            formatCurrency(\n                              Math.max(tokenAmount - receiveFee, 0),\n                              tokenUnit,\n                              true\n                            )\n                          }}\n                        </div>\n                        <div class=\"col-auto\">\n                          <q-spinner size=\"sm\" class=\"q-ml-sm\" />\n                        </div>\n                      </div>\n                    </div>\n                  </transition>\n                  <transition name=\"fade\">\n                    <div\n                      v-if=\"receiveError && !isReceiving\"\n                      class=\"receive-error text-negative q-mb-md q-mt-xl text-subtitle2\"\n                    >\n                      {{ receiveError }}\n                    </div>\n                  </transition>\n\n                  <!-- swap mint selection -->\n                  <SwapIncomingTokenToKnownMint\n                    v-if=\"swapSelected\"\n                    v-model:target-mint=\"selectedSwapMintUrl\"\n                    :swap-processing=\"swapProcessing\"\n                    :swap-error=\"swapError\"\n                    :swap-blocking=\"swapBlocking\"\n                    :source-mint-info=\"sourceMintInfo\"\n                    :source-mint=\"tokenMint\"\n                    @close=\"handleSwapClose\"\n                  />\n                </div>\n                <ParseInputComponent\n                  v-else\n                  key=\"token-empty\"\n                  v-model=\"receiveData.tokensBase64\"\n                  :placeholder=\"$t('ParseInputComponent.placeholder.receive')\"\n                  :has-camera=\"hasCamera\"\n                  :ndef-supported=\"ndefSupported\"\n                  :scanning-card=\"scanningCard\"\n                  :nfc-label=\"$t('ReceiveTokenDialog.actions.nfc.label')\"\n                  :nfc-tooltip=\"\n                    $t(\n                      'ReceiveTokenDialog.actions.nfc.tooltips.ndef_supported_text'\n                    )\n                  \"\n                  @enter=\"handleReceive\"\n                  @paste=\"pasteToParseDialog(true)\"\n                  @scan=\"showCamera\"\n                  @nfc=\"toggleScanner\"\n                />\n              </transition>\n            </div>\n          </div>\n        </div>\n\n        <!-- Bottom fixed receive action -->\n        <transition appear enter-active-class=\"animated fadeIn\">\n          <div class=\"bottom-panel\" v-if=\"tokenDecodesCorrectly\">\n            <div class=\"row justify-center q-pb-lg q-pt-sm\">\n              <div\n                class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n                style=\"max-width: 600px\"\n              >\n                <template v-if=\"isNotLockedToUs\">\n                  <div\n                    class=\"receive-error text-negative text-subtitle2 text-left\"\n                  >\n                    {{\n                      $t(\"ReceiveTokenDialog.errors.p2pk_lock_mismatch.label\")\n                    }}\n                  </div>\n                </template>\n                <template v-else-if=\"swapSelected\">\n                  <q-btn\n                    class=\"full-width\"\n                    unelevated\n                    size=\"lg\"\n                    @click=\"handleSwapToTrustedMint\"\n                    color=\"primary\"\n                    rounded\n                    :loading=\"swapProcessing || swapBlocking\"\n                    :disabled=\"!canConfirmSwap\"\n                  >\n                    {{\n                      $t(\n                        \"ReceiveTokenDialog.actions.receive_to_selected_mint.label\"\n                      )\n                    }}\n                    <template v-slot:loading>\n                      <q-spinner size=\"xs\" />\n                    </template>\n                    <q-tooltip>{{\n                      $t(\"ReceiveTokenDialog.actions.confirm_swap.tooltip_text\")\n                    }}</q-tooltip>\n                  </q-btn>\n                </template>\n                <template v-else>\n                  <q-btn\n                    @click=\"addPendingTokenToHistory(receiveData.tokensBase64)\"\n                    color=\"primary\"\n                    rounded\n                    flat\n                    class=\"full-width q-mb-md\"\n                  >\n                    {{ $t(\"ReceiveTokenDialog.actions.later.label\") }}\n                    <q-tooltip>{{\n                      $t(\"ReceiveTokenDialog.actions.later.tooltip_text\")\n                    }}</q-tooltip>\n                  </q-btn>\n                  <q-btn\n                    v-if=\"\n                      enableReceiveSwaps &&\n                      activeMintUrl &&\n                      mints.length > 0 &&\n                      !(\n                        mints.length === 1 &&\n                        tokenMint &&\n                        mints[0]?.url === tokenMint\n                      )\n                    \"\n                    @click=\"openSwap\"\n                    color=\"primary\"\n                    rounded\n                    outline\n                    class=\"full-width q-mb-md\"\n                  >\n                    {{ $t(\"ReceiveTokenDialog.actions.swap.label\") }}\n                    <q-tooltip>{{\n                      $t(\"ReceiveTokenDialog.actions.swap.tooltip_text\")\n                    }}</q-tooltip>\n                  </q-btn>\n                  <q-btn\n                    class=\"full-width\"\n                    unelevated\n                    size=\"lg\"\n                    @click=\"handleReceive\"\n                    color=\"primary\"\n                    rounded\n                    :disabled=\"addMintBlocking || isReceiving\"\n                    :loading=\"swapBlocking || isReceiving\"\n                  >\n                    {{\n                      knowThisMint\n                        ? addMintBlocking\n                          ? $t(\n                              \"ReceiveTokenDialog.actions.receive.label_adding_mint\"\n                            )\n                          : $t(\n                              \"ReceiveTokenDialog.actions.receive.label_known_mint\"\n                            )\n                        : $t(\"ReceiveTokenDialog.actions.receive.label\")\n                    }}\n                    <template v-slot:loading>\n                      <q-spinner />\n                    </template>\n                  </q-btn>\n                </template>\n              </div>\n            </div>\n          </div>\n        </transition>\n      </div>\n    </q-card>\n  </q-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { useReceiveTokensStore } from \"src/stores/receiveTokensStore\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { useMintsStore } from \"src/stores/mints\";\nimport { useTokensStore } from \"src/stores/tokens\";\nimport { useCameraStore } from \"src/stores/camera\";\nimport { useP2PKStore } from \"src/stores/p2pk\";\nimport { usePRStore } from \"src/stores/payment-request\";\nimport { usePriceStore } from \"src/stores/price\";\nimport { useSwapStore } from \"src/stores/swap\";\nimport { useSettingsStore } from \"src/stores/settings\";\nimport token from \"src/js/token\";\nimport { Mint, GetInfoResponse } from \"@cashu/cashu-ts\";\nimport { Token } from \"@cashu/cashu-ts\";\nimport type { StoredMint } from \"src/stores/mints\";\n\nimport TokenInformation from \"components/TokenInformation.vue\";\nimport TokenStringRender from \"components/TokenStringRender.vue\";\nimport SwapIncomingTokenToKnownMint from \"src/components/SwapIncomingTokenToKnownMint.vue\";\nimport ToolTipInfo from \"src/components/ToolTipInfo.vue\";\nimport ParseInputComponent from \"components/ParseInputComponent.vue\";\n\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\n\ndeclare const windowMixin: any;\n\nexport default defineComponent({\n  name: \"ReceiveTokenDialog\",\n  mixins: [windowMixin],\n  components: {\n    TokenInformation,\n    TokenStringRender,\n    SwapIncomingTokenToKnownMint,\n    ToolTipInfo,\n    ParseInputComponent,\n  },\n  data: function () {\n    return {\n      showP2PKDialog: false,\n      swapSelected: false,\n      untrustedMintInfo: null as GetInfoResponse | null,\n      swapProcessing: false,\n      swapError: false,\n      isReceiving: false as boolean,\n      receiveError: \"\" as string,\n      selectedSwapMintUrl: \"\",\n      isNotLockedToUs: false as boolean,\n    };\n  },\n  watch: {\n    watchClipboardPaste(val) {\n      if (val) {\n        this.$nextTick(() => {\n          this.pasteToParseDialog();\n          this.watchClipboardPaste = false;\n        });\n      }\n    },\n    tokenMint: {\n      async handler(newMintUrl) {\n        if (newMintUrl && !this.mints.find((m: any) => m.url === newMintUrl)) {\n          // This is an untrusted mint, try to fetch its info\n          await this.fetchUntrustedMintInfo(newMintUrl);\n        }\n        this.syncSwapTargetWithDefaults();\n      },\n      immediate: true,\n    },\n    showReceiveTokens(val: boolean) {\n      if (!val) {\n        this.isReceiving = false;\n        this.receiveError = \"\";\n        this.swapSelected = false;\n        this.swapProcessing = false;\n        this.swapError = false;\n        this.selectedSwapMintUrl = \"\";\n        this.isNotLockedToUs = false;\n      }\n    },\n    \"receiveData.tokensBase64\"(newVal: string) {\n      if (newVal !== undefined) {\n        this.receiveError = \"\";\n        this.isNotLockedToUs = false;\n      }\n    },\n    swapSelected(newVal: boolean) {\n      if (newVal) {\n        this.syncSwapTargetWithDefaults();\n      }\n    },\n    mints: {\n      handler() {\n        this.syncSwapTargetWithDefaults();\n      },\n      deep: true,\n    },\n  },\n  computed: {\n    ...mapWritableState(useReceiveTokensStore, [\n      \"showReceiveTokens\",\n      \"watchClipboardPaste\",\n      \"receiveData\",\n      \"scanningCard\",\n    ]),\n    ...mapState(useUiStore, [\"tickerShort\", \"ndefSupported\"]),\n    ...mapState(usePriceStore, [\n      \"bitcoinPrice\",\n      \"bitcoinPrices\",\n      \"currentCurrencyPrice\",\n    ]),\n    ...mapState(useSettingsStore, [\"bitcoinPriceCurrency\"]),\n    ...mapState(useMintsStore, [\n      \"activeMintUrl\",\n      \"activeProofs\",\n      \"activeUnit\",\n      \"addMintBlocking\",\n      \"mints\",\n    ]),\n    ...mapState(useSettingsStore, [\"enableReceiveSwaps\"]),\n    ...mapWritableState(useMintsStore, [\"addMintData\", \"showAddMintDialog\"]),\n    ...mapWritableState(usePRStore, [\"showPRDialog\"]),\n    ...mapState(useCameraStore, [\"hasCamera\"]),\n    ...mapState(useP2PKStore, [\"p2pkKeys\"]),\n    ...mapState(usePRStore, [\"enablePaymentRequest\"]),\n    ...mapState(useSwapStore, [\"swapBlocking\"]),\n    ...mapWritableState(useUiStore, [\"showReceiveDialog\"]),\n    tokenDecodesCorrectly: function () {\n      // Try decoding token directly\n      if (this.decodeToken(this.receiveData.tokensBase64) !== undefined) {\n        return true;\n      }\n\n      // Fall back to peanut check\n      return this.decodePeanut(this.receiveData.tokensBase64) !== undefined;\n    },\n    knowThisMint: function () {\n      const tokenJson = this.decodeToken(this.receiveData.tokensBase64);\n      if (tokenJson == undefined) {\n        return false;\n      }\n      return this.knowThisMintOfTokenJson(tokenJson);\n    },\n    tokenAmount: function () {\n      if (!this.tokenDecodesCorrectly) {\n        return 0;\n      }\n      const decodedToken = this.decodeToken(this.receiveData.tokensBase64);\n      return this.getProofs(decodedToken).reduce(\n        (sum, el) => (sum += el.amount),\n        0\n      );\n    },\n    receiveFee: function () {\n      return this.getFeesForProofs(\n        this.getProofs(this.decodeToken(this.receiveData.tokensBase64))\n      );\n    },\n    tokenUnit: function () {\n      if (!this.tokenDecodesCorrectly) {\n        return \"\";\n      }\n      const decodedToken = this.decodeToken(this.receiveData.tokensBase64);\n      return token.getUnit(decodedToken);\n    },\n    tokenMint: function () {\n      if (!this.tokenDecodesCorrectly) {\n        return \"\";\n      }\n      const decodedToken = this.decodeToken(this.receiveData.tokensBase64);\n      return this.getMint(decodedToken);\n    },\n    maxLengthForTokenString: function () {\n      return Math.floor(this.$q.screen.height / 3.5);\n    },\n    sourceMintInfo: function () {\n      if (!this.tokenMint) {\n        return null;\n      }\n      const mint = (this.mints as StoredMint[]).find(\n        (m) => m.url === this.tokenMint\n      );\n      if (!mint) {\n        // Use untrusted mint info if available\n        return {\n          nickname: this.untrustedMintInfo?.name || null,\n          shorturl: this.getShortUrl(this.tokenMint),\n          iconUrl: this.untrustedMintInfo?.icon_url || null,\n        };\n      }\n      // If mint exists but doesn't have complete info yet, fall back to untrustedMintInfo\n      const mintNickname = mint.nickname || mint.info?.name;\n      const mintIconUrl = mint.info?.icon_url;\n      return {\n        nickname: mintNickname || this.untrustedMintInfo?.name || null,\n        shorturl: this.getShortUrl(mint.url),\n        iconUrl: mintIconUrl || this.untrustedMintInfo?.icon_url || null,\n      };\n    },\n    swapTargetMint: function () {\n      if (!this.selectedSwapMintUrl) {\n        return null;\n      }\n      return (\n        (this.mints as StoredMint[]).find(\n          (m) => m.url === this.selectedSwapMintUrl\n        ) || null\n      );\n    },\n    canConfirmSwap: function (): boolean {\n      return (\n        !!this.swapTargetMint &&\n        this.selectedSwapMintUrl !== this.tokenMint &&\n        !this.swapProcessing &&\n        !this.swapBlocking\n      );\n    },\n  },\n  methods: {\n    ...mapActions(useWalletStore, [\"redeem\", \"getFeesForProofs\"]),\n    ...mapActions(useCameraStore, [\"closeCamera\", \"showCamera\"]),\n    ...mapActions(useTokensStore, [\"addPendingToken\"]),\n    ...mapActions(useP2PKStore, [\n      \"getPrivateKeyForP2PKEncodedToken\",\n      \"generateKeypair\",\n      \"showLastKey\",\n    ]),\n    ...mapActions(useMintsStore, [\"addMint\"]),\n    ...mapActions(useReceiveTokensStore, [\n      \"decodeToken\",\n      \"knowThisMintOfTokenJson\",\n      \"toggleScanner\",\n      \"pasteToParseDialog\",\n      \"receiveToken\",\n    ]),\n    formatCurrency(value: number, currency: string, showBalance = false) {\n      return useUiStore().formatCurrency(value, currency, showBalance);\n    },\n    handleReceive: async function () {\n      if (this.isReceiving) {\n        return;\n      }\n      this.receiveError = \"\";\n      if (!this.tokenDecodesCorrectly) {\n        this.receiveError = this.$t(\n          \"ReceiveTokenDialog.errors.invalid_token.label\"\n        ) as string;\n        return;\n      }\n      this.isReceiving = true;\n      try {\n        await this.receiveToken(this.receiveData.tokensBase64);\n      } catch (error: any) {\n        console.error(\"Receive token error:\", error);\n        const rawMessage =\n          error && typeof error === \"object\" && \"message\" in error\n            ? (error as Error).message\n            : String(error ?? \"\");\n        if (\n          rawMessage.toLowerCase &&\n          rawMessage.toLowerCase().includes(\"no tokens provided\")\n        ) {\n          this.receiveError = this.$t(\n            \"ReceiveTokenDialog.errors.invalid_token.label\"\n          ) as string;\n        } else {\n          this.receiveError =\n            rawMessage ||\n            (this.$t(\n              \"ReceiveTokenDialog.errors.invalid_token.label\"\n            ) as string);\n        }\n      } finally {\n        this.isReceiving = false;\n      }\n    },\n    getShortUrl: function (url: string) {\n      try {\n        const urlObj = new URL(url);\n        return urlObj.hostname;\n      } catch {\n        return url;\n      }\n    },\n    // TOKEN METHODS\n    decodePeanut: function (peanut) {\n      try {\n        let decoded = [];\n        const chars = Array.from(peanut);\n        if (!chars.length) return undefined;\n        const fromVariationSelector = function (char) {\n          const codePoint = char.codePointAt(0);\n\n          // Handle Variation Selectors (VS1-VS16): U+FE00 to U+FE0F\n          if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) {\n            // Maps FE00->0, FE01->1, ..., FE0F->15\n            const byteValue = codePoint - 0xfe00;\n            return String.fromCharCode(byteValue);\n          }\n\n          // Handle Variation Selectors Supplement (VS17-VS256): U+E0100 to U+E01EF\n          if (codePoint >= 0xe0100 && codePoint <= 0xe01ef) {\n            // Maps E0100->16, E0101->17, ..., E01EF->255\n            const byteValue = codePoint - 0xe0100 + 16;\n            return String.fromCharCode(byteValue);\n          }\n\n          // No Variation Selector\n          return null;\n        };\n        // Check all input chars for peanut data\n        for (const char of chars) {\n          const byte = fromVariationSelector(char);\n          if (byte === null && decoded.length > 0) {\n            break;\n          } else if (byte === null) {\n            continue;\n          }\n          decoded.push(byte); // got some\n        }\n        // Switch out token if we found peanut data\n        decoded = decoded.join(\"\");\n        if (decoded) {\n          this.receiveData.tokensBase64 = decoded;\n        }\n        return this.decodeToken(decoded);\n      } catch (error) {\n        return undefined;\n      }\n    },\n    getProofs: function (decoded_token: Token) {\n      return token.getProofs(decoded_token);\n    },\n    getMint: function (decoded_token: Token) {\n      return token.getMint(decoded_token);\n    },\n    tokenAlreadyInHistory: function (tokenStr: string) {\n      const tokensStore = useTokensStore();\n      return (\n        tokensStore.historyTokens.find((t) => t.token === tokenStr) !==\n        undefined\n      );\n    },\n    addPendingTokenToHistory: function (tokenStr: string) {\n      if (this.tokenAlreadyInHistory(tokenStr)) {\n        this.notifySuccess(\n          this.$i18n.t(\n            \"ReceiveTokenDialog.actions.later.already_in_history_success_text\"\n          )\n        );\n        this.showReceiveTokens = false;\n        return;\n      }\n      const tokensStore = useTokensStore();\n      const decodedToken = this.decodeToken(tokenStr);\n      const mintInToken = this.getMint(decodedToken);\n      const unitInToken = token.getUnit(decodedToken);\n      // get amount from decodedToken.token.proofs[..].amount\n      const amount = this.getProofs(decodedToken).reduce(\n        (sum, el) => (sum += el.amount),\n        0\n      );\n\n      tokensStore.addPendingToken({\n        amount: amount,\n        token: tokenStr,\n        mintInToken: mintInToken,\n        unitInToken: unitInToken,\n      });\n      this.showReceiveTokens = false;\n      // show success notification\n      this.notifySuccess(\n        this.$i18n.t(\n          \"ReceiveTokenDialog.actions.later.added_to_history_success_text\"\n        )\n      );\n    },\n    handleSwapToTrustedMint: async function () {\n      try {\n        if (!this.canConfirmSwap) {\n          return;\n        }\n        this.swapProcessing = true;\n        this.swapError = false;\n        const targetMint = this.swapTargetMint;\n        if (!targetMint) {\n          throw new Error(\"No target mint selected\");\n        }\n        await useReceiveTokensStore().meltTokenToMint(\n          this.receiveData.tokensBase64,\n          targetMint\n        );\n        this.swapSelected = false;\n        this.swapProcessing = false;\n      } catch (error) {\n        console.error(\"Swap failed:\", error);\n        this.swapProcessing = false;\n        this.swapError = true;\n      }\n    },\n    openSwap() {\n      if (!this.getAvailableSwapMints().length) {\n        return;\n      }\n      this.syncSwapTargetWithDefaults();\n      this.swapSelected = true;\n      this.swapError = false;\n    },\n    handleSwapClose() {\n      this.swapSelected = false;\n    },\n    getAvailableSwapMints(): StoredMint[] {\n      const availableMints = Array.isArray(this.mints)\n        ? (this.mints as StoredMint[])\n        : [];\n      return availableMints.filter(\n        (mint) => mint.url && mint.url !== this.tokenMint\n      );\n    },\n    syncSwapTargetWithDefaults() {\n      const eligible = this.getAvailableSwapMints();\n      if (!eligible.length) {\n        if (this.selectedSwapMintUrl) {\n          this.selectedSwapMintUrl = \"\";\n        }\n        return;\n      }\n      if (\n        this.selectedSwapMintUrl &&\n        eligible.some((mint: any) => mint.url === this.selectedSwapMintUrl)\n      ) {\n        return;\n      }\n      const mintsStore = useMintsStore();\n      const activeCandidateRaw = mintsStore.activeMintUrl as unknown;\n      let activeCandidate = \"\";\n      if (typeof activeCandidateRaw === \"string\") {\n        activeCandidate = activeCandidateRaw;\n      } else if (\n        activeCandidateRaw &&\n        typeof activeCandidateRaw === \"object\" &&\n        \"value\" in activeCandidateRaw\n      ) {\n        const candidateValue = (activeCandidateRaw as { value?: string }).value;\n        if (typeof candidateValue === \"string\") {\n          activeCandidate = candidateValue;\n        }\n      }\n      if (\n        activeCandidate &&\n        activeCandidate !== this.tokenMint &&\n        eligible.some((mint: any) => mint.url === activeCandidate)\n      ) {\n        this.selectedSwapMintUrl = activeCandidate;\n        return;\n      }\n      this.selectedSwapMintUrl = eligible[0].url;\n    },\n    fetchUntrustedMintInfo: async function (mintUrl: string) {\n      try {\n        const mint = new Mint(mintUrl);\n        const info = await mint.getInfo();\n        this.untrustedMintInfo = info;\n      } catch (error) {\n        console.log(\"Could not fetch untrusted mint info:\", error);\n        this.untrustedMintInfo = null;\n      }\n    },\n    handleNotLockedToUs: function (value: boolean) {\n      this.isNotLockedToUs = value;\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.custom-btn {\n  background: $grey-9;\n  color: white;\n  border-radius: 8px;\n  height: 60px;\n  box-shadow: none;\n  font-size: 14px;\n}\n\n.full-width-card {\n  width: 100%;\n  max-width: 600px;\n  margin: 0 auto;\n}\n\n.q-dialog__inner > div {\n  border-top-left-radius: 20px;\n  border-top-right-radius: 20px;\n}\n\n.icon-background {\n  background-color: $grey-10;\n  border-radius: 8px;\n  padding: 8px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.lucide {\n  width: 24px;\n  height: 24px;\n}\n\n.close-btn-position {\n  position: absolute;\n  right: 16px;\n  bottom: 22px;\n  z-index: 100;\n}\n\n.q-card-top {\n  border-top-left-radius: 0px !important;\n  border-top-right-radius: 0px !important;\n}\n\n.cashub-nowrap {\n  word-break: break-all;\n  -webkit-hyphens: none;\n  -moz-hyphens: none;\n  hyphens: none;\n  font-size: 0.9em;\n  font-family: monospace;\n}\n\n.relative-container {\n  position: relative;\n}\n\n.floating-button {\n  position: absolute;\n  top: 10px;\n  right: 0px;\n  z-index: 100;\n  padding: 1px;\n  background-color: var(--q-primary);\n  border-radius: 50%;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n}\n\n.receive-fullscreen {\n  height: 100vh;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n\n.scroll-container {\n  overflow-y: auto;\n  overflow-x: hidden;\n}\n.floating-close-btn {\n  position: absolute;\n  left: 16px;\n  top: 50%;\n  transform: translateY(-50%);\n  z-index: 1;\n}\n.fixed-title-height {\n  height: 24px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.bottom-panel {\n  margin-top: auto;\n  background: var(--q-color-grey-1);\n  box-shadow: 0 -8px 16px rgba(0, 0, 0, 0.05);\n  padding-bottom: env(safe-area-inset-bottom, 0px);\n}\n\n.token-input {\n  // rounded input border\n  height: 180px;\n  border-radius: 10px;\n  border: 1px solid $grey-2;\n  padding: 10px 20px;\n  font-size: 14px;\n  font-family: monospace;\n  background-color: var(--q-background);\n  color: var(--q-text);\n  text-decoration: none !important;\n  &:focus {\n    border-color: $primary;\n  }\n  // hide inner border from q-input\n  &::before {\n    display: none;\n  }\n  &::after {\n    display: none;\n  }\n  &:disabled {\n    background-color: var(--q-background-disabled);\n    color: var(--q-text-disabled);\n  }\n  &:hover {\n    background-color: var(--q-background-hover);\n    color: var(--q-text-hover);\n  }\n}\n\n/* Swap Section Styles */\n.swap-section {\n  padding: 16px;\n  background: rgba(255, 255, 255, 0.03);\n  border-radius: 12px;\n  border: 1px solid rgba(255, 255, 255, 0.08);\n}\n\n.swap-mint-info {\n  padding: 12px;\n  background: rgba(255, 255, 255, 0.05);\n  border-radius: 10px;\n  border: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.swap-mint-name {\n  font-size: 16px;\n  font-weight: 500;\n  color: white;\n  line-height: 1.3;\n}\n\n.swap-mint-url {\n  font-size: 14px;\n  line-height: 1.3;\n  margin-top: 2px;\n}\n\n.swap-arrow-icon {\n  width: 24px;\n  height: 24px;\n  color: rgba(255, 255, 255, 0.5);\n}\n\n.swap-destination-section {\n  margin-top: 8px;\n}\n\n.swap-section-label {\n  font-size: 14px;\n  font-weight: 500;\n  color: rgba(255, 255, 255, 0.7);\n}\n\n.swap-info-tip {\n  display: flex;\n  align-items: center;\n  padding: 12px;\n  background: rgba(33, 150, 243, 0.1);\n  border-radius: 8px;\n  border: 1px solid rgba(33, 150, 243, 0.2);\n}\n\n.swap-info-text {\n  font-size: 13px;\n  color: rgba(255, 255, 255, 0.8);\n  line-height: 1.4;\n}\n\n.receiving-state {\n  position: relative;\n}\n\n.receive-error {\n  padding: 12px;\n  border-radius: 8px;\n  background-color: rgba(244, 67, 54, 0.1);\n}\n\n.fade-enter-active,\n.fade-leave-active {\n  transition: opacity 0.3s ease;\n}\n\n.fade-enter-from,\n.fade-leave-to {\n  opacity: 0;\n}\n</style>\n"
  },
  {
    "path": "src/components/RemoveMintDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showRemoveMintDialogLocal\"\n    backdrop-filter=\"blur(4px) brightness(50%)\"\n    transition-show=\"fade\"\n    transition-hide=\"fade\"\n  >\n    <q-card class=\"remove-mint-dialog\">\n      <!-- Header Section -->\n      <div class=\"remove-mint-header q-pa-md\">\n        <div class=\"remove-mint-title-row\">\n          <h4 class=\"remove-mint-title q-my-none\">\n            {{ $t(\"RemoveMintDialog.title\") }}\n          </h4>\n        </div>\n      </div>\n\n      <!-- Content Section -->\n      <div class=\"remove-mint-content q-px-md q-pb-md\">\n        <div class=\"q-mb-lg\">\n          <div v-if=\"mintToRemove.nickname\" class=\"q-mb-md\">\n            <label class=\"input-label\">{{\n              $t(\"RemoveMintDialog.nickname.label\")\n            }}</label>\n            <div class=\"mint-data-display\">{{ mintToRemove.nickname }}</div>\n          </div>\n\n          <div class=\"q-mb-md\">\n            <label class=\"input-label\">{{\n              $t(\"RemoveMintDialog.balances.label\")\n            }}</label>\n            <div class=\"mint-data-display\">\n              <q-badge\n                v-for=\"unit in mintClass(mintToRemove).units\"\n                :key=\"unit\"\n                color=\"primary\"\n                :label=\"\n                  formatCurrency(\n                    mintClass(mintToRemove).unitBalance(unit),\n                    unit\n                  )\n                \"\n                class=\"q-mr-sm\"\n              />\n            </div>\n          </div>\n\n          <label class=\"input-label\">{{\n            $t(\"RemoveMintDialog.inputs.mint_url.label\")\n          }}</label>\n          <q-input\n            outlined\n            readonly\n            :model-value=\"mintToRemove.url\"\n            dense\n            class=\"mint-input\"\n            filled\n            type=\"textarea\"\n            autogrow\n            style=\"font-family: monospace; font-size: 0.9em\"\n          ></q-input>\n        </div>\n\n        <div class=\"q-mb-lg\">\n          <span class=\"remove-mint-description\">\n            {{ $t(\"RemoveMintDialog.warning_text\") }}\n          </span>\n        </div>\n\n        <div class=\"action-buttons\">\n          <q-btn flat class=\"cancel-btn\" v-close-popup>\n            {{ $t(\"RemoveMintDialog.actions.cancel.label\") }}\n          </q-btn>\n          <q-spacer></q-spacer>\n          <q-btn\n            color=\"negative\"\n            class=\"remove-btn\"\n            @click=\"removeMintLocal\"\n            v-close-popup\n          >\n            {{ $t(\"RemoveMintDialog.actions.confirm.label\") }}\n          </q-btn>\n        </div>\n      </div>\n    </q-card>\n  </q-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, computed } from \"vue\";\nimport { MintClass } from \"src/stores/mints\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { useMintsStore } from \"src/stores/mints\";\n\nexport default defineComponent({\n  name: \"RemoveMintDialog\",\n  props: {\n    mintToRemove: {\n      type: Object,\n      required: true,\n    },\n    showRemoveMintDialog: {\n      type: Boolean,\n      required: true,\n    },\n  },\n  emits: [\"remove\", \"update:showRemoveMintDialog\"],\n  setup(props, { emit }) {\n    const mintsStore = useMintsStore();\n    const showRemoveMintDialogLocal = computed({\n      get: () => props.showRemoveMintDialog,\n      set: (value) => emit(\"update:showRemoveMintDialog\", value),\n    });\n\n    const removeMintLocal = () => {\n      emit(\"remove\", props.mintToRemove.url);\n      mintsStore.showMintInfoDialog = false;\n      mintsStore.showEditMintDialog = false;\n\n      // If we're on the mint details page, navigate back to home\n      if (window.location.pathname === \"/mintdetails\") {\n        window.history.back();\n      }\n    };\n    const mintClass = (mint) => {\n      return new MintClass(mint);\n    };\n    const formatCurrency = (amount, unit) => {\n      return useUiStore().formatCurrency(amount, unit);\n    };\n\n    return {\n      removeMintLocal,\n      showRemoveMintDialogLocal,\n      mintClass,\n      formatCurrency,\n    };\n  },\n});\n</script>\n\n<style scoped>\n.remove-mint-dialog {\n  width: 100%;\n  max-width: 450px;\n  border-radius: 16px;\n  overflow: hidden;\n}\n\n.remove-mint-header {\n  position: relative;\n  padding-top: 20px;\n}\n\n.remove-mint-title-row {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n}\n\n.remove-mint-title {\n  font-size: 24px;\n  font-weight: 700;\n  letter-spacing: -0.5px;\n  font-family: \"Inter\", sans-serif;\n}\n\n.remove-mint-content {\n  padding-top: 0;\n}\n\n.remove-mint-description {\n  font-size: 15px;\n  line-height: 1.5;\n  font-weight: 400;\n  margin-top: 0;\n  opacity: 0.7;\n  font-family: \"Inter\", sans-serif;\n}\n\n.input-label {\n  display: block;\n  font-size: 13px;\n  font-weight: 600;\n  margin-bottom: 8px;\n  text-transform: uppercase;\n  letter-spacing: 0.5px;\n  opacity: 0.7;\n  font-family: \"Inter\", sans-serif;\n}\n\n.mint-data-display {\n  padding: 12px;\n  background-color: rgba(0, 0, 0, 0.03);\n  border-radius: 8px;\n  min-height: 48px;\n  display: flex;\n  align-items: center;\n  font-family: \"Inter\", sans-serif;\n}\n\n.body--dark .mint-data-display {\n  background-color: rgba(255, 255, 255, 0.05);\n}\n\n.mint-input {\n  border-radius: 8px;\n  font-size: 16px;\n  font-family: \"Inter\", sans-serif;\n}\n\n.mint-note {\n  background-color: rgba(0, 0, 0, 0.03);\n  padding: 12px;\n  border-radius: 8px;\n  font-family: \"Inter\", sans-serif;\n}\n\n.body--dark .mint-note {\n  background-color: rgba(255, 255, 255, 0.05);\n}\n\n/* Completely remove all input animations */\n:deep(.mint-input) {\n  /* Disable all transitions on the input and its children except for background-color */\n  * {\n    transition: none !important;\n    animation: none !important;\n  }\n\n  /* Add a smooth transition just for the background-color */\n  transition: background-color 0.2s ease-in-out !important;\n}\n\n:deep(.mint-input .q-field__focus-target) {\n  border-radius: 8px;\n}\n\n:deep(.mint-input .q-focus-helper) {\n  /* Remove animation completely */\n  opacity: 0 !important;\n  display: none !important; /* Hide it completely */\n}\n\n/* Add subtle focus/active state - theme responsive */\n:deep(.mint-input.q-field--focused) {\n  background-color: rgba(255, 255, 255, 0.1);\n}\n\n/* For dark mode, adjust the focus color */\n:deep(.body--dark .mint-input.q-field--focused) {\n  background-color: rgba(255, 255, 255, 0.07);\n}\n\n/* For light mode, use a darker shade for contrast */\n:deep(.body--light .mint-input.q-field--focused) {\n  background-color: rgba(0, 0, 0, 0.05);\n}\n\n/* Remove any ripple effects */\n:deep(.mint-input .q-ripple) {\n  display: none !important;\n}\n\n/* Remove any before/after pseudo-elements that might animate */\n:deep(.mint-input .q-field__control:before),\n:deep(.mint-input .q-field__control:after) {\n  display: none !important;\n}\n\n/* Ensure no border animations */\n:deep(.mint-input .q-field__control) {\n  border-radius: 8px;\n  transition: none !important;\n}\n\n:deep(.mint-input .q-field__native) {\n  padding: 12px;\n  font-family: \"Inter\", sans-serif;\n}\n\n/* Make sure input placeholders use Inter font */\n:deep(.mint-input .q-field__native),\n:deep(.mint-input .q-field__input),\n:deep(.mint-input .q-placeholder) {\n  font-family: \"Inter\", sans-serif;\n}\n\n.action-buttons {\n  display: flex;\n  justify-content: space-between;\n  margin-top: 32px;\n}\n\n.cancel-btn {\n  font-weight: 600;\n  padding: 8px 16px;\n  border-radius: 8px;\n  font-family: \"Inter\", sans-serif;\n}\n\n.remove-btn {\n  font-weight: 700;\n  padding: 8px 20px;\n  border-radius: 8px;\n  transition: all 0.2s ease;\n  font-family: \"Inter\", sans-serif;\n}\n\n.remove-btn:hover {\n  transform: translateY(-1px);\n}\n</style>\n"
  },
  {
    "path": "src/components/RestoreView.vue",
    "content": "<template>\n  <div style=\"max-width: 800px; margin: 0 auto\">\n    <!-- Mnemonic seed phrase input -->\n    <div v-if=\"!onboarding\" class=\"q-px-xs text-left\" on-left>\n      <q-list padding>\n        <q-item>\n          <q-item-section>\n            <q-item-label overline class=\"text-weight-bold\">\n              {{ $t(\"RestoreView.seed_phrase.label\") }}\n            </q-item-label>\n            <q-item-label caption>\n              {{ $t(\"RestoreView.seed_phrase.caption\") }}\n            </q-item-label>\n            <div class=\"row q-pt-md\">\n              <div class=\"col-12\">\n                <q-input\n                  outlined\n                  v-model=\"mnemonicInput\"\n                  :label=\"\n                    $t('RestoreView.seed_phrase.inputs.seed_phrase.label')\n                  \"\n                  autogrow\n                  type=\"textarea\"\n                  :error=\"mnemonicError !== ''\"\n                  :error-message=\"mnemonicError\"\n                >\n                  <template v-slot:append>\n                    <q-btn\n                      flat\n                      dense\n                      icon=\"content_paste\"\n                      @click=\"pasteMnemonic\"\n                      class=\"cursor-pointer q-mt-md\"\n                    ></q-btn>\n                  </template>\n                </q-input>\n              </div>\n            </div>\n          </q-item-section>\n        </q-item>\n      </q-list>\n    </div>\n\n    <!-- Information about restoring mints -->\n    <div v-if=\"!onboarding\" class=\"q-px-xs text-left\" on-left>\n      <q-list padding>\n        <q-item>\n          <q-item-section>\n            <q-item-label overline class=\"text-weight-bold\">\n              {{ $t(\"RestoreView.information.label\") }}\n            </q-item-label>\n            <q-item-label caption>\n              {{ $t(\"RestoreView.information.caption\") }}\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n      </q-list>\n    </div>\n\n    <!-- Information about adding mints -->\n    <div v-if=\"!onboarding\" class=\"q-px-xs text-left q-mt-md\" on-left>\n      <q-list padding>\n        <q-item>\n          <q-item-section>\n            <q-item-label overline class=\"text-weight-bold\">\n              {{ $t(\"RestoreView.restore_mints.label\") }}\n            </q-item-label>\n            <q-item-label caption>\n              {{ $t(\"RestoreView.restore_mints.caption\") }}\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n      </q-list>\n    </div>\n\n    <!-- List of mints with restore buttons and balance badges -->\n    <div class=\"q-pb-md q-px-xs text-left\" on-left>\n      <!-- Restore Selected Mints Button (Primary Action) -->\n      <div v-if=\"mints.length > 0\" class=\"primary-action-section q-pb-md\">\n        <q-btn\n          color=\"primary\"\n          size=\"md\"\n          rounded\n          @click=\"restoreSelectedMints\"\n          :disabled=\"\n            (onboarding ? false : !isMnemonicValid) ||\n            restoringState ||\n            selectedMintsCount === 0\n          \"\n          :loading=\"restoringState\"\n          class=\"q-px-md\"\n        >\n          <q-icon name=\"restore\" class=\"q-mr-sm\" />\n          {{\n            $t(\"RestoreView.actions.restore_selected_mints.label\", {\n              count: selectedMintsCount,\n            })\n          }}\n        </q-btn>\n      </div>\n\n      <!-- Select All/Deselect All Buttons -->\n      <div\n        v-if=\"mints.length > 0 && !onboarding\"\n        class=\"selection-buttons q-px-sm q-pb-md\"\n      >\n        <q-btn\n          flat\n          dense\n          size=\"md\"\n          color=\"primary\"\n          @click=\"selectAllMints\"\n          :disabled=\"allSelected\"\n          class=\"q-mr-md q-px-md\"\n        >\n          {{ $t(\"RestoreView.actions.select_all.label\") }}\n        </q-btn>\n        <q-btn\n          flat\n          dense\n          size=\"md\"\n          color=\"grey\"\n          @click=\"deselectAllMints\"\n          :disabled=\"!anySelected\"\n          class=\"q-px-md\"\n        >\n          {{ $t(\"RestoreView.actions.deselect_all.label\") }}\n        </q-btn>\n      </div>\n\n      <!-- Mints List with Card Design -->\n      <div class=\"q-pt-md\">\n        <div v-for=\"mint in mints\" :key=\"mint.url\" class=\"q-mb-md\">\n          <q-item\n            clickable\n            @click=\"toggleMintSelection(mint.url)\"\n            class=\"mint-card cursor-pointer\"\n            :style=\"{\n              'border-radius': '10px',\n              border: selectedMints.has(mint.url)\n                ? '1px solid var(--q-primary)'\n                : '1px solid rgba(128, 128, 128, 0.2)',\n              padding: '0px',\n              position: 'relative',\n              'background-color': selectedMints.has(mint.url)\n                ? 'rgba(var(--q-primary-rgb), 0.1)'\n                : 'transparent',\n            }\"\n            :disable=\"(onboarding ? false : !isMnemonicValid) || restoringState\"\n          >\n            <div class=\"full-width\" style=\"position: relative\">\n              <div class=\"row items-center q-pa-md\">\n                <!-- Checkbox Section -->\n                <q-item-section avatar>\n                  <q-checkbox\n                    :model-value=\"selectedMints.has(mint.url)\"\n                    @update:model-value=\"toggleMintSelection(mint.url)\"\n                    @click.stop\n                    color=\"primary\"\n                    class=\"clickable-checkbox\"\n                  />\n                </q-item-section>\n\n                <div class=\"col\">\n                  <div class=\"row items-center\">\n                    <!-- Mint Avatar -->\n                    <q-avatar\n                      v-if=\"getMintIconUrl(mint)\"\n                      size=\"34px\"\n                      class=\"q-mr-sm\"\n                    >\n                      <q-img\n                        spinner-color=\"white\"\n                        spinner-size=\"xs\"\n                        :src=\"getMintIconUrl(mint)\"\n                        alt=\"Mint Icon\"\n                        style=\"height: 34px; max-width: 34px; font-size: 12px\"\n                      />\n                    </q-avatar>\n\n                    <div class=\"mint-info-container\">\n                      <!-- Mint Name -->\n                      <div\n                        v-if=\"mint.nickname || mint.info?.name\"\n                        class=\"mint-name\"\n                      >\n                        {{ mint.nickname || mint.info?.name }}\n                      </div>\n                      <!-- Mint URL -->\n                      <div class=\"text-grey-6 mint-url\">\n                        {{ mint.url }}\n                      </div>\n                    </div>\n                  </div>\n                </div>\n              </div>\n\n              <!-- Balance and Restore Section -->\n              <div class=\"row justify-between q-pb-md q-pl-lg q-pr-md\">\n                <div class=\"col\">\n                  <!-- Currency units with regular text styling -->\n                  <div class=\"row q-gutter-x-sm\">\n                    <div\n                      v-for=\"unit in mintClass(mint).units\"\n                      :key=\"unit\"\n                      class=\"currency-unit-badge\"\n                    >\n                      <span class=\"currency-unit-text\">\n                        {{\n                          formatCurrency(\n                            mintClass(mint).unitBalance(unit),\n                            unit\n                          )\n                        }}\n                      </span>\n                    </div>\n                  </div>\n\n                  <!-- Restore Progress -->\n                  <div v-if=\"restoringMint === mint.url\" class=\"q-mt-sm\">\n                    <div class=\"text-grey-6 q-mb-xs\" style=\"font-size: 12px\">\n                      {{ restoreStatus }}\n                    </div>\n                    <q-linear-progress\n                      :value=\"restoreProgress\"\n                      color=\"primary\"\n                      style=\"height: 4px; border-radius: 2px\"\n                    />\n                  </div>\n                </div>\n\n                <div v-if=\"!onboarding\" class=\"col-auto\">\n                  <q-btn\n                    color=\"secondary\"\n                    size=\"sm\"\n                    rounded\n                    dense\n                    flat\n                    @click.stop=\"restoreMintForMint(mint.url)\"\n                    :disabled=\"!isMnemonicValid || restoringState\"\n                    :loading=\"restoringMint === mint.url\"\n                    icon=\"restore\"\n                    class=\"q-px-sm\"\n                  >\n                    <q-tooltip>{{\n                      $t(\"RestoreView.actions.restore.label\")\n                    }}</q-tooltip>\n                  </q-btn>\n                </div>\n              </div>\n            </div>\n          </q-item>\n        </div>\n      </div>\n    </div>\n\n    <!-- Nostr Mint Restore Component -->\n    <NostrMintRestore\n      v-if=\"!onboarding\"\n      :mnemonic=\"normalisedMnemonic\"\n      :is-mnemonic-valid=\"isMnemonicValid\"\n    />\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport { useMintsStore, MintClass } from \"src/stores/mints\";\nimport { useRestoreStore } from \"src/stores/restore\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { notifyError, notifySuccess } from \"src/js/notify\";\nimport NostrMintRestore from \"./NostrMintRestore.vue\";\nimport { validateMnemonic as validateBip39Mnemonic } from \"@scure/bip39\";\nimport { wordlist } from \"@scure/bip39/wordlists/english\";\n\nexport default defineComponent({\n  name: \"RestoreView\",\n  mixins: [windowMixin],\n  components: {\n    NostrMintRestore,\n  },\n  props: {\n    onboarding: { type: Boolean, default: false },\n  },\n  data() {\n    return {\n      restoreAllMintsText: this.$i18n.t(\n        \"RestoreView.actions.restore_all_mints.label\"\n      ),\n      selectedMints: new Set(), // Track selected mint URLs\n    };\n  },\n  computed: {\n    ...mapState(useMintsStore, [\"mints\"]),\n    ...mapWritableState(useWalletStore, [\"mnemonic\"]),\n    ...mapWritableState(useRestoreStore, [\n      \"mnemonicToRestore\",\n      \"restoreProgress\",\n    ]),\n    ...mapState(useRestoreStore, [\n      \"restoringState\",\n      \"restoringMint\",\n      \"restoreStatus\",\n    ]),\n\n    mnemonicInput: {\n      get(): string {\n        return this.mnemonicToRestore || \"\";\n      },\n      set(v: string) {\n        // lowercase live, keep spacing as typed to avoid cursor jumps\n        this.mnemonicToRestore = (v || \"\").toLowerCase();\n      },\n    },\n    normalisedMnemonic(): string {\n      return (this.mnemonicToRestore || \"\")\n        .trim()\n        .toLowerCase()\n        .split(/\\s+/)\n        .filter(Boolean)\n        .join(\" \");\n    },\n    mnemonicWordCount(): number {\n      return this.normalisedMnemonic\n        ? this.normalisedMnemonic.split(\" \").length\n        : 0;\n    },\n    isMnemonicValid(): boolean {\n      if (this.onboarding) return true;\n      if (this.mnemonicWordCount < 12) return false;\n      return validateBip39Mnemonic(this.normalisedMnemonic, wordlist);\n    },\n    mnemonicError(): string {\n      if (this.onboarding) return \"\";\n\n      const count = this.mnemonicWordCount;\n      if (count === 0) return \"\";\n      if (count < 12) return `${count}/12 words entered`;\n\n      if (!validateBip39Mnemonic(this.normalisedMnemonic, wordlist)) {\n        return this.$i18n.t(\"RestoreView.actions.validate.error\");\n      }\n\n      return \"\";\n    },\n    allSelected() {\n      return (\n        this.mints.length > 0 &&\n        this.mints.every((mint) => this.selectedMints.has(mint.url))\n      );\n    },\n    anySelected() {\n      return this.selectedMints.size > 0;\n    },\n    selectedMintsCount() {\n      return this.selectedMints.size;\n    },\n  },\n  mounted() {\n    if (this.onboarding) {\n      this.selectAllMints();\n    }\n  },\n  watch: {\n    mints: {\n      handler() {\n        if (this.onboarding) {\n          this.selectAllMints();\n        }\n      },\n      deep: true,\n    },\n    onboarding(newVal) {\n      if (newVal) {\n        this.selectAllMints();\n      }\n    },\n  },\n  methods: {\n    ...mapActions(useRestoreStore, [\"restoreMint\"]),\n    ...mapActions(useUiStore, [\"pasteFromClipboard\"]),\n    mintClass(mint) {\n      return new MintClass(mint);\n    },\n    getMintIconUrl: function (mint) {\n      if (mint.info) {\n        if (mint.info.icon_url) {\n          return mint.info.icon_url;\n        } else {\n          return undefined;\n        }\n      } else {\n        return undefined;\n      }\n    },\n    formatCurrency(amount, unit) {\n      return useUiStore().formatCurrency(amount, unit);\n    },\n    toggleMintSelection(mintUrl) {\n      if (this.selectedMints.has(mintUrl)) {\n        this.selectedMints.delete(mintUrl);\n      } else {\n        this.selectedMints.add(mintUrl);\n      }\n    },\n    selectAllMints() {\n      this.mints.forEach((mint) => {\n        this.selectedMints.add(mint.url);\n      });\n    },\n    deselectAllMints() {\n      this.selectedMints.clear();\n    },\n    async restoreSelectedMints() {\n      if (this.selectedMintsCount === 0) {\n        return;\n      }\n\n      if (!this.onboarding) {\n        if (!this.isMnemonicValid) return;\n        this.mnemonicToRestore = this.normalisedMnemonic;\n      }\n\n      const selectedMintUrls = Array.from(this.selectedMints);\n      let i = 0;\n\n      try {\n        for (const mintUrl of selectedMintUrls) {\n          this.restoreAllMintsText = this.$i18n.t(\n            \"RestoreView.actions.restore_selected_mints.in_progress\",\n            {\n              index: ++i,\n              length: selectedMintUrls.length,\n            }\n          );\n          await this.restoreMint(mintUrl);\n        }\n        notifySuccess(\n          this.$i18n.t(\"RestoreView.actions.restore_selected_mints.success\", {\n            count: selectedMintUrls.length,\n          })\n        );\n        // Clear selections after successful restore\n        this.deselectAllMints();\n      } catch (error) {\n        console.error(\"Error restoring selected mints:\", error);\n        notifyError(\n          this.$i18n.t(\"RestoreView.actions.restore_selected_mints.error\", {\n            error: error.message || error,\n          })\n        );\n      } finally {\n        this.restoreAllMintsText = this.$i18n.t(\n          \"RestoreView.actions.restore_all_mints.label\"\n        );\n      }\n    },\n    async restoreMintForMint(mintUrl) {\n      if (!this.onboarding) {\n        if (!this.isMnemonicValid) return;\n        this.mnemonicToRestore = this.normalisedMnemonic;\n      }\n\n      try {\n        this.restoreAllMintsText = this.$i18n.t(\n          \"RestoreView.actions.restore.in_progress\"\n        );\n        await this.restoreMint(mintUrl);\n      } catch (error) {\n        console.error(\"Error restoring mint:\", error);\n        notifyError(\n          this.$i18n.t(\"RestoreView.actions.restore.error\", {\n            error: error.message || error,\n          })\n        );\n      } finally {\n        this.restoreAllMintsText = this.$i18n.t(\n          \"RestoreView.actions.restore_all_mints.label\"\n        );\n      }\n    },\n    async pasteMnemonic() {\n      try {\n        const text = await this.pasteFromClipboard();\n        this.mnemonicToRestore = (text || \"\")\n          .trim()\n          .split(/\\s+/)\n          .filter(Boolean)\n          .join(\" \");\n      } catch (error) {\n        notifyError(this.$i18n.t(\"RestoreView.actions.paste.error\"));\n      }\n    },\n    async restoreAllMints() {\n      let i = 0;\n      if (!this.onboarding) {\n        if (!this.isMnemonicValid) return;\n        this.mnemonicToRestore = this.normalisedMnemonic;\n      }\n      try {\n        for (const mint of this.mints) {\n          this.restoreAllMintsText = this.$i18n.t(\n            \"RestoreView.actions.restore_all_mints.in_progress\",\n            {\n              index: ++i,\n              length: this.mints.length,\n            }\n          );\n          await this.restoreMint(mint.url);\n        }\n        notifySuccess(\n          this.$i18n.t(\"RestoreView.actions.restore_all_mints.success\")\n        );\n      } catch (error) {\n        console.error(\"Error restoring mints:\", error);\n        notifyError(\n          this.$i18n.t(\"RestoreView.actions.restore_all_mints.error\", {\n            error: error.message || error,\n          })\n        );\n      } finally {\n        this.restoreAllMintsText = this.$i18n.t(\n          \"RestoreView.actions.restore_all_mints.label\"\n        );\n      }\n    },\n  },\n});\n</script>\n\n<style>\n@import \"src/css/mintlist.css\";\n\n/* Fallback action section - specific to RestoreView */\n.fallback-action-section {\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n}\n\n/* Primary action section */\n.primary-action-section {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n</style>\n"
  },
  {
    "path": "src/components/SendDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showSendDialog\"\n    position=\"bottom\"\n    :maximized=\"$q.screen.lt.sm\"\n    transition-show=\"slide-up\"\n    transition-hide=\"slide-down\"\n  >\n    <q-card class=\"drawer-card text-white full-width-card q-pb-lg\">\n      <q-card-section class=\"row items-center q-pb-sm\">\n        <q-btn flat round dense v-close-popup class=\"q-ml-sm\" color=\"primary\">\n          <XIcon />\n        </q-btn>\n        <div class=\"col text-center\">\n          <span class=\"text-h6\">{{ $t(\"SendDialog.title\") }}</span>\n        </div>\n        <q-btn\n          flat\n          round\n          dense\n          class=\"q-mr-sm\"\n          @click=\"showCamera\"\n          color=\"primary\"\n        >\n          <ScanIcon />\n        </q-btn>\n      </q-card-section>\n\n      <q-card-section class=\"q-pa-md\">\n        <div class=\"q-gutter-y-md\">\n          <div class=\"action-row\" @click=\"showSendTokensDialog\">\n            <div class=\"row items-center no-wrap\">\n              <div class=\"icon-circle\">\n                <CoinsIcon :size=\"24\" />\n              </div>\n              <div class=\"col q-ml-md\">\n                <div class=\"text-body1 text-weight-medium\">\n                  {{ $t(\"SendDialog.actions.ecash.label\") }}\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <div class=\"action-row\" @click=\"showParseDialog\">\n            <div class=\"row items-center no-wrap\">\n              <div class=\"icon-circle\">\n                <ZapIcon :size=\"24\" />\n              </div>\n              <div class=\"col q-ml-md\">\n                <div class=\"text-body1 text-weight-medium\">\n                  {{ $t(\"SendDialog.actions.lightning.label\") }}\n                </div>\n              </div>\n            </div>\n          </div>\n        </div>\n      </q-card-section>\n    </q-card>\n  </q-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { useReceiveTokensStore } from \"src/stores/receiveTokensStore\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useCameraStore } from \"src/stores/camera\";\nimport { useSendTokensStore } from \"src/stores/sendTokensStore\";\nimport { useSettingsStore } from \"../stores/settings\";\nimport { useMintsStore } from \"src/stores/mints\";\nimport {\n  X as XIcon,\n  Banknote as BanknoteIcon,\n  Zap as ZapIcon,\n  Scan as ScanIcon,\n  Coins as CoinsIcon,\n} from \"lucide-vue-next\";\nimport { notifyWarning } from \"src/js/notify\";\n\nexport default defineComponent({\n  name: \"SendDialog\",\n  components: {\n    XIcon,\n    CoinsIcon,\n    ZapIcon,\n    ScanIcon,\n  },\n  mixins: [windowMixin],\n  props: {},\n  data: function () {\n    return {\n      currentPage: 1,\n      pageSize: 5,\n    };\n  },\n  computed: {\n    ...mapState(useMintsStore, [\"mints\"]),\n    ...mapWritableState(useUiStore, [\n      \"showInvoiceDetails\",\n      \"tab\",\n      \"showSendDialog\",\n      \"showReceiveDialog\",\n    ]),\n    ...mapWritableState(useWalletStore, [\n      \"invoiceHistory\",\n      \"invoiceData\",\n      \"payInvoiceData\",\n    ]),\n    ...mapWritableState(useCameraStore, [\"camera\"]),\n    ...mapWritableState(useSendTokensStore, [\n      \"showSendTokens\",\n      \"sendData\",\n      \"showLockInput\",\n    ]),\n    canMakePayments: function () {\n      if (!this.mints.length) {\n        return false;\n      } else {\n        return true;\n      }\n    },\n  },\n  methods: {\n    ...mapActions(useCameraStore, [\"closeCamera\", \"showCamera\"]),\n    showParseDialog: function () {\n      if (!this.canMakePayments) {\n        notifyWarning(\n          this.$i18n.t(\"SendDialog.actions.lightning.error_no_mints\")\n        );\n        this.showSendDialog = false;\n        return;\n      }\n      this.payInvoiceData.show = true;\n      this.payInvoiceData.invoice = null;\n      this.payInvoiceData.lnurlpay = null;\n      this.payInvoiceData.domain = \"\";\n      this.payInvoiceData.lnurlauth = null;\n      this.payInvoiceData.input.request = \"\";\n      this.payInvoiceData.input.comment = \"\";\n      this.camera.show = false;\n      this.showSendDialog = false;\n    },\n    showSendTokensDialog: function () {\n      console.log(\"##### showSendTokensDialog\");\n      if (!this.canMakePayments) {\n        notifyWarning(this.$i18n.t(\"SendDialog.actions.ecash.error_no_mints\"));\n        this.showSendDialog = false;\n        return;\n      }\n      this.sendData.tokens = \"\";\n      this.sendData.tokensBase64 = \"\";\n      this.sendData.amount = null;\n      this.sendData.memo = \"\";\n      this.sendData.p2pkPubkey = \"\";\n      this.sendData.paymentRequest = undefined;\n      this.showSendDialog = false;\n      this.showSendTokens = true;\n      this.showLockInput = false;\n    },\n  },\n  created: function () {},\n});\n</script>\n\n<style lang=\"scss\" scoped>\n::v-deep .q-dialog__backdrop {\n  backdrop-filter: blur(8px);\n  background: rgba(0, 0, 0, 0.4) !important;\n}\n\n.q-dialog__inner > div {\n  border-top-left-radius: 20px !important;\n  border-top-right-radius: 20px !important;\n  border-bottom-left-radius: 0px !important;\n  border-bottom-right-radius: 0px !important;\n}\n\n.drawer-card {\n  background: #1a1a1a;\n}\n\n.action-row {\n  background: rgba(255, 255, 255, 0.06);\n  border-radius: 12px;\n  padding: 12px 16px;\n  cursor: pointer;\n  transition: background 0.2s ease;\n\n  &:active {\n    background: rgba(255, 255, 255, 0.1);\n  }\n}\n\n.icon-circle {\n  width: 48px;\n  height: 48px;\n  border-radius: 50%;\n  background: rgba(255, 255, 255, 0.1);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  flex-shrink: 0;\n}\n\n.lucide {\n  width: 24px;\n  height: 24px;\n  color: white;\n}\n</style>\n"
  },
  {
    "path": "src/components/SendPaymentRequest.vue",
    "content": "<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: 600px\"\n  >\n    <q-btn\n      class=\"full-width\"\n      :rounded=\"buttonRounded\"\n      :dense=\"buttonDense\"\n      :unelevated=\"buttonUnelevated\"\n      :size=\"buttonSize\"\n      :color=\"buttonColor\"\n      :class=\"computedButtonClasses\"\n      :disable=\"disable || !sendData.paymentRequest\"\n      :loading=\"isLoading\"\n      @click=\"clickPaymentRequest\"\n    >\n      <q-icon v-if=\"!isLoading\" name=\"send\" class=\"q-pr-xs\" />\n      <q-spinner v-else size=\"1em\" class=\"q-mr-md\" />\n      {{ resolvedLabel }}\n    </q-btn>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from \"vue\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport { useSendTokensStore } from \"src/stores/sendTokensStore\";\nimport { usePRStore } from \"src/stores/payment-request\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { notifyError } from \"src/js/notify\";\nimport { PaymentRequest, PaymentRequestTransportType } from \"@cashu/cashu-ts\";\n\ndeclare const windowMixin: any;\n\nexport default defineComponent({\n  name: \"SendPaymentRequest\",\n  mixins: [windowMixin],\n  props: {\n    buttonLabel: {\n      type: String,\n      default: \"\",\n    },\n    disable: {\n      type: Boolean,\n      default: false,\n    },\n    showDetails: {\n      type: Boolean,\n      default: true,\n    },\n    fullWidth: {\n      type: Boolean,\n      default: false,\n    },\n    prepareToken: {\n      type: Function as PropType<() => Promise<string | undefined>>,\n      default: null,\n    },\n    buttonClass: {\n      type: [String, Array, Object] as PropType<\n        string | string[] | Record<string, boolean>\n      >,\n      default: () => [],\n    },\n    buttonColor: {\n      type: String,\n      default: \"primary\",\n    },\n    buttonRounded: {\n      type: Boolean,\n      default: true,\n    },\n    buttonDense: {\n      type: Boolean,\n      default: true,\n    },\n    buttonUnelevated: {\n      type: Boolean,\n      default: false,\n    },\n    buttonSize: {\n      type: String,\n      default: \"md\",\n    },\n  },\n  emits: [\"success\"],\n  data: function () {\n    return {\n      loading: false,\n    };\n  },\n  watch: {},\n  computed: {\n    ...mapWritableState(useSendTokensStore, [\n      \"showSendTokens\",\n      \"showLockInput\",\n      \"sendData\",\n    ]),\n    ...mapState(useUiStore, [\"globalMutexLock\"]),\n    isLoading(): boolean {\n      return this.loading || this.globalMutexLock;\n    },\n    resolvedLabel(): string {\n      if (this.buttonLabel) {\n        return this.buttonLabel;\n      }\n      const transport = this.getPaymentRequestTransportType(\n        this.sendData.paymentRequest\n      );\n      if (!transport) {\n        return this.$t(\"SendPaymentRequest.actions.pay.label\") as string;\n      }\n      return this.$t(\"SendPaymentRequest.actions.pay_via.label\", {\n        transport,\n      }) as string;\n    },\n    detailText(): string {\n      const target = this.getPaymentRequestTarget(this.sendData.paymentRequest);\n      if (!target) {\n        return \"\";\n      }\n      return this.$t(\"SendPaymentRequest.info.pay_to\", {\n        target,\n      }) as string;\n    },\n    computedButtonClasses():\n      | string\n      | string[]\n      | Record<string, boolean>\n      | Array<string | Record<string, boolean>> {\n      const classes: Array<string | Record<string, boolean>> = [];\n      if (this.fullWidth) {\n        classes.push(\"full-width\");\n      }\n      const extra = this.buttonClass;\n      if (Array.isArray(extra)) {\n        classes.push(...extra);\n      } else if (typeof extra === \"string\" && extra.trim().length > 0) {\n        classes.push(extra);\n      } else if (extra && typeof extra === \"object\") {\n        classes.push(extra);\n      }\n      return classes;\n    },\n  },\n  methods: {\n    ...mapActions(usePRStore, [\"parseAndPayPaymentRequest\"]),\n    async clickPaymentRequest() {\n      if (this.disable || !this.sendData.paymentRequest) {\n        return;\n      }\n      this.loading = true;\n      try {\n        let tokenStr = this.sendData.tokensBase64;\n        if (this.prepareToken) {\n          const prepared = await this.prepareToken();\n          if (prepared) {\n            tokenStr = prepared;\n          }\n        }\n        if (!tokenStr) {\n          throw new Error(\"No ecash available for payment request.\");\n        }\n        const success = await this.parseAndPayPaymentRequest(\n          this.sendData.paymentRequest,\n          tokenStr\n        );\n        if (success) {\n          this.$emit(\"success\");\n        }\n      } catch (error: any) {\n        console.error(\"Error paying payment request:\", error);\n        notifyError(`${error?.message ?? error}`, \"Could not pay request\");\n      } finally {\n        this.loading = false;\n      }\n    },\n    getPaymentRequestTransportType(request?: PaymentRequest) {\n      if (!request || !request.transport) {\n        return \"\";\n      }\n      const transports = request.transport;\n      for (const transport of transports) {\n        if (transport.type == PaymentRequestTransportType.NOSTR) {\n          return \"Nostr\";\n        }\n        if (transport.type == PaymentRequestTransportType.POST) {\n          return \"HTTP\";\n        }\n      }\n      return \"\";\n    },\n    getPaymentRequestTarget(request?: PaymentRequest) {\n      if (!request || !request.transport) {\n        return \"\";\n      }\n      const transports = request.transport;\n      for (const transport of transports) {\n        if (transport.type == PaymentRequestTransportType.NOSTR) {\n          return `${transport.target.slice(0, 20)}..${transport.target.slice(\n            -10\n          )}`;\n        }\n        if (transport.type == PaymentRequestTransportType.POST) {\n          try {\n            const url = new URL(transport.target);\n            return url.hostname;\n          } catch (error) {\n            console.error(\n              `Invalid URL in transport.target: ${transport.target}`,\n              error\n            );\n            return this.$t(\"SendPaymentRequest.info.invalid_url\") as string;\n          }\n        }\n      }\n      return \"\";\n    },\n  },\n});\n</script>\n"
  },
  {
    "path": "src/components/SendTokenDialog.vue",
    "content": "<template>\n  <q-dialog\n    v-model=\"showSendTokens\"\n    maximized\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n    transition-show=\"fade\"\n    transition-hide=\"fade\"\n    no-backdrop-dismiss\n    @keydown.esc=\"showSendTokens = false\"\n  >\n    <q-card class=\"q-pa-none q-pt-none qcard\">\n      <!--  enter send data (full-screen) -->\n      <div\n        v-if=\"!sendData.tokens\"\n        class=\"column fit send-fullscreen\"\n        :class=\"$q.dark.isActive ? 'bg-dark' : 'bg-white'\"\n      >\n        <!-- Header -->\n        <div class=\"row items-center q-pa-md\" style=\"position: relative\">\n          <q-btn\n            v-close-popup\n            flat\n            round\n            icon=\"close\"\n            color=\"grey\"\n            class=\"floating-close-btn\"\n          />\n          <div class=\"col text-center fixed-title-height\">\n            <q-item-label\n              class=\"dialog-header q-mt-sm\"\n              :class=\"$q.dark.isActive ? 'text-white' : 'text-black'\"\n            >\n              {{ $t(\"SendTokenDialog.title\") }}\n            </q-item-label>\n          </div>\n          <div\n            class=\"row items-center q-gutter-sm\"\n            style=\"position: absolute; right: 16px\"\n          >\n            <q-btn\n              flat\n              dense\n              color=\"primary\"\n              round\n              @click=\"showLockInput = !showLockInput\"\n            >\n              <LockIcon size=\"1.2em\" />\n              <q-tooltip>{{\n                $t(\"SendTokenDialog.actions.lock.label\")\n              }}</q-tooltip>\n            </q-btn>\n            <q-btn\n              flat\n              dense\n              size=\"lg\"\n              color=\"primary\"\n              @click=\"toggleUnit()\"\n              :label=\"activeUnitLabel\"\n            />\n          </div>\n        </div>\n\n        <!-- Payment request info -->\n        <div v-if=\"sendData.paymentRequest\" class=\"row justify-center q-pt-sm\">\n          <div\n            class=\"col-12 col-sm-11 col-md-8 q-px-lg\"\n            style=\"max-width: 600px\"\n          >\n            <PaymentRequestInfo :request=\"sendData.paymentRequest\" />\n          </div>\n        </div>\n\n        <!-- Mint selection -->\n        <div class=\"row justify-center\">\n          <div\n            class=\"col-12 col-sm-11 col-md-8 q-px-lg q-mb-sm\"\n            style=\"max-width: 600px\"\n          >\n            <ChooseMint />\n          </div>\n        </div>\n\n        <!-- Amount display -->\n        <div class=\"col column items-center justify-center q-px-lg amount-area\">\n          <!-- Floating P2PK input overlay -->\n          <transition\n            appear\n            enter-active-class=\"animated fadeIn\"\n            leave-active-class=\"animated fadeOut\"\n          >\n            <div v-if=\"showLockInput\" class=\"p2pk-overlay\">\n              <div class=\"row justify-center\">\n                <div class=\"col-12 q-px-lg\" style=\"max-width: 600px\">\n                  <div class=\"row items-center\">\n                    <div :class=\"!sendData.p2pkPubkey ? 'col-8' : 'col-12'\">\n                      <q-input\n                        v-model=\"sendData.p2pkPubkey\"\n                        :label=\"\n                          sendData.p2pkPubkey && !isLocked\n                            ? $t(\n                                'SendTokenDialog.inputs.p2pk_pubkey.label_invalid'\n                              )\n                            : $t('SendTokenDialog.inputs.p2pk_pubkey.label')\n                        \"\n                        outlined\n                        clearable\n                        :color=\"sendData.p2pkPubkey && !isLocked ? 'red' : ''\"\n                        @keyup.enter=\"lockTokens\"\n                      />\n                    </div>\n                    <div class=\"col-4 q-pl-sm\" v-if=\"!sendData.p2pkPubkey\">\n                      <q-btn\n                        unelevated\n                        v-if=\"canPasteFromClipboard\"\n                        icon=\"content_paste\"\n                        @click=\"pasteToP2PKField\"\n                      >\n                        <q-tooltip>{{\n                          $t(\n                            \"SendTokenDialog.actions.paste_p2pk_pubkey.tooltip_text\"\n                          )\n                        }}</q-tooltip>\n                      </q-btn>\n                      <q-btn\n                        align=\"center\"\n                        flat\n                        outline\n                        color=\"primary\"\n                        round\n                        @click=\"showCamera\"\n                      >\n                        <ScanIcon size=\"1.5em\" />\n                      </q-btn>\n                    </div>\n                  </div>\n                </div>\n              </div>\n            </div>\n          </transition>\n          <AmountInputComponent\n            v-if=\"!showLockInput\"\n            v-model=\"sendData.amount\"\n            :muted=\"insufficientFunds\"\n            :enabled=\"!sendData.tokens\"\n            :show-fiat-conversion=\"!insufficientFunds\"\n            @enter=\"sendTokens\"\n            @fiat-mode-changed=\"fiatKeyboardMode = $event\"\n          >\n            <template #overlay>\n              <q-badge\n                v-if=\"isLocked && !showLockInput\"\n                rounded\n                color=\"positive\"\n                class=\"locked-badge\"\n              >\n                <LockIcon\n                  size=\"1.3em\"\n                  style=\"margin-right: 6px; margin-bottom: 4px\"\n                />\n                <span\n                  class=\"text-caption text-weight-medium\"\n                  style=\"font-size: 14px\"\n                  >locked</span\n                >\n              </q-badge>\n              <div\n                v-if=\"insufficientFunds && sendData.amount\"\n                outline\n                rounded\n                size=\"md\"\n                class=\"amount-warning-badge\"\n              >\n                <transition name=\"wobble\" mode=\"out-in\" appear>\n                  <span\n                    v-if=\"insufficientFunds && sendData.amount\"\n                    :key=\"'warn-text-' + String(sendData.amount ?? '')\"\n                    class=\"text-caption text-weight-medium text-grey-6\"\n                    style=\"font-size: 16px; display: inline-block\"\n                  >\n                    {{\n                      $t(\n                        \"PayInvoiceDialog.invoice.balance_too_low_warning_text\"\n                      )\n                    }}\n                  </span>\n                </transition>\n              </div>\n            </template>\n          </AmountInputComponent>\n        </div>\n\n        <!-- Numeric keypad -->\n        <div class=\"bottom-panel\">\n          <div class=\"keypad-wrapper\">\n            <NumericKeyboard\n              :force-visible=\"true\"\n              :hide-close=\"true\"\n              :hide-enter=\"true\"\n              :hide-comma=\"\n                (activeUnit === 'sat' || activeUnit === 'msat') &&\n                !fiatKeyboardMode\n              \"\n              :model-value=\"String(sendData.amount ?? 0)\"\n              @update:modelValue=\"(val: string | number) => (sendData.amount = Number(val))\"\n              @done=\"sendTokens\"\n            />\n          </div>\n          <!-- Send action below keyboard -->\n          <div class=\"row justify-center q-pb-lg q-pt-sm\">\n            <div\n              class=\"col-12 col-sm-11 col-md-8 q-px-md\"\n              style=\"max-width: 600px; max-height: 60px\"\n            >\n              <SendPaymentRequest\n                v-if=\"sendData.paymentRequest\"\n                :button-label=\"$t('SendTokenDialog.actions.pay.label')\"\n                :disable=\"paymentRequestButtonDisabled\"\n                :show-details=\"false\"\n                :full-width=\"true\"\n                :button-rounded=\"true\"\n                :button-dense=\"false\"\n                :button-unelevated=\"true\"\n                button-size=\"lg\"\n                button-color=\"primary\"\n                :button-class=\"[]\"\n                :prepare-token=\"preparePaymentRequestTokens\"\n                @success=\"handlePaymentRequestSuccess\"\n              />\n              <q-btn\n                v-else\n                class=\"full-width\"\n                unelevated\n                size=\"lg\"\n                :disable=\"\n                  sendData.amount == null ||\n                  sendData.amount <= 0 ||\n                  insufficientFunds ||\n                  (sendData.p2pkPubkey != '' &&\n                    !isValidPubkey(sendData.p2pkPubkey))\n                \"\n                @click=\"sendTokens\"\n                color=\"primary\"\n                rounded\n                type=\"submit\"\n                :loading=\"globalMutexLock\"\n              >\n                {{ $t(\"SendTokenDialog.actions.send.label\") }}\n                <template v-slot:loading>\n                  <q-spinner />\n                </template>\n              </q-btn>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- show ecash details -->\n      <div v-else class=\"text-center\">\n        <DisplayTokenComponent />\n      </div>\n    </q-card>\n  </q-dialog>\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { useSendTokensStore } from \"src/stores/sendTokensStore\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { useProofsStore } from \"src/stores/proofs\";\nimport { useMintsStore } from \"src/stores/mints\";\nimport { useTokensStore } from \"src/stores/tokens\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport { useSettingsStore } from \"src/stores/settings\";\nimport { useWorkersStore } from \"src/stores/workers\";\nimport { usePriceStore } from \"src/stores/price\";\nimport { useCameraStore } from \"src/stores/camera\";\nimport { useP2PKStore } from \"src/stores/p2pk\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport ChooseMint from \"components/ChooseMint.vue\";\nimport NumericKeyboard from \"components/NumericKeyboard.vue\";\nimport DisplayTokenComponent from \"components/DisplayTokenComponent.vue\";\nimport AmountInputComponent from \"components/AmountInputComponent.vue\";\nimport SendPaymentRequest from \"components/SendPaymentRequest.vue\";\nimport PaymentRequestInfo from \"components/PaymentRequestInfo.vue\";\nimport {\n  ChevronLeft as ChevronLeftIcon,\n  Clipboard as ClipboardIcon,\n  FileText as FileTextIcon,\n  Lock as LockIcon,\n  Scan as ScanIcon,\n} from \"lucide-vue-next\";\ndeclare const windowMixin: any;\nexport default defineComponent({\n  name: \"SendTokenDialog\",\n  mixins: [windowMixin],\n  components: {\n    ChooseMint,\n    NumericKeyboard,\n    DisplayTokenComponent,\n    AmountInputComponent,\n    SendPaymentRequest,\n    PaymentRequestInfo,\n    ScanIcon,\n    LockIcon,\n  },\n  props: {},\n  data: function () {\n    return {\n      fiatKeyboardMode: false as boolean,\n    };\n  },\n  computed: {\n    ...mapWritableState(useSendTokensStore, [\n      \"showSendTokens\",\n      \"showLockInput\",\n      \"sendData\",\n    ]),\n    ...mapWritableState(useCameraStore, [\"camera\", \"hasCamera\"]),\n    ...mapState(useUiStore, [\n      \"tickerShort\",\n      \"canPasteFromClipboard\",\n      \"globalMutexLock\",\n      \"ndefSupported\",\n      \"webShareSupported\",\n    ]),\n    ...mapWritableState(useUiStore, [\"showNumericKeyboard\"]),\n    ...mapState(useMintsStore, [\n      \"mints\",\n      \"activeProofs\",\n      \"activeUnit\",\n      \"activeUnitLabel\",\n      \"activeUnitCurrencyMultiplyer\",\n      \"activeMintUrl\",\n      \"activeBalance\",\n    ]),\n    ...mapState(useSettingsStore, [\n      \"checkSentTokens\",\n      \"includeFeesInSendAmount\",\n      \"nfcEncoding\",\n      \"useNumericKeyboard\",\n    ]),\n    ...mapState(usePriceStore, [\n      \"bitcoinPrice\",\n      \"bitcoinPrices\",\n      \"currentCurrencyPrice\",\n    ]),\n    ...mapState(useSettingsStore, [\"bitcoinPriceCurrency\"]),\n    ...mapState(useWorkersStore, [\"tokenWorkerRunning\"]),\n    insufficientFunds: function (): boolean {\n      if (this.sendData.amount == null) return false;\n      return (\n        this.activeBalance <\n        this.sendData.amount * this.activeUnitCurrencyMultiplyer\n      );\n    },\n    // canSpendOffline: function (): boolean {\n    //   if (!this.sendData.amount) {\n    //     return false;\n    //   }\n    //   // check if entered amount is the same as the result of coinSelect(spendableProofs(activeProofs), amount)\n    //   try {\n    //     let spendableProofs = this.spendableProofs(\n    //       this.activeProofs,\n    //       this.sendData.amount * this.activeUnitCurrencyMultiplyer\n    //     );\n    //     const mints = useMintsStore() as any;\n    //     const mintWallet = this.mintWalletSync(\n    //       mints.activeMintUrl,\n    //       mints.activeUnit\n    //     );\n    //     let selectedProofs = this.coinSelect(\n    //       spendableProofs,\n    //       mintWallet,\n    //       this.sendData.amount * this.activeUnitCurrencyMultiplyer,\n    //       this.includeFeesInSendAmount\n    //     );\n    //     const feesToAdd = this.includeFeesInSendAmount\n    //       ? this.getFeesForProofs(selectedProofs)\n    //       : 0;\n    //     const sumSelectedProofs = selectedProofs\n    //       .flat()\n    //       .reduce((sum: number, el: any) => (sum += el.amount), 0);\n    //     return (\n    //       sumSelectedProofs ==\n    //       this.sendData.amount * this.activeUnitCurrencyMultiplyer + feesToAdd\n    //     );\n    //   } catch (error: any) {\n    //     console.error(error);\n    //     return false;\n    //   }\n    // },\n    isLocked: function (): boolean {\n      return Boolean(\n        this.sendData.p2pkPubkey != \"\" &&\n          this.isValidPubkey(this.sendData.p2pkPubkey)\n      );\n    },\n    paymentRequestButtonDisabled(): boolean {\n      if (!this.sendData.paymentRequest) {\n        return true;\n      }\n      if (\n        this.sendData.amount == null ||\n        this.sendData.amount <= 0 ||\n        this.insufficientFunds\n      ) {\n        return true;\n      }\n      if (this.globalMutexLock) {\n        return true;\n      }\n      return false;\n    },\n  },\n  watch: {\n    showSendTokens: function (val) {\n      if (val) {\n        this.$nextTick(() => {\n          // if we're entering the amount etc, show the keyboard\n          if (!this.sendData.tokensBase64.length) {\n            this.showNumericKeyboard = true;\n          } else {\n            this.showNumericKeyboard = false;\n          }\n        });\n\n        // if we open the dialog from the history, let's check the\n        if (\n          this.sendData.historyToken &&\n          this.sendData.historyToken.amount < 0 &&\n          this.sendData.historyToken.status === \"pending\"\n        ) {\n          if (!this.checkSentTokens) {\n            console.log(\n              \"settingsStore.checkSentTokens is disabled, skipping token check\"\n            );\n            return;\n          }\n          const unspent = this.checkTokenSpendable(\n            this.sendData.historyToken,\n            false\n          );\n          if (!unspent) {\n            this.sendData.historyToken.status = \"paid\";\n          }\n        }\n      } else {\n        clearInterval(this.qrInterval);\n        this.sendData.data = \"\";\n        this.sendData.tokensBase64 = \"\";\n        this.sendData.historyToken = null;\n        this.sendData.paymentRequest = null;\n      }\n    },\n  },\n  methods: {\n    ...mapActions(useWorkersStore, [\"clearAllWorkers\"]),\n    ...mapActions(useWalletStore, [\n      \"send\",\n      \"sendToLock\",\n      \"coinSelect\",\n      \"spendableProofs\",\n      \"getFeesForProofs\",\n      \"onTokenPaid\",\n      \"mintWallet\",\n      \"checkTokenSpendable\",\n    ]),\n    ...mapActions(useProofsStore, [\"serializeProofs\"]),\n    ...mapActions(useTokensStore, [\n      \"addPendingToken\",\n      \"setTokenPaid\",\n      \"deleteToken\",\n    ]),\n    ...mapActions(useP2PKStore, [\"isValidPubkey\", \"maybeConvertNpub\"]),\n    ...mapActions(useCameraStore, [\"closeCamera\", \"showCamera\"]),\n    ...mapActions(useMintsStore, [\"toggleUnit\"]),\n    handlePaymentRequestSuccess: function () {\n      this.showSendTokens = false;\n    },\n    preparePaymentRequestTokens: async function () {\n      if (!this.sendData.paymentRequest) {\n        return undefined;\n      }\n      if (this.sendData.tokensBase64) {\n        return this.sendData.tokensBase64;\n      }\n      if (this.sendData.amount == null || this.sendData.amount <= 0) {\n        throw new Error(\n          this.$t(\"SendTokenDialog.errors.amount_required\") as string\n        );\n      }\n      if (this.insufficientFunds) {\n        throw new Error(\n          this.$t(\n            \"PayInvoiceDialog.invoice.balance_too_low_warning_text\"\n          ) as string\n        );\n      }\n      const sendAmount = Math.floor(\n        this.sendData.amount * this.activeUnitCurrencyMultiplyer\n      );\n      const mintWallet = await this.mintWallet(\n        this.activeMintUrl,\n        this.activeUnit,\n        true\n      );\n      const { sendProofs } = await this.send(\n        this.activeProofs,\n        mintWallet,\n        sendAmount,\n        true,\n        this.includeFeesInSendAmount\n      );\n      const serialized = this.serializeProofs(sendProofs);\n      if (!serialized) {\n        throw new Error(\n          this.$t(\"SendTokenDialog.errors.serialization_failed\") as string\n        );\n      }\n      this.sendData.tokens = \"\";\n      this.sendData.tokensBase64 = serialized;\n      this.sendData.historyAmount = -sendAmount;\n      const historyToken = {\n        amount: -sendAmount,\n        token: serialized,\n        unit: this.activeUnit,\n        mint: this.activeMintUrl,\n        paymentRequest: this.sendData.paymentRequest,\n        status: \"pending\",\n      };\n      if (!this.sendData.historyToken) {\n        const _id = this.addPendingToken(historyToken);\n        (historyToken as any).id = _id;\n        if (!this.g.offline) {\n          this.onTokenPaid(historyToken);\n        }\n      }\n      this.sendData.historyToken = historyToken as any;\n      return serialized;\n    },\n    lockTokens: async function () {\n      if (!this.sendData.amount) {\n        throw new Error(\"Amount is required\");\n      }\n      const sendAmount = Math.floor(\n        this.sendData.amount * this.activeUnitCurrencyMultiplyer\n      );\n      try {\n        // keep firstProofs, send scndProofs and delete them (invalidate=true)\n        const mintWallet = await this.mintWallet(\n          this.activeMintUrl,\n          this.activeUnit,\n          true\n        );\n        const { _, sendProofs } = await this.sendToLock(\n          this.activeProofs,\n          mintWallet,\n          sendAmount,\n          this.sendData.p2pkPubkey\n        );\n        // update UI\n        this.sendData.tokens = sendProofs;\n\n        this.sendData.tokensBase64 = this.serializeProofs(sendProofs);\n        const historyToken = {\n          amount: -this.sendData.amount,\n          token: this.sendData.tokensBase64,\n          unit: this.activeUnit,\n          mint: this.activeMintUrl,\n        };\n        const _id = this.addPendingToken(historyToken as any);\n        (historyToken as any).id = _id;\n        this.sendData.historyToken = historyToken as any;\n\n        if (!this.g.offline) {\n          this.onTokenPaid(historyToken);\n        }\n      } catch (error: any) {\n        console.error(error);\n      }\n    },\n    sendTokens: async function () {\n      /*\n      calls send, displays token and kicks off the spendableWorker\n      */\n      this.sendData.p2pkPubkey = this.maybeConvertNpub(\n        this.sendData.p2pkPubkey\n      );\n      if (\n        this.sendData.p2pkPubkey &&\n        this.isValidPubkey(this.sendData.p2pkPubkey)\n      ) {\n        await this.lockTokens();\n        return;\n      }\n\n      try {\n        const sendAmount = Math.floor(\n          this.sendData.amount * this.activeUnitCurrencyMultiplyer\n        );\n        const mintWallet = await this.mintWallet(\n          this.activeMintUrl,\n          this.activeUnit,\n          false\n        );\n        // keep firstProofs, send scndProofs and delete them (invalidate=true)\n        const { _, sendProofs } = await this.send(\n          this.activeProofs,\n          mintWallet,\n          sendAmount,\n          true,\n          this.includeFeesInSendAmount\n        );\n\n        // update UI\n        this.sendData.tokens = sendProofs;\n        this.sendData.tokensBase64 = this.serializeProofs(sendProofs);\n        this.sendData.historyAmount =\n          -this.sendData.amount * this.activeUnitCurrencyMultiplyer;\n\n        const historyToken = {\n          amount: -sendAmount,\n          token: this.sendData.tokensBase64,\n          unit: this.activeUnit,\n          mint: this.activeMintUrl,\n          paymentRequest: this.sendData.paymentRequest,\n          status: \"pending\",\n        };\n        const _id = this.addPendingToken(historyToken as any);\n        (historyToken as any).id = _id;\n        this.sendData.historyToken = historyToken as any;\n\n        if (!this.g.offline) {\n          this.onTokenPaid(historyToken);\n        }\n      } catch (error: any) {\n        console.error(error);\n      }\n    },\n    pasteToP2PKField: async function () {\n      console.log(\"pasteToParseDialog\");\n      const text = await useUiStore().pasteFromClipboard();\n      this.sendData.p2pkPubkey = text.trim();\n    },\n    shareToken: async function () {\n      if (!this.webShareSupported) {\n        console.log(\"Web Share API not supported\");\n        return;\n      }\n\n      const shareData = {\n        text: `cashu:${this.sendData.tokensBase64}`,\n      };\n\n      try {\n        await navigator.share(shareData);\n        console.log(\"Token shared successfully\");\n      } catch (error: any) {\n        if ((error as any).name !== \"AbortError\") {\n          console.error(\"Error sharing token:\", error);\n        }\n      }\n    },\n  },\n});\n</script>\n<style scoped>\n.wobble-enter-active {\n  animation: wobble-keyframes 600ms ease-out;\n  transform-origin: center;\n  will-change: transform;\n}\n.wobble-leave-active {\n  animation: none !important;\n}\n@keyframes wobble-keyframes {\n  0% {\n    transform: translateX(0) rotate(0deg);\n  }\n  15% {\n    transform: translateX(-8px) rotate(-3deg);\n  }\n  30% {\n    transform: translateX(8px) rotate(3deg);\n  }\n  45% {\n    transform: translateX(-6px) rotate(-2deg);\n  }\n  60% {\n    transform: translateX(6px) rotate(2deg);\n  }\n  75% {\n    transform: translateX(-3px) rotate(-1deg);\n  }\n  100% {\n    transform: translateX(0) rotate(0deg);\n  }\n}\n.send-fullscreen {\n  height: 100vh;\n  display: flex;\n  flex-direction: column;\n  overflow: hidden;\n}\n.floating-close-btn {\n  position: absolute;\n  left: 16px;\n  top: 50%;\n  transform: translateY(-50%);\n  z-index: 1;\n}\n.fixed-title-height {\n  height: 24px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n.amount-area {\n  flex: 1;\n  position: relative;\n}\n.amount-container {\n  position: relative;\n  display: inline-block;\n  max-width: 90vw;\n}\n.amount-display {\n  font-size: clamp(56px, 11vw, 80px);\n  line-height: 1.1;\n  white-space: nowrap;\n  max-width: 100%;\n}\n.fiat-display {\n  font-size: 14px;\n}\n.p2pk-overlay {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  z-index: 3;\n}\n.locked-badge {\n  position: absolute;\n  top: -50px;\n  left: 50%;\n  transform: translateX(-50%);\n  z-index: 2;\n}\n.amount-warning-badge {\n  position: absolute;\n  top: calc(100% - 20px);\n  left: 50%;\n  transform: translateX(-50%);\n  z-index: 2;\n  pointer-events: none;\n  font-size: 16px;\n  white-space: nowrap;\n}\n.bottom-panel {\n  margin-top: auto;\n  background: var(--q-color-grey-1);\n  padding-bottom: env(safe-area-inset-bottom, 0px);\n}\n.keypad-wrapper {\n  width: 100%;\n  display: flex;\n  justify-content: center;\n}\n</style>\n"
  },
  {
    "path": "src/components/SettingsView.vue",
    "content": "<template>\n  <div style=\"max-width: 800px; margin: 0 auto\">\n    <!-- BACKUP & RESTORE SECTION -->\n    <div class=\"section-divider q-my-md\">\n      <div class=\"divider-line\"></div>\n      <div class=\"divider-text\">\n        {{ $t(\"Settings.sections.backup_restore\") }}\n      </div>\n      <div class=\"divider-line\"></div>\n    </div>\n\n    <div class=\"q-py-sm q-px-xs text-left\" on-left>\n      <q-list padding>\n        <q-item>\n          <q-item-section>\n            <q-item-label overline class=\"text-weight-bold\">{{\n              $t(\"Settings.backup_restore.backup_seed.title\")\n            }}</q-item-label>\n            <q-item-label caption\n              >{{ $t(\"Settings.backup_restore.backup_seed.description\") }}\n            </q-item-label>\n            <div class=\"row q-pt-md\">\n              <div class=\"col-12\">\n                <q-input\n                  outlined\n                  readonly\n                  v-model=\"hiddenMnemonic\"\n                  :label=\"\n                    $t('Settings.backup_restore.backup_seed.seed_phrase_label')\n                  \"\n                  class=\"seed-phrase\"\n                  autogrow\n                >\n                  <template v-slot:append>\n                    <q-btn\n                      flat\n                      dense\n                      icon=\"visibility\"\n                      color=\"primary\"\n                      class=\"cursor-pointer q-mt-md\"\n                      @click=\"toggleMnemonicVisibility\"\n                    ></q-btn>\n                    <q-btn\n                      flat\n                      dense\n                      icon=\"content_copy\"\n                      color=\"primary\"\n                      class=\"cursor-pointer q-mt-md\"\n                      @click=\"copyText(mnemonic)\"\n                    ></q-btn>\n                  </template>\n                </q-input>\n              </div>\n            </div>\n          </q-item-section>\n        </q-item>\n      </q-list>\n    </div>\n\n    <!-- restore -->\n    <div class=\"q-py-sm q-px-xs text-left\" on-left>\n      <q-list padding>\n        <q-item>\n          <q-item-section>\n            <q-item-label overline class=\"text-weight-bold\">{{\n              $t(\"Settings.backup_restore.restore_ecash.title\")\n            }}</q-item-label>\n            <q-item-label caption>\n              {{ $t(\"Settings.backup_restore.restore_ecash.description\") }}\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n        <q-item>\n          <q-btn\n            class=\"q-ml-sm q-px-md\"\n            color=\"primary\"\n            size=\"sm\"\n            rounded\n            outline\n            to=\"/restore\"\n            >{{ $t(\"Settings.backup_restore.restore_ecash.button\") }}</q-btn\n          >\n        </q-item>\n      </q-list>\n    </div>\n\n    <!-- LIGHTNING ADDRESS SECTION -->\n    <div class=\"section-divider q-my-md\">\n      <div class=\"divider-line\"></div>\n      <div class=\"divider-text\">\n        {{ $t(\"Settings.sections.lightning_address\") }}\n      </div>\n      <div class=\"divider-line\"></div>\n    </div>\n    <!-- nostr -->\n    <div class=\"q-py-sm q-px-xs text-left\" on-left>\n      <q-list padding>\n        <q-item>\n          <q-item-section>\n            <q-item-label overline class=\"text-weight-bold\">{{\n              $t(\"Settings.lightning_address.title\")\n            }}</q-item-label>\n            <q-item-label caption>{{\n              $t(\"Settings.lightning_address.description\")\n            }}</q-item-label>\n          </q-item-section>\n        </q-item>\n        <q-item class=\"q-px-md\">\n          <q-item-section class=\"q-mx-none q-pl-none\">\n            <!-- toggle to turn Lightning address on and off in new row -->\n            <div class=\"row q-pt-md\">\n              <q-toggle v-model=\"npcEnabled\" color=\"primary\" />\n              <q-item-section>\n                <q-item-label title>{{\n                  $t(\"Settings.lightning_address.enable.toggle\")\n                }}</q-item-label>\n                <q-item-label caption>\n                  {{ $t(\"Settings.lightning_address.enable.description\") }}\n                </q-item-label>\n              </q-item-section>\n            </div>\n          </q-item-section>\n        </q-item>\n        <q-item v-if=\"npcEnabled\">\n          <div class=\"row\">\n            <div class=\"col-12\">\n              <q-input outlined v-model=\"npcAddress\" dense rounded readonly>\n                <template v-slot:append>\n                  <q-spinner size=\"sm\" v-if=\"npcLoading\" />\n                  <q-icon\n                    name=\"content_copy\"\n                    @click=\"copyText(npcAddress)\"\n                    size=\"xs\"\n                    color=\"grey\"\n                    class=\"q-mr-sm cursor-pointer\"\n                  >\n                    <q-tooltip>{{\n                      $t(\"Settings.lightning_address.address.copy_tooltip\")\n                    }}</q-tooltip>\n                  </q-icon>\n                </template>\n              </q-input>\n            </div>\n            <div class=\"row q-pt-md\">\n              <q-toggle v-model=\"automaticClaim\" color=\"primary\" />\n              <q-item-section>\n                <q-item-label title>{{\n                  $t(\"Settings.lightning_address.automatic_claim.toggle\")\n                }}</q-item-label>\n                <q-item-label caption\n                  >{{\n                    $t(\"Settings.lightning_address.automatic_claim.description\")\n                  }}\n                </q-item-label>\n              </q-item-section>\n            </div>\n          </div>\n        </q-item>\n\n        <!-- NOSTR KEYS SECTION -->\n        <div class=\"section-divider q-my-md\">\n          <div class=\"divider-line\"></div>\n          <div class=\"divider-text\">\n            {{ $t(\"Settings.sections.nostr.title\") }}\n          </div>\n          <div class=\"divider-line\"></div>\n        </div>\n\n        <q-item>\n          <q-item-section>\n            <q-item-label overline>{{\n              $t(\"Settings.nostr_keys.title\")\n            }}</q-item-label>\n            <q-item-label caption>{{\n              $t(\"Settings.nostr_keys.description\")\n            }}</q-item-label>\n          </q-item-section>\n        </q-item>\n        <!-- initWalletSeedPrivateKeySigner -->\n        <q-item\n          :active=\"signerType === 'SEED'\"\n          active-class=\"text-weight-bold text-primary\"\n          clickable\n        >\n          <q-item-section avatar>\n            <q-icon\n              :color=\"signerType === 'SEED' ? 'primary' : 'grey'\"\n              :name=\"\n                signerType === 'SEED'\n                  ? 'check_circle'\n                  : 'radio_button_unchecked'\n              \"\n              @click=\"handleSeedClick\"\n              class=\"cursor-pointer\"\n            />\n          </q-item-section>\n          <q-item-section\n            lines=\"1\"\n            class=\"cursor-pointer\"\n            style=\"word-break: break-word\"\n          >\n            <q-item-label title>{{\n              $t(\"Settings.nostr_keys.wallet_seed.title\")\n            }}</q-item-label>\n            <q-item-label caption\n              >{{ $t(\"Settings.nostr_keys.wallet_seed.description\") }}\n            </q-item-label>\n            <q-item-label\n              caption\n              v-if=\"signerType === 'SEED' && seedSignerPrivateKeyNsec\"\n            >\n              <q-badge\n                class=\"cursor-pointer q-mt-xs\"\n                @click=\"copyText(seedSignerPrivateKeyNsec)\"\n                outline\n                color=\"grey\"\n              >\n                <q-icon\n                  name=\"content_copy\"\n                  size=\"0.8em\"\n                  color=\"grey\"\n                  class=\"q-mr-xs\"\n                ></q-icon\n                >{{ $t(\"Settings.nostr_keys.wallet_seed.copy_nsec\") }}\n              </q-badge>\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n        <!-- Nip46Signer -->\n        <q-item\n          :active=\"signerType === 'NIP46'\"\n          active-class=\"text-weight-bold text-primary\"\n          clickable\n          v-if=\"false\"\n        >\n          <q-item-section avatar>\n            <q-icon\n              :color=\"signerType === 'NIP46' ? 'primary' : 'grey'\"\n              :name=\"\n                signerType === 'NIP46'\n                  ? 'check_circle'\n                  : 'radio_button_unchecked'\n              \"\n              @click=\"handleBunkerClick\"\n              class=\"cursor-pointer\"\n            />\n          </q-item-section>\n          <q-item-section\n            lines=\"1\"\n            class=\"cursor-pointer\"\n            style=\"word-break: break-word\"\n          >\n            <q-item-label title>{{\n              $t(\"Settings.nostr_keys.nsec_bunker.title\")\n            }}</q-item-label>\n            <q-item-label caption\n              >{{ $t(\"Settings.nostr_keys.nsec_bunker.description\") }}\n            </q-item-label>\n          </q-item-section>\n          <q-item-section side v-if=\"signerType === 'NIP46'\">\n            <q-icon\n              name=\"delete_outline\"\n              @click=\"handleResetNip46Signer\"\n              class=\"cursor-pointer\"\n              ><q-tooltip>{{\n                $t(\"Settings.nostr_keys.nsec_bunker.delete_tooltip\")\n              }}</q-tooltip>\n            </q-icon>\n          </q-item-section>\n        </q-item>\n        <q-item\n          :active=\"signerType === 'PRIVATEKEY'\"\n          active-class=\"text-weight-bold text-primary\"\n          clickable\n        >\n          <q-item-section avatar>\n            <q-icon\n              :color=\"signerType === 'PRIVATEKEY' ? 'primary' : 'grey'\"\n              :name=\"\n                signerType === 'PRIVATEKEY'\n                  ? 'check_circle'\n                  : 'radio_button_unchecked'\n              \"\n              @click=\"handleNsecClick\"\n              class=\"cursor-pointer\"\n            />\n          </q-item-section>\n          <q-item-section\n            lines=\"1\"\n            class=\"cursor-pointer\"\n            style=\"word-break: break-word\"\n          >\n            <q-item-label title>{{\n              $t(\"Settings.nostr_keys.use_nsec.title\")\n            }}</q-item-label>\n            <q-item-label caption\n              >{{ $t(\"Settings.nostr_keys.use_nsec.description\") }}\n            </q-item-label>\n          </q-item-section>\n          <q-item-section side v-if=\"signerType === 'PRIVATEKEY'\">\n            <q-icon\n              name=\"delete_outline\"\n              @click=\"handleResetPrivateKeySigner\"\n              class=\"cursor-pointer\"\n              ><q-tooltip>{{\n                $t(\"Settings.nostr_keys.use_nsec.delete_tooltip\")\n              }}</q-tooltip></q-icon\n            >\n          </q-item-section>\n        </q-item>\n        <!-- Nip07Signer -->\n        <q-item\n          :active=\"signerType === 'NIP07'\"\n          active-class=\"text-weight-bold text-primary\"\n          clickable\n          v-if=\"nip07SignerAvailable\"\n        >\n          <q-item-section avatar>\n            <q-icon\n              :color=\"signerType === 'NIP07' ? 'primary' : 'grey'\"\n              :name=\"\n                signerType === 'NIP07'\n                  ? 'check_circle'\n                  : 'radio_button_unchecked'\n              \"\n              @click=\"handleExtensionClick\"\n              class=\"cursor-pointer\"\n            />\n          </q-item-section>\n          <q-item-section\n            lines=\"1\"\n            class=\"cursor-pointer\"\n            style=\"word-break: break-word\"\n          >\n            <q-item-label title>{{\n              $t(\"Settings.nostr_keys.signing_extension.title\")\n            }}</q-item-label>\n            <q-item-label caption v-if=\"nip07SignerAvailable\"\n              >{{ $t(\"Settings.nostr_keys.signing_extension.description\") }}\n            </q-item-label>\n            <q-item-label caption v-else\n              >{{ $t(\"Settings.nostr_keys.signing_extension.not_found\") }}\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n      </q-list>\n      <q-expansion-item\n        dense\n        dense-toggle\n        class=\"text-left\"\n        :label=\"$t('Settings.sections.nostr.relays.expand_label')\"\n      >\n        <q-item>\n          <q-item-section>\n            <q-item-label overline>{{\n              $t(\"Settings.sections.nostr.relays.add.title\")\n            }}</q-item-label>\n            <q-item-label caption\n              >{{ $t(\"Settings.sections.nostr.relays.add.description\") }}\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n        <q-item>\n          <q-item-section>\n            <q-input\n              outlined\n              rounded\n              dense\n              v-model=\"newRelay\"\n              :label=\"$t('Settings.sections.nostr.relays.list.title')\"\n              append\n            >\n              <template v-slot:append>\n                <q-btn\n                  flat\n                  dense\n                  icon=\"add\"\n                  color=\"primary\"\n                  @click=\"addRelay\"\n                ></q-btn>\n              </template>\n            </q-input>\n          </q-item-section>\n        </q-item>\n        <q-item>\n          <q-item-section>\n            <q-item-label overline>{{\n              $t(\"Settings.sections.nostr.relays.list.title\")\n            }}</q-item-label>\n            <q-item-label caption\n              >{{ $t(\"Settings.sections.nostr.relays.list.description\") }}\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n        <q-item v-for=\"relay in relays\" :key=\"relay\" clickable>\n          <q-item-section class=\"q-mx-none q-pl-none\" style=\"max-width: 1.2em\">\n            <q-icon\n              name=\"content_copy\"\n              @click=\"copyText(relay)\"\n              size=\"xs\"\n              color=\"grey\"\n              class=\"q-mr-sm cursor-pointer\"\n              ><q-tooltip>{{\n                $t(\"Settings.sections.nostr.relays.list.copy_tooltip\")\n              }}</q-tooltip></q-icon\n            >\n          </q-item-section>\n          <q-item-section class=\"q-mx-none q-pl-none\" style=\"max-width: 1.5em\">\n            <q-icon\n              name=\"delete_outline\"\n              @click=\"removeRelay(relay)\"\n              size=\"1.3em\"\n              color=\"grey\"\n              class=\"q-mr-sm cursor-pointer\"\n              ><q-tooltip>{{\n                $t(\"Settings.sections.nostr.relays.list.remove_tooltip\")\n              }}</q-tooltip></q-icon\n            >\n          </q-item-section>\n          <q-item-section style=\"max-width: 10rem\" class=\"cursor-pointer\">\n            <q-item-label caption>{{ relay }} </q-item-label>\n          </q-item-section>\n        </q-item>\n      </q-expansion-item>\n      <!-- Web of trust actions -->\n      <q-item>\n        <q-item-section>\n          <q-item-label overline>\n            {{ $t(\"Settings.web_of_trust.title\") }}\n          </q-item-label>\n          <q-item-label caption>\n            {{\n              $t(\"Settings.web_of_trust.known_pubkeys\", { wotCount: wotCount })\n            }}\n          </q-item-label>\n        </q-item-section>\n      </q-item>\n      <q-item>\n        <q-item-section>\n          <div class=\"row justify-end\">\n            <q-btn flat dense :loading=\"wotLoading\" @click=\"crawlWebOfTrust(2)\">\n              {{\n                hasCrawlCheckpoint && !wotLoading\n                  ? $t(\"Settings.web_of_trust.continue_crawl\")\n                  : signerType === \"SEED\"\n                  ? $t(\"Settings.web_of_trust.crawl_odell\")\n                  : $t(\"Settings.web_of_trust.crawl_wot\")\n              }}\n            </q-btn>\n            <q-btn\n              v-if=\"wotLoading\"\n              flat\n              dense\n              class=\"q-ml-sm\"\n              color=\"negative\"\n              @click=\"cancelCrawl\"\n            >\n              {{ $t(\"Settings.web_of_trust.pause\") }}\n            </q-btn>\n            <q-btn\n              v-if=\"!wotLoading\"\n              flat\n              dense\n              class=\"q-ml-sm\"\n              :disable=\"wotLoading\"\n              @click=\"resetWebOfTrust\"\n            >\n              {{ $t(\"Settings.web_of_trust.reset\") }}\n            </q-btn>\n          </div>\n        </q-item-section>\n      </q-item>\n      <q-item v-if=\"wotLoading || crawlTotal > 0\">\n        <q-item-section>\n          <q-linear-progress\n            rounded\n            size=\"10px\"\n            color=\"primary\"\n            :value=\"crawlTotal > 0 ? crawlProcessed / crawlTotal : 0\"\n          />\n          <div class=\"text-caption q-mt-xs\">\n            {{\n              $t(\"Settings.web_of_trust.progress\", {\n                crawlProcessed: crawlProcessed,\n                crawlTotal: crawlTotal,\n              })\n            }}\n          </div>\n        </q-item-section>\n      </q-item>\n    </div>\n\n    <!-- PAYMENT REQUESTS SECTION -->\n    <div class=\"section-divider q-my-md\">\n      <div class=\"divider-line\"></div>\n      <div class=\"divider-text\">\n        {{ $t(\"Settings.sections.payment_requests\") }}\n      </div>\n      <div class=\"divider-line\"></div>\n    </div>\n\n    <!-- payment requests -->\n    <div class=\"q-py-sm q-px-xs text-left\" on-left>\n      <q-item class=\"q-pt-lg\">\n        <q-item-section>\n          <q-item-label overline class=\"text-weight-bold\">{{\n            $t(\"Settings.payment_requests.title\")\n          }}</q-item-label>\n          <q-item-label caption>{{\n            $t(\"Settings.payment_requests.description\")\n          }}</q-item-label>\n        </q-item-section>\n      </q-item>\n      <q-item>\n        <q-toggle\n          v-model=\"enablePaymentRequest\"\n          :label=\"$t('Settings.payment_requests.enable_toggle')\"\n          color=\"primary\"\n        />\n      </q-item>\n    </div>\n    <div v-if=\"enablePaymentRequest\" class=\"q-pb-sm q-px-xs text-left\" on-left>\n      <q-item>\n        <q-toggle\n          v-model=\"receivePaymentRequestsAutomatically\"\n          color=\"primary\"\n        />\n        <q-item-section>\n          <q-item-label title>{{\n            $t(\"Settings.payment_requests.claim_automatically.toggle\")\n          }}</q-item-label>\n          <q-item-label caption\n            >{{\n              $t(\"Settings.payment_requests.claim_automatically.description\")\n            }}\n          </q-item-label>\n        </q-item-section>\n      </q-item>\n    </div>\n\n    <!-- NOSTR WALLET CONNECT SECTION -->\n    <div class=\"section-divider q-my-md\">\n      <div class=\"divider-line\"></div>\n      <div class=\"divider-text\">\n        {{ $t(\"Settings.sections.nostr_wallet_connect\") }}\n      </div>\n      <div class=\"divider-line\"></div>\n    </div>\n\n    <!-- ln address -->\n    <div class=\"q-py-sm q-px-xs text-left\" on-left>\n      <q-list padding>\n        <!-- NWC -->\n\n        <q-item class=\"q-pt-lg\">\n          <q-item-section>\n            <q-item-label overline class=\"text-weight-bold\">{{\n              $t(\"Settings.nostr_wallet_connect.title\")\n            }}</q-item-label>\n            <q-item-label caption>{{\n              $t(\"Settings.nostr_wallet_connect.description\")\n            }}</q-item-label>\n          </q-item-section>\n        </q-item>\n        <!-- use a q-toggle to turn nwc on and off -->\n        <q-item>\n          <q-toggle\n            v-model=\"enableNwc\"\n            :label=\"$t('Settings.nostr_wallet_connect.enable_toggle')\"\n            color=\"primary\"\n          />\n        </q-item>\n        <!-- <q-item>\n          <q-btn\n            v-if=\"false\"\n            class=\"q-ml-sm q-px-md\"\n            color=\"primary\"\n            rounded\n            outline\n            @click=\"listenToNWCCommands()\"\n            >Link wallet</q-btn\n          >\n        </q-item> -->\n        <q-item v-if=\"enableNwc\">\n          <q-item-section>\n            <!-- <q-item-label overline>Connections</q-item-label> -->\n            <q-item-label caption\n              >{{ $t(\"Settings.nostr_wallet_connect.payments_note\") }}\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n        <div v-if=\"enableNwc\">\n          <q-item\n            v-for=\"connection in connections\"\n            :key=\"getConnectionString(connection)\"\n          >\n            <q-item-section\n              class=\"q-mx-none q-pl-none\"\n              style=\"max-width: 1.5em\"\n            >\n              <q-icon\n                name=\"content_copy\"\n                @click=\"copyText(getConnectionString(connection))\"\n                size=\"xs\"\n                color=\"grey\"\n                class=\"q-mr-sm cursor-pointer\"\n                ><q-tooltip>{{\n                  $t(\"Settings.nostr_wallet_connect.connection.copy_tooltip\")\n                }}</q-tooltip></q-icon\n              >\n            </q-item-section>\n            <q-item-section\n              class=\"q-mx-none q-pl-none\"\n              style=\"max-width: 1.5em\"\n            >\n              <q-icon\n                name=\"qr_code\"\n                @click=\"showNWCEntry(connection)\"\n                size=\"1.3em\"\n                color=\"grey\"\n                class=\"q-mr-sm cursor-pointer\"\n              >\n                <q-tooltip>{{\n                  $t(\"Settings.nostr_wallet_connect.connection.qr_tooltip\")\n                }}</q-tooltip>\n              </q-icon>\n            </q-item-section>\n            <q-item-section style=\"max-width: 10rem\">\n              <!-- <q-item-label\n                caption\n                clickable\n                style=\"word-break: break-word\"\n                @click=\"showNWCEntry(connection.connectionString)\"\n                >********************</q-item-label\n              > -->\n              <!-- input for allowanceleft -->\n              <q-input\n                type=\"number\"\n                outlined\n                rounded\n                dense\n                v-model=\"connection.allowanceLeft\"\n                :label=\"\n                  $t('Settings.nostr_wallet_connect.connection.allowance_label')\n                \"\n              >\n              </q-input>\n            </q-item-section>\n          </q-item>\n        </div>\n      </q-list>\n    </div>\n\n    <!-- HARDWARE FEATURES SECTION -->\n    <div v-if=\"ndefSupported\" class=\"section-divider q-my-md\">\n      <div class=\"divider-line\"></div>\n      <div class=\"divider-text\">\n        {{ $t(\"Settings.sections.hardware_features\") }}\n      </div>\n      <div class=\"divider-line\"></div>\n    </div>\n\n    <div class=\"q-py-sm q-px-xs text-left\" on-left>\n      <q-list padding>\n        <!-- Web NFC -->\n        <div v-if=\"ndefSupported\" class=\"q-px-xs text-left\" on-left>\n          <q-list padding>\n            <q-item>\n              <q-item-section>\n                <q-item-label overline class=\"text-weight-bold\">{{\n                  $t(\"Settings.hardware_features.webnfc.title\")\n                }}</q-item-label>\n                <q-item-label caption>\n                  {{ $t(\"Settings.hardware_features.webnfc.description\") }}\n                </q-item-label>\n              </q-item-section>\n            </q-item>\n            <q-item clickable @click=\"nfcEncoding = 'text'\">\n              <q-item-section avatar>\n                <q-icon\n                  :color=\"nfcEncoding === 'text' ? 'primary' : 'grey'\"\n                  :name=\"\n                    nfcEncoding === 'text'\n                      ? 'check_circle'\n                      : 'radio_button_unchecked'\n                  \"\n                  class=\"cursor-pointer\"\n                />\n              </q-item-section>\n              <q-item-section\n                lines=\"1\"\n                class=\"cursor-pointer\"\n                style=\"word-break: break-word\"\n              >\n                <q-item-label title>{{\n                  $t(\"Settings.hardware_features.webnfc.text.title\")\n                }}</q-item-label>\n                <q-item-label caption>\n                  {{ $t(\"Settings.hardware_features.webnfc.text.description\") }}\n                </q-item-label>\n              </q-item-section>\n            </q-item>\n            <q-item clickable @click=\"nfcEncoding = 'weburl'\">\n              <q-item-section avatar>\n                <q-icon\n                  :color=\"nfcEncoding === 'weburl' ? 'primary' : 'grey'\"\n                  :name=\"\n                    nfcEncoding === 'weburl'\n                      ? 'check_circle'\n                      : 'radio_button_unchecked'\n                  \"\n                  class=\"cursor-pointer\"\n                />\n              </q-item-section>\n              <q-item-section\n                lines=\"1\"\n                class=\"cursor-pointer\"\n                style=\"word-break: break-word\"\n              >\n                <q-item-label title>{{\n                  $t(\"Settings.hardware_features.webnfc.weburl.title\")\n                }}</q-item-label>\n                <q-item-label caption>\n                  {{\n                    $t(\"Settings.hardware_features.webnfc.weburl.description\")\n                  }}\n                </q-item-label>\n              </q-item-section>\n            </q-item>\n            <q-item clickable @click=\"nfcEncoding = 'binary'\">\n              <q-item-section avatar>\n                <q-icon\n                  :color=\"nfcEncoding === 'binary' ? 'primary' : 'grey'\"\n                  :name=\"\n                    nfcEncoding === 'binary'\n                      ? 'check_circle'\n                      : 'radio_button_unchecked'\n                  \"\n                  class=\"cursor-pointer\"\n                />\n              </q-item-section>\n              <q-item-section>\n                <q-item-label title>{{\n                  $t(\"Settings.hardware_features.webnfc.binary.title\")\n                }}</q-item-label>\n                <q-item-label caption>\n                  {{\n                    $t(\"Settings.hardware_features.webnfc.binary.description\")\n                  }}\n                </q-item-label>\n              </q-item-section>\n            </q-item>\n            <q-item>\n              <q-toggle\n                v-model=\"showNfcButtonInDrawer\"\n                :label=\"\n                  $t('Settings.hardware_features.webnfc.quick_access.toggle')\n                \"\n                color=\"primary\"\n              /> </q-item\n            ><q-item class=\"q-pt-none\">\n              <q-item-label caption\n                >{{\n                  $t(\n                    \"Settings.hardware_features.webnfc.quick_access.description\"\n                  )\n                }}\n              </q-item-label>\n            </q-item>\n          </q-list>\n        </div>\n\n        <!-- P2PK SECTION -->\n        <div class=\"section-divider q-my-md\">\n          <div class=\"divider-line\"></div>\n          <div class=\"divider-text\">\n            {{ $t(\"Settings.sections.p2pk_features\") }}\n          </div>\n          <div class=\"divider-line\"></div>\n        </div>\n\n        <!-- P2PK -->\n        <div class=\"q-py-sm q-px-xs text-left\" on-left>\n          <q-list padding>\n            <q-item>\n              <q-item-section>\n                <q-item-label overline class=\"text-weight-bold\">{{\n                  $t(\"Settings.p2pk_features.title\")\n                }}</q-item-label>\n                <q-item-label caption>{{\n                  $t(\"Settings.p2pk_features.description\")\n                }}</q-item-label>\n              </q-item-section>\n            </q-item>\n            <q-item style=\"display: inline-block\">\n              <q-btn\n                class=\"q-ml-sm q-px-md\"\n                color=\"primary\"\n                size=\"sm\"\n                rounded\n                outline\n                @click=\"generateKeypair\"\n                >{{ $t(\"Settings.p2pk_features.generate_button\") }}</q-btn\n              >\n            </q-item>\n            <q-item style=\"display: inline-block\">\n              <q-btn\n                class=\"q-ml-sm q-px-md\"\n                color=\"primary\"\n                size=\"sm\"\n                rounded\n                outline\n                @click=\"importNsec\"\n                >{{ $t(\"Settings.p2pk_features.import_button\") }}</q-btn\n              >\n            </q-item>\n            <q-item>\n              <q-toggle\n                v-model=\"showP2PkButtonInDrawer\"\n                :label=\"$t('Settings.p2pk_features.quick_access.toggle')\"\n                color=\"primary\"\n              /> </q-item\n            ><q-item class=\"q-pt-none\">\n              <q-item-label caption\n                >{{ $t(\"Settings.p2pk_features.quick_access.description\") }}\n              </q-item-label>\n            </q-item>\n          </q-list>\n          <q-item v-if=\"p2pkKeys.length\">\n            <q-expansion-item\n              dense\n              dense-toggle\n              v-if=\"p2pkKeys.length\"\n              class=\"text-left\"\n              :label=\"\n                $t('Settings.p2pk_features.keys_expansion.label', {\n                  count: p2pkKeys.length,\n                })\n              \"\n            >\n              <q-item v-for=\"key in p2pkKeys\" :key=\"key.privakey\">\n                <q-item-section\n                  class=\"q-mx-none q-pl-none\"\n                  style=\"max-width: 1.05em\"\n                >\n                  <q-icon\n                    name=\"content_copy\"\n                    @click=\"copyText(key.publicKey)\"\n                    size=\"1.2em\"\n                    color=\"grey\"\n                    class=\"q-mr-xs cursor-pointer\"\n                  />\n                </q-item-section>\n                <q-item-section>\n                  <q-item-label\n                    caption\n                    clickable\n                    style=\"word-break: break-word; font-size: 0.65rem\"\n                    class=\"q-mx-sm\"\n                    @click=\"showP2PKKeyEntry(key.publicKey)\"\n                    >{{ key.publicKey }}</q-item-label\n                  >\n                </q-item-section>\n                <q-item-section side>\n                  <q-badge\n                    v-if=\"key.used\"\n                    :label=\"\n                      $t('Settings.p2pk_features.keys_expansion.used_badge')\n                    \"\n                    color=\"primary\"\n                    class=\"q-mr-sm\"\n                  />\n                </q-item-section>\n                <q-item-section\n                  class=\"q-mx-none q-pl-none\"\n                  style=\"max-width: 1.05em\"\n                >\n                  <q-icon\n                    name=\"qr_code\"\n                    @click=\"showP2PKKeyEntry(key.publicKey)\"\n                    size=\"1em\"\n                    color=\"grey\"\n                    class=\"q-mr-xs cursor-pointer\"\n                  />\n                </q-item-section>\n              </q-item>\n            </q-expansion-item>\n          </q-item>\n        </div>\n\n        <!-- PRIVACY SECTION -->\n        <div class=\"section-divider q-my-md\">\n          <div class=\"divider-line\"></div>\n          <div class=\"divider-text\">{{ $t(\"Settings.sections.privacy\") }}</div>\n          <div class=\"divider-line\"></div>\n        </div>\n\n        <q-item>\n          <q-item-section>\n            <q-item-label overline class=\"text-weight-bold q-pt-sm\">{{\n              $t(\"Settings.privacy.title\")\n            }}</q-item-label>\n            <q-item-label caption>\n              {{ $t(\"Settings.privacy.description\") }}\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n        <div>\n          <!-- nostr mint backup settings -->\n          <q-item>\n            <q-toggle\n              v-model=\"nostrMintBackupEnabled\"\n              :label=\"$t('Settings.experimental.nostr_mint_backup.toggle')\"\n              color=\"primary\"\n              @update:model-value=\"onNostrMintBackupToggle\"\n            >\n            </q-toggle>\n          </q-item>\n          <q-item class=\"q-pt-none\">\n            <q-item-label caption>\n              {{ $t(\"Settings.experimental.nostr_mint_backup.description\") }}\n            </q-item-label>\n          </q-item>\n\n          <!-- periodically check incoming invoices -->\n          <q-item>\n            <q-toggle\n              v-model=\"checkIncomingInvoices\"\n              :label=\"$t('Settings.privacy.check_incoming.toggle')\"\n              color=\"primary\"\n            >\n            </q-toggle>\n          </q-item>\n          <q-item class=\"q-pt-none\">\n            <q-item-label caption\n              >{{ $t(\"Settings.privacy.check_incoming.description\") }}\n            </q-item-label>\n          </q-item>\n          <!-- check pending invoices on startup -->\n          <q-item>\n            <q-toggle\n              v-model=\"checkInvoicesOnStartup\"\n              :label=\"$t('Settings.privacy.check_startup.toggle')\"\n              color=\"primary\"\n            >\n            </q-toggle>\n          </q-item>\n          <q-item class=\"q-pt-none\">\n            <q-item-label caption\n              >{{ $t(\"Settings.privacy.check_startup.description\") }}\n            </q-item-label>\n          </q-item>\n          <!-- periodically check incoming invoices -->\n          <q-item>\n            <q-toggle\n              v-model=\"periodicallyCheckIncomingInvoices\"\n              :label=\"$t('Settings.privacy.check_all.toggle')\"\n              color=\"primary\"\n            >\n            </q-toggle>\n          </q-item>\n          <q-item class=\"q-pt-none\">\n            <q-item-label caption\n              >{{ $t(\"Settings.privacy.check_all.description\") }}\n            </q-item-label>\n          </q-item>\n\n          <!-- check outgoing token state setting -->\n          <q-item>\n            <q-toggle\n              v-model=\"checkSentTokens\"\n              :label=\"$t('Settings.privacy.check_sent.toggle')\"\n              color=\"primary\"\n            /> </q-item\n          ><q-item class=\"q-pt-none\">\n            <q-item-label caption\n              >{{ $t(\"Settings.privacy.check_sent.description\") }}\n            </q-item-label>\n          </q-item>\n          <!-- websockets -->\n          <q-item v-if=\"checkIncomingInvoices || checkSentTokens\">\n            <q-toggle\n              v-if=\"checkIncomingInvoices || checkSentTokens\"\n              v-model=\"useWebsockets\"\n              :label=\"$t('Settings.privacy.websockets.toggle')\"\n              color=\"primary\"\n            >\n            </q-toggle> </q-item\n          ><q-item\n            class=\"q-pt-none\"\n            v-if=\"checkIncomingInvoices || checkSentTokens\"\n          >\n            <q-item-label caption\n              >{{ $t(\"Settings.privacy.websockets.description\") }}\n            </q-item-label>\n          </q-item>\n          <!-- price check setting -->\n          <q-item>\n            <q-toggle\n              v-model=\"getBitcoinPrice\"\n              :label=\"$t('Settings.privacy.bitcoin_price.toggle')\"\n              color=\"primary\"\n            /> </q-item\n          ><q-item class=\"q-pt-none\">\n            <q-item-label caption\n              >{{ $t(\"Settings.privacy.bitcoin_price.description\") }}\n            </q-item-label>\n          </q-item>\n          <!-- currency selection -->\n          <q-item v-if=\"getBitcoinPrice\">\n            <q-item-section>\n              <q-item-label overline class=\"text-weight-bold\">{{\n                $t(\"Settings.privacy.bitcoin_price.currency.title\")\n              }}</q-item-label>\n              <q-item-label caption>{{\n                $t(\"Settings.privacy.bitcoin_price.currency.description\")\n              }}</q-item-label>\n            </q-item-section>\n          </q-item>\n          <q-item v-if=\"getBitcoinPrice\">\n            <q-item-section>\n              <q-select\n                v-model=\"bitcoinPriceCurrency\"\n                :options=\"currencyOptions\"\n                rounded\n                outlined\n                dense\n                emit-value\n                map-options\n                @update:model-value=\"onCurrencyChange\"\n              />\n            </q-item-section>\n          </q-item>\n        </div>\n\n        <div class=\"section-divider q-my-md\">\n          <div class=\"divider-line\"></div>\n          <div class=\"divider-text\">\n            {{ $t(\"Settings.sections.experimental\") }}\n          </div>\n          <div class=\"divider-line\"></div>\n        </div>\n        <!-- enable receive swaps -->\n        <q-item>\n          <q-item-section>\n            <q-item-label overline class=\"text-weight-bold\">{{\n              $t(\"Settings.experimental.title\")\n            }}</q-item-label>\n            <q-item-label caption>\n              {{ $t(\"Settings.experimental.description\") }}\n            </q-item-label>\n          </q-item-section>\n        </q-item>\n        <q-item>\n          <q-toggle\n            v-model=\"enableReceiveSwaps\"\n            :label=\"$t('Settings.experimental.receive_swaps.toggle')\"\n            color=\"primary\"\n          >\n            <q-badge\n              color=\"primary\"\n              :label=\"$t('Settings.experimental.receive_swaps.badge')\"\n              class=\"q-mx-sm\"\n            ></q-badge>\n          </q-toggle>\n        </q-item>\n        <q-item class=\"q-pt-none\">\n          <q-item-label caption\n            >{{ $t(\"Settings.experimental.receive_swaps.description\") }}\n          </q-item-label>\n        </q-item>\n        <q-item>\n          <q-toggle\n            v-model=\"autoPasteEcashReceive\"\n            :label=\"$t('Settings.experimental.auto_paste.toggle')\"\n            color=\"primary\"\n          /> </q-item\n        ><q-item class=\"q-pt-none\">\n          <q-item-label caption\n            >{{ $t(\"Settings.experimental.auto_paste.description\") }}\n          </q-item-label>\n        </q-item>\n\n        <!-- auditor settings -->\n        <q-item>\n          <q-toggle\n            v-model=\"auditorEnabled\"\n            :label=\"$t('Settings.experimental.auditor.toggle')\"\n            color=\"primary\"\n          >\n            <q-badge\n              color=\"primary\"\n              :label=\"$t('Settings.experimental.auditor.badge')\"\n              class=\"q-mx-sm\"\n            ></q-badge>\n          </q-toggle>\n        </q-item>\n        <div v-if=\"auditorEnabled\">\n          <q-item class=\"q-pt-none\">\n            <q-item-label caption\n              >{{ $t(\"Settings.experimental.auditor.description\") }}\n            </q-item-label>\n          </q-item>\n          <div class=\"row q-mx-md\">\n            <div class=\"col-12\">\n              <q-input\n                v-model=\"auditorUrl\"\n                :label=\"$t('Settings.experimental.auditor.url_label')\"\n                color=\"primary\"\n                outlined\n                dense\n                rounded\n                :disable=\"!auditorEnabled\"\n              >\n                <template v-slot:append>\n                  <q-btn\n                    dense\n                    flat\n                    icon=\"content_copy\"\n                    @click=\"copyText(auditorUrl)\"\n                    size=\"sm\"\n                    color=\"grey\"\n                  />\n                </template>\n              </q-input>\n            </div>\n          </div>\n          <div class=\"row q-mx-md q-mt-md\">\n            <div class=\"col-12\">\n              <q-input\n                v-model=\"auditorApiUrl\"\n                :label=\"$t('Settings.experimental.auditor.api_url_label')\"\n                color=\"primary\"\n                outlined\n                dense\n                rounded\n                :disable=\"!auditorEnabled\"\n              >\n                <template v-slot:append>\n                  <q-btn\n                    dense\n                    flat\n                    icon=\"content_copy\"\n                    @click=\"copyText(auditorApiUrl)\"\n                    size=\"sm\"\n                    color=\"grey\"\n                  />\n                </template>\n              </q-input>\n            </div>\n          </div>\n        </div>\n        <!-- npcv2 settings -->\n        <div class=\"row q-mx-md q-mt-md\">\n          <div class=\"col-12\">\n            <div class=\"row q-pt-md\">\n              <q-toggle\n                v-model=\"npcV2Enabled\"\n                :label=\"$t('Settings.npub_cash.use_npubx')\"\n                color=\"primary\"\n              >\n                <q-badge\n                  color=\"primary\"\n                  :label=\"$t('Settings.experimental.receive_swaps.badge')\"\n                  class=\"q-mx-sm\"\n                ></q-badge>\n              </q-toggle>\n            </div>\n          </div>\n        </div>\n        <div v-if=\"npcV2Enabled\">\n          <div class=\"row q-mx-md q-mt-md\">\n            <div class=\"col-12\">\n              <q-input outlined v-model=\"npcV2Address\" dense rounded readonly>\n                <template v-slot:append>\n                  <q-icon\n                    name=\"content_copy\"\n                    @click=\"copyText(npcV2Address)\"\n                    size=\"xs\"\n                    color=\"grey\"\n                    class=\"q-mr-sm cursor-pointer\"\n                  >\n                    <q-tooltip>{{\n                      $t(\"Settings.npub_cash.copy_lightning_address\")\n                    }}</q-tooltip>\n                  </q-icon>\n                </template>\n              </q-input>\n            </div>\n          </div>\n          <div class=\"row q-mx-md\">\n            <div class=\"col-12 q-pt-md\">\n              <q-item-label caption>{{\n                $t(\"Settings.npub_cash.v2_mint\")\n              }}</q-item-label>\n              <q-input\n                outlined\n                v-model=\"npcV2Mint\"\n                dense\n                rounded\n                readonly\n                class=\"q-pt-sm\"\n              >\n              </q-input>\n              <div class=\"q-pt-sm\">\n                <ChooseMint\n                  v-model=\"npcV2Mint\"\n                  :title=\"\n                    $t('Settings.lightning_address.npc_v2.choose_mint_title')\n                  \"\n                  :placeholder=\"\n                    $t(\n                      'Settings.lightning_address.npc_v2.choose_mint_placeholder'\n                    )\n                  \"\n                  :show-balances=\"false\"\n                  :dense=\"true\"\n                  :rounded=\"true\"\n                  :require-active-mint=\"false\"\n                />\n              </div>\n            </div>\n            <div class=\"row q-pt-md\">\n              <q-toggle v-model=\"npcV2ClaimAutomatically\" color=\"primary\" />\n              <q-item-section>\n                <q-item-label title>{{\n                  $t(\"Settings.lightning_address.automatic_claim.toggle\")\n                }}</q-item-label>\n                <q-item-label caption\n                  >{{\n                    $t(\"Settings.lightning_address.automatic_claim.description\")\n                  }}\n                </q-item-label>\n              </q-item-section>\n            </div>\n          </div>\n        </div>\n\n        <!-- multinut settings -->\n        <div class=\"row q-mx-md q-mt-md\">\n          <div class=\"col-12\">\n            <div class=\"row q-pt-md\">\n              <q-toggle\n                v-model=\"multinutEnabled\"\n                :label=\"$t('Settings.multinut.use_multinut')\"\n                color=\"primary\"\n              >\n                <q-badge\n                  color=\"primary\"\n                  :label=\"$t('Settings.experimental.receive_swaps.badge')\"\n                  class=\"q-mx-sm\"\n                ></q-badge>\n                <q-item-label caption>\n                  {{ $t(\"Settings.experimental.multinut.description\") }}\n                </q-item-label>\n              </q-toggle>\n            </div>\n          </div>\n        </div>\n\n        <div class=\"section-divider q-my-md\">\n          <div class=\"divider-line\"></div>\n          <div class=\"divider-text\">\n            {{ $t(\"Settings.sections.appearance\") }}\n          </div>\n          <div class=\"divider-line\"></div>\n        </div>\n\n        <!-- use numeric keyboard -->\n        <div class=\"q-py-sm q-px-xs text-left\" on-left>\n          <q-list padding>\n            <q-item>\n              <q-item-section>\n                <q-item-label overline class=\"text-weight-bold\">{{\n                  $t(\"Settings.appearance.keyboard.title\")\n                }}</q-item-label>\n                <q-item-label caption>{{\n                  $t(\"Settings.appearance.keyboard.description\")\n                }}</q-item-label>\n              </q-item-section>\n            </q-item>\n            <q-item>\n              <q-toggle\n                v-model=\"useNumericKeyboard\"\n                :label=\"$t('Settings.appearance.keyboard.toggle')\"\n                color=\"primary\"\n              /> </q-item\n            ><q-item class=\"q-pt-none\">\n              <q-item-label caption\n                >{{ $t(\"Settings.appearance.keyboard.toggle_description\") }}\n              </q-item-label>\n            </q-item>\n          </q-list>\n        </div>\n\n        <!-- bip177 -->\n        <div class=\"q-py-sm q-px-xs text-left\" on-left>\n          <q-list padding>\n            <q-item>\n              <q-item-section>\n                <q-item-label overline class=\"text-weight-bold\">{{\n                  $t(\"Settings.appearance.bip177.title\")\n                }}</q-item-label>\n                <q-item-label caption>{{\n                  $t(\"Settings.appearance.bip177.description\")\n                }}</q-item-label>\n              </q-item-section>\n            </q-item>\n            <q-item>\n              <q-toggle\n                v-model=\"bip177BitcoinSymbol\"\n                :label=\"$t('Settings.appearance.bip177.toggle')\"\n                color=\"primary\"\n              />\n            </q-item>\n          </q-list>\n        </div>\n\n        <!-- theme -->\n        <div class=\"q-py-mb q-px-xs text-left\" on-left>\n          <q-list padding>\n            <q-item>\n              <q-item-section>\n                <q-item-label overline class=\"text-weight-bold\">{{\n                  $t(\"Settings.appearance.theme.title\")\n                }}</q-item-label>\n                <q-item-label caption\n                  >{{ $t(\"Settings.appearance.theme.description\") }}\n                </q-item-label>\n                <!-- <div class=\"row q-py-md\">\n              <q-btn dense flat rounded @click=\"toggleDarkMode\" size=\"md\"\n                >Toggle dark mode<q-icon\n                  class=\"q-ml-sm\"\n                  :name=\"$q.dark.isActive ? 'brightness_3' : 'wb_sunny'\"\n                />\n              </q-btn>\n            </div> -->\n                <div class=\"row q-pt-md\">\n                  <q-btn\n                    v-if=\"themes.includes('monochrome')\"\n                    dense\n                    flat\n                    @click=\"changeColor('monochrome')\"\n                    icon=\"format_color_fill\"\n                    color=\"grey\"\n                    size=\"md\"\n                    ><q-tooltip>{{\n                      $t(\"Settings.appearance.theme.tooltips.mono\")\n                    }}</q-tooltip>\n                  </q-btn>\n                  <q-btn\n                    v-if=\"themes.includes('cyber')\"\n                    dense\n                    flat\n                    @click=\"changeColor('cyber')\"\n                    icon=\"format_color_fill\"\n                    color=\"green\"\n                    size=\"md\"\n                    ><q-tooltip>{{\n                      $t(\"Settings.appearance.theme.tooltips.cyber\")\n                    }}</q-tooltip>\n                  </q-btn>\n                  <q-btn\n                    v-if=\"themes.includes('freedom')\"\n                    dense\n                    flat\n                    @click=\"changeColor('freedom')\"\n                    icon=\"format_color_fill\"\n                    color=\"pink-13\"\n                    size=\"md\"\n                    ><q-tooltip>{{\n                      $t(\"Settings.appearance.theme.tooltips.freedom\")\n                    }}</q-tooltip>\n                  </q-btn>\n                  <q-btn\n                    v-if=\"themes.includes('classic')\"\n                    dense\n                    flat\n                    @click=\"changeColor('classic')\"\n                    icon=\"format_color_fill\"\n                    color=\"deep-purple\"\n                    size=\"md\"\n                    ><q-tooltip>{{\n                      $t(\"Settings.appearance.theme.tooltips.nostr\")\n                    }}</q-tooltip>\n                  </q-btn>\n                  <q-btn\n                    v-if=\"themes.includes('bitcoin')\"\n                    dense\n                    flat\n                    @click=\"changeColor('bitcoin')\"\n                    icon=\"format_color_fill\"\n                    color=\"orange\"\n                    size=\"md\"\n                    ><q-tooltip>{{\n                      $t(\"Settings.appearance.theme.tooltips.bitcoin\")\n                    }}</q-tooltip>\n                  </q-btn>\n                  <q-btn\n                    v-if=\"themes.includes('mint')\"\n                    dense\n                    flat\n                    @click=\"changeColor('mint')\"\n                    icon=\"format_color_fill\"\n                    color=\"light-green-9\"\n                    size=\"md\"\n                    ><q-tooltip>{{\n                      $t(\"Settings.appearance.theme.tooltips.mint\")\n                    }}</q-tooltip> </q-btn\n                  ><q-btn\n                    v-if=\"themes.includes('autumn')\"\n                    dense\n                    flat\n                    @click=\"changeColor('autumn')\"\n                    icon=\"format_color_fill\"\n                    color=\"brown\"\n                    size=\"md\"\n                    ><q-tooltip>{{\n                      $t(\"Settings.appearance.theme.tooltips.nut\")\n                    }}</q-tooltip>\n                  </q-btn>\n                  <q-btn\n                    v-if=\"themes.includes('salvador')\"\n                    dense\n                    flat\n                    @click=\"changeColor('salvador')\"\n                    icon=\"format_color_fill\"\n                    color=\"blue-10\"\n                    size=\"md\"\n                    ><q-tooltip>{{\n                      $t(\"Settings.appearance.theme.tooltips.blu\")\n                    }}</q-tooltip>\n                  </q-btn>\n                  <q-btn\n                    v-if=\"themes.includes('flamingo')\"\n                    dense\n                    flat\n                    @click=\"changeColor('flamingo')\"\n                    icon=\"format_color_fill\"\n                    color=\"pink-3\"\n                    size=\"md\"\n                    ><q-tooltip>{{\n                      $t(\"Settings.appearance.theme.tooltips.flamingo\")\n                    }}</q-tooltip>\n                  </q-btn>\n                </div>\n              </q-item-section>\n            </q-item>\n          </q-list>\n        </div>\n\n        <!-- language picker: hidden for till i18n integration is finished -->\n        <!-- LANGUAGE SECTION -->\n        <div class=\"section-divider q-my-md\">\n          <div class=\"divider-line\"></div>\n          <div class=\"divider-text\">{{ $t(\"Settings.language.title\") }}</div>\n          <div class=\"divider-line\"></div>\n        </div>\n\n        <div class=\"q-py-sm q-px-xs text-left\" on-left>\n          <q-list padding>\n            <q-item>\n              <q-item-section>\n                <q-item-label overline class=\"text-weight-bold\">{{\n                  $t(\"Settings.language.title\")\n                }}</q-item-label>\n                <q-item-label caption>{{\n                  $t(\"Settings.language.description\")\n                }}</q-item-label>\n              </q-item-section>\n            </q-item>\n            <q-item>\n              <q-item-section>\n                <q-select\n                  v-model=\"selectedLanguage\"\n                  :options=\"languageOptions\"\n                  rounded\n                  outlined\n                  dense\n                  emit-value\n                  map-options\n                  @update:model-value=\"changeLanguage\"\n                />\n              </q-item-section>\n            </q-item>\n          </q-list>\n        </div>\n\n        <!-- developer settings -->\n\n        <q-expansion-item\n          class=\"q-pt-lg\"\n          dense\n          dense-toggle\n          icon=\"code\"\n          :label=\"$t('Settings.advanced.title')\"\n        >\n          <div>\n            <q-item class=\"q-pt-lg\">\n              <q-item-section>\n                <q-item-label overline>{{\n                  $t(\"Settings.advanced.developer.title\")\n                }}</q-item-label>\n                <q-item-label caption>{{\n                  $t(\"Settings.advanced.developer.description\")\n                }}</q-item-label>\n              </q-item-section>\n            </q-item>\n            <div>\n              <!-- check proofs spendable setting -->\n              <q-item>\n                <q-item-section>\n                  <div class=\"row q-pt-md\">\n                    <div class=\"col-12\" v-if=\"!confirmMnemonic\">\n                      <q-btn\n                        flat\n                        dense\n                        @click=\"confirmMnemonic = !confirmMnemonic\"\n                        >{{\n                          $t(\"Settings.advanced.developer.new_seed.button\")\n                        }}</q-btn\n                      >\n                      <row>\n                        <q-item-label class=\"q-px-sm\" caption\n                          >{{\n                            $t(\n                              \"Settings.advanced.developer.new_seed.description\"\n                            )\n                          }}\n                        </q-item-label>\n                      </row>\n                    </div>\n                    <div class=\"col-12\" v-if=\"confirmMnemonic\">\n                      <span\n                        >{{\n                          $t(\n                            \"Settings.advanced.developer.new_seed.confirm_question\"\n                          )\n                        }}\n                      </span>\n                      <q-btn\n                        flat\n                        dense\n                        class=\"q-ml-sm\"\n                        color=\"warning\"\n                        @click=\"confirmMnemonic = false\"\n                        >{{\n                          $t(\"Settings.advanced.developer.new_seed.cancel\")\n                        }}</q-btn\n                      >\n                      <q-btn\n                        flat\n                        dense\n                        class=\"q-ml-sm\"\n                        color=\"secondary\"\n                        @click=\"\n                          confirmMnemonic = false;\n                          generateNewMnemonic();\n                        \"\n                        >{{\n                          $t(\"Settings.advanced.developer.new_seed.confirm\")\n                        }}</q-btn\n                      >\n                    </div>\n                  </div>\n                </q-item-section>\n              </q-item>\n              <q-item>\n                <q-item-section>\n                  <row>\n                    <q-btn\n                      dense\n                      flat\n                      outline\n                      click\n                      @click=\"checkActiveProofsSpendable\"\n                      >{{\n                        $t(\"Settings.advanced.developer.remove_spent.button\")\n                      }}</q-btn\n                    ></row\n                  ><row>\n                    <q-item-label class=\"q-px-sm\" caption\n                      >{{\n                        $t(\n                          \"Settings.advanced.developer.remove_spent.description\"\n                        )\n                      }}\n                    </q-item-label>\n                  </row>\n                </q-item-section>\n              </q-item>\n              <q-item>\n                <q-item-section>\n                  <row>\n                    <q-btn dense flat outline click @click=\"toggleTerminal\">\n                      {{\n                        $t(\"Settings.advanced.developer.debug_console.button\")\n                      }}\n                    </q-btn></row\n                  ><row>\n                    <q-item-label class=\"q-px-sm\" caption\n                      >{{\n                        $t(\n                          \"Settings.advanced.developer.debug_console.description\"\n                        )\n                      }}\n                    </q-item-label>\n                  </row>\n                </q-item-section>\n              </q-item>\n              <q-item>\n                <q-item-section>\n                  <row>\n                    <q-btn dense flat outline click @click=\"exportActiveProofs\">\n                      {{\n                        $t(\"Settings.advanced.developer.export_proofs.button\")\n                      }}\n                    </q-btn></row\n                  ><row>\n                    <q-item-label class=\"q-px-sm\" caption\n                      >{{\n                        $t(\n                          \"Settings.advanced.developer.export_proofs.description\"\n                        )\n                      }}\n                    </q-item-label>\n                  </row>\n                </q-item-section>\n              </q-item>\n              <q-item>\n                <q-item-section>\n                  <row>\n                    <!-- add a caption, not a button here -->\n                    <q-item-label class=\"q-pb-sm\">{{\n                      $t(\"Settings.advanced.developer.keyset_counters.title\")\n                    }}</q-item-label></row\n                  >\n                  <row>\n                    <q-item-label class=\"q-px-sm\" caption\n                      >{{\n                        $t(\n                          \"Settings.advanced.developer.keyset_counters.description\"\n                        )\n                      }}\n                    </q-item-label>\n                  </row>\n                  <row class=\"q-pa-sm\">\n                    <row\n                      class=\"q-px-sm\"\n                      v-for=\"(mintCounter, mintUrl) in keysetCountersByMint\"\n                      :key=\"mintUrl\"\n                    >\n                      <q-item-label class=\"q-px-xs\" caption>\n                        {{ shortUrl(mintUrl) }}\n                      </q-item-label>\n                      <q-btn\n                        dense\n                        v-for=\"(counter, id) in mintCounter\"\n                        :key=\"id\"\n                        flat\n                        click\n                        @click=\"increaseKeysetCounter(counter.id, 1)\"\n                        >{{ counter.id }} -\n                        {{\n                          $t(\n                            \"Settings.advanced.developer.keyset_counters.counter\",\n                            { count: counter.counter }\n                          )\n                        }}\n                      </q-btn>\n                    </row>\n                  </row>\n                </q-item-section>\n              </q-item>\n              <q-item>\n                <q-item-section>\n                  <row>\n                    <q-btn\n                      dense\n                      flat\n                      outline\n                      click\n                      @click=\"unsetAllReservedProofs\"\n                    >\n                      {{\n                        $t(\"Settings.advanced.developer.unset_reserved.button\")\n                      }}\n                    </q-btn></row\n                  ><row>\n                    <q-item-label class=\"q-px-sm\" caption\n                      >{{\n                        $t(\n                          \"Settings.advanced.developer.unset_reserved.description\"\n                        )\n                      }}\n                    </q-item-label>\n                  </row>\n                </q-item-section>\n              </q-item>\n              <q-item>\n                <q-item-section>\n                  <row>\n                    <q-btn dense flat outline click @click=\"showOnboarding\">\n                      {{\n                        $t(\"Settings.advanced.developer.show_onboarding.button\")\n                      }}\n                    </q-btn></row\n                  ><row>\n                    <q-item-label class=\"q-px-sm\" caption\n                      >{{\n                        $t(\n                          \"Settings.advanced.developer.show_onboarding.description\"\n                        )\n                      }}\n                    </q-item-label>\n                  </row>\n                </q-item-section>\n              </q-item>\n              <q-item>\n                <q-item-section>\n                  <row>\n                    <q-btn\n                      v-if=\"!confirmNuke\"\n                      dense\n                      flat\n                      outline\n                      click\n                      @click=\"confirmNuke = !confirmNuke\"\n                    >\n                      {{\n                        $t(\"Settings.advanced.developer.reset_wallet.button\")\n                      }}\n                    </q-btn></row\n                  ><row v-if=\"!confirmNuke\">\n                    <q-item-label class=\"q-px-sm\" caption\n                      >{{\n                        $t(\n                          \"Settings.advanced.developer.reset_wallet.description\"\n                        )\n                      }}\n                    </q-item-label>\n                  </row>\n                  <row v-if=\"confirmNuke\">\n                    <span>{{\n                      $t(\n                        \"Settings.advanced.developer.reset_wallet.confirm_question\"\n                      )\n                    }}</span>\n                    <q-btn\n                      flat\n                      dense\n                      class=\"q-ml-sm\"\n                      color=\"primary\"\n                      @click=\"confirmNuke = false\"\n                      >{{\n                        $t(\"Settings.advanced.developer.reset_wallet.cancel\")\n                      }}</q-btn\n                    >\n                    <q-btn\n                      flat\n                      dense\n                      class=\"q-ml-sm\"\n                      color=\"warning\"\n                      @click=\"\n                        confirmNuke = false;\n                        nukeWallet();\n                      \"\n                      >{{\n                        $t(\"Settings.advanced.developer.reset_wallet.confirm\")\n                      }}</q-btn\n                    >\n                  </row>\n                </q-item-section>\n              </q-item>\n              <q-item>\n                <q-item-section>\n                  <row>\n                    <q-btn\n                      v-if=\"!confirmImport\"\n                      dense\n                      flat\n                      outline\n                      click\n                      @click=\"confirmImport = !confirmImport\"\n                    >\n                      {{\n                        $t(\"Settings.advanced.developer.import_wallet.button\")\n                      }}\n                    </q-btn>\n                  </row>\n                  <row v-if=\"!confirmImport\">\n                    <q-item-label class=\"q-px-sm\" caption\n                      >{{\n                        $t(\n                          \"Settings.advanced.developer.import_wallet.description\"\n                        )\n                      }}\n                    </q-item-label>\n                  </row>\n                  <row v-if=\"confirmImport\">\n                    <span>{{\n                      $t(\n                        \"Settings.advanced.developer.import_wallet.confirm_question\"\n                      )\n                    }}</span>\n                    <q-btn\n                      flat\n                      dense\n                      class=\"q-ml-sm\"\n                      color=\"primary\"\n                      @click=\"confirmImport = false\"\n                      >{{\n                        $t(\"Settings.advanced.developer.import_wallet.cancel\")\n                      }}</q-btn\n                    >\n                    <q-btn\n                      flat\n                      dense\n                      class=\"q-ml-sm\"\n                      color=\"warning\"\n                      @click=\"\n                        confirmImport = false;\n                        browseBackupFile();\n                      \"\n                      >{{\n                        $t(\"Settings.advanced.developer.import_wallet.confirm\")\n                      }}</q-btn\n                    >\n                  </row>\n                  <input\n                    type=\"file\"\n                    ref=\"fileUpload\"\n                    accept=\".json\"\n                    style=\"display: none\"\n                    @change=\"onChangeFileUpload\"\n                  />\n                </q-item-section>\n              </q-item>\n              <q-item>\n                <q-item-section>\n                  <row>\n                    <q-btn dense flat outline click @click=\"exportWalletState\">\n                      {{\n                        $t(\"Settings.advanced.developer.export_wallet.button\")\n                      }}\n                    </q-btn></row\n                  ><row>\n                    <q-item-label class=\"q-px-sm\" caption\n                      >{{\n                        $t(\n                          \"Settings.advanced.developer.export_wallet.description\"\n                        )\n                      }}\n                    </q-item-label>\n                  </row>\n                </q-item-section>\n              </q-item>\n            </div>\n          </div>\n        </q-expansion-item>\n      </q-list>\n    </div>\n  </div>\n\n  <!-- P2PK DIALOG -->\n  <P2PKDialog v-model=\"showP2PKDialog\" />\n\n  <!-- NWC DIALOG -->\n  <NWCDialog v-model=\"showNWCDialog\" />\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport P2PKDialog from \"./P2PKDialog.vue\";\nimport NWCDialog from \"./NWCDialog.vue\";\nimport ChooseMint from \"./ChooseMint.vue\";\n\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport { useMintsStore, MintClass } from \"src/stores/mints\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { map } from \"underscore\";\nimport { useSettingsStore } from \"src/stores/settings\";\nimport { useNostrStore } from \"src/stores/nostr\";\nimport { useNPCStore } from \"src/stores/npubcash\";\nimport { useP2PKStore } from \"src/stores/p2pk\";\nimport { useNWCStore } from \"src/stores/nwc\";\nimport { useUiStore } from \"../stores/ui\";\nimport { useWorkersStore } from \"src/stores/workers\";\nimport { useProofsStore } from \"src/stores/proofs\";\nimport { usePRStore } from \"../stores/payment-request\";\nimport { useRestoreStore } from \"src/stores/restore\";\nimport { useDexieStore } from \"../stores/dexie\";\nimport { useReceiveTokensStore } from \"../stores/receiveTokensStore\";\nimport { useWelcomeStore } from \"src/stores/welcome\";\nimport { useStorageStore } from \"src/stores/storage\";\nimport { useNPCV2Store } from \"src/stores/npcv2\";\nimport { useNostrMintBackupStore } from \"src/stores/nostrMintBackup\";\nimport { usePriceStore } from \"src/stores/price\";\nimport { useI18n } from \"vue-i18n\";\nimport { useNostrUserStore } from \"src/stores/nostrUser\";\n\nexport default defineComponent({\n  name: \"SettingsView\",\n  mixins: [windowMixin],\n  components: {\n    P2PKDialog,\n    NWCDialog,\n    ChooseMint,\n  },\n  props: {},\n  data: function () {\n    return {\n      themes: [\n        \"monochrome\",\n        \"classic\",\n        \"bitcoin\",\n        \"mint\",\n        \"autumn\",\n        \"salvador\",\n        \"freedom\",\n        \"cyber\",\n        \"flamingo\",\n      ],\n      selectedLanguage: navigator.language || \"en-US\",\n      languageOptions: [\n        { label: \"English\", value: \"en-US\" },\n        { label: \"Español\", value: \"es-ES\" },\n        { label: \"Italiano\", value: \"it-IT\" },\n        { label: \"Deutsch\", value: \"de-DE\" },\n        { label: \"Français\", value: \"fr-FR\" },\n        { label: \"Čeština\", value: \"cs-CZ\" },\n        { label: \"Português (Brasil)\", value: \"pt-BR\" },\n        { label: \"Svenska\", value: \"sv-SE\" },\n        { label: \"Ελληνικά\", value: \"el-GR\" },\n        { label: \"Türkçe\", value: \"tr-TR\" },\n        { label: \"ไทย\", value: \"th-TH\" },\n        { label: \"العربية\", value: \"ar-SA\" },\n        { label: \"中文\", value: \"zh-CN\" },\n        { label: \"日本語\", value: \"ja-JP\" },\n      ],\n      currencyOptions: [\n        { label: \"US Dollar (USD)\", value: \"USD\" },\n        { label: \"Euro (EUR)\", value: \"EUR\" },\n\n        { label: \"Australian Dollar (AUD)\", value: \"AUD\" },\n        { label: \"Brazilian Real (BRL)\", value: \"BRL\" },\n        { label: \"Canadian Dollar (CAD)\", value: \"CAD\" },\n        { label: \"Swiss Franc (CHF)\", value: \"CHF\" },\n        { label: \"Chinese Yuan (CNY)\", value: \"CNY\" },\n        { label: \"Czech Koruna (CZK)\", value: \"CZK\" },\n        { label: \"Danish Krone (DKK)\", value: \"DKK\" },\n        { label: \"British Pound (GBP)\", value: \"GBP\" },\n        { label: \"Hong Kong Dollar (HKD)\", value: \"HKD\" },\n        { label: \"Hungarian Forint (HUF)\", value: \"HUF\" },\n        { label: \"Israeli Shekel (ILS)\", value: \"ILS\" },\n        { label: \"Indian Rupee (INR)\", value: \"INR\" },\n        { label: \"Japanese Yen (JPY)\", value: \"JPY\" },\n        { label: \"South Korean Won (KRW)\", value: \"KRW\" },\n        { label: \"Mexican Peso (MXN)\", value: \"MXN\" },\n        { label: \"New Zealand Dollar (NZD)\", value: \"NZD\" },\n        { label: \"Norwegian Krone (NOK)\", value: \"NOK\" },\n        { label: \"Polish Zloty (PLN)\", value: \"PLN\" },\n        { label: \"Russian Ruble (RUB)\", value: \"RUB\" },\n        { label: \"Swedish Krona (SEK)\", value: \"SEK\" },\n        { label: \"Singapore Dollar (SGD)\", value: \"SGD\" },\n        { label: \"Thai Baht (THB)\", value: \"THB\" },\n        { label: \"Turkish Lira (TRY)\", value: \"TRY\" },\n        { label: \"South African Rand (ZAR)\", value: \"ZAR\" },\n      ],\n      discoveringMints: false,\n      hideMnemonic: true,\n      confirmMnemonic: false,\n      confirmNuke: false,\n      confirmImport: false,\n      nip46Token: \"\",\n      nip07SignerAvailable: false,\n      newRelay: \"\",\n    };\n  },\n  computed: {\n    ...mapWritableState(useSettingsStore, [\n      \"getBitcoinPrice\",\n      \"bitcoinPriceCurrency\",\n      \"checkSentTokens\",\n      \"useWebsockets\",\n      \"nfcEncoding\",\n      \"useNumericKeyboard\",\n      \"periodicallyCheckIncomingInvoices\",\n      \"checkIncomingInvoices\",\n      \"checkInvoicesOnStartup\",\n      \"enableReceiveSwaps\",\n      \"showNfcButtonInDrawer\",\n      \"autoPasteEcashReceive\",\n      \"auditorEnabled\",\n      \"auditorUrl\",\n      \"auditorApiUrl\",\n      \"bip177BitcoinSymbol\",\n      \"multinutEnabled\",\n      \"nostrMintBackupEnabled\",\n    ]),\n    ...mapState(useP2PKStore, [\"p2pkKeys\"]),\n    ...mapWritableState(useP2PKStore, [\n      \"showP2PKDialog\",\n      \"showP2PkButtonInDrawer\",\n    ]),\n    ...mapWritableState(useNWCStore, [\n      \"nwcEnabled\",\n      \"connections\",\n      \"showNWCDialog\",\n      \"showNWCData\",\n    ]),\n    ...mapWritableState(useNostrStore, [\"relays\"]),\n    ...mapState(useMintsStore, [\"activeMintUrl\", \"mints\", \"activeProofs\"]),\n    ...mapState(useNPCStore, [\"npcLoading\"]),\n    ...mapState(useNostrStore, [\n      \"pubkey\",\n      \"signerType\",\n      \"seedSignerPrivateKeyNsec\",\n    ]),\n    ...mapState(useNostrUserStore, [\n      \"wotCount\",\n      \"wotLoading\",\n      \"crawlProcessed\",\n      \"crawlTotal\",\n      \"hasCrawlCheckpoint\",\n    ]),\n    ...mapState(useWalletStore, [\"mnemonic\"]),\n    ...mapState(useUiStore, [\"ndefSupported\"]),\n    ...mapWritableState(useNPCV2Store, [\n      \"npcV2Loading\",\n      \"npcV2Enabled\",\n      \"npcV2Address\",\n      \"npcV2Mint\",\n      \"npcV2ClaimAutomatically\",\n    ]),\n    ...mapWritableState(useNPCStore, [\n      \"npcAddress\",\n      \"npcEnabled\",\n      \"automaticClaim\",\n    ]),\n    ...mapWritableState(useWalletStore, [\"keysetCounters\"]),\n    ...mapWritableState(useMintsStore, [\n      \"addMintData\",\n      \"showAddMintDialog\",\n      \"showRemoveMintDialog\",\n    ]),\n    ...mapWritableState(usePRStore, [\n      \"enablePaymentRequest\",\n      \"receivePaymentRequestsAutomatically\",\n    ]),\n\n    keysetCountersByMint() {\n      const mints = this.mints;\n      const keysetCountersByMint = {}; // {mintUrl: [keysetCounter: {id: string, count: number}, ...]}\n      for (const mint of mints) {\n        const mintIds = mint.keysets.map((keyset) => keyset.id);\n        const keysetCounterThisMint = this.keysetCounters.filter((entry) =>\n          mintIds.includes(entry.id)\n        );\n        keysetCountersByMint[mint.url] = keysetCounterThisMint;\n      }\n      return keysetCountersByMint;\n    },\n    hiddenMnemonic() {\n      if (this.hideMnemonic) {\n        return this.mnemonic\n          .split(\" \")\n          .map((w) => \"*\".repeat(6))\n          .join(\" \");\n      } else {\n        return this.mnemonic;\n      }\n    },\n    enableNwc: {\n      get() {\n        return this.nwcEnabled;\n      },\n      set(value) {\n        this.nwcEnabled = value;\n      },\n    },\n  },\n  watch: {\n    pubkey: {\n      immediate: true,\n      handler(newPk) {\n        const nostrUser = useNostrUserStore();\n        if (newPk) {\n          nostrUser.setPubkey(newPk);\n          nostrUser.updateUserProfile(true);\n        }\n      },\n    },\n    enableNwc: function () {\n      if (this.enableNwc) {\n        this.listenToNWCCommands();\n      } else {\n        this.unsubscribeNWC();\n      }\n    },\n    npcEnabled: async function () {\n      if (this.npcEnabled) {\n        await this.initSigner();\n        await this.generateNPCConnection();\n      } else {\n        this.npcAddress = \"\";\n      }\n    },\n    npcV2Enabled: async function () {\n      if (this.npcV2Enabled) {\n        await this.initSigner();\n        await this.generateNPCV2Connection();\n      } else {\n        this.npcV2Address = \"\";\n      }\n    },\n    npcV2Mint: async function (newMintUrl, oldMintUrl) {\n      if (this.npcV2Enabled && newMintUrl && newMintUrl !== oldMintUrl) {\n        await this.changeMintUrl(newMintUrl);\n      }\n    },\n  },\n  methods: {\n    ...mapActions(useNostrStore, [\n      \"init\",\n      \"initNip07Signer\",\n      \"initNip46Signer\",\n      \"initPrivateKeySigner\",\n      \"initWalletSeedPrivateKeySigner\",\n      \"checkNip07Signer\",\n      \"resetPrivateKeySigner\",\n      \"resetNip46Signer\",\n      \"initSigner\",\n    ]),\n    ...mapActions(useNWCStore, [\n      \"generateNWCConnection\",\n      \"listenToNWCCommands\",\n      \"unsubscribeNWC\",\n      \"getConnectionString\",\n    ]),\n    ...mapActions(useP2PKStore, [\n      \"importNsec\",\n      \"generateKeypair\",\n      \"showKeyDetails\",\n    ]),\n    ...mapActions(useMintsStore, [\n      \"addMint\",\n      \"removeMint\",\n      \"activateMintUrl\",\n      \"updateMint\",\n    ]),\n    ...mapActions(useWalletStore, [\n      \"newMnemonic\",\n      \"decodeRequest\",\n      \"checkProofsSpendable\",\n      \"increaseKeysetCounter\",\n    ]),\n    ...mapActions(useProofsStore, [\"serializeProofs\"]),\n    ...mapActions(useNPCStore, [\"generateNPCConnection\"]),\n    ...mapActions(useNPCV2Store, [\"generateNPCV2Connection\", \"changeMintUrl\"]),\n    ...mapActions(useRestoreStore, [\"restoreMint\"]),\n    ...mapActions(useDexieStore, [\"deleteAllTables\"]),\n    ...mapActions(useStorageStore, [\"restoreFromBackup\", \"exportWalletState\"]),\n    ...mapActions(usePriceStore, [\n      \"fetchBitcoinPrice\",\n      \"updateBitcoinPriceForCurrentCurrency\",\n    ]),\n    ...mapActions(useNostrUserStore, [\n      \"setPubkey\",\n      \"updateUserProfile\",\n      \"crawlWebOfTrust\",\n      \"cancelCrawl\",\n      \"resetWebOfTrust\",\n    ]),\n    generateNewMnemonic: async function () {\n      this.newMnemonic();\n      await this.initSigner();\n      await this.generateNPCConnection();\n    },\n    shortUrl: function (url) {\n      return getShortUrl(url);\n    },\n    toggleMnemonicVisibility: function () {\n      this.hideMnemonic = !this.hideMnemonic;\n    },\n    toggleTerminal: function () {\n      useUiStore().toggleDebugConsole();\n    },\n    unsetAllReservedProofs: async function () {\n      // mark all this.proofs as reserved=false\n      const proofsStore = useProofsStore();\n      await proofsStore.setReserved(await proofsStore.getProofs(), false);\n      this.notifySuccess(\"All reserved proofs unset\");\n    },\n    checkActiveProofsSpendable: async function () {\n      // iterate over this.activeProofs in batches of 50 and check if they are spendable\n      const wallet = await useWalletStore().mintWallet(\n        this.activeMintUrl,\n        this.activeUnit\n      );\n      const proofs = this.activeProofs.flat();\n      console.log(\"Checking proofs\", proofs);\n      const allSpentProofs = [];\n      const batch_size = 50;\n      for (let i = 0; i < proofs.length; i += batch_size) {\n        console.log(\"Checking proofs\", i, i + batch_size);\n        const batch = proofs.slice(i, i + batch_size);\n        const spent = await this.checkProofsSpendable(batch, wallet, true);\n        allSpentProofs.push(spent);\n      }\n      const spentProofs = allSpentProofs.flat();\n      if (spentProofs.length > 0) {\n        console.log(\"Spent proofs\", spentProofs);\n        this.notifySuccess(\"Removed \" + spentProofs.length + \" spent proofs\");\n      } else {\n        this.notifySuccess(\"No spent proofs found\");\n      }\n    },\n    showP2PKKeyEntry: async function (pubKey) {\n      this.showKeyDetails(pubKey);\n      this.showP2PKDialog = true;\n    },\n    showNWCEntry: async function (connection) {\n      this.showNWCData = {\n        connection,\n        connectionString: this.getConnectionString(connection),\n      };\n      this.showNWCDialog = true;\n    },\n    exportActiveProofs: async function () {\n      // export active proofs\n      const token = await this.serializeProofs(this.activeProofs);\n      this.copyText(token);\n    },\n    handleSeedClick: async function () {\n      await this.initWalletSeedPrivateKeySigner();\n      await this.generateNPCConnection();\n      await this.generateNPCV2Connection();\n      const nostr = useNostrStore();\n      const nostrUser = useNostrUserStore();\n      nostrUser.setPubkey(nostr.pubkey);\n      await nostrUser.updateUserProfile(true);\n    },\n    handleExtensionClick: async function () {\n      await this.initNip07Signer();\n      await this.generateNPCConnection();\n      await this.generateNPCV2Connection();\n      const nostr = useNostrStore();\n      const nostrUser = useNostrUserStore();\n      nostrUser.setPubkey(nostr.pubkey);\n      await nostrUser.updateUserProfile(true);\n    },\n    handleBunkerClick: async function () {\n      await this.initNip46Signer();\n      await this.generateNPCConnection();\n      await this.generateNPCV2Connection();\n      const nostr = useNostrStore();\n      const nostrUser = useNostrUserStore();\n      nostrUser.setPubkey(nostr.pubkey);\n      await nostrUser.updateUserProfile(true);\n    },\n    handleNsecClick: async function () {\n      await this.initPrivateKeySigner();\n      await this.generateNPCConnection();\n      await this.generateNPCV2Connection();\n      const nostr = useNostrStore();\n      const nostrUser = useNostrUserStore();\n      nostrUser.setPubkey(nostr.pubkey);\n      await nostrUser.updateUserProfile(true);\n    },\n    handleResetPrivateKeySigner: async function () {\n      await this.resetPrivateKeySigner();\n      await this.generateNPCConnection();\n      await this.generateNPCV2Connection();\n    },\n    handleResetNip46Signer: async function () {\n      await this.resetNip46Signer();\n      await this.generateNPCConnection();\n      await this.generateNPCV2Connection();\n    },\n    showOnboarding: function () {\n      const welcomeStore = useWelcomeStore();\n      welcomeStore.resetWelcome();\n      this.$router.push(\"/welcome\");\n    },\n    nukeWallet: async function () {\n      // create a backup just in case\n      await this.exportWalletState();\n      // clear dexie tables\n      this.deleteAllTables();\n      // clear nostr user databases\n      useNostrUserStore().clearAllDatabases();\n      // clear mint reviews database\n      try {\n        const { useMintRecommendationsStore } = await import(\n          \"src/stores/mintRecommendations\"\n        );\n        await useMintRecommendationsStore().clearAllDatabases();\n      } catch {}\n      localStorage.clear();\n      window.location.href = \"/\";\n    },\n    browseBackupFile: function () {\n      this.$refs.fileUpload.click();\n    },\n    onChangeFileUpload: function () {\n      const file = this.$refs.fileUpload.files[0];\n      if (file) {\n        this.readBackupFile(file);\n      }\n    },\n    readBackupFile: function (file) {\n      const reader = new FileReader();\n      reader.onload = (f) => {\n        try {\n          const content = f.target.result;\n          const backup = JSON.parse(content);\n          this.restoreFromBackup(backup);\n        } catch (error) {\n          console.error(\"Error reading backup file:\", error);\n          this.notifyError(\"Invalid backup file format\");\n        }\n      };\n      reader.onerror = () => {\n        this.notifyError(\"Error reading file\");\n      };\n      reader.readAsText(file);\n    },\n    addRelay: function () {\n      if (this.newRelay) {\n        this.newRelay = this.newRelay.trim();\n        // if relay is already in relays, don't add it, send notification\n        if (this.relays.includes(this.newRelay)) {\n          this.notifyWarning(\"Relay already added\");\n        } else {\n          this.relays.push(this.newRelay);\n          this.newRelay = \"\";\n        }\n      }\n    },\n    removeRelay: function (relay) {\n      this.relays = this.relays.filter((r) => r !== relay);\n    },\n    onNostrMintBackupToggle: async function (enabled) {\n      const nostrMintBackupStore = useNostrMintBackupStore();\n\n      if (enabled) {\n        try {\n          await nostrMintBackupStore.enableBackup();\n          this.notifySuccess(\n            this.$t(\n              \"Settings.experimental.nostr_mint_backup.notifications.enabled\"\n            )\n          );\n        } catch (error) {\n          console.error(\"Failed to enable Nostr mint backup:\", error);\n          this.notifyError(\n            this.$t(\n              \"Settings.experimental.nostr_mint_backup.notifications.failed\"\n            )\n          );\n          // Revert the toggle\n          this.nostrMintBackupEnabled = false;\n        }\n      } else {\n        nostrMintBackupStore.disableBackup();\n        this.notifySuccess(\n          this.$t(\n            \"Settings.experimental.nostr_mint_backup.notifications.disabled\"\n          )\n        );\n      }\n    },\n    changeLanguage(locale) {\n      // Set the i18n locale\n      this.$i18n.locale = locale;\n\n      // Store the selected language in localStorage\n      localStorage.setItem(\"cashu.language\", locale);\n\n      // // Reload the page to apply the language change\n      // setTimeout(() => {\n      //   window.location.reload();\n      // }, 300);\n    },\n    onCurrencyChange: async function (currency) {\n      // Fetch fresh rates if they're stale, since we get all currencies at once\n      const priceStore = usePriceStore();\n      if (\n        Date.now() - priceStore.bitcoinPriceLastUpdated >\n        priceStore.bitcoinPriceMinRefreshInterval\n      ) {\n        await this.fetchBitcoinPrice();\n      } else {\n        // Update the main bitcoinPrice to reflect the new currency selection\n        this.updateBitcoinPriceForCurrentCurrency();\n      }\n    },\n  },\n  created: async function () {\n    this.nip07SignerAvailable = await this.checkNip07Signer();\n    console.log(\"Nip07 signer available\", this.nip07SignerAvailable);\n    // Set the initial selected language based on the current locale\n    this.selectedLanguage =\n      this.languageOptions.find((option) => option.value === this.$i18n.locale)\n        ?.label || \"Language\";\n  },\n});\n</script>\n<style>\n/* Section Divider */\n.section-divider {\n  display: flex;\n  align-items: center;\n  width: 100%;\n  margin-bottom: 24px;\n}\n\n.divider-line {\n  flex: 1;\n  height: 1px;\n  background-color: #48484a;\n}\n\n.divider-text {\n  padding: 0 10px;\n  font-size: 14px;\n  font-weight: 600;\n  color: #ffffff;\n}\n</style>\n"
  },
  {
    "path": "src/components/SwapIncomingTokenToKnownMint.vue",
    "content": "<template>\n  <div class=\"swap-section q-mt-md\">\n    <!-- Header with cancel button -->\n    <div class=\"row items-center q-mb-md\">\n      <div class=\"col text-h6\" style=\"font-size: 18px\">\n        {{ $t(\"ReceiveTokenDialog.swap_section.title\") }}\n      </div>\n      <!-- Processing/Error status -->\n      <div v-if=\"swapProcessing || swapError\" class=\"row items-center no-wrap\">\n        <span\n          class=\"q-mr-sm text-subtitle2\"\n          :class=\"swapError ? 'text-red' : ''\"\n          style=\"font-size: 0.875rem\"\n        >\n          {{\n            swapError\n              ? $t(\"ReceiveTokenDialog.actions.swap.failed\")\n              : $t(\"ReceiveTokenDialog.actions.swap.processing\")\n          }}\n        </span>\n        <q-spinner v-if=\"swapProcessing\" color=\"primary\" size=\"20px\" />\n        <q-btn\n          v-if=\"swapError\"\n          flat\n          round\n          dense\n          icon=\"close\"\n          @click=\"$emit('close')\"\n          color=\"grey-6\"\n        >\n          <q-tooltip>{{\n            $t(\"ReceiveTokenDialog.actions.cancel_swap.tooltip_text\")\n          }}</q-tooltip>\n        </q-btn>\n      </div>\n      <!-- Normal close button (hidden during processing) -->\n      <q-btn\n        v-else\n        flat\n        round\n        dense\n        icon=\"close\"\n        @click=\"$emit('close')\"\n        :disable=\"swapBlocking\"\n        color=\"grey-6\"\n      >\n        <q-tooltip>{{\n          $t(\"ReceiveTokenDialog.actions.cancel_swap.tooltip_text\")\n        }}</q-tooltip>\n      </q-btn>\n    </div>\n\n    <!-- Source Mint (Token Origin) -->\n    <div class=\"swap-source-section\">\n      <div class=\"swap-section-label q-mb-sm\">\n        {{ $t(\"ReceiveTokenDialog.swap_section.source_label\") }}\n      </div>\n      <div class=\"swap-mint-info\">\n        <div class=\"row items-center no-wrap\">\n          <!-- Mint Icon -->\n          <q-avatar size=\"48px\" class=\"q-mr-md\">\n            <q-img\n              v-if=\"sourceMintInfo?.iconUrl\"\n              :src=\"sourceMintInfo.iconUrl\"\n              spinner-color=\"white\"\n              spinner-size=\"xs\"\n            >\n              <template v-slot:error>\n                <div class=\"row items-center justify-center full-height\">\n                  <q-icon name=\"account_balance\" color=\"grey-7\" size=\"24px\" />\n                </div>\n              </template>\n            </q-img>\n            <q-icon v-else name=\"account_balance\" color=\"grey-7\" size=\"24px\" />\n          </q-avatar>\n\n          <!-- Mint Info -->\n          <div class=\"col text-left\">\n            <div class=\"swap-mint-name\">\n              {{ sourceMintInfo?.nickname || sourceMintInfo?.shorturl }}\n            </div>\n            <div class=\"swap-mint-url text-grey-6\">\n              {{ sourceMintInfo?.shorturl }}\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- Arrow Indicator -->\n    <div class=\"row justify-center q-my-sm\">\n      <ArrowDownIcon class=\"swap-arrow-icon\" />\n    </div>\n\n    <!-- Destination Mint Selector -->\n    <div class=\"swap-destination-section\">\n      <div class=\"swap-section-label q-mb-sm\">\n        {{ $t(\"ReceiveTokenDialog.swap_section.destination_label\") }}\n      </div>\n      <ChooseMint\n        v-model=\"selectedMintUrl\"\n        :dry-run=\"true\"\n        :exclude-mint=\"sourceMint\"\n      />\n    </div>\n\n    <!-- Info Tip about fees -->\n    <ToolTipInfo\n      class=\"q-mt-md\"\n      :text=\"$t('ReceiveTokenDialog.swap_section.fee_info')\"\n    />\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, PropType } from \"vue\";\nimport ChooseMint from \"src/components/ChooseMint.vue\";\nimport { ArrowDown as ArrowDownIcon } from \"lucide-vue-next\";\nimport ToolTipInfo from \"src/components/ToolTipInfo.vue\";\nimport { useMintsStore } from \"stores/mints\";\n\nexport default defineComponent({\n  name: \"SwapIncomingTokenToKnownMint\",\n  components: {\n    ChooseMint,\n    ArrowDownIcon,\n    ToolTipInfo,\n  },\n  props: {\n    swapProcessing: {\n      type: Boolean,\n      required: true,\n    },\n    swapError: {\n      type: Boolean,\n      required: true,\n    },\n    swapBlocking: {\n      type: Boolean,\n      required: true,\n    },\n    sourceMintInfo: {\n      type: Object as PropType<{\n        nickname?: string | null;\n        shorturl?: string | null;\n        iconUrl?: string | null;\n      } | null>,\n      required: false,\n      default: null,\n    },\n    sourceMint: {\n      type: String,\n      required: false,\n      default: \"\",\n    },\n    targetMint: {\n      type: String,\n      required: false,\n      default: \"\",\n    },\n  },\n  emits: [\"close\", \"update:targetMint\"],\n  data: function () {\n    return {\n      selectedMintUrl: this.targetMint || \"\",\n    };\n  },\n  created() {\n    const mintsStore = useMintsStore();\n    const activeMintCandidate = mintsStore.activeMintUrl as unknown;\n    let initialTarget = \"\";\n    if (typeof activeMintCandidate === \"string\") {\n      initialTarget = activeMintCandidate;\n    } else if (\n      activeMintCandidate &&\n      typeof activeMintCandidate === \"object\" &&\n      \"value\" in activeMintCandidate\n    ) {\n      const candidateValue = (activeMintCandidate as { value?: string }).value;\n      if (typeof candidateValue === \"string\") {\n        initialTarget = candidateValue;\n      }\n    }\n    if (!this.selectedMintUrl && initialTarget) {\n      this.selectedMintUrl = initialTarget;\n    }\n  },\n  watch: {\n    targetMint: {\n      handler(newVal: string) {\n        if (newVal !== this.selectedMintUrl) {\n          this.selectedMintUrl = newVal || \"\";\n        }\n      },\n      immediate: true,\n    },\n    selectedMintUrl(newVal: string) {\n      this.$emit(\"update:targetMint\", newVal);\n    },\n    sourceMint(newVal: string) {\n      if (newVal && newVal === this.selectedMintUrl) {\n        this.selectedMintUrl = \"\";\n      }\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n/* Swap Section Styles */\n.swap-section {\n  padding: 16px;\n  background: rgba(255, 255, 255, 0.03);\n  border-radius: 12px;\n  border: 1px solid rgba(255, 255, 255, 0.08);\n}\n\n.swap-mint-info {\n  padding: 12px;\n  background: rgba(255, 255, 255, 0.05);\n  border-radius: 10px;\n  border: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.swap-mint-name {\n  font-size: 16px;\n  font-weight: 500;\n  color: white;\n  line-height: 1.3;\n}\n\n.swap-mint-url {\n  font-size: 14px;\n  line-height: 1.3;\n  margin-top: 2px;\n}\n\n.swap-arrow-icon {\n  width: 24px;\n  height: 24px;\n  color: rgba(255, 255, 255, 0.5);\n}\n\n.swap-destination-section {\n  margin-top: 8px;\n}\n\n.swap-section-label {\n  font-size: 14px;\n  font-weight: 500;\n  color: rgba(255, 255, 255, 0.7);\n}\n\n.swap-info-tip {\n  display: flex;\n  align-items: center;\n  padding: 12px;\n  background: rgba(33, 150, 243, 0.1);\n  border-radius: 8px;\n  border: 1px solid rgba(33, 150, 243, 0.2);\n}\n\n.swap-info-text {\n  font-size: 13px;\n  color: rgba(255, 255, 255, 0.8);\n  line-height: 1.4;\n}\n</style>\n"
  },
  {
    "path": "src/components/ToggleUnit.vue",
    "content": "<template>\n  <q-btn\n    rounded\n    outline\n    :color=\"color\"\n    @click=\"toggleUnit()\"\n    :label=\"activeUnitLabelAdopted\"\n  />\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport { mapActions, mapState } from \"pinia\";\nimport { useMintsStore } from \"stores/mints\";\nexport default defineComponent({\n  name: \"ToggleUnit\",\n  mixins: [windowMixin],\n  props: {\n    balanceView: {\n      type: Boolean,\n      required: false,\n    },\n    color: {\n      type: String,\n      default: \"primary\",\n    },\n  },\n  data: function () {\n    return {\n      chosenMint: null,\n    };\n  },\n  mounted() {},\n  watch: {},\n  computed: {\n    ...mapState(useMintsStore, [\"activeUnit\", \"activeUnitLabel\"]),\n    activeUnitLabelAdopted: function () {\n      // we want to show BTC instead of SAT\n      if (this.activeUnit === \"sat\") {\n        return \"BTC\";\n      } else {\n        return this.activeUnitLabel;\n      }\n    },\n  },\n  methods: {\n    ...mapActions(useMintsStore, [\"toggleUnit\"]),\n  },\n});\n</script>\n"
  },
  {
    "path": "src/components/TokenInformation.vue",
    "content": "<template>\n  <div class=\"token-information-container q-px-md\">\n    <!-- Amount Header -->\n    <div v-if=\"!hideAmount\" class=\"token-header-container\">\n      <div class=\"token-amount\">\n        {{ displayUnit }}\n      </div>\n    </div>\n\n    <!-- Token Details Section -->\n    <div class=\"token-details-section q-mt-md q-mb-lg\">\n      <!-- Fee (if applicable) -->\n      <div v-if=\"receiveFee > 0 && !hideFee\" class=\"detail-item q-mb-md\">\n        <div class=\"detail-label\">\n          <arrow-down-up-icon size=\"20\" color=\"#9E9E9E\" class=\"detail-icon\" />\n          <div class=\"detail-name\">{{ $t(\"TokenInformation.fee\") }}</div>\n        </div>\n        <div class=\"detail-value\">\n          {{ formatCurrency(receiveFee, tokenUnit, true) }}\n        </div>\n      </div>\n\n      <!-- Unit -->\n      <div v-if=\"!hideUnit\" class=\"detail-item q-mb-md\">\n        <div class=\"detail-label\">\n          <banknote-icon size=\"20\" color=\"#9E9E9E\" class=\"detail-icon\" />\n          <div class=\"detail-name\">{{ $t(\"TokenInformation.unit\") }}</div>\n        </div>\n        <div class=\"detail-value\">{{ tokenUnit.toUpperCase() }}</div>\n      </div>\n\n      <!-- Fiat -->\n      <div v-if=\"showFiat && !hideFiat\" class=\"detail-item q-mb-md\">\n        <div class=\"detail-label\">\n          <banknote-icon size=\"20\" color=\"#9E9E9E\" class=\"detail-icon\" />\n          <div class=\"detail-name\">{{ $t(\"TokenInformation.fiat\") }}</div>\n        </div>\n        <div class=\"detail-value\">\n          {{ formatCurrency(fiatAmount, bitcoinPriceCurrency, true) }}\n        </div>\n      </div>\n\n      <!-- P2PK Lock Status (if locked) -->\n      <div\n        v-if=\"isLocked(proofsToShow) && !hideP2PK\"\n        class=\"detail-item q-mb-md\"\n      >\n        <div class=\"detail-label\">\n          <lock-icon size=\"20\" color=\"#9E9E9E\" class=\"detail-icon\" />\n          <div class=\"detail-name\">{{ $t(\"TokenInformation.p2pk\") }}</div>\n        </div>\n        <div\n          class=\"detail-value\"\n          :class=\"{\n            'p2pk-locked-me': isLockedToUs(proofsToShow),\n            'p2pk-locked-warning':\n              isLocked(proofsToShow) && !isLockedToUs(proofsToShow),\n          }\"\n        >\n          {{\n            isLockedToUs(proofsToShow)\n              ? $t(\"TokenInformation.locked_to_you\")\n              : $t(\"TokenInformation.locked\")\n          }}\n        </div>\n      </div>\n\n      <!-- Mint URL -->\n      <div v-if=\"!hideMint\" class=\"detail-item q-mb-md\">\n        <div class=\"detail-label\">\n          <building-icon size=\"20\" color=\"#9E9E9E\" class=\"detail-icon\" />\n          <div class=\"detail-name\">{{ $t(\"TokenInformation.mint\") }}</div>\n        </div>\n        <div class=\"detail-value\">\n          {{ tokenMintUrl }}\n          <q-spinner v-if=\"addMintBlocking\" size=\"sm\" class=\"q-ml-xs\" />\n        </div>\n      </div>\n\n      <!-- Memo (if available) -->\n      <div v-if=\"displayMemo && !hideMemo\" class=\"detail-item q-mb-md\">\n        <div class=\"detail-label\">\n          <message-circle-icon size=\"20\" color=\"#9E9E9E\" class=\"detail-icon\" />\n          <div class=\"detail-name\">{{ $t(\"TokenInformation.memo\") }}</div>\n        </div>\n        <div\n          class=\"detail-value\"\n          style=\"max-width: 60%; word-break: break-word; white-space: normal\"\n        >\n          {{ displayMemo }}\n        </div>\n      </div>\n\n      <!-- Payment Request (if applicable) -->\n      <div v-if=\"paymentRequestId\" class=\"detail-item\">\n        <div class=\"detail-label\">\n          <banknote-icon size=\"20\" color=\"#9E9E9E\" class=\"detail-icon\" />\n          <div class=\"detail-name\">\n            {{ $t(\"TokenInformation.payment_request\") }}\n          </div>\n        </div>\n        <div class=\"detail-value\">{{ $t(\"TokenInformation.nostr\") }}</div>\n      </div>\n    </div>\n  </div>\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport { mapActions, mapState } from \"pinia\";\nimport { useMintsStore } from \"stores/mints\";\nimport { useWalletStore } from \"stores/wallet\";\nimport { useP2PKStore } from \"src/stores/p2pk\";\nimport { usePriceStore } from \"src/stores/price\";\nimport { useSettingsStore } from \"src/stores/settings\";\nimport token from \"src/js/token\";\nimport {\n  ArrowDownUp as ArrowDownUpIcon,\n  Building as BuildingIcon,\n  Lock as LockIcon,\n  Banknote as BanknoteIcon,\n  MessageCircle as MessageCircleIcon,\n} from \"lucide-vue-next\";\n\nexport default defineComponent({\n  name: \"TokenInformation\",\n  mixins: [windowMixin],\n  components: {\n    ArrowDownUpIcon,\n    BuildingIcon,\n    LockIcon,\n    BanknoteIcon,\n    MessageCircleIcon,\n  },\n  props: {\n    encodedToken: String,\n    paymentRequestId: {\n      type: String,\n      required: false,\n      default: undefined,\n    },\n    hideAmount: {\n      type: Boolean,\n      default: false,\n    },\n    hideUnit: {\n      type: Boolean,\n      default: false,\n    },\n    hideFiat: {\n      type: Boolean,\n      default: false,\n    },\n    hideMemo: {\n      type: Boolean,\n      default: false,\n    },\n    hideP2PK: {\n      type: Boolean,\n      default: false,\n    },\n    hideMint: {\n      type: Boolean,\n      default: false,\n    },\n    hideFee: {\n      type: Boolean,\n      default: false,\n    },\n  },\n  emits: [\"notLockedToUs\"],\n  data: function () {\n    return {};\n  },\n  watch: {\n    proofsToShow: {\n      handler(newProofs) {\n        if (newProofs && newProofs.length > 0) {\n          const locked = this.isLocked(newProofs);\n          const lockedToUs = this.isLockedToUs(newProofs);\n          // Emit true if locked but not locked to us, false otherwise\n          this.$emit(\"notLockedToUs\", locked && !lockedToUs);\n        } else {\n          this.$emit(\"notLockedToUs\", false);\n        }\n      },\n      immediate: true,\n    },\n  },\n  computed: {\n    ...mapState(useMintsStore, [\n      \"activeMintUrl\",\n      \"activeProofs\",\n      \"mints\",\n      \"activeUnit\",\n      \"addMintBlocking\",\n    ]),\n    ...mapState(usePriceStore, [\"bitcoinPrice\", \"currentCurrencyPrice\"]),\n    ...mapState(useSettingsStore, [\"bitcoinPriceCurrency\"]),\n    proofsToShow: function () {\n      return token.getProofs(token.decode(this.encodedToken));\n    },\n    sumProofs: function () {\n      const proofs = token.getProofs(token.decode(this.encodedToken));\n      return proofs.flat().reduce((sum, el) => (sum += el.amount), 0);\n    },\n    displayUnit: function () {\n      const display = this.formatCurrency(this.sumProofs, this.tokenUnit, true);\n      return display;\n    },\n    tokenUnit: function () {\n      return token.getUnit(token.decode(this.encodedToken));\n    },\n    tokenMintUrl: function () {\n      const mint = token.getMint(token.decode(this.encodedToken));\n      return getShortUrl(mint);\n    },\n    displayMemo: function () {\n      return token.getMemo(token.decode(this.encodedToken));\n    },\n    showFiat: function () {\n      return this.tokenUnit === \"sat\" && !!this.bitcoinPrice;\n    },\n    fiatAmount: function () {\n      if (!this.showFiat) return 0;\n      // sumProofs is in sats, currentCurrencyPrice is fiat per BTC\n      return (this.currentCurrencyPrice / 100000000) * this.sumProofs;\n    },\n    receiveFee: function () {\n      try {\n        const tokenJson = token.decode(this.encodedToken);\n        const proofs = token.getProofs(tokenJson);\n        if (!proofs?.length) {\n          return 0;\n        }\n        const mintUrl = token.getMint(tokenJson);\n        const unit = token.getUnit(tokenJson);\n        const walletStore = useWalletStore();\n        const wallet = walletStore.mintWalletSync(mintUrl, unit);\n        const fee = wallet.getFeesForProofs(proofs);\n        return fee || 0;\n      } catch (e) {\n        return 0;\n      }\n    },\n  },\n  methods: {\n    ...mapActions(useP2PKStore, [\"isLocked\", \"isLockedToUs\"]),\n    getProofsMint: function (proofs) {\n      // unique keyset IDs of proofs\n      const uniqueIds = [...new Set(proofs.map((p) => p.id))];\n      // mints that have any of the keyset IDs\n      const mints_keysets = this.mints.filter((m) =>\n        m.keysets.some((r) => uniqueIds.indexOf(r) >= 0)\n      );\n      // what we put into the JSON\n      const mints = mints_keysets.map(\n        (m) => [{ url: m.url, ids: m.keysets }][0]\n      );\n      if (mints.length == 0) {\n        return \"\";\n      } else {\n        return getShortUrl(mints[0].url);\n      }\n    },\n    mintKnownToUs: function (proofs) {\n      // unique keyset IDs of proofs\n      const uniqueIds = [...new Set(proofs.map((p) => p.id))];\n      // mints that have any of the keyset IDs\n      return (\n        this.mints.filter((m) =>\n          m.keysets.some((r) => uniqueIds.indexOf(r.id) >= 0)\n        ).length > 0\n      );\n    },\n    shareToken: async function () {\n      if (navigator.share) {\n        const shareData = {\n          text: this.encodedToken,\n        };\n        try {\n          await navigator.share(shareData);\n        } catch (error: any) {\n          if (error?.name !== \"AbortError\") {\n            console.error(\"Error sharing token:\", error);\n          }\n        }\n      }\n    },\n    copyToken: async function () {\n      try {\n        await navigator.clipboard.writeText(this.encodedToken);\n        this.$q.notify({\n          message: this.$t(\"TokenInformation.token_copied\"),\n          color: \"positive\",\n          position: \"top\",\n          timeout: 1000,\n        });\n      } catch (error) {\n        console.error(\"Failed to copy to clipboard:\", error);\n      }\n    },\n  },\n});\n</script>\n\n<style scoped>\n.token-information-container {\n  width: 100%;\n  color: white;\n}\n\n/* Token Header - matches mint-header-container from MintDetailsPage */\n.token-header-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  width: 100%;\n}\n\n.token-amount {\n  font-size: 36px;\n  font-weight: 700;\n  text-align: center;\n}\n\n/* Action Buttons Section */\n.action-buttons-section {\n  width: 100%;\n}\n\n/* Token Details Section - exact match from MintDetailsPage */\n.token-details-section {\n  width: 100%;\n}\n\n.detail-item {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  width: 100%;\n}\n\n.detail-label {\n  display: flex;\n  align-items: center;\n}\n\n.detail-icon {\n  margin-right: 10px;\n}\n\n.detail-name {\n  font-size: 16px;\n  font-weight: 600;\n  color: #9e9e9e;\n}\n\n.detail-value {\n  font-size: 16px;\n  font-weight: 600;\n  color: white;\n  text-align: right;\n  max-width: 60%;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n/* Golden shine for P2PK locked-to-you */\n.p2pk-locked-me {\n  color: #ffd700;\n  background: linear-gradient(\n    90deg,\n    #b8860b 0%,\n    #ffd700 40%,\n    #fff6b7 50%,\n    #ffd700 60%,\n    #b8860b 100%\n  );\n  -webkit-background-clip: text;\n  -webkit-text-fill-color: transparent;\n  animation: shine 2.5s linear infinite;\n  text-shadow: 0 0 6px rgba(255, 215, 0, 0.35);\n}\n\n/* Warning colors for P2PK locked (not to us) */\n.p2pk-locked-warning {\n  color: #ff9800;\n  text-shadow: 0 0 6px rgba(255, 152, 0, 0.4);\n}\n\n@keyframes shine {\n  0% {\n    background-position: -200px 0;\n  }\n  100% {\n    background-position: 200px 0;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/components/TokenStringRender.vue",
    "content": "<template>\n  <q-card class=\"token-card\">\n    <!-- Token string as background filling entire card -->\n    <div class=\"token-background\">\n      {{ displayedToken }}\n    </div>\n\n    <q-card-section class=\"q-pa-md token-content\">\n      <!-- Bottom Row: Mint Name + Amount -->\n      <div class=\"row items-end justify-between bottom-info\">\n        <div class=\"mint-name\">{{ mintName }}</div>\n        <div class=\"token-amount text-h4\">{{ displayAmount }}</div>\n      </div>\n    </q-card-section>\n  </q-card>\n</template>\n\n<script lang=\"ts\">\nimport { computed, defineComponent, onBeforeUnmount, ref, watch } from \"vue\";\nimport token from \"src/js/token\";\nimport { getShortUrl } from \"src/js/wallet-helpers\";\nimport { useMintsStore } from \"src/stores/mints\";\nimport { useUiStore } from \"src/stores/ui\";\n\nexport default defineComponent({\n  name: \"TokenStringRender\",\n  props: {\n    tokenString: {\n      type: String,\n      required: true,\n    },\n    maxLength: {\n      type: Number,\n      required: false,\n      default: 200,\n    },\n  },\n  setup(props) {\n    const mintsStore = useMintsStore();\n    const uiStore = useUiStore();\n    const displayedToken = ref(\"\");\n\n    // Decode token and extract data\n    const decodedToken = computed(() => {\n      if (!props.tokenString) return null;\n      try {\n        return token.decode(props.tokenString);\n      } catch {\n        return null;\n      }\n    });\n\n    const tokenAmount = computed(() => {\n      if (!decodedToken.value) return 0;\n      try {\n        const proofs = token.getProofs(decodedToken.value);\n        return proofs.reduce((sum, el) => sum + el.amount, 0);\n      } catch {\n        return 0;\n      }\n    });\n\n    const tokenUnit = computed(() => {\n      if (!decodedToken.value) return \"\";\n      try {\n        return token.getUnit(decodedToken.value);\n      } catch {\n        return \"\";\n      }\n    });\n\n    const mintUrl = computed(() => {\n      if (!decodedToken.value) return \"\";\n      try {\n        return token.getMint(decodedToken.value);\n      } catch {\n        return \"\";\n      }\n    });\n\n    const mintName = computed(() => {\n      if (!mintUrl.value) return \"\";\n      const mint = mintsStore.mints.find((m) => m.url === mintUrl.value);\n      return mint?.info?.name || getShortUrl(mintUrl.value);\n    });\n\n    const truncatedTokenString = computed(() => {\n      if (!props.tokenString) {\n        return \"\";\n      }\n      // Show the full token string, it will be wrapped and fill the card\n      return props.tokenString;\n    });\n\n    const displayAmount = computed(() => {\n      if (!tokenAmount.value || !tokenUnit.value) return \"\";\n      return uiStore.formatCurrency(tokenAmount.value, tokenUnit.value, true);\n    });\n\n    // Animation for truncated token string\n    const scrambleChars =\n      \"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+[]{}<>?/\\\\|~\";\n    let animationFrameId: number | null = null;\n\n    const stopAnimation = () => {\n      if (animationFrameId !== null) {\n        cancelAnimationFrame(animationFrameId);\n        animationFrameId = null;\n      }\n    };\n\n    const animateToken = (tokenStr: string) => {\n      stopAnimation();\n\n      if (!tokenStr) {\n        displayedToken.value = \"\";\n        return;\n      }\n\n      if (typeof window === \"undefined\") {\n        displayedToken.value = tokenStr;\n        return;\n      }\n\n      const targetLength = tokenStr.length;\n      const duration = Math.max(2500, Math.min(1200, targetLength * 12));\n      const start = performance.now();\n\n      const step = (now: number) => {\n        const elapsed = now - start;\n        const progress = Math.min(elapsed / duration, 1);\n        const growthProgress = Math.min(progress / 0.5, 1);\n        const revealProgress = Math.min(Math.max((progress - 0.3) / 0.7, 0), 1);\n\n        const currentLength = Math.max(\n          1,\n          Math.floor(growthProgress * targetLength)\n        );\n        const revealCount = Math.floor(revealProgress * targetLength);\n        let result = \"\";\n\n        for (let i = 0; i < currentLength; i += 1) {\n          if (i < revealCount) {\n            result += tokenStr[i];\n          } else {\n            const randomIndex = Math.floor(\n              Math.random() * scrambleChars.length\n            );\n            result += scrambleChars[randomIndex];\n          }\n        }\n\n        displayedToken.value = result;\n\n        if (progress < 1) {\n          animationFrameId = requestAnimationFrame(step);\n        } else {\n          displayedToken.value = tokenStr;\n          animationFrameId = null;\n        }\n      };\n\n      animationFrameId = requestAnimationFrame(step);\n    };\n\n    watch(\n      truncatedTokenString,\n      (newValue) => {\n        animateToken(newValue);\n      },\n      { immediate: true }\n    );\n\n    onBeforeUnmount(() => {\n      stopAnimation();\n    });\n\n    return {\n      displayedToken,\n      truncatedTokenString,\n      mintName,\n      displayAmount,\n    };\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.token-card {\n  background: linear-gradient(\n    135deg,\n    rgba(var(--q-primary-rgb), 0.18) 0%,\n    rgba(var(--q-primary-rgb), 0.08) 40%,\n    rgba(0, 0, 0, 0.15) 100%\n  ) !important;\n  border-radius: 10px;\n  border: 1px solid rgba(255, 255, 255, 0.12);\n  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3) !important;\n  backdrop-filter: blur(10px);\n  position: relative;\n  overflow: hidden;\n  min-height: 200px;\n  height: 200px;\n\n  &::before {\n    content: \"\";\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    height: 1px;\n    background: linear-gradient(\n      90deg,\n      transparent 0%,\n      rgba(255, 255, 255, 0.3) 50%,\n      transparent 100%\n    );\n    z-index: 1;\n  }\n\n  &::after {\n    content: \"\";\n    position: absolute;\n    top: -50%;\n    left: -50%;\n    width: 200%;\n    height: 200%;\n    background: radial-gradient(\n      circle at 30% 30%,\n      rgba(255, 255, 255, 0.08) 0%,\n      transparent 50%\n    );\n    pointer-events: none;\n    z-index: 0;\n  }\n}\n\n.token-background {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  padding: 16px;\n  font-size: 0.75rem;\n  font-weight: 400;\n  color: rgba(158, 158, 158, 0.817);\n  word-break: break-all;\n  font-family: monospace;\n  line-height: 1.4;\n  overflow: hidden;\n  z-index: 1;\n  mask-image: linear-gradient(\n    to bottom,\n    rgba(0, 0, 0, 1) 0%,\n    rgba(0, 0, 0, 1) 40%,\n    rgba(0, 0, 0, 0.2) 65%,\n    rgba(0, 0, 0, 0) 75%\n  );\n  -webkit-mask-image: linear-gradient(\n    to bottom,\n    rgba(0, 0, 0, 1) 0%,\n    rgba(0, 0, 0, 1) 40%,\n    rgba(0, 0, 0, 0.2) 65%,\n    rgba(0, 0, 0, 0) 75%\n  );\n}\n\n.token-content {\n  position: relative;\n  z-index: 2;\n  display: flex;\n  flex-direction: column;\n  justify-content: flex-end;\n  height: 100%;\n}\n\n.bottom-info {\n  width: 100%;\n}\n\n.mint-name {\n  font-weight: 400;\n  color: white;\n  font-size: 14px;\n}\n\n.token-amount {\n  font-weight: 700;\n  color: white;\n  line-height: 1.2;\n}\n</style>\n"
  },
  {
    "path": "src/components/ToolTipInfo.vue",
    "content": "<template>\n  <div class=\"tool-tip-info\">\n    <q-icon name=\"info\" size=\"16px\" class=\"q-mr-xs\" />\n    <span class=\"tool-tip-info-text\">{{ text }}</span>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\n\nexport default defineComponent({\n  name: \"ToolTipInfo\",\n  props: {\n    text: {\n      type: String,\n      required: true,\n    },\n  },\n});\n</script>\n\n<style lang=\"scss\" scoped>\n.tool-tip-info {\n  display: flex;\n  align-items: center;\n  padding: 12px;\n  background: rgba(33, 150, 243, 0.1);\n  border-radius: 8px;\n  border: 1px solid rgba(33, 150, 243, 0.2);\n}\n\n.tool-tip-info-text {\n  font-size: 13px;\n  color: rgba(255, 255, 255, 0.8);\n  line-height: 1.4;\n}\n</style>\n"
  },
  {
    "path": "src/components/WelcomeDialog.vue",
    "content": "<template>\n  <q-dialog\n    class=\"z-top\"\n    persistent\n    position=\"top\"\n    @drop=\"dragFile\"\n    @dragover=\"allowDrop\"\n    backdrop-filter=\"blur(2px) brightness(60%)\"\n  >\n    <q-card class=\"q-pa-lg z-top\">\n      <q-toolbar>\n        <!-- <q-avatar>\n          <img\n            src=\"https://raw.githubusercontent.com/cashubtc/cashu-ui/main/ui/icons/circle/128x128.png\"\n          />\n        </q-avatar> -->\n        <q-toolbar-title\n          ><span class=\"text-weight-bold\">Cashu</span> ecash\n          wallet</q-toolbar-title\n        >\n      </q-toolbar>\n      <q-card-section>\n        <p>Please take a moment to read the following information.</p>\n        <p>\n          <strong>Install and add to home screen.</strong>\n          For the best experience, use this wallet with your device's native web\n          browser (Safari on iOS, Chrome on Android). In Chrome click the\n          hamburger menu at the upper right. In Safari click the share button\n          and press the Add to Home screen button.\n        </p>\n        <p>\n          <strong>Back up your seed phrase.</strong> This wallet stores ecash\n          tokens in its database. If you delete your browser data without\n          backing up, you will lose your tokens. Make sure to back up your\n          wallet seed phrase in the settings.\n        </p>\n        <p>\n          <strong>This software is in BETA!</strong> We hold no responsibility\n          for people losing access to funds. Use at your own risk! This wallet\n          is not affiliated with any mint. This code is open-source and licensed\n          under the MIT license.\n        </p>\n        <!-- wallet backup restore -->\n        <input\n          type=\"file\"\n          id=\"fileUpload\"\n          ref=\"fileUpload\"\n          v-on:change=\"onChangeFileUpload()\"\n        />\n\n        <div class=\"row q-mt-lg\">\n          <q-btn\n            outline\n            class=\"q-mx-sm\"\n            v-if=\"\n              getPwaDisplayMode() == 'browser' &&\n              deferredPWAInstallPrompt != null\n            \"\n            color=\"primary\"\n            @click=\"triggerPwaInstall()\"\n            >Install Cashu</q-btn\n          >\n          <q-btn\n            flat\n            size=\"0.6rem\"\n            class=\"q-mx-xs q-px-none\"\n            @click=\"copyText(baseURL)\"\n            >Copy URL</q-btn\n          >\n          <q-btn\n            flat\n            size=\"0.6rem\"\n            class=\"q-mx-xs q-px-none\"\n            color=\"warning\"\n            icon=\"upload_for_offline\"\n            @click=\"browseBackupFile\"\n            >Restore<q-tooltip\n              >You can drag &amp; drop the wallet backup here!</q-tooltip\n            ></q-btn\n          >\n          <q-btn\n            v-close-popup\n            outline\n            size=\"0.8rem\"\n            class=\"q-ml-auto\"\n            @click=\"setWelcomeDialogSeen()\"\n            >Continue</q-btn\n          >\n        </div>\n      </q-card-section>\n    </q-card>\n  </q-dialog>\n</template>\n<style scoped>\n#fileUpload {\n  display: none;\n}\n</style>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { mapActions, mapState } from \"pinia\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useStorageStore } from \"src/stores/storage\";\n\nexport default defineComponent({\n  name: \"WelcomeDialog\",\n  mixins: [windowMixin],\n  props: {\n    welcomeDialog: Object,\n    triggerPwaInstall: Function,\n    setTab: Function,\n    getPwaDisplayMode: Function,\n    setWelcomeDialogSeen: Function,\n  },\n  data: function () {\n    return {};\n  },\n  watch: {},\n  computed: {\n    ...mapState(useWalletStore, [\"mnemonic\"]),\n  },\n  methods: {\n    ...mapActions(useStorageStore, [\"restoreFromBackup\"]),\n    readFile(file) {\n      const reader = new FileReader();\n      reader.onload = (f) => {\n        const content = f.target.result;\n        const backup = JSON.parse(content);\n\n        this.restoreFromBackup(backup);\n      };\n      reader.readAsText(file);\n    },\n    dragFile(ev) {\n      ev.preventDefault();\n\n      const files = ev.dataTransfer.files;\n      const file = files[0];\n\n      this.readFile(file);\n    },\n    allowDrop(ev) {\n      ev.preventDefault();\n    },\n    onChangeFileUpload() {\n      const file = this.$refs.fileUpload.files[0];\n\n      this.readFile(file);\n    },\n    browseBackupFile() {\n      this.$refs.fileUpload.click();\n    },\n  },\n});\n</script>\n"
  },
  {
    "path": "src/components/iOSPWAPrompt.vue",
    "content": "<template>\n  <transition enter-active-class=\"animated fadeInUp\">\n    <div\n      v-if=\"showIosPWAPrompt\"\n      class=\"pwa-prompt q-pa-md q-mx-auto text-center\"\n    >\n      <div class=\"pwa-prompt-content\">\n        <i18n-t keypath=\"iOSPWAPrompt.text\" tag=\"span\">\n          <template v-slot:icon>\n            <q-icon name=\"ios_share\" size=\"sm\" />\n          </template>\n          <template v-slot:buttonText>\n            <strong>{{ $t(\"iOSPWAPrompt.buttonText\") }}</strong>\n          </template>\n        </i18n-t>\n        <q-btn\n          flat\n          icon=\"close\"\n          @click=\"closePrompt\"\n          size=\"sm\"\n          class=\"close-btn q-px-sm\"\n        />\n      </div>\n\n      <div class=\"pwa-prompt-arrow\"></div>\n    </div>\n  </transition>\n</template>\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nexport default defineComponent({\n  name: \"iOSPWAPrompt\",\n  mixins: [windowMixin],\n  props: {},\n  data: function () {\n    return {\n      showIosPWAPromptLocal:\n        localStorage.getItem(\"cashu.ui.showIosPWAPrompt\") != \"seen\",\n      showIosPWAPrompt: false,\n    };\n  },\n  mounted() {\n    if (\n      this.showIosPWAPromptLocal &&\n      this.isiOsSafari() &&\n      !this.isInStandaloneMode()\n    ) {\n      this.showIosPWAPrompt = true;\n    }\n  },\n  watch: {},\n  computed: {},\n  methods: {\n    closePrompt() {\n      localStorage.setItem(\"cashu.ui.showIosPWAPrompt\", \"seen\");\n      this.showIosPWAPrompt = false;\n    },\n    isiOsSafari() {\n      const userAgent = window.navigator.userAgent.toLowerCase();\n      return /iphone|ipod/.test(userAgent) && /safari/.test(userAgent);\n    },\n    isInStandaloneMode() {\n      return \"standalone\" in window.navigator && window.navigator.standalone;\n    },\n  },\n});\n</script>\n<style scoped>\n@keyframes moveUpDown {\n  0%,\n  100% {\n    transform: translateY(0);\n  }\n  50% {\n    transform: translateY(-10px);\n  }\n}\n\n.pwa-prompt {\n  position: fixed;\n  bottom: 0;\n  left: 0;\n  right: 0;\n  margin: 0 auto;\n  z-index: 9999;\n  text-align: center;\n  display: flex;\n  flex-direction: column; /* Add this line */\n  align-items: center; /* Add this line */\n  justify-content: center;\n  animation: moveUpDown 1s infinite; /* Add this line for animation */\n}\n\n.pwa-prompt-content {\n  display: inline-flex;\n  align-items: center;\n  background-color: black;\n  padding: 10px;\n  border: 1px solid #ccc;\n  border-radius: 8px;\n}\n\n.pwa-prompt-content q-icon {\n  margin-right: 5px;\n}\n\n.pwa-prompt-arrow {\n  width: 0;\n  height: 0;\n  border-left: 10px solid transparent;\n  border-right: 10px solid transparent;\n  border-top: 10px solid white;\n  margin: 2px auto;\n  text-align: center;\n}\n</style>\n"
  },
  {
    "path": "src/css/app.scss",
    "content": "// app global css in SCSS form\n\n// Import Inter font from Google Fonts\n@import url(\"https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap\");\n\n// Apply Inter as the primary font throughout the application\nbody {\n  font-family: \"Inter\", -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto,\n    Oxygen, Ubuntu, Cantarell, \"Fira Sans\", \"Droid Sans\", \"Helvetica Neue\",\n    sans-serif;\n}\n\n// Dialog header typography\n// This class provides proper letter-spacing for dialog titles\n// The default Quasar 'overline' class has excessive letter-spacing for headers\n.dialog-header {\n  font-size: 1.2rem; // 16px\n  font-weight: 500; // Medium weight for headers\n  letter-spacing: -0.01em; // Tight letter spacing for better readability on headers\n  text-transform: none; // No uppercase transformation\n  line-height: 1.5; // Good line height for readability\n}\n\n// prevent pull to refresh\nhtml,\nbody {\n  overscroll-behavior: none;\n}\n\n// Map CSS env() safe area to our custom variable for PWA iOS\n:root {\n  --safe-area-inset-top: env(safe-area-inset-top);\n}\n\n// margin for top elements for ios safe area\nbody,\n.q-drawer,\n.q-header,\n.q-dialog__inner > div {\n  margin-top: var(--safe-area-inset-top);\n}\n\n.q-dialog__inner > div {\n  border-top-left-radius: 0px;\n  border-top-right-radius: 0px;\n  border-bottom-right-radius: 20px !important;\n  border-bottom-left-radius: 20px !important;\n}\n\n.q-card {\n  border-top-left-radius: 20px;\n  border-top-right-radius: 20px;\n}\n\n.q-dialog__inner--minimized > div {\n  border-top-left-radius: 20px;\n  border-top-right-radius: 20px;\n  border-bottom-right-radius: 20px;\n  border-bottom-left-radius: 20px;\n  max-width: 650px;\n}\n\n.custom-btn {\n  background: $grey-9;\n  color: white;\n  border-radius: 8px;\n  height: 80px;\n  box-shadow: none;\n  font-size: 18px;\n}\n\n.custom-btn:hover {\n  background: $grey-8;\n}\n\n.custom-btn-text {\n  font-size: 18px !important;\n  color: white;\n  padding-top: 2px;\n}\n\n.full-width-card {\n  width: 100%;\n  max-width: 600px;\n  margin: 0 auto;\n}\n\n.animated.tada {\n  animation-duration: 1s;\n}\n.animated.bounceIn {\n  animation-duration: 1s;\n}\n.animated.bounce {\n  animation-duration: 1s;\n}\n\n.q-checkbox__svg {\n  color: var(--q-dark);\n}\n"
  },
  {
    "path": "src/css/base.scss",
    "content": "$themes: (\n  \"classic\": (\n    primary: #935af5,\n    secondary: #b45af5,\n    dark: #1f2234,\n    info: #333646,\n    marginal-bg: #1f2234,\n    marginal-text: #fff,\n  ),\n  \"bitcoin\": (\n    primary: #ff9853,\n    secondary: #ff8753,\n    dark: #2d293b,\n    info: #333646,\n    marginal-bg: #2d293b,\n    marginal-text: #fff,\n  ),\n  \"freedom\": (\n    primary: #e22156,\n    secondary: #b91a45,\n    dark: #000,\n    info: #1b1b1b,\n    marginal-bg: #000,\n    marginal-text: #fff,\n  ),\n  \"salvador\": (\n    primary: #2d68d5,\n    secondary: #1366cb,\n    dark: #242424,\n    info: #333646,\n    marginal-bg: #242424,\n    marginal-text: #fff,\n  ),\n  \"mint\": (\n    primary: #3ab77d,\n    secondary: #27b065,\n    dark: #1f342b,\n    info: #334642,\n    marginal-bg: #1f342b,\n    marginal-text: #fff,\n  ),\n  \"autumn\": (\n    primary: #b7763a,\n    secondary: #b07927,\n    dark: #34291f,\n    info: #463f33,\n    marginal-bg: #342a1f,\n    marginal-text: rgb(255, 255, 255),\n  ),\n  \"flamingo\": (\n    primary: #ff64b4,\n    secondary: #ff61b3,\n    dark: #56353f,\n    info: #56353a,\n    marginal-bg: #56353a,\n    marginal-text: rgb(255, 255, 255),\n  ),\n  \"monochrome\": (\n    primary: #ededed,\n    secondary: #d5d5d5,\n    dark: #000,\n    info: rgb(39, 39, 39),\n    marginal-bg: #000,\n    marginal-text: rgb(255, 255, 255),\n  ),\n  \"cyber\": (\n    primary: #00ff00,\n    secondary: #00ff00,\n    dark: #000,\n    info: #1b1b1b,\n    marginal-bg: #000,\n    marginal-text: rgb(255, 255, 255),\n  ),\n);\n@each $theme, $colors in $themes {\n  @each $name, $color in $colors {\n    @if $name== \"dark\" {\n      [data-theme=\"#{$theme}\"] .q-drawer--dark,\n      body[data-theme=\"#{$theme}\"].body--dark,\n      [data-theme=\"#{$theme}\"] .q-menu--dark {\n        background: $color !important;\n      }\n      // set a darker body bg for all themes, when in \"dark mode\"\n      body[data-theme=\"#{$theme}\"].body--dark {\n        background: scale-color($color, $lightness: -60%);\n      }\n    }\n    @if $name== \"info\" {\n      [data-theme=\"#{$theme}\"] .q-card--dark,\n      [data-theme=\"#{$theme}\"] .q-stepper--dark {\n        background: $color !important;\n      }\n    }\n  }\n  [data-theme=\"#{$theme}\"] {\n    @each $name, $color in $colors {\n      .bg-#{$name} {\n        background: $color !important;\n      }\n      .text-#{$name} {\n        color: $color !important;\n      }\n    }\n  }\n}\n\n@each $theme, $colors in $themes {\n  [data-theme=\"#{$theme}\"] {\n    @each $name, $color in $colors {\n      @if $name == \"primary\" {\n        --q-primary: #{$color};\n      }\n      @if $name == \"secondary\" {\n        --q-secondary: #{$color};\n      }\n    }\n    @each $name, $color in $colors {\n      .bg-#{$name} {\n        background: $color !important;\n      }\n      .text-#{$name} {\n        color: $color !important;\n      }\n    }\n  }\n}\n\n[data-theme=\"monochrome\"] .q-badge.bg-primary,\n[data-theme=\"cyber\"] .q-badge.bg-primary {\n  background: primary !important;\n  color: #0a0a0a !important;\n}\n\n[data-theme=\"monochrome\"] .q-btn.bg-primary,\n[data-theme=\"cyber\"] .q-btn.bg-primary {\n  background: primary !important;\n  color: #0a0a0a !important;\n}\n\n[data-theme=\"freedom\"] .q-drawer--dark {\n  background: #0a0a0a !important;\n}\n\n[data-theme=\"freedom\"] .q-header {\n  background: #0a0a0a !important;\n}\n\n[v-cloak] {\n  display: none;\n}\n\nbody.body--dark .q-table--dark {\n  background: transparent;\n}\n\nbody.body--dark .q-field--error {\n  .text-negative,\n  .q-field__messages {\n    color: yellow !important;\n  }\n}\n\n.qcard {\n  width: 500px;\n}\n\n.q-table--dense {\n  th:first-child,\n  td:first-child,\n  .q-table__bottom {\n    padding-left: 6px !important;\n  }\n  th:last-child,\n  td:last-child,\n  .q-table__bottom {\n    padding-right: 6px !important;\n  }\n}\n\na {\n  color: var(--primary-color);\n  text-decoration: underline;\n  font-weight: bold;\n}\n\na.inherit {\n  color: inherit;\n  text-decoration: none;\n}\n\n// QR video\nvideo {\n  border-radius: 3px;\n}\n\n// Material icons font\n@font-face {\n  font-family: \"Material Icons\";\n  font-style: normal;\n  font-weight: 400;\n  src: url(../fonts/material-icons-v50.woff2) format(\"woff2\");\n}\n\n.material-icons {\n  font-family: \"Material Icons\";\n  font-weight: normal;\n  font-style: normal;\n  font-size: 24px;\n  line-height: 1;\n  letter-spacing: normal;\n  text-transform: none;\n  display: inline-block;\n  white-space: nowrap;\n  word-wrap: normal;\n  direction: ltr;\n  -moz-font-feature-settings: \"liga\";\n  font-feature-settings: \"liga\";\n  -moz-osx-font-smoothing: grayscale;\n}\n\n.q-rating__icon {\n  font-size: 1em;\n}\n\n// text-wrap\n.text-wrap {\n  word-break: break-word;\n}\n\n.q-card {\n  code {\n    overflow-wrap: break-word;\n  }\n}\n\n.q-card {\n  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14),\n    0 3px 1px -2px rgba(0, 0, 0, 0.12);\n  border-radius: 4px;\n  vertical-align: top;\n}\n\n.shadow-2 {\n  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2), 0 2px 2px rgba(0, 0, 0, 0.14),\n    0 3px 1px -2px rgba(0, 0, 0, 0.12);\n}\n\n::-webkit-scrollbar {\n  display: none;\n}\n"
  },
  {
    "path": "src/css/mintlist.css",
    "content": "/* Shared styles for mint list items across MintSettings, RestoreView, and NostrMintRestore */\n\n/* Common mint card styling */\n.mint-card {\n  border-radius: 10px;\n  border: 1px solid rgba(128, 128, 128, 0.2);\n  padding: 0px;\n  position: relative;\n  transition: border-color 0.2s ease;\n}\n\n.mint-card:hover {\n  border-color: rgba(128, 128, 128, 0.4) !important;\n}\n\n.mint-card.q-loading {\n  opacity: 0.5;\n  pointer-events: none;\n}\n\n/* Mint information container */\n.mint-info-container {\n  display: flex;\n  flex-direction: column;\n  min-width: 0;\n  /* This is crucial for text wrapping */\n  flex: 1;\n}\n\n/* Mint name styling with proper text wrapping */\n.mint-name {\n  text-align: left;\n  font-size: 16px;\n  font-weight: 600;\n  line-height: 20px;\n  word-wrap: break-word;\n  overflow-wrap: break-word;\n  word-break: break-word;\n  white-space: normal;\n  max-width: 100%;\n}\n\n/* Mint URL styling with proper text wrapping */\n.mint-url {\n  text-align: left;\n  font-size: 12px;\n  line-height: 16px;\n  font-family: monospace !important;\n  margin-top: 4px;\n  word-wrap: break-word;\n  overflow-wrap: break-word;\n  word-break: break-all;\n  /* For URLs, break-all is better */\n  white-space: normal;\n  max-width: 100%;\n}\n\n/* Currency unit badges */\n.currency-unit-badge {\n  border-radius: 4px;\n  background-color: #1d1d1d;\n  display: inline-block;\n  padding: 4px 8px;\n  margin: 4px 4px 4px 0;\n}\n\n.currency-unit-text {\n  color: white;\n  font-size: 14px;\n  font-weight: 500;\n}\n\n/* Loading spinner positioning */\n.mint-loading-spinner {\n  position: absolute;\n  top: 12px;\n  right: 30px;\n  z-index: 10;\n}\n\n/* Error badge positioning */\n.error-badge {\n  position: absolute;\n  top: 4px;\n  right: 30px;\n  z-index: 10;\n}\n\n/* Transition animations */\n.fade-enter-active,\n.fade-leave-active {\n  transition: transform 1s ease, opacity 1s ease;\n}\n\n/* Selection buttons styling */\n.selection-buttons {\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n}\n\n/* Primary action section */\n.primary-action-section {\n  display: flex;\n  align-items: center;\n  justify-content: flex-start;\n}\n\n/* Clickable checkbox */\n.clickable-checkbox {\n  cursor: pointer;\n}\n\n/* Mint item selected state */\n.mint-item-selected {\n  border-color: var(--q-primary) !important;\n  background-color: rgba(var(--q-primary-rgb), 0.1);\n}\n\n/* Ensure text alignment is consistent */\n.text-left {\n  text-align: left !important;\n}\n"
  },
  {
    "path": "src/css/quasar.variables.scss",
    "content": "// Quasar SCSS (& Sass) Variables\n// --------------------------------------------------\n// To customize the look and feel of this app, you can override\n// the Sass/SCSS variables found in Quasar's source Sass/SCSS files.\n\n// Check documentation for full list of Quasar variables\n\n// Your own variables (that are declared here) and Quasar's own\n// ones will be available out of the box in your .vue/.scss/.sass files\n\n// It's highly recommended to change the default colors\n// to match your app's branding.\n// Tip: Use the \"Theme Builder\" on Quasar's documentation website.\n\n$primary: #1976d2;\n$secondary: #26a69a;\n$accent: #9c27b0;\n\n$dark: #1d1d1d;\n$dark-page: #121212;\n\n$positive: #21ba45;\n$negative: #c10015;\n$info: #31ccec;\n$warning: #f2c037;\n"
  },
  {
    "path": "src/i18n/ar-SA/index.ts",
    "content": "export default {\n  MultinutPicker: {\n    payment: \"دفع متعدد الجوز\",\n    selectMints: \"حدد واحدًا أو أكثر من mints لتنفيذ الدفع منه.\",\n    totalSelectedBalance: \"إجمالي الرصيد المحدد\",\n    multiMintPay: \"دفع متعدد Mint\",\n    balanceNotEnough: \"رصيد متعدد mints غير كافٍ لتلبية هذه الفاتورة\",\n    failed: \"فشل في المعالجة: {error}\",\n    paid: \"تم دفع {amount} عبر Lightning\",\n  },\n\n  global: {\n    copy_to_clipboard: {\n      success: \"تم النسخ إلى الحافظة!\",\n    },\n    actions: {\n      add_mint: {\n        label: \"إضافة Mint\",\n      },\n      cancel: {\n        label: \"إلغاء\",\n      },\n      copy: {\n        label: \"نسخ\",\n      },\n      close: {\n        label: \"إغلاق\",\n      },\n      enter: {\n        label: \"إدخال\",\n      },\n      lock: {\n        label: \"قفل\",\n      },\n      paste: {\n        label: \"لصق\",\n      },\n      receive: {\n        label: \"استلام\",\n      },\n      scan: {\n        label: \"مسح ضوئي\",\n      },\n      send: {\n        label: \"إرسال\",\n      },\n      swap: {\n        label: \"تبديل\",\n      },\n      update: {\n        label: \"تحديث\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"عنوان URL للـ Mint\",\n      },\n    },\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"الرصيد منخفض جدًا\",\n      received: \"تم استلام {amount}\",\n      fee: \" (رسوم: {fee})\",\n      could_not_request_mint: \"لا يمكن طلب الإنشاء\",\n      invoice_still_pending: \"الفاتورة لا تزال معلقة\",\n      paid_lightning: \"تم دفع {amount} عبر شبكة لايتنينج\",\n      payment_pending_refresh: \"الدفع معلق. قم بتحديث الفاتورة يدويًا.\",\n      sent: \"تم إرسال {amount}\",\n      token_still_pending: \"الرمز لا يزال معلقًا\",\n      received_lightning: \"تم استلام {amount} عبر شبكة لايتنينج\",\n      lightning_payment_failed: \"فشل الدفع عبر لايتنينج\",\n      failed_to_decode_invoice: \"فشل فك ترميز الفاتورة\",\n      invalid_lnurl: \"LNURL غير صالح\",\n      lnurl_error: \"خطأ في LNURL\",\n      no_amount: \"لا يوجد مبلغ\",\n      no_lnurl_data: \"لا توجد بيانات LNURL\",\n      no_price_data: \"لا توجد بيانات سعرية.\",\n      please_try_again: \"يرجى المحاولة مرة أخرى.\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"تمت إضافة الـ Mint بالفعل\",\n        added: \"تمت إضافة الـ Mint\",\n        not_found: \"لم يتم العثور على الـ Mint\",\n        activation_failed: \"فشل تنشيط الـ Mint\",\n        no_active_mint: \"لا يوجد Mint نشط\",\n        unit_activation_failed: \"فشل تنشيط الوحدة\",\n        unit_not_supported: \"الوحدة غير مدعومة من قبل الـ Mint\",\n        activated: \"تم تنشيط الـ Mint\",\n        could_not_connect: \"تعذر الاتصال بالـ Mint\",\n        could_not_get_info: \"تعذر الحصول على معلومات الـ Mint\",\n        could_not_get_keys: \"تعذر الحصول على مفاتيح الـ Mint\",\n        could_not_get_keysets: \"تعذر الحصول على مجموعات مفاتيح الـ Mint\",\n        mint_validation_error: \"خطأ في التحقق من صحة mint\",\n        removed: \"تمت إزالة الـ Mint\",\n        error: \"خطأ في الـ Mint\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"الإعدادات\",\n        settings: {\n          title: \"الإعدادات\",\n          caption: \"تهيئة المحفظة\",\n        },\n      },\n      terms: {\n        title: \"الشروط\",\n        terms: {\n          title: \"الشروط\",\n          caption: \"شروط الخدمة\",\n        },\n      },\n      links: {\n        title: \"روابط\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"تبرع\",\n          caption: \"دعم Cashu\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"غير متصل\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"إعادة التحميل في { countdown }\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"المرحلة التجريبية – لا تستخدم مع أموال حقيقية!\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"المحفظة\",\n      },\n    },\n  },\n  Settings: {\n    language: {\n      title: \"اللغة\",\n      description: \"الرجاء اختيار لغتك المفضلة من القائمة أدناه.\",\n    },\n    sections: {\n      backup_restore: \"النسخ الاحتياطي والاستعادة\",\n      lightning_address: \"عنوان LIGHTNING\",\n      nostr_keys: \"مفاتيح NOSTR\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"انقر لتعديل المرحلات\",\n          add: {\n            title: \"إضافة مُرحِل\",\n            description:\n              \"تستخدم محفظتك هذه المُرحِلات لعمليات nostr مثل طلبات الدفع وربط محفظة nostr والنسخ الاحتياطية.\",\n          },\n          list: {\n            title: \"المُرحِلات\",\n            description: \"ستتصل محفظتك بهذه المُرحِلات.\",\n            copy_tooltip: \"نسخ المُرحِل\",\n            remove_tooltip: \"إزالة المُرحِل\",\n          },\n        },\n      },\n      payment_requests: \"طلبات الدفع\",\n      nostr_wallet_connect: \"اتصال محفظة NOSTR\",\n      hardware_features: \"ميزات الأجهزة\",\n      p2pk_features: \"ميزات P2PK\",\n      privacy: \"الخصوصية\",\n      experimental: \"تجريبي\",\n      appearance: \"المظهر\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"نسخ عبارة الاستعادة احتياطيًا\",\n        description:\n          \"يمكن لعبارة الاستعادة الخاصة بك استعادة محفظتك. احتفظ بها آمنة وخصوصية.\",\n        seed_phrase_label: \"عبارة الاستعادة\",\n      },\n      restore_ecash: {\n        title: \"استعادة ecash\",\n        description:\n          \"يتيح لك معالج الاستعادة استرداد ecash المفقود من عبارة استعادة. لن تتأثر عبارة استعادة محفظتك الحالية، وسيسمح لك المعالج فقط باستعادة ecash من عبارة استعادة أخرى.\",\n        button: \"استعادة\",\n      },\n    },\n    lightning_address: {\n      title: \"عنوان Lightning\",\n      description: \"استلام المدفوعات إلى عنوان Lightning الخاص بك.\",\n      enable: {\n        toggle: \"تمكين\",\n        description: \"عنوان Lightning مع npub.cash\",\n      },\n      address: {\n        copy_tooltip: \"نسخ عنوان Lightning\",\n      },\n      automatic_claim: {\n        toggle: \"المطالبة تلقائيًا\",\n        description: \"استلام المدفوعات الواردة تلقائيًا.\",\n      },\n      npc_v2: {\n        choose_mint_title: \"اختر mint لـ npub.cash v2\",\n        choose_mint_placeholder: \"حدد mint...\",\n      },\n    },\n    nostr_keys: {\n      title: \"مفاتيح nostr الخاصة بك\",\n      description: \"قم بتعيين مفاتيح nostr لعنوان Lightning الخاص بك.\",\n      wallet_seed: {\n        title: \"عبارة استعادة المحفظة\",\n        description: \"إنشاء زوج مفاتيح nostr من عبارة استعادة المحفظة\",\n        copy_nsec: \"نسخ nsec\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"استخدام مخبأ NIP-46\",\n        delete_tooltip: \"حذف الاتصال\",\n      },\n      use_nsec: {\n        title: \"استخدام nsec الخاص بك\",\n        description: \"هذه الطريقة خطيرة ولا ينصح بها\",\n        delete_tooltip: \"حذف nsec\",\n      },\n      signing_extension: {\n        title: \"ملحق التوقيع\",\n        description: \"استخدام ملحق التوقيع NIP-07\",\n        not_found: \"لم يتم العثور على ملحق التوقيع NIP-07\",\n      },\n    },\n    payment_requests: {\n      title: \"طلبات الدفع\",\n      description:\n        \"تسمح لك طلبات الدفع بتلقي المدفوعات عبر nostr. إذا قمت بتمكين هذا، ستقوم محفظتك بالاشتراك في مرحلات nostr الخاصة بك.\",\n      enable_toggle: \"تمكين طلبات الدفع\",\n      claim_automatically: {\n        toggle: \"المطالبة تلقائيًا\",\n        description: \"استلام المدفوعات الواردة تلقائيًا.\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"اتصال محفظة Nostr (NWC)\",\n      description: \"استخدم NWC للتحكم في محفظتك من أي تطبيق آخر.\",\n      enable_toggle: \"تمكين NWC\",\n      payments_note:\n        \"يمكنك فقط استخدام NWC للمدفوعات من رصيد Bitcoin الخاص بك. ستتم المدفوعات من mint النشط الخاص بك.\",\n      connection: {\n        copy_tooltip: \"نسخ سلسلة الاتصال\",\n        qr_tooltip: \"إظهار رمز QR\",\n        allowance_label: \"المسموح به المتبقي (سات)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"اختر الترميز للكتابة على بطاقات NFC\",\n        text: {\n          title: \"نص\",\n          description: \"تخزين الرمز كنص عادي\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"تخزين URL لهذه المحفظة مع الرمز\",\n        },\n        binary: {\n          title: \"ثنائي\",\n          description: \"تخزين الرموز كبيانات ثنائية\",\n        },\n        quick_access: {\n          toggle: \"وصول سريع إلى NFC\",\n          description:\n            \"مسح بطاقات NFC بسرعة في قائمة استلام Ecash. يضيف هذا الخيار زر NFC إلى قائمة استلام Ecash.\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"إنشاء زوج مفاتيح لاستلام ecash مقفلة بـ P2PK. تحذير: هذه الميزة تجريبية. استخدمها بكميات صغيرة فقط. إذا فقدت مفاتيحك الخاصة، فلن يتمكن أحد من فتح ecash المقفلة بها بعد الآن.\",\n      generate_button: \"إنشاء مفتاح\",\n      import_button: \"استيراد nsec\",\n      quick_access: {\n        toggle: \"وصول سريع للقفل\",\n        description:\n          \"استخدم هذا لعرض مفتاح قفل P2PK الخاص بك بسرعة في قائمة استلام ecash.\",\n      },\n      keys_expansion: {\n        label: \"انقر لاستعراض {count} مفتاح\",\n        used_badge: \"مستخدم\",\n      },\n    },\n    privacy: {\n      title: \"الخصوصية\",\n      description: \"تؤثر هذه الإعدادات على خصوصيتك.\",\n      check_incoming: {\n        toggle: \"التحقق من الفاتورة الواردة\",\n        description:\n          \"إذا تم تمكين هذا، ستقوم المحفظة بفحص أحدث فاتورة في الخلفية. يزيد هذا من استجابة المحفظة مما يسهل عملية البصمة. يمكنك التحقق من الفواتير غير المدفوعة يدويًا في علامة التبويب الفواتير.\",\n      },\n      check_startup: {\n        toggle: \"التحقق من الفواتير المعلقة عند بدء التشغيل\",\n        description:\n          \"إذا تم تمكين هذا، ستقوم المحفظة بفحص الفواتير المعلقة من آخر 24 ساعة عند بدء التشغيل.\",\n      },\n      check_all: {\n        toggle: \"التحقق من جميع الفواتير\",\n        description:\n          \"إذا تم تمكين هذا، ستقوم المحفظة بالتحقق بشكل دوري من الفواتير غير المدفوعة في الخلفية لمدة تصل إلى أسبوعين. يزيد هذا من نشاط المحفظة عبر الإنترنت مما يسهل عملية البصمة. يمكنك التحقق من الفواتير غير المدفوعة يدويًا في علامة التبويب الفواتير.\",\n      },\n      check_sent: {\n        toggle: \"التحقق من ecash المرسلة\",\n        description:\n          \"إذا تم تمكين هذا، ستقوم المحفظة باستخدام فحوصات خلفية دورية لتحديد ما إذا تم استرداد الرموز المرسلة. يزيد هذا من نشاط المحفظة عبر الإنترنت مما يسهل عملية البصمة.\",\n      },\n      websockets: {\n        toggle: \"استخدام WebSockets\",\n        description:\n          \"إذا تم تمكين هذا، ستقوم المحفظة باستخدام اتصالات WebSocket طويلة الأمد لاستلام التحديثات حول الفواتير المدفوعة والرموز المستخدمة من mints. يزيد هذا من استجابة المحفظة ولكنه يسهل أيضًا عملية البصمة.\",\n      },\n      bitcoin_price: {\n        toggle: \"الحصول على سعر الصرف من Coinbase\",\n        description:\n          \"إذا تم تمكين هذا، سيتم جلب سعر صرف Bitcoin الحالي من coinbase.com وسيتم عرض رصيدك المحول.\",\n        currency: {\n          title: \"العملة الورقية\",\n          description: \"اختر العملة الورقية لعرض سعر البيتكوين.\",\n        },\n      },\n    },\n    experimental: {\n      title: \"تجريبي\",\n      description: \"هذه الميزات تجريبية.\",\n      receive_swaps: {\n        toggle: \"استلام عمليات التبديل\",\n        badge: \"بيتا\",\n        description:\n          \"خيار تبديل Ecash المستلم إلى mint النشط الخاص بك في مربع حوار استلام Ecash.\",\n      },\n      auto_paste: {\n        toggle: \"لصق Ecash تلقائيًا\",\n        description:\n          \"لصق ecash تلقائيًا في الحافظة الخاصة بك عند الضغط على استلام، ثم Ecash، ثم لصق. قد يتسبب اللصق التلقائي في مشاكل في واجهة المستخدم في iOS، قم بإيقاف تشغيله إذا واجهت مشاكل.\",\n      },\n      auditor: {\n        toggle: \"تمكين المدقق\",\n        badge: \"بيتا\",\n        description:\n          \"إذا تم تمكين هذا، ستعرض المحفظة معلومات المدقق في مربع حوار تفاصيل mint. المدقق هو خدمة طرف ثالث تراقب موثوقية mints.\",\n        url_label: \"عنوان URL للمدقق\",\n        api_url_label: \"عنوان URL لواجهة برمجة تطبيقات المدقق\",\n      },\n      multinut: {\n        toggle: \"تمكين Multinut\",\n        description:\n          \"إذا تم تمكينه، ستستخدم المحفظة Multinut لدفع الفواتير من عدة mints في وقت واحد.\",\n      },\n      nostr_mint_backup: {\n        toggle: \"النسخ الاحتياطي لقائمة mint على Nostr\",\n        description:\n          \"إذا تم تمكينه، سيتم نسخ قائمة mint الخاصة بك احتياطيًا تلقائيًا إلى مرحلات Nostr باستخدام مفاتيح Nostr التي تم تكوينها. يتيح لك هذا استعادة قائمة mint الخاصة بك عبر الأجهزة.\",\n        notifications: {\n          enabled: \"تمكين النسخ الاحتياطي لـ Nostr mint\",\n          disabled: \"تعطيل النسخ الاحتياطي لـ Nostr mint\",\n          failed: \"فشل تمكين النسخ الاحتياطي لـ Nostr mint\",\n        },\n      },\n    },\n    appearance: {\n      keyboard: {\n        title: \"لوحة مفاتيح على الشاشة\",\n        description: \"استخدم لوحة المفاتيح الرقمية لإدخال المبالغ.\",\n        toggle: \"استخدام لوحة المفاتيح الرقمية\",\n        toggle_description:\n          \"إذا تم تمكين هذا، سيتم استخدام لوحة المفاتيح الرقمية لإدخال المبالغ.\",\n      },\n      theme: {\n        title: \"المظهر\",\n        description: \"تغيير مظهر محفظتك.\",\n        tooltips: {\n          mono: \"أحادي\",\n          cyber: \"سايبر\",\n          freedom: \"حرية\",\n          nostr: \"نوستر\",\n          bitcoin: \"بيتكوين\",\n          mint: \"مينت\",\n          nut: \"جوز\",\n          blu: \"أزرق\",\n          flamingo: \"فلامينجو\",\n        },\n      },\n      bip177: {\n        title: \"رمز البيتكوين\",\n        description: \"استخدم رمز ₿ بدلاً من sats.\",\n        toggle: \"استخدام رمز ₿\",\n      },\n    },\n    web_of_trust: {\n      title: \"شبكة الثقة\",\n      known_pubkeys: \"المفاتيح العامة المعروفة: {wotCount}\",\n      continue_crawl: \"متابعة الزحف\",\n      crawl_odell: \"الزحف إلى شبكة ثقة ODELL\",\n      crawl_wot: \"الزحف إلى شبكة الثقة\",\n      pause: \"إيقاف مؤقت\",\n      reset: \"إعادة تعيين\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"استخدام npubx.cash\",\n      copy_lightning_address: \"نسخ عنوان Lightning\",\n      v2_mint: \"npub.cash v2 mint\",\n    },\n    multinut: {\n      use_multinut: \"استخدام Multinut\",\n    },\n    advanced: {\n      title: \"متقدم\",\n      developer: {\n        title: \"إعدادات المطور\",\n        description: \"الإعدادات التالية هي للتطوير والتصحيح.\",\n        new_seed: {\n          button: \"إنشاء عبارة استعادة جديدة\",\n          description:\n            \"سيؤدي هذا إلى إنشاء عبارة استعادة جديدة. يجب عليك إرسال رصيدك بالكامل إلى نفسك لتتمكن من استعادته باستخدام عبارة استعادة جديدة.\",\n          confirm_question: \"هل أنت متأكد أنك تريد إنشاء عبارة استعادة جديدة؟\",\n          cancel: \"إلغاء\",\n          confirm: \"تأكيد\",\n        },\n        remove_spent: {\n          button: \"إزالة البراهين المستخدمة\",\n          description:\n            \"تحقق مما إذا كانت رموز ecash من mints النشطة الخاصة بك قد تم استخدامها وإزالة المستخدمة من محفظتك. استخدم هذا فقط إذا كانت محفظتك عالقة.\",\n        },\n        debug_console: {\n          button: \"تبديل وحدة تحكم التصحيح\",\n          description:\n            \"افتح طرفية تصحيح Javascript. لا تلصق أبدًا أي شيء في هذه الطرفية لا تفهمه. قد يحاول لص خداعك للصق رمز ضار هنا.\",\n        },\n        export_proofs: {\n          button: \"تصدير البراهين النشطة\",\n          description:\n            \"نسخ رصيدك بالكامل من mint النشط كرمز Cashu إلى الحافظة الخاصة بك. سيؤدي هذا فقط إلى تصدير الرموز من mint والوحدة المحددة. لتصدير كامل، حدد mint ووحدة مختلفين وقم بالتصدير مرة أخرى.\",\n        },\n        keyset_counters: {\n          title: \"زيادة عدادات مجموعة المفاتيح\",\n          description:\n            'انقر على معرّف مجموعة المفاتيح لزيادة عدادات مسار الاشتقاق لمجموعات المفاتيح في محفظتك. هذا مفيد إذا رأيت خطأ \"النواتج تم توقيعها بالفعل\".',\n          counter: \"العداد: {count}\",\n        },\n        unset_reserved: {\n          button: \"إلغاء تعيين جميع الرموز المحجوزة\",\n          description:\n            'تضع هذه المحفظة علامة على ecash الصادرة المعلقة كمحجوزة (وتطرحها من رصيدك) لمنع محاولات الإنفاق المزدوج. هذا الزر سيلغي تعيين جميع الرموز المحجوزة بحيث يمكن استخدامها مرة أخرى. إذا قمت بذلك، قد تتضمن محفظتك براهين مستخدمة. اضغط على زر \"إزالة البراهين المستخدمة\" للتخلص منها.',\n        },\n        show_onboarding: {\n          button: \"إظهار شاشة الترحيب\",\n          description: \"إظهار شاشة الترحيب مرة أخرى.\",\n        },\n        reset_wallet: {\n          button: \"إعادة تعيين بيانات المحفظة\",\n          description:\n            \"إعادة تعيين بيانات محفظتك. تحذير: سيؤدي هذا إلى حذف كل شيء! تأكد من إنشاء نسخة احتياطية أولاً.\",\n          confirm_question: \"هل أنت متأكد أنك تريد حذف بيانات محفظتك؟\",\n          cancel: \"إلغاء\",\n          confirm: \"حذف المحفظة\",\n        },\n        export_wallet: {\n          button: \"تصدير بيانات المحفظة\",\n          description:\n            \"تنزيل نسخة من محفظتك. يمكنك استعادة محفظتك من هذا الملف في شاشة الترحيب لمحفظة جديدة. سيكون هذا الملف غير متزامن إذا واصلت استخدام محفظتك بعد تصديره.\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"انضم إلى mint\",\n    subtitle:\n      \"لم تنضم إلى أي Cashu mint بعد. أضف عنوان URL لـ mint في الإعدادات أو استلم ecash من mint جديد للبدء.\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"استلام Ecash\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"السجل\",\n      },\n      invoices: {\n        label: \"الفواتير\",\n      },\n      mints: {\n        label: \"Mints\",\n      },\n    },\n    install: {\n      text: \"تثبيت\",\n      tooltip: \"تثبيت Cashu\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"لا.\",\n    text: \"علامة تبويب أخرى قيد التشغيل بالفعل. أغلق هذه العلامة وحاول مرة أخرى.\",\n    actions: {\n      retry: {\n        label: \"إعادة المحاولة\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"عذرًا، لا يوجد شيء هنا…\",\n    actions: {\n      home: {\n        label: \"العودة إلى الصفحة الرئيسية\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Mint\",\n    },\n    mintBalance: {\n      label: \"الرصيد\",\n    },\n    mintError: {\n      label: \"خطأ في Mint\",\n    },\n    pending: {\n      label: \"معلق\",\n      tooltip: \"التحقق من جميع الرموز المعلقة\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"السابق\",\n      },\n      next: {\n        label: \"التالي\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"أهلاً بك في Cashu\",\n    text: \"Cashu.me هي محفظة Bitcoin مجانية ومفتوحة المصدر تستخدم ecash للحفاظ على أموالك آمنة وخصوصية.\",\n    actions: {\n      more: {\n        label: \"انقر لمعرفة المزيد\",\n      },\n    },\n    p1: {\n      text: \"Cashu هو بروتوكول ecash مجاني ومفتوح المصدر لـ Bitcoin. يمكنك معرفة المزيد عنه على { link }.\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"هذه المحفظة غير تابعة لأي mint. لاستخدام هذه المحفظة، تحتاج إلى الاتصال بواحد أو أكثر من Cashu mints التي تثق بها.\",\n    },\n    p3: {\n      text: \"تخزن هذه المحفظة ecash لا يمكنك الوصول إليها إلا أنت. إذا قمت بحذف بيانات المتصفح الخاص بك دون نسخ احتياطي لعبارة الاستعادة، فستفقد رموزك.\",\n    },\n    p4: {\n      text: \"هذه المحفظة في مرحلة بيتا. لا نتحمل أي مسؤولية عن فقدان الأشخاص الوصول إلى أموالهم. استخدم على مسؤوليتك الخاصة! هذا الكود مفتوح المصدر ومرخص تحت رخصة MIT.\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"تثبيت PWA\",\n    alt: { pwa_example: \"مثال تثبيت PWA\" },\n    installing: \"جارٍ التثبيت…\",\n    instruction: {\n      intro: {\n        text: \"للحصول على أفضل تجربة، استخدم هذه المحفظة مع متصفح الويب الأصلي لجهازك لتثبيتها كتطبيق ويب تقدمي. افعل هذا الآن.\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"انقر على القائمة (أعلى اليمين)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"اضغط على { buttonText }\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"انقر على مشاركة (أسفل)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"اضغط على { buttonText }\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"بعد تثبيت هذا التطبيق على جهازك، أغلق نافذة المتصفح هذه واستخدم التطبيق من شاشتك الرئيسية.\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"نجاح!\",\n        text: \"أنت تستخدم Cashu كتطبيق PWA. أغلق أي نوافذ متصفح أخرى مفتوحة واستخدم التطبيق من شاشتك الرئيسية.\",\n        nextSteps:\n          \"يمكنك الآن إغلاق هذا اللسان وفتح التطبيق من الشاشة الرئيسية.\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"انقر على { icon } و { buttonText }\",\n    buttonText: \"إضافة إلى الشاشة الرئيسية\",\n  },\n  AndroidPWAPrompt: {\n    text: \"انقر على { icon } و { buttonText }\",\n    buttonText: \"إضافة إلى الشاشة الرئيسية\",\n  },\n  WelcomeSlide3: {\n    title: \"عبارة الاستعادة الخاصة بك\",\n    text: \"احفظ عبارة الاستعادة الخاصة بك في مدير كلمات مرور أو على الورق. عبارة الاستعادة الخاصة بك هي الطريقة الوحيدة لاستعادة أموالك إذا فقدت الوصول إلى هذا الجهاز.\",\n    inputs: {\n      seed_phrase: {\n        label: \"عبارة الاستعادة\",\n        caption: \"يمكنك رؤية عبارة الاستعادة الخاصة بك في الإعدادات.\",\n      },\n      checkbox: {\n        label: \"لقد كتبتها\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"الشروط\",\n    actions: {\n      more: {\n        label: \"قراءة شروط الخدمة\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"لقد قرأت وأقبل هذه الشروط والأحكام\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"إعداد محفظتك\",\n    text: \"هل تريد الاستعادة من عبارة الاستعادة أم إنشاء محفظة جديدة؟\",\n    options: {\n      new: {\n        title: \"إنشاء محفظة جديدة\",\n        subtitle: \"توليد عبارة استعادة جديدة وإضافة mints.\",\n      },\n      recover: {\n        title: \"استعادة المحفظة\",\n        subtitle: \"أدخل عبارة الاستعادة، واستعد المِنْت و ecash.\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"إضافة mints\",\n    text: \"المِنْت خوادم تساعدك على إرسال واستلام ecash. اختر mint مكتشفًا أو أضِف واحدًا يدويًا. يمكنك التخطي والإضافة لاحقًا.\",\n    sections: { your_mints: \"المِنْت الخاصة بك\" },\n    restoring: \"جارٍ استعادة المِنْت…\",\n    placeholder: { mint_url: \"https://\" },\n  },\n  WelcomeRecoverSeed: {\n    title: \"أدخل عبارة الاستعادة\",\n    text: \"الصق أو اكتب عبارة من 12 كلمة للاستعادة.\",\n    inputs: { word: \"الكلمة { index }\" },\n    actions: { paste_all: \"لصق الكل\" },\n    disclaimer: \"تُستخدم عبارة الاستعادة محليًا فقط لاشتقاق مفاتيح محفظتك.\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"استعد ecash الخاصة بك\",\n    text: \"ابحث عن البراهين غير المصروفة على المِنْت المُكوَّنة لديك وأضِفها إلى محفظتك.\",\n  },\n  MintRatings: {\n    title: \"مراجعات المِنْت\",\n    reviews: \"مراجعات\",\n    ratings: \"التقييمات\",\n    no_reviews: \"لا توجد مراجعات\",\n    your_review: \"مراجعتك\",\n    no_reviews_to_display: \"لا توجد مراجعات للعرض.\",\n    no_rating: \"لا يوجد تقييم\",\n    out_of: \"من\",\n    rows: \"Reviews\",\n    sort: \"ترتيب\",\n    sort_options: {\n      newest: \"الأحدث\",\n      oldest: \"الأقدم\",\n      highest: \"الأعلى\",\n      lowest: \"الأقل\",\n    },\n    actions: { write_review: \"اكتب مراجعة\" },\n    empty_state_subtitle:\n      \"ساعد من خلال ترك مراجعة. شارك تجربتك مع هذا المِنْت وساعد الآخرين من خلال ترك مراجعة.\",\n  },\n  CreateMintReview: {\n    title: \"مراجعة المِنْت\",\n    publishing_as: \"النشر باسم\",\n    inputs: {\n      rating: { label: \"التقييم\" },\n      review: { label: \"مراجعة (اختياري)\" },\n    },\n    actions: {\n      publish: { label: \"نشر\", in_progress: \"جارٍ النشر…\" },\n    },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"استعادة من عبارة الاستعادة\",\n      caption:\n        \"أدخل عبارة الاستعادة الخاصة بك لاستعادة محفظتك. قبل الاستعادة، تأكد من أنك أضفت جميع mints التي استخدمتها من قبل.\",\n      inputs: {\n        seed_phrase: {\n          label: \"عبارة الاستعادة\",\n          caption: \"يمكنك رؤية عبارة الاستعادة الخاصة بك في الإعدادات.\",\n        },\n      },\n    },\n    information: {\n      label: \"معلومات\",\n      caption:\n        \"سيقوم المعالج فقط باستعادة ecash من عبارة استعادة أخرى، ولن تتمكن من استخدام عبارة الاستعادة هذه أو تغيير عبارة استعادة المحفظة التي تستخدمها حاليًا. هذا يعني أن ecash المستعادة لن تكون محمية بواسطة عبارة الاستعادة الحالية طالما لم ترسل ecash إلى نفسك مرة واحدة.\",\n    },\n    restore_mints: {\n      label: \"استعادة Mints\",\n      caption:\n        'حدد mint للاستعادة. يمكنك إضافة المزيد من mints في الشاشة الرئيسية تحت \"Mints\" واستعادتها هنا.',\n    },\n    actions: {\n      paste: {\n        error: \"فشل قراءة محتويات الحافظة.\",\n      },\n      validate: {\n        error: \"يجب أن تكون الكلمة التذكيرية 12 كلمة على الأقل.\",\n      },\n      select_all: {\n        label: \"تحديد الكل\",\n      },\n      deselect_all: {\n        label: \"إلغاء تحديد الكل\",\n      },\n      restore: {\n        label: \"استعادة\",\n        in_progress: \"استعادة mint…\",\n        error: \"خطأ في استعادة mint: { error }\",\n      },\n      restore_all_mints: {\n        label: \"استعادة جميع Mints\",\n        in_progress: \"استعادة mint { index } من { length } …\",\n        success: \"تمت الاستعادة بنجاح\",\n        error: \"خطأ في استعادة mints: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"استعادة المِنْتات المحددة ({count})\",\n        in_progress: \"استعادة mint { index } من { length } …\",\n        success: \"تمت استعادة {count} mint(s) بنجاح\",\n        error: \"خطأ في استعادة mints المحددة: { error }\",\n      },\n    },\n    nostr_mints: {\n      label: \"استعادة Mints من Nostr\",\n      caption:\n        \"ابحث عن نسخ احتياطية لـ mint مخزنة على مرحلات Nostr باستخدام عبارة الاستعادة الخاصة بك. سيساعدك هذا على اكتشاف mints التي استخدمتها سابقًا.\",\n      search_button: \"البحث عن نسخ احتياطية لـ Mint\",\n      select_all: \"تحديد الكل\",\n      deselect_all: \"إلغاء تحديد الكل\",\n      backed_up: \"تم النسخ الاحتياطي\",\n      already_added: \"تمت الإضافة بالفعل\",\n      add_selected: \"إضافة المحدد ({count})\",\n      no_backups_found: \"لم يتم العثور على نسخ احتياطية لـ mint\",\n      no_backups_hint:\n        \"تأكد من تمكين النسخ الاحتياطي لـ Nostr mint في الإعدادات لنسخ قائمة mint الخاصة بك احتياطيًا تلقائيًا.\",\n      invalid_mnemonic: \"الرجاء إدخال عبارة استعادة صالحة قبل البحث.\",\n      search_error: \"فشل البحث عن نسخ احتياطية لـ mint.\",\n      add_error: \"فشل إضافة mints المحددة.\",\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"إضافة mint\",\n      description:\n        \"أدخل عنوان URL لـ Cashu mint للاتصال به. هذه المحفظة غير تابعة لأي mint.\",\n      inputs: {\n        nickname: {\n          placeholder: \"الاسم المستعار (مثلاً Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"عنوان URL غير صالح\",\n        },\n        scan: {\n          label: \"مسح رمز QR\",\n        },\n      },\n    },\n    discover: {\n      title: \"اكتشف mints\",\n      overline: \"اكتشف\",\n      caption: \"اكتشف mints التي أوصى بها المستخدمون الآخرون على nostr.\",\n      actions: {\n        discover: {\n          label: \"اكتشف mints\",\n          in_progress: \"جارٍ التحميل…\",\n          error_no_mints: \"لم يتم العثور على mints\",\n          success: \"تم العثور على { length } mints\",\n        },\n      },\n      recommendations: {\n        overline: \"تم العثور على { length } mints\",\n        caption:\n          \"تمت التوصية بهذه mints من قبل مستخدمي Nostr الآخرين. كن حذرًا وقم ببحثك الخاص قبل استخدام أي mint.\",\n        actions: {\n          browse: {\n            label: \"انقر لاستعراض mints\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"تبديل\",\n      overline: \"تبديل بين عدة Mints\",\n      caption:\n        \"تبديل الأموال بين mints عبر Lightning. ملاحظة: اترك مساحة لرسوم Lightning المحتملة. إذا لم تنجح الدفعة الواردة، فتحقق من الفاتورة يدويًا.\",\n      inputs: {\n        from: {\n          label: \"من\",\n        },\n        to: {\n          label: \"إلى\",\n        },\n        amount: {\n          label: \"المبلغ ({ ticker })\",\n        },\n      },\n      actions: {\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n    },\n    error_badge: \"خطأ\",\n    reviews_text: \"مراجعات\",\n    no_reviews_yet: \"لا توجد مراجعات بعد\",\n    discover_mints_button: \"اكتشف mints\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - استمر في المسح الضوئي\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"استلام Lightning\",\n    create_invoice_title: \"إنشاء فاتورة\",\n    inputs: {\n      amount: {\n        label: \"المبلغ ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"إنشاء فاتورة\",\n        label_blocked: \"جارٍ إنشاء الفاتورة…\",\n        in_progress: \"إنشاء\",\n      },\n    },\n    invoice: {\n      caption: \"فاتورة Lightning\",\n      status_paid_text: \"تم الدفع!\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"إرسال\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"لا يوجد mints متوفرة\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"لا يوجد mints متوفرة\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"إرسال Ecash\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"غير متصل\",\n    inputs: {\n      amount: {\n        label: \"المبلغ ({ ticker }) *\",\n        invalid_too_much_error_text: \"أكثر من اللازم\",\n      },\n      p2pk_pubkey: {\n        label: \"المفتاح العام للمستلم\",\n        label_invalid: \"المفتاح العام للمستلم\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"نسخ الرمز التعبيري\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"نسخ الرابط\",\n      },\n      share: {\n        tooltip_text: \"مشاركة ecash\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"حذف من السجل\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"فلاش إلى بطاقة NFC\",\n          ndef_unsupported_text: \"NDEF غير مدعوم\",\n        },\n      },\n    },\n  },\n  ReceiveDialog: {\n    title: \"استلام\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"لا يوجد mints متوفرة\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"تحتاج إلى الاتصال بـ mint لاستلام عبر Lightning\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"استلام Ecash\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"طلب\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"مسح ضوئي…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"استلام Ecash\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"لصق رمز Cashu\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"رمز غير صالح\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"غير قادر على الاستلام. قفل P2PK لهذا الرمز لا يطابق المفتاح العام الخاص بك.\",\n      },\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"إضافة mint…\",\n      },\n      swap: {\n        label: \"@:global.actions.swap.label\",\n        tooltip_text: \"التبديل إلى mint موثوق به\",\n        caption: \"تبديل { value }\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"إلغاء التبديل\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      later: {\n        label: \"استلام لاحقًا\",\n        tooltip_text: \"أضف إلى السجل للاستلام لاحقًا\",\n        already_in_history_success_text: \"Ecash موجود بالفعل في السجل\",\n        added_to_history_success_text: \"تمت إضافة Ecash إلى السجل\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"القراءة من بطاقة NFC\",\n          ndef_unsupported_text: \"NDEF غير مدعوم\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"مفتاح P2PK\",\n      description: \"استلام ecash مقفلة بهذا المفتاح\",\n      used_warning_text:\n        \"تحذير: تم استخدام هذا المفتاح من قبل. استخدم مفتاحًا جديدًا لخصوصية أفضل.\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"إنشاء مفتاح جديد\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"طلب دفع\",\n      description: \"استلام المدفوعات عبر Nostr\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"طلب جديد\",\n      },\n      add_amount: {\n        label: \"إضافة مبلغ\",\n      },\n      use_active_mint: {\n        label: \"أي mint\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"أدخل المبلغ\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"تم تعطيل لوحة المفاتيح. يمكنك إعادة تمكين لوحة المفاتيح في الإعدادات.\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"اتصال محفظة Nostr\",\n      description:\n        \"تحكم في محفظتك عن بعد باستخدام NWC. اضغط على رمز الاستجابة السريعة لربط محفظتك بتطبيق متوافق.\",\n      warning_text:\n        \"تحذير: أي شخص لديه حق الوصول إلى سلسلة الاتصال هذه يمكنه بدء مدفوعات من محفظتك. لا تشاركها!\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"رسالة Mint\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"جهة الاتصال\",\n    },\n    details: {\n      title: \"تفاصيل Mint\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"عرض الكل\",\n          },\n          hide: {\n            label: \"إخفاء\",\n          },\n        },\n      },\n      currency: {\n        label: \"العملة\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"الإصدار\",\n      },\n    },\n    actions: {\n      title: \"الإجراءات\",\n      copy_mint_url: {\n        label: \"نسخ عنوان URL للـ mint\",\n      },\n      delete: {\n        label: \"حذف mint\",\n      },\n      edit: {\n        label: \"تعديل mint\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"حدد mint\",\n    badge_mint_error_text: \"خطأ\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"لا يوجد سجل حتى الآن\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"منذ { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"التحقق من الحالة\",\n      },\n      receive: {\n        tooltip_text: \"استلام\",\n      },\n      filter_pending: {\n        label: \"تصفية المعلقة\",\n      },\n      show_all: {\n        label: \"عرض الكل\",\n      },\n    },\n    old_token_not_found_error_text: \"لم يتم العثور على الرمز القديم\",\n  },\n  InvoiceTable: {\n    empty_text: \"لا يوجد فواتير حتى الآن\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"انقر للنسخ\",\n      date_label: \"منذ { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"التحقق من الحالة\",\n      },\n      filter_pending: {\n        label: \"تصفية المعلقة\",\n      },\n      show_all: {\n        label: \"عرض الكل\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"هل أنت متأكد أنك تريد حذف هذا الـ mint؟\",\n    nickname: {\n      label: \"الاسم المستعار\",\n    },\n    balances: {\n      label: \"الأرصدة\",\n    },\n    warning_text:\n      \"ملاحظة: نظرًا لأن هذه المحفظة حذرة، فلن يتم حذف ecash الخاص بك من هذا الـ mint فعليًا ولكنه سيظل مخزنًا على جهازك. ستراه يظهر مرة أخرى إذا قمت بإعادة إضافة هذا الـ mint لاحقًا.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"إزالة mint\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"رمز Cashu أو عنوان Lightning\",\n      receive: \"رمز Cashu\",\n      pay: \"عنوان Lightning أو فاتورة\",\n    },\n    qr_scanner: {\n      title: \"مسح رمز QR\",\n      description: \"اضغط للمسح عنوان\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"دفع Lightning\",\n      inputs: {\n        invoice_data: {\n          label: \"فاتورة Lightning أو عنوان\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"{ payee } يطلب { value } { ticker }\",\n      amount_range_label: \"{ payee } يطلب{br}بين { min } و { max } { ticker }\",\n      sending_to_lightning_address: \"إرسال إلى { address }\",\n      inputs: {\n        amount: {\n          label: \"المبلغ ({ ticker }) *\",\n        },\n        comment: {\n          label: \"تعليق (اختياري)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"دفع { value }\",\n      paying: \"جاري الدفع\",\n      paid: \"تم الدفع\",\n      fee: \"الرسوم\",\n      memo: {\n        label: \"مذكرة\",\n      },\n      processing_info_text: \"جاري المعالجة…\",\n      balance_too_low_warning_text: \"الرصيد منخفض جدًا\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"دفع\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"خطأ\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"تعديل mint\",\n    inputs: {\n      nickname: {\n        label: \"الاسم المستعار\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"هل تثق في هذا الـ mint؟\",\n    description:\n      \"قبل استخدام هذا الـ mint، تأكد من أنك تثق به. قد يصبح الـ mints ضارًا أو يتوقف عن العمل في أي وقت.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"إضافة mint\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"الرجاء إدخال كلمة تذكيرية\",\n    restore_mint_error_text: \"خطأ في استعادة mint: { error }\",\n    prepare_info_text: \"تحضير عملية الاستعادة…\",\n    restored_proofs_for_keyset_info_text:\n      \"تم استعادة { restoreCounter } برهان لمجموعة المفاتيح { keysetId }\",\n    checking_proofs_for_keyset_info_text:\n      \"التحقق من البراهين من { startIndex } إلى { endIndex } لمجموعة المفاتيح { keysetId }\",\n    no_proofs_info_text: \"لم يتم العثور على براهين للاستعادة\",\n    restored_amount_success_text: \"تم استعادة { amount }\",\n  },\n  swap: {\n    in_progress_warning_text: \"التبديل قيد التقدم\",\n    invalid_swap_data_error_text: \"بيانات تبديل غير صالحة\",\n    swap_error_text: \"خطأ في التبديل\",\n  },\n  TokenInformation: {\n    fee: \"الرسوم\",\n    unit: \"الوحدة\",\n    fiat: \"العملة الورقية\",\n    p2pk: \"P2PK\",\n    locked: \"مقفل\",\n    locked_to_you: \"مقفل لك\",\n    mint: \"دار السك\",\n    memo: \"مذكرة\",\n    payment_request: \"طلب دفع\",\n    nostr: \"Nostr\",\n    token_copied: \"تم نسخ الرمز المميز إلى الحافظة\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/cs-CZ/index.ts",
    "content": "export default {\n  global: {\n    copy_to_clipboard: {\n      success: \"Zkopírováno do schránky!\",\n    },\n    actions: {\n      add_mint: {\n        label: \"Přidat mint\",\n      },\n      cancel: {\n        label: \"Zrušit\",\n      },\n      copy: {\n        label: \"Kopírovat\",\n      },\n      close: {\n        label: \"Zavřít\",\n      },\n      enter: {\n        label: \"Potvrdit\",\n      },\n      lock: {\n        label: \"Uzamknout\",\n      },\n      paste: {\n        label: \"Vložit\",\n      },\n      receive: {\n        label: \"Přijmout\",\n      },\n      scan: {\n        label: \"Skenovat\",\n      },\n      send: {\n        label: \"Odeslat\",\n      },\n      pay: {\n        label: \"Zaplatit\",\n      },\n      swap: {\n        label: \"Směnit\",\n      },\n      update: {\n        label: \"Aktualizovat\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"URL mintu\",\n      },\n    },\n  },\n  common: {\n    fee: \"Poplatek\",\n  },\n  MultinutPicker: {\n    payment: \"Platba Multinut\",\n    selectMints:\n      \"Vyberte jeden nebo více mintů, ze kterých se má platba provést.\",\n    totalSelectedBalance: \"Celkový vybraný zůstatek\",\n    multiMintPay: \"Platba z více mintů\",\n    balanceNotEnough: \"Zůstatek napříč minty nestačí k uhrazení této faktury\",\n    failed: \"Zpracování selhalo: {error}\",\n    paid: \"Zaplaceno {amount} přes Lightning\",\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"Zůstatek je příliš nízký\",\n      received: \"Přijato {amount}\",\n      fee: \" (poplatek: {fee})\",\n      could_not_request_mint: \"Nepodařilo se požádat mint\",\n      invoice_still_pending: \"Faktura zatím nevyřízena\",\n      paid_lightning: \"Zaplaceno {amount} přes Lightning\",\n      payment_pending_refresh: \"Platba nevyřízena. Obnovte fakturu ručně.\",\n      sent: \"Odesláno {amount}\",\n      token_still_pending: \"Token stále nevyřízen\",\n      received_lightning: \"Přijato {amount} přes Lightning\",\n      lightning_payment_failed: \"Lightning platba selhala\",\n      failed_to_decode_invoice: \"Nepodařilo se dekódovat fakturu\",\n      invalid_lnurl: \"Neplatné LNURL\",\n      lnurl_error: \"Chyba LNURL\",\n      no_amount: \"Nebyla zadána částka\",\n      no_lnurl_data: \"Žádná LNURL data\",\n      no_price_data: \"Žádná cenová data.\",\n      please_try_again: \"Zkuste to prosím znovu.\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"Mint je již přidán\",\n        added: \"Mint přidán\",\n        not_found: \"Mint nebyl nalezen\",\n        activation_failed: \"Aktivace mintu selhala\",\n        no_active_mint: \"Žádný aktivní mint\",\n        unit_activation_failed: \"Aktivace jednotky selhala\",\n        unit_not_supported: \"Jednotka není mintem podporována\",\n        activated: \"Mint aktivován\",\n        could_not_connect: \"Nelze se připojit k mintu\",\n        could_not_get_info: \"Nelze získat informace o mintu\",\n        could_not_get_keys: \"Nelze získat klíče mintu\",\n        could_not_get_keysets: \"Nelze získat keysety mintu\",\n        mint_validation_error: \"Chyba ověření mintu\",\n        removed: \"Mint odebrán\",\n        error: \"Chyba mintu\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"Nastavení\",\n        settings: {\n          title: \"Nastavení\",\n          caption: \"Nastavení peněženky\",\n        },\n      },\n      terms: {\n        title: \"Podmínky\",\n        terms: {\n          title: \"Podmínky\",\n          caption: \"Podmínky služby\",\n        },\n      },\n      links: {\n        title: \"Odkazy\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"GitHub\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"Darovat\",\n          caption: \"Podpořit Cashu\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"Offline\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"Obnovení za { countdown }\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"Staging – nepoužívejte se skutečnými prostředky!\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"Peněženka\",\n      },\n    },\n  },\n  Settings: {\n    language: {\n      title: \"Jazyk\",\n      description: \"Vyberte si preferovaný jazyk ze seznamu níže.\",\n    },\n    sections: {\n      backup_restore: \"ZÁLOHA A OBNOVA\",\n      lightning_address: \"LIGHTNING ADRESA\",\n      nostr_keys: \"NOSTR KLÍČE\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"Klikněte pro úpravu relayů\",\n          add: {\n            title: \"Přidat relay\",\n            description:\n              \"Vaše peněženka používá tyto relaye pro Nostr operace, jako jsou žádosti o platbu, Nostr Wallet Connect a zálohy.\",\n          },\n          list: {\n            title: \"Relaye\",\n            description: \"Vaše peněženka se připojí k těmto relayům.\",\n            copy_tooltip: \"Kopírovat relay\",\n            remove_tooltip: \"Odebrat relay\",\n          },\n        },\n      },\n      payment_requests: \"ŽÁDOSTI O PLATBU\",\n      nostr_wallet_connect: \"NOSTR WALLET CONNECT\",\n      hardware_features: \"HARDWAROVÉ FUNKCE\",\n      p2pk_features: \"P2PK FUNKCE\",\n      privacy: \"SOUKROMÍ\",\n      experimental: \"EXPERIMENTÁLNÍ\",\n      appearance: \"VZHLED\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"Záloha seed fráze\",\n        description:\n          \"Seed fráze umožňuje obnovit vaši peněženku. Uchovávejte ji v bezpečí a v soukromí.\",\n        seed_phrase_label: \"Seed fráze\",\n      },\n      restore_ecash: {\n        title: \"Obnovit ecash\",\n        description:\n          \"Průvodce obnovou vám umožní obnovit ztracený ecash pomocí mnemotechnické seed fráze. Seed fráze vaší aktuální peněženky zůstane nedotčena – průvodce umožňuje obnovu ecashe pouze z jiné seed fráze.\",\n        button: \"Obnovit\",\n      },\n    },\n    lightning_address: {\n      title: \"Lightning adresa\",\n      description: \"Přijímejte platby na svou Lightning adresu.\",\n      enable: {\n        toggle: \"Povolit\",\n        description: \"Lightning adresa s npub.cash\",\n      },\n      address: {\n        copy_tooltip: \"Kopírovat Lightning adresu\",\n      },\n      automatic_claim: {\n        toggle: \"Přijímat automaticky\",\n        description: \"Automaticky přijímat příchozí platby.\",\n      },\n      npc_v2: {\n        choose_mint_title: \"Vyberte mint pro npub.cash v2\",\n        choose_mint_placeholder: \"Vyberte mint…\",\n      },\n    },\n\n    nostr_keys: {\n      title: \"Vaše Nostr klíče\",\n      description:\n        \"Vaše Nostr klíče budou použity k určení vaší Lightning adresy.\",\n      wallet_seed: {\n        title: \"Seed fráze peněženky\",\n        description: \"Vygenerovat Nostr klíčový pár ze seedu peněženky\",\n        copy_nsec: \"Kopírovat nsec\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"Použít NIP-46 bunker\",\n        delete_tooltip: \"Odstranit připojení\",\n      },\n      use_nsec: {\n        title: \"Použít vlastní nsec\",\n        description: \"Tato metoda je nebezpečná a nedoporučuje se\",\n        delete_tooltip: \"Odstranit nsec\",\n      },\n      signing_extension: {\n        title: \"Podepisovací rozšíření\",\n        description: \"Použít podepisovací rozšíření NIP-07\",\n        not_found: \"Nenalezeno žádné podepisovací rozšíření NIP-07\",\n      },\n    },\n\n    payment_requests: {\n      title: \"Žádosti o platbu\",\n      description:\n        \"Žádosti o platbu vám umožňují přijímat platby přes Nostr. Pokud tuto funkci povolíte, vaše peněženka se přihlásí k odběru vašich Nostr relayů.\",\n      enable_toggle: \"Povolit žádosti o platbu\",\n      claim_automatically: {\n        toggle: \"Přijímat automaticky\",\n        description: \"Automaticky přijímat příchozí platby.\",\n      },\n    },\n\n    nostr_wallet_connect: {\n      title: \"Nostr Wallet Connect (NWC)\",\n      description:\n        \"Použijte NWC k ovládání své peněženky z jakékoli jiné aplikace.\",\n      enable_toggle: \"Povolit NWC\",\n      payments_note:\n        \"NWC lze použít pouze pro platby z vašeho bitcoinového zůstatku. Platby budou prováděny z aktivního mintu.\",\n      connection: {\n        copy_tooltip: \"Kopírovat připojovací řetězec\",\n        qr_tooltip: \"Zobrazit QR kód\",\n        allowance_label: \"Zbývající limit (sat)\",\n      },\n    },\n\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"Vyberte kódování pro zápis na NFC karty\",\n        text: {\n          title: \"Text\",\n          description: \"Uložit token jako prostý text\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"Uložit URL této peněženky spolu s tokenem\",\n        },\n        binary: {\n          title: \"Binární\",\n          description: \"Uložit tokeny jako binární data\",\n        },\n        quick_access: {\n          toggle: \"Rychlý přístup k NFC\",\n          description:\n            \"Rychlé skenování NFC karet v nabídce Přijmout ecash. Tato volba přidá tlačítko NFC do nabídky Přijmout ecash.\",\n        },\n      },\n    },\n\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"Vygenerujte klíčový pár pro příjem ecashe uzamčeného pomocí P2PK. Varování: Tato funkce je experimentální. Používejte ji pouze s malými částkami. Pokud ztratíte své soukromé klíče, nikdo už nebude schopen ecash uzamčený k nim odemknout.\",\n      generate_button: \"Vygenerovat klíč\",\n      import_button: \"Importovat nsec\",\n      quick_access: {\n        toggle: \"Rychlý přístup k uzamčení\",\n        description:\n          \"Použijte tuto možnost pro rychlé zobrazení vašeho P2PK uzamykacího klíče v nabídce Přijmout ecash.\",\n      },\n      keys_expansion: {\n        label: \"Klikněte pro zobrazení {count} klíčů\",\n        used_badge: \"použito\",\n      },\n    },\n\n    privacy: {\n      title: \"Soukromí\",\n      description: \"Tato nastavení ovlivňují vaše soukromí.\",\n      check_incoming: {\n        toggle: \"Kontrolovat příchozí faktury\",\n        description:\n          \"Pokud je povoleno, peněženka bude na pozadí kontrolovat nejnovější fakturu. To zvyšuje odezvu peněženky, ale také usnadňuje fingerprinting. Nezaplacené faktury můžete kontrolovat ručně na kartě Faktury.\",\n      },\n      check_startup: {\n        toggle: \"Kontrolovat čekající faktury při spuštění\",\n        description:\n          \"Pokud je povoleno, peněženka při spuštění zkontroluje čekající faktury z posledních 24 hodin.\",\n      },\n      check_all: {\n        toggle: \"Kontrolovat všechny faktury\",\n        description:\n          \"Pokud je povoleno, peněženka bude až po dobu dvou týdnů periodicky kontrolovat nezaplacené faktury na pozadí. To zvyšuje síťovou aktivitu peněženky a usnadňuje fingerprinting. Nezaplacené faktury můžete kontrolovat ručně na kartě Faktury.\",\n      },\n      check_sent: {\n        toggle: \"Kontrolovat odeslaný ecash\",\n        description:\n          \"Pokud je povoleno, peněženka bude pomocí periodických kontrol na pozadí zjišťovat, zda byly odeslané tokeny uplatněny. To zvyšuje síťovou aktivitu peněženky a usnadňuje fingerprinting.\",\n      },\n      websockets: {\n        toggle: \"Používat WebSockety\",\n        description:\n          \"Pokud je povoleno, peněženka bude používat dlouhodobá WebSocket připojení pro přijímání aktualizací o zaplacených fakturách a utracených tokenech z mintů. To zvyšuje odezvu peněženky, ale také usnadňuje fingerprinting.\",\n      },\n      bitcoin_price: {\n        toggle: \"Získávat kurz z Coinbase\",\n        description:\n          \"Pokud je povoleno, aktuální kurz Bitcoinu bude načítán z coinbase.com a převedený zůstatek bude zobrazen.\",\n        currency: {\n          title: \"Fiat měna\",\n          description: \"Vyberte fiat měnu pro zobrazení ceny Bitcoinu.\",\n        },\n      },\n    },\n\n    experimental: {\n      title: \"Experimentální\",\n      description: \"Tyto funkce jsou experimentální.\",\n      receive_swaps: {\n        toggle: \"Přijímat swapy\",\n        badge: \"Beta\",\n        description:\n          \"Možnost směnit přijatý ecash na váš aktivní mint v dialogu Přijmout ecash.\",\n      },\n      auto_paste: {\n        toggle: \"Automaticky vkládat ecash\",\n        description:\n          \"Automaticky vloží ecash ze schránky při stisknutí Přijmout → Ecash → Vložit. Automatické vkládání může na iOS způsobovat problémy v UI – pokud k nim dochází, vypněte tuto funkci.\",\n      },\n      auditor: {\n        toggle: \"Povolit auditora\",\n        badge: \"Beta\",\n        description:\n          \"Pokud je povoleno, peněženka zobrazí informace o auditorovi v dialogu detailů mintu. Auditor je služba třetí strany, která sleduje spolehlivost mintů.\",\n        url_label: \"URL auditora\",\n        api_url_label: \"API URL auditora\",\n      },\n      multinut: {\n        toggle: \"Povolit Multinut\",\n        description:\n          \"Pokud je povoleno, peněženka použije Multinut k placení faktur z více mintů najednou.\",\n      },\n      nostr_mint_backup: {\n        toggle: \"Zálohovat seznam mintů na Nostr\",\n        description:\n          \"Pokud je povoleno, váš seznam mintů bude automaticky zálohován na Nostr relaye pomocí nakonfigurovaných Nostr klíčů. To umožňuje obnovu seznamu mintů napříč zařízeními.\",\n        notifications: {\n          enabled: \"Záloha mintů na Nostr povolena\",\n          disabled: \"Záloha mintů na Nostr zakázána\",\n          failed: \"Nepodařilo se povolit zálohu mintů na Nostr\",\n        },\n      },\n    },\n    appearance: {\n      keyboard: {\n        title: \"Klávesnice na obrazovce\",\n        description: \"Použít číselnou klávesnici pro zadávání částek.\",\n        toggle: \"Použít číselnou klávesnici\",\n        toggle_description:\n          \"Pokud je povoleno, pro zadávání částek bude použita číselná klávesnice.\",\n      },\n      theme: {\n        title: \"Vzhled\",\n        description: \"Změňte vzhled své peněženky.\",\n        tooltips: {\n          mono: \"mono\",\n          cyber: \"cyber\",\n          freedom: \"freedom\",\n          nostr: \"nostr\",\n          bitcoin: \"bitcoin\",\n          mint: \"mint\",\n          nut: \"nut\",\n          blu: \"blu\",\n          flamingo: \"flamingo\",\n        },\n      },\n      bip177: {\n        title: \"Symbol Bitcoinu\",\n        description: \"Používat symbol ₿ místo satů.\",\n        toggle: \"Používat symbol ₿\",\n      },\n    },\n    web_of_trust: {\n      title: \"Web of trust\",\n      known_pubkeys: \"Známé pubkey: {wotCount}\",\n      continue_crawl: \"Pokračovat v procházení\",\n      crawl_odell: \"Procházet ODELLŮV WEB OF TRUST\",\n      crawl_wot: \"Procházet web of trust\",\n      pause: \"Pozastavit\",\n      reset: \"Resetovat\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"Použít npubx.cash\",\n      copy_lightning_address: \"Kopírovat Lightning adresu\",\n      v2_mint: \"mint pro npub.cash v2\",\n    },\n    multinut: {\n      use_multinut: \"Použít Multinut\",\n    },\n    advanced: {\n      title: \"Pokročilé\",\n      developer: {\n        title: \"Vývojářská nastavení\",\n        description: \"Následující nastavení slouží pro vývoj a ladění.\",\n        new_seed: {\n          button: \"Vygenerovat novou seed frázi\",\n          description:\n            \"Tímto se vygeneruje nová seed fráze. Abyste ji mohli později obnovit, musíte si nejprve poslat celý zůstatek sami sobě.\",\n          confirm_question: \"Opravdu chcete vygenerovat novou seed frázi?\",\n          cancel: \"Zrušit\",\n          confirm: \"Potvrdit\",\n        },\n        remove_spent: {\n          button: \"Odstranit utracené proofy\",\n          description:\n            \"Zkontroluje, zda jsou ecash tokeny z vašich aktivních mintů utracené, a odstraní ty utracené z peněženky. Používejte pouze v případě, že je peněženka zaseknutá.\",\n        },\n        debug_console: {\n          button: \"Přepnout ladicí konzoli\",\n          description:\n            \"Otevře ladicí Javascript konzoli. Nikdy sem nevkládejte nic, čemu nerozumíte. Útočník by vás mohl přimět vložit sem škodlivý kód.\",\n        },\n        export_proofs: {\n          button: \"Exportovat aktivní proofy\",\n          description:\n            \"Zkopíruje celý váš zůstatek z aktivního mintu jako Cashu token do schránky. Exportuje se pouze vybraný mint a jednotka. Pro kompletní export vyberte jiný mint a jednotku a export opakujte.\",\n        },\n        keyset_counters: {\n          title: \"Navýšit čítače keysetů\",\n          description:\n            \"Kliknutím na ID keysetu zvýšíte čítače derivační cesty pro keysety ve vaší peněžence. To je užitečné, pokud se zobrazí chyba „outputs have already been signed“.\",\n          counter: \"čítač: {count}\",\n        },\n        unset_reserved: {\n          button: \"Zrušit rezervaci všech tokenů\",\n          description:\n            \"Tato peněženka označuje čekající odchozí ecash jako rezervovaný (a odečítá jej ze zůstatku), aby zabránila pokusům o double-spend. Toto tlačítko zruší rezervaci všech tokenů, aby je bylo možné znovu použít. Pokud tak učiníte, peněženka může obsahovat utracené proofy. Pro jejich odstranění použijte tlačítko „Odstranit utracené proofy“.\",\n        },\n        show_onboarding: {\n          button: \"Zobrazit onboarding\",\n          description: \"Znovu zobrazí úvodní obrazovky.\",\n        },\n        reset_wallet: {\n          button: \"Resetovat data peněženky\",\n          description:\n            \"Resetuje data vaší peněženky. Varování: Tímto dojde ke smazání všeho! Nezapomeňte si nejprve vytvořit zálohu.\",\n          confirm_question: \"Opravdu chcete smazat data peněženky?\",\n          cancel: \"Zrušit\",\n          confirm: \"Smazat peněženku\",\n        },\n        export_wallet: {\n          button: \"Exportovat data peněženky\",\n          description:\n            \"Stáhne výpis vaší peněženky. Z tohoto souboru můžete obnovit peněženku na úvodní obrazovce nové peněženky. Pokud budete peněženku po exportu dál používat, soubor nebude aktuální.\",\n        },\n        import_wallet: {\n          button: \"Importovat zálohu peněženky\",\n          description:\n            \"Obnoví peněženku z dříve exportovaného záložního souboru. Tímto nahradíte aktuální data peněženky daty ze zálohy.\",\n          confirm_question: \"Opravdu chcete obnovit data peněženky?\",\n          cancel: \"Zrušit\",\n          confirm: \"IMPORTOVAT ZÁLOHU PENĚŽENKY\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"Připojte mint\",\n    subtitle:\n      \"Ještě jste se nepřipojili k žádnému Cashu mintu. Přidejte URL mintu v nastavení nebo přijměte ecash z nového mintu, abyste mohli začít.\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"Přijmout Ecash\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"Historie\",\n      },\n      invoices: {\n        label: \"Faktury\",\n      },\n      mints: {\n        label: \"Minty\",\n      },\n    },\n    install: {\n      text: \"Instalovat\",\n      tooltip: \"Nainstalovat Cashu\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"Ne.\",\n    text: \"Jiná karta už běží. Zavřete ji a zkuste to znovu.\",\n    actions: {\n      retry: {\n        label: \"Zkusit znovu\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"Oops. Nic tu není…\",\n    actions: {\n      home: {\n        label: \"Zpět domů\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Mint\",\n    },\n    mintBalance: {\n      label: \"Zůstatek\",\n    },\n    mintError: {\n      label: \"Chyba mintu\",\n    },\n    pending: {\n      label: \"Čekající\",\n      tooltip: \"Zkontrolovat všechny čekající tokeny\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"Předchozí\",\n      },\n      next: {\n        label: \"Další\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"Vítejte v Cashu\",\n    text: \"Cashu.me je bezplatná a open-source bitcoinová peněženka, která používá ecash pro bezpečné a soukromé uchování vašich prostředků.\",\n    actions: {\n      more: {\n        label: \"Klikněte pro více informací\",\n      },\n    },\n    p1: {\n      text: \"Cashu je bezplatný a open-source ecash protokol pro Bitcoin. Více informací najdete na { link }.\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"Tato peněženka není spojena s žádným mintem. Chcete-li ji používat, musíte se připojit k jednomu nebo více Cashu mintům, kterým důvěřujete.\",\n    },\n    p3: {\n      text: \"Tato peněženka uchovává ecash, ke kterému máte přístup pouze vy. Pokud smažete data prohlížeče bez zálohy seed fráze, ztratíte své tokeny.\",\n    },\n    p4: {\n      text: \"Tato peněženka je v beta verzi. Nenese odpovědnost za ztrátu přístupu k prostředkům. Používejte na vlastní riziko! Tento kód je open-source a licencován pod licencí MIT.\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"Nainstalujte PWA\",\n    alt: {\n      pwa_example: \"Příklad instalace PWA\",\n    },\n    installing: \"Instaluje se…\",\n    instruction: {\n      intro: {\n        text: \"Pro nejlepší zážitek používejte tuto peněženku ve webovém prohlížeči vašeho zařízení a nainstalujte ji jako Progressive Web App.\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Klepněte na menu (vpravo nahoře)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Stiskněte { buttonText }\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Klepněte na sdílení (dole)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Stiskněte { buttonText }\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"Jakmile tuto aplikaci nainstalujete, zavřete okno prohlížeče a používejte aplikaci z domovské obrazovky.\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"Úspěch!\",\n        text: \"Používáte Cashu jako PWA. Zavřete všechny ostatní okna prohlížeče a používejte aplikaci z domovské obrazovky.\",\n        nextSteps:\n          \"Nyní můžete zavřít tuto kartu prohlížeče a otevřít aplikaci z domovské obrazovky.\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"Klepněte na { icon } a { buttonText }\",\n    buttonText: \"Přidat na domovskou obrazovku\",\n  },\n  AndroidPWAPrompt: {\n    text: \"Klepněte na { icon } a { buttonText }\",\n    buttonText: \"Přidat na domovskou obrazovku\",\n  },\n  WelcomeSlide3: {\n    title: \"Vaše seed fráze\",\n    text: \"Uložte svou seed frázi do správce hesel nebo na papír. Vaše seed fráze je jediný způsob, jak obnovit prostředky, pokud ztratíte přístup k tomuto zařízení.\",\n    inputs: {\n      seed_phrase: {\n        label: \"Seed fráze\",\n        caption: \"Seed frázi najdete v nastavení.\",\n      },\n      checkbox: {\n        label: \"Zapsal(a) jsem si ji\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"Podmínky\",\n    actions: {\n      more: {\n        label: \"Přečíst Podmínky služby\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"Přečetl(a) jsem a souhlasím s těmito podmínkami\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"Nastavte svou peněženku\",\n    text: \"Chcete obnovit ze seed fráze nebo vytvořit novou peněženku?\",\n    options: {\n      new: {\n        title: \"Vytvořit novou peněženku\",\n        subtitle: \"Vygenerovat nový seed a přidat minty.\",\n      },\n      recover: {\n        title: \"Obnovit peněženku\",\n        subtitle: \"Zadejte svou seed frázi, obnovte minty a ecash.\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"Přidat minty\",\n    text: \"Minty jsou servery, které vám pomáhají odesílat a přijímat ecash. Vyberte nalezený mint nebo přidejte ručně. Přeskočte a přidejte minty později.\",\n    sections: {\n      your_mints: \"Vaše minty\",\n    },\n    restoring: \"Obnovují se minty…\",\n    placeholder: {\n      mint_url: \"https://\",\n    },\n  },\n  WelcomeRecoverSeed: {\n    title: \"Zadejte seed frázi\",\n    text: \"Vložte nebo napište svou 12slovnou seed frázi pro obnovení.\",\n    inputs: {\n      word: \"Slovo { index }\",\n    },\n    actions: {\n      paste_all: \"Vložit vše\",\n    },\n    disclaimer:\n      \"Vaše seed fráze se používá pouze lokálně pro odvození klíčů peněženky.\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"Obnovte svůj ecash\",\n    text: \"Skenujte nevyužité proofy na nakonfigurovaných mintech a přidejte je do peněženky.\",\n  },\n  MintRatings: {\n    title: \"Recenze mintu\",\n    reviews: \"recenze\",\n    ratings: \"Hodnocení\",\n    no_reviews: \"Žádné recenze nebyly nalezeny\",\n    your_review: \"Vaše recenze\",\n    no_reviews_to_display: \"Žádné recenze k zobrazení.\",\n    no_rating: \"Bez hodnocení\",\n    out_of: \"z\",\n    rows: \"Recenze\",\n    sort: \"Třídit\",\n    sort_options: {\n      newest: \"Nejnovější\",\n      oldest: \"Nejstarší\",\n      highest: \"Nejvyšší\",\n      lowest: \"Nejnižší\",\n    },\n    actions: {\n      write_review: \"Napsat recenzi\",\n    },\n    empty_state_subtitle:\n      \"Pomozte tím, že napíšete recenzi. Sdílejte svou zkušenost s tímto mintem a pomozte ostatním.\",\n  },\n\n  CreateMintReview: {\n    title: \"Recenze mintu\",\n    publishing_as: \"Publikujete jako\",\n    inputs: {\n      rating: { label: \"Hodnocení\" },\n      review: { label: \"Recenze (volitelně)\" },\n    },\n    actions: {\n      publish: { label: \"Odeslat recenzi\", in_progress: \"Odesílání…\" },\n    },\n  },\n\n  RestoreView: {\n    seed_phrase: {\n      label: \"Obnovit ze seed fráze\",\n      caption:\n        \"Zadejte svou seed frázi pro obnovení peněženky. Před obnovením se ujistěte, že jste přidali všechny minty, které jste dříve používali.\",\n      inputs: {\n        seed_phrase: {\n          label: \"Seed fráze\",\n          caption: \"Seed frázi najdete v nastavení.\",\n        },\n      },\n    },\n    information: {\n      label: \"Informace\",\n      caption:\n        \"Tento průvodce obnoví pouze ecash z jiné seed fráze, nebudete moci tuto seed frázi používat ani měnit seed frázi aktuální peněženky. Obnovený ecash tak nebude chráněn vaší aktuální seed frází, dokud ho jednou nepošlete sami sobě.\",\n    },\n    restore_mints: {\n      label: \"Obnovit minty\",\n      caption:\n        'Vyberte mint k obnovení. Další minty můžete přidat na hlavní obrazovce pod \"Minty\" a obnovit je zde.',\n    },\n    actions: {\n      paste: {\n        error: \"Nepodařilo se načíst obsah schránky.\",\n      },\n      validate: {\n        error: \"Mnemonic musí mít alespoň 12 slov.\",\n      },\n      select_all: {\n        label: \"Vybrat vše\",\n      },\n      deselect_all: {\n        label: \"Odznačit vše\",\n      },\n      restore: {\n        label: \"Obnovit\",\n        in_progress: \"Obnovuje se mint …\",\n        error: \"Chyba při obnově mintu: { error }\",\n      },\n      restore_all_mints: {\n        label: \"Obnovit všechny minty\",\n        in_progress: \"Obnovuje se mint { index } z { length } …\",\n        success: \"Obnova dokončena úspěšně\",\n        error: \"Chyba při obnově mintů: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"Obnovit vybrané minty ({count})\",\n        in_progress: \"Obnovuje se mint { index } z { length } …\",\n        success: \"Úspěšně obnoven(a) {count} mint(y)\",\n        error: \"Chyba při obnově vybraných mintů: { error }\",\n      },\n    },\n    nostr_mints: {\n      label: \"Obnovit minty z Nostr\",\n      caption:\n        \"Hledejte zálohy mintů uložené na Nostr relays pomocí vaší seed fráze. Pomůže to objevit minty, které jste dříve používali.\",\n      search_button: \"Hledat zálohy mintů\",\n      select_all: \"Vybrat vše\",\n      deselect_all: \"Odznačit vše\",\n      backed_up: \"Zálohováno\",\n      already_added: \"Již přidáno\",\n      add_selected: \"Přidat vybrané ({count})\",\n      no_backups_found: \"Nenalezeny žádné zálohy mintů\",\n      no_backups_hint:\n        \"Ujistěte se, že záloha mintů na Nostr je v nastavení povolena, aby se váš seznam mintů automaticky zálohoval.\",\n      invalid_mnemonic: \"Než budete hledat, zadejte platnou seed frázi.\",\n      search_error: \"Nepodařilo se vyhledat zálohy mintů.\",\n      add_error: \"Nepodařilo se přidat vybrané minty.\",\n    },\n  },\n\n  MintSettings: {\n    add: {\n      title: \"Přidat mint\",\n      description:\n        \"Zadejte URL Cashu mintu, ke kterému se chcete připojit. Tato peněženka není spojena s žádným mintem.\",\n      inputs: {\n        nickname: {\n          placeholder: \"Přezdívka (např. Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"Neplatná URL\",\n        },\n        scan: {\n          label: \"Skenovat QR kód\",\n        },\n      },\n    },\n    discover: {\n      title: \"Objevte minty\",\n      overline: \"Objevovat\",\n      caption: \"Objevte minty, které doporučili jiní uživatelé na Nostr.\",\n      actions: {\n        discover: {\n          label: \"Objevovat minty\",\n          in_progress: \"Načítá se…\",\n          error_no_mints: \"Nenalezeny žádné minty\",\n          success: \"Nalezeno { length } mintů\",\n        },\n      },\n      recommendations: {\n        overline: \"Nalezeno { length } mintů\",\n        caption:\n          \"Tyto minty doporučili ostatní uživatelé Nostr. Buďte opatrní a prověřte mint před použitím.\",\n        actions: {\n          browse: {\n            label: \"Klikněte pro prohlížení mintů\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"Swap\",\n      overline: \"Multimint Swapy\",\n      actions: {\n        receove_to_trusted_mint: {\n          label: \"Přijmout do důvěryhodného mintu\",\n        },\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n      caption:\n        \"Prohození prostředků mezi minty přes Lightning. Poznámka: Nechte rezervu na poplatky Lightning. Pokud příchozí platba neproběhne, zkontrolujte fakturu manuálně.\",\n      inputs: {\n        from: {\n          label: \"Odesílatel\",\n        },\n        to: {\n          label: \"Příjemce\",\n        },\n        amount: {\n          label: \"Částka ({ ticker })\",\n        },\n      },\n    },\n    error_badge: \"Chyba\",\n    reviews_text: \"recenze\",\n    no_reviews_yet: \"Žádné recenze\",\n    discover_mints_button: \"Objevovat minty\",\n  },\n\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - Pokračovat ve skenování\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n\n  InvoiceDetailDialog: {\n    title: \"Přijmout Lightning\",\n    create_invoice_title: \"Vytvořit fakturu\",\n    inputs: {\n      amount: {\n        label: \"Částka ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"Vytvořit fakturu\",\n        label_blocked: \"Vytváří se faktura…\",\n        in_progress: \"Vytváří se\",\n      },\n    },\n    invoice: {\n      caption: \"Faktura Lightning\",\n      status_paid_text: \"Zaplaceno!\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n\n  SendDialog: {\n    title: \"Odeslat\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Žádné minty k dispozici\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"Žádné minty k dispozici\",\n      },\n    },\n  },\n\n  SendTokenDialog: {\n    title: \"Odeslat Ecash\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"Offline\",\n    inputs: {\n      amount: {\n        label: \"Částka ({ ticker }) *\",\n        invalid_too_much_error_text: \"Příliš mnoho\",\n      },\n      p2pk_pubkey: {\n        label: \"Veřejný klíč příjemce\",\n        label_invalid: \"Neplatný veřejný klíč příjemce\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"Kopírovat emoji\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"Kopírovat odkaz\",\n      },\n      share: {\n        tooltip_text: \"Sdílet ecash\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      pay: {\n        label: \"@:global.actions.pay.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"Smazat z historie\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"Zapsat na NFC kartu\",\n          ndef_unsupported_text: \"NDEF není podporováno\",\n        },\n      },\n    },\n    errors: {\n      amount_required: \"Nejprve zadejte částku.\",\n      serialization_failed: \"Nepodařilo se připravit ecash token.\",\n    },\n  },\n\n  SendPaymentRequest: {\n    actions: {\n      pay: {\n        label: \"Zaplatit\",\n      },\n      pay_via: {\n        label: \"Zaplatit přes {transport}\",\n      },\n    },\n    info: {\n      pay_to: \"Zaplatit {target}\",\n      invalid_url: \"Neplatná URL\",\n    },\n  },\n\n  PaymentRequestInfo: {\n    title_with_transport: \"Platební požadavek přes {transport}\",\n    title: \"Platební požadavek\",\n    subtitle: \"Zaplatit {target}\",\n    subtitle_fallback: \"Platební požadavek\",\n    invalid_url: \"Neplatná URL\",\n  },\n\n  ReceiveDialog: {\n    title: \"Přijmout\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Žádné minty k dispozici\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"Pro příjem přes Lightning se musíte připojit k mintu\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"Přijmout Ecash\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"Požádat\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"Skenuje se…\",\n      },\n    },\n  },\n\n  ReceiveTokenDialog: {\n    title: \"Přijmout Ecash\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"Vložit Cashu token\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"Neplatný token\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"Nelze přijmout. P2PK zámek tohoto tokenu neodpovídá vašemu veřejnému klíči.\",\n      },\n    },\n    unknown_mint_info_text:\n      \"Neznámý mint. Bude přidán po přijetí tohoto tokenu.\",\n    swap_section: {\n      title: \"Swap\",\n      source_label: \"Odesílatel\",\n      destination_label: \"Příjemce\",\n      fee_info: \"Tento swap podléhá poplatkům Lightning sítě.\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"Přidává se mint…\",\n      },\n      swap: {\n        label: \"Přijmout do důvěryhodného mintu\",\n        tooltip_text: \"Swap do důvěryhodného mintu\",\n        caption: \"Swap { value }\",\n        processing: \"Probíhá swap…\",\n        failed: \"Swap selhal\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"Zrušit swap\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      receive_to_selected_mint: {\n        label: \"Přijmout do vybraného mintu\",\n      },\n      later: {\n        label: \"Přijmout později\",\n        tooltip_text: \"Přidat do historie pro pozdější příjem\",\n        already_in_history_success_text: \"Ecash již v historii\",\n        added_to_history_success_text: \"Ecash přidán do historie\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"Přečíst z NFC karty\",\n          ndef_unsupported_text: \"NDEF není podporováno\",\n        },\n      },\n    },\n  },\n\n  P2PKDialog: {\n    p2pk: {\n      caption: \"P2PK klíč\",\n      description: \"Přijímat ecash uzamčený tímto klíčem\",\n      used_warning_text:\n        \"Varování: Tento klíč byl již použit. Pro lepší soukromí použijte nový klíč.\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"Vygenerovat nový klíč\",\n      },\n    },\n  },\n\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"Platební požadavek\",\n      description: \"Přijímat platby přes Nostr\",\n    },\n    received_total: \"Celkem přijato\",\n    no_payments_yet: \"Žádné platby dosud\",\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"Nový požadavek\",\n      },\n      add_amount: {\n        label: \"Přidat částku\",\n      },\n      use_active_mint: {\n        label: \"Libovolný mint\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"Zadejte částku\",\n      },\n    },\n  },\n\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"Klávesnice zakázána. Opět ji můžete povolit v nastavení.\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostr Wallet Connect\",\n      description:\n        \"Ovládejte svou peněženku vzdáleně pomocí NWC. Stiskněte QR kód pro propojení peněženky s kompatibilní aplikací.\",\n      warning_text:\n        \"Varování: kdokoli s přístupem k tomuto spojovacímu řetězci může provádět platby z vaší peněženky. Nesdílejte!\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n\n  MintMotdMessage: {\n    title: \"Zpráva mintu\",\n  },\n\n  MintDetailsDialog: {\n    contact: {\n      title: \"Kontakt\",\n    },\n    details: {\n      title: \"Detaily mintu\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"Zobrazit vše\",\n          },\n          hide: {\n            label: \"Skrýt\",\n          },\n        },\n      },\n      currency: {\n        label: \"Měna\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"Verze\",\n      },\n    },\n    actions: {\n      title: \"Akce\",\n      copy_mint_url: {\n        label: \"Kopírovat URL mintu\",\n      },\n      delete: {\n        label: \"Smazat mint\",\n      },\n      edit: {\n        label: \"Upravit mint\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"Vyberte mint\",\n    placeholder: \"Vyberte mint\",\n    available_text: \"dostupný\",\n    sheet_title: \"Vybrat mint\",\n    badge_mint_error_text: \"Chyba\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n\n  HistoryTable: {\n    empty_text: \"Žádná historie zatím\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"před { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Zkontrolovat stav\",\n      },\n      receive: {\n        tooltip_text: \"Přijmout\",\n      },\n      filter_pending: {\n        label: \"Filtrovat čekající\",\n      },\n      show_all: {\n        label: \"Zobrazit vše\",\n      },\n    },\n    old_token_not_found_error_text: \"Starý token nenalezen\",\n  },\n\n  InvoiceTable: {\n    empty_text: \"Žádné faktury zatím\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"Klikněte pro kopírování\",\n      date_label: \"před { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Zkontrolovat stav\",\n      },\n      filter_pending: {\n        label: \"Filtrovat čekající\",\n      },\n      show_all: {\n        label: \"Zobrazit vše\",\n      },\n    },\n  },\n\n  RemoveMintDialog: {\n    title: \"Opravdu chcete smazat tento mint?\",\n    nickname: {\n      label: \"Přezdívka\",\n    },\n    balances: {\n      label: \"Zůstatky\",\n    },\n    warning_text:\n      \"Poznámka: Protože je tato peněženka paranoidní, váš ecash z tohoto mintu nebude skutečně smazán, ale zůstane uložen na zařízení. Uvidíte ho znovu, pokud tento mint později znovu přidáte.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"Odstranit mint\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n\n  ParseInputComponent: {\n    placeholder: {\n      default: \"Cashu token nebo Lightning adresa\",\n      receive: \"Cashu token\",\n      pay: \"Lightning adresa nebo faktura\",\n    },\n    qr_scanner: {\n      title: \"Skenovat QR kód\",\n      description: \"Klikněte pro skenování adresy\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"Zaplatit Lightning\",\n      inputs: {\n        invoice_data: {\n          label: \"Lightning faktura nebo adresa\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"{ payee } požaduje { value } { ticker }\",\n      amount_range_label:\n        \"{ payee } požaduje{br}mezi { min } a { max } { ticker }\",\n      sending_to_lightning_address: \"Odesílám na { address }\",\n      inputs: {\n        amount: {\n          label: \"Částka ({ ticker }) *\",\n        },\n        comment: {\n          label: \"Komentář (volitelné)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"Zaplatit { value }\",\n      paying: \"Probíhá platba\",\n      paid: \"Zaplaceno\",\n      fee: \"Poplatek\",\n      memo: {\n        label: \"Poznámka\",\n      },\n      processing_info_text: \"Zpracovává se…\",\n      balance_too_low_warning_text: \"Zůstatek příliš nízký\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"Zaplatit\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"Chyba\",\n        },\n      },\n    },\n  },\n\n  EditMintDialog: {\n    title: \"Upravit mint\",\n    inputs: {\n      nickname: {\n        label: \"Přezdívka\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"Důvěřujete tomuto mintu?\",\n    description:\n      \"Před použitím tohoto mintu se ujistěte, že mu důvěřujete. Mints mohou kdykoli přestat fungovat nebo se stát škodlivými.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"Přidává se mint\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"Zadejte mnemotechnickou frázi\",\n    restore_mint_error_text: \"Chyba při obnově mintu: { error }\",\n    prepare_info_text: \"Připravuji proces obnovy…\",\n    restored_proofs_for_keyset_info_text:\n      \"Obnoveno { restoreCounter } důkazů pro keyset { keysetId }\",\n    checking_proofs_for_keyset_info_text:\n      \"Kontroluji důkazy { startIndex } až { endIndex } pro keyset { keysetId }\",\n    no_proofs_info_text: \"Nebyly nalezeny žádné důkazy k obnově\",\n    restored_amount_success_text: \"Obnoveno { amount }\",\n  },\n  swap: {\n    in_progress_warning_text: \"Probíhá swap\",\n    invalid_swap_data_error_text: \"Neplatná data pro swap\",\n    swap_error_text: \"Chyba při swapu\",\n  },\n  TokenInformation: {\n    fee: \"Poplatek\",\n    unit: \"Jednotka\",\n    fiat: \"Fiat\",\n    p2pk: \"P2PK\",\n    locked: \"Uzamčeno\",\n    locked_to_you: \"Uzamčeno pro vás\",\n    mint: \"Mint\",\n    memo: \"Poznámka\",\n    payment_request: \"Platební požadavek\",\n    nostr: \"Nostr\",\n    token_copied: \"Token zkopírován do schránky\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/de-DE/index.ts",
    "content": "export default {\n  MultinutPicker: {\n    payment: \"Multinut-Zahlung\",\n    selectMints:\n      \"Wählen Sie eine oder mehrere Mints aus, um eine Zahlung auszuführen.\",\n    totalSelectedBalance: \"Gesamtes ausgewähltes Guthaben\",\n    multiMintPay: \"Multi-Mint-Zahlung\",\n    balanceNotEnough:\n      \"Das Multi-Mint-Guthaben reicht nicht aus, um diese Rechnung zu begleichen\",\n    failed: \"Verarbeitung fehlgeschlagen: {error}\",\n    paid: \"{amount} über Lightning bezahlt\",\n  },\n\n  global: {\n    copy_to_clipboard: {\n      success: \"In die Zwischenablage kopiert!\",\n    },\n    actions: {\n      add_mint: {\n        label: \"Mint hinzufügen\",\n      },\n      cancel: {\n        label: \"Abbrechen\",\n      },\n      copy: {\n        label: \"Kopieren\",\n      },\n      close: {\n        label: \"Schließen\",\n      },\n      enter: {\n        label: \"Eingeben\",\n      },\n      lock: {\n        label: \"Sperren\",\n      },\n      paste: {\n        label: \"Einfügen\",\n      },\n      receive: {\n        label: \"Empfangen\",\n      },\n      scan: {\n        label: \"Scannen\",\n      },\n      send: {\n        label: \"Senden\",\n      },\n      swap: {\n        label: \"Tauschen\",\n      },\n      update: {\n        label: \"Aktualisieren\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"Mint URL\",\n      },\n    },\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"Guthaben ist zu niedrig\",\n      received: \"{amount} empfangen\",\n      fee: \" (Gebühr: {fee})\",\n      could_not_request_mint: \"Mint konnte nicht angefordert werden\",\n      invoice_still_pending: \"Rechnung noch ausstehend\",\n      paid_lightning: \"{amount} über Lightning bezahlt\",\n      payment_pending_refresh:\n        \"Zahlung ausstehend. Rechnung manuell aktualisieren.\",\n      sent: \"{amount} gesendet\",\n      token_still_pending: \"Token noch ausstehend\",\n      received_lightning: \"{amount} über Lightning empfangen\",\n      lightning_payment_failed: \"Lightning-Zahlung fehlgeschlagen\",\n      failed_to_decode_invoice: \"Rechnung konnte nicht dekodiert werden\",\n      invalid_lnurl: \"Ungültige LNURL\",\n      lnurl_error: \"LNURL Fehler\",\n      no_amount: \"Kein Betrag\",\n      no_lnurl_data: \"Keine LNURL-Daten\",\n      no_price_data: \"Keine Preisdaten.\",\n      please_try_again: \"Bitte versuchen Sie es erneut.\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"Mint bereits hinzugefügt\",\n        added: \"Mint hinzugefügt\",\n        not_found: \"Mint nicht gefunden\",\n        activation_failed: \"Mint-Aktivierung fehlgeschlagen\",\n        no_active_mint: \"Kein aktives Mint\",\n        unit_activation_failed: \"Einheiten-Aktivierung fehlgeschlagen\",\n        unit_not_supported: \"Einheit wird von Mint nicht unterstützt\",\n        activated: \"Mint aktiviert\",\n        could_not_connect: \"Verbindung zum Mint nicht möglich\",\n        could_not_get_info: \"Mint-Information konnte nicht abgerufen werden\",\n        could_not_get_keys: \"Mint-Schlüssel konnten nicht abgerufen werden\",\n        could_not_get_keysets: \"Mint-Keysets konnten nicht abgerufen werden\",\n        mint_validation_error: \"Mint-Validierungsfehler\",\n        removed: \"Mint entfernt\",\n        error: \"Mint-Fehler\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"Einstellungen\",\n        settings: {\n          title: \"Einstellungen\",\n          caption: \"Wallet-Konfiguration\",\n        },\n      },\n      terms: {\n        title: \"Bedingungen\",\n        terms: {\n          title: \"Bedingungen\",\n          caption: \"Nutzungsbedingungen\",\n        },\n      },\n      links: {\n        title: \"Links\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"Spenden\",\n          caption: \"Cashu unterstützen\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"Offline\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"Neu laden in { countdown }\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"Staging – nicht mit echten Geldern verwenden!\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"Wallet\",\n      },\n    },\n  },\n  Settings: {\n    language: {\n      title: \"Sprache\",\n      description:\n        \"Bitte wählen Sie Ihre bevorzugte Sprache aus der Liste unten.\",\n    },\n    sections: {\n      backup_restore: \"SICHERUNG & WIEDERHERSTELLUNG\",\n      lightning_address: \"LIGHTNING ADRESSE\",\n      nostr_keys: \"NOSTR SCHLÜSSEL\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"Klicken, um Relays zu bearbeiten\",\n          add: {\n            title: \"Relay hinzufügen\",\n            description:\n              \"Ihre Wallet verwendet diese Relays für Nostr‑Operationen wie Zahlungsanforderungen, Nostr Wallet Connect und Backups.\",\n          },\n          list: {\n            title: \"Relays\",\n            description: \"Ihre Wallet verbindet sich mit diesen Relays.\",\n            copy_tooltip: \"Relay kopieren\",\n            remove_tooltip: \"Relay entfernen\",\n          },\n        },\n      },\n      payment_requests: \"ZAHLUNGSANFORDERUNGEN\",\n      nostr_wallet_connect: \"NOSTR WALLET CONNECT\",\n      hardware_features: \"HARDWARE FUNKTIONEN\",\n      p2pk_features: \"P2PK FUNKTIONEN\",\n      privacy: \"DATENSCHUTZ\",\n      experimental: \"EXPERIMENTELL\",\n      appearance: \"AUSSEHEN\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"Seed-Phrase sichern\",\n        description:\n          \"Ihre Seed-Phrase kann Ihre Wallet wiederherstellen. Bewahren Sie sie sicher und privat auf.\",\n        seed_phrase_label: \"Seed-Phrase\",\n      },\n      restore_ecash: {\n        title: \"Ecash wiederherstellen\",\n        description:\n          \"Der Wiederherstellungs-Assistent ermöglicht es Ihnen, verlorenes Ecash von einer mnemonischen Seed-Phrase wiederherzustellen. Die Seed-Phrase Ihrer aktuellen Wallet bleibt unverändert, der Assistent erlaubt es Ihnen lediglich, Ecash von einer anderen Seed-Phrase zu restaurieren.\",\n        button: \"Wiederherstellen\",\n      },\n    },\n    lightning_address: {\n      title: \"Lightning Adresse\",\n      description: \"Zahlungen an Ihre Lightning Adresse empfangen.\",\n      enable: {\n        toggle: \"Aktivieren\",\n        description: \"Lightning Adresse mit npub.cash\",\n      },\n      address: {\n        copy_tooltip: \"Lightning Adresse kopieren\",\n      },\n      automatic_claim: {\n        toggle: \"Automatisch beanspruchen\",\n        description: \"Eingehende Zahlungen automatisch empfangen.\",\n      },\n      npc_v2: {\n        choose_mint_title: \"Wählen Sie eine Mint für npub.cash v2\",\n        choose_mint_placeholder: \"Wählen Sie eine Mint...\",\n      },\n    },\n    web_of_trust: {\n      title: \"Vertrauensnetzwerk\",\n      known_pubkeys: \"Bekannte Pubkeys: {wotCount}\",\n      continue_crawl: \"Crawl fortsetzen\",\n      crawl_odell: \"ODELL'S WEB OF TRUST crawlen\",\n      crawl_wot: \"Web of Trust crawlen\",\n      pause: \"Pausieren\",\n      reset: \"Zurücksetzen\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"npubx.cash verwenden\",\n      copy_lightning_address: \"Lightning-Adresse kopieren\",\n      v2_mint: \"npub.cash v2 Mint\",\n    },\n    multinut: {\n      use_multinut: \"Multinut verwenden\",\n    },\n    nostr_keys: {\n      title: \"Ihre Nostr-Schlüssel\",\n      description:\n        \"Legen Sie die Nostr-Schlüssel für Ihre Lightning-Adresse fest.\",\n      wallet_seed: {\n        title: \"Wallet Seed-Phrase\",\n        description: \"Nostr-Schlüsselpaar aus Wallet-Seed generieren\",\n        copy_nsec: \"Nsec kopieren\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"Einen NIP-46 Bunker verwenden\",\n        delete_tooltip: \"Verbindung löschen\",\n      },\n      use_nsec: {\n        title: \"Ihren Nsec verwenden\",\n        description: \"Diese Methode ist gefährlich und wird nicht empfohlen\",\n        delete_tooltip: \"Nsec löschen\",\n      },\n      signing_extension: {\n        title: \"Signaturerweiterung\",\n        description: \"Eine NIP-07 Signaturerweiterung verwenden\",\n        not_found: \"Keine NIP-07 Signaturerweiterung gefunden\",\n      },\n    },\n    payment_requests: {\n      title: \"Zahlungsanforderungen\",\n      description:\n        \"Zahlungsanforderungen ermöglichen es Ihnen, Zahlungen über Nostr zu empfangen. Wenn Sie dies aktivieren, abonniert Ihre Wallet Ihre Nostr-Relays.\",\n      enable_toggle: \"Zahlungsanforderungen aktivieren\",\n      claim_automatically: {\n        toggle: \"Automatisch beanspruchen\",\n        description: \"Eingehende Zahlungen automatisch empfangen.\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"Nostr Wallet Connect (NWC)\",\n      description:\n        \"Verwenden Sie NWC, um Ihre Wallet von jeder anderen Anwendung aus zu steuern.\",\n      enable_toggle: \"NWC aktivieren\",\n      payments_note:\n        \"Sie können NWC nur für Zahlungen von Ihrem Bitcoin-Guthaben verwenden. Zahlungen werden von Ihrer aktiven Mint vorgenommen.\",\n      connection: {\n        copy_tooltip: \"Verbindungsstring kopieren\",\n        qr_tooltip: \"QR-Code anzeigen\",\n        allowance_label: \"Restliches Guthaben (sat)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description:\n          \"Wählen Sie die Kodierung für das Schreiben auf NFC-Karten\",\n        text: {\n          title: \"Text\",\n          description: \"Token als Klartext speichern\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"URL zu dieser Wallet mit Token speichern\",\n        },\n        binary: {\n          title: \"Binary\",\n          description: \"Token als Binärdaten speichern\",\n        },\n        quick_access: {\n          toggle: \"Schnellzugriff auf NFC\",\n          description:\n            \"Schnelles Scannen von NFC-Karten im Ecash empfangen-Menü. Diese Option fügt einen NFC-Button zum Ecash empfangen-Menü hinzu.\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"Generieren Sie ein Schlüsselpaar, um P2PK-gesperrten Ecash zu erhalten. Warnung: Diese Funktion ist experimentell. Nur mit kleinen Beträgen verwenden. Wenn Sie Ihre privaten Schlüssel verlieren, kann niemand mehr den darauf gesperrten Ecash freischalten.\",\n      generate_button: \"Schlüssel generieren\",\n      import_button: \"Nsec importieren\",\n      quick_access: {\n        toggle: \"Schnellzugriff auf Sperre\",\n        description:\n          \"Verwenden Sie dies, um Ihren P2PK-Sperrschlüssel schnell im Ecash empfangen-Menü anzuzeigen.\",\n      },\n      keys_expansion: {\n        label: \"Klicken, um {count} Schlüssel zu durchsuchen\",\n        used_badge: \"verwendet\",\n      },\n    },\n    privacy: {\n      title: \"Datenschutz\",\n      description: \"Diese Einstellungen beeinflussen Ihren Datenschutz.\",\n      check_incoming: {\n        toggle: \"Eingehende Rechnung prüfen\",\n        description:\n          \"Wenn aktiviert, prüft die Wallet die neueste Rechnung im Hintergrund. Dies erhöht die Reaktionsfähigkeit der Wallet, was das Fingerprinting erleichtert. Sie können unbezahlte Rechnungen manuell im Reiter 'Rechnungen' prüfen.\",\n      },\n      check_startup: {\n        toggle: \"Ausstehende Rechnungen beim Start prüfen\",\n        description:\n          \"Wenn aktiviert, prüft die Wallet beim Start ausstehende Rechnungen der letzten 24 Stunden.\",\n      },\n      check_all: {\n        toggle: \"Alle Rechnungen prüfen\",\n        description:\n          \"Wenn aktiviert, prüft die Wallet unbezahlte Rechnungen im Hintergrund für bis zu zwei Wochen. Dies erhöht die Online-Aktivität der Wallet, was das Fingerprinting erleichtert. Sie können unbezahlte Rechnungen manuell im Reiter 'Rechnungen' prüfen.\",\n      },\n      check_sent: {\n        toggle: \"Gesendeten Ecash prüfen\",\n        description:\n          \"Wenn aktiviert, verwendet die Wallet periodische Hintergrundprüfungen, um festzustellen, ob gesendete Token eingelöst wurden. Dies erhöht die Online-Aktivität der Wallet, was das Fingerprinting erleichtert.\",\n      },\n      websockets: {\n        toggle: \"WebSockets verwenden\",\n        description:\n          \"Wenn aktiviert, verwendet die Wallet langlebige WebSocket-Verbindungen, um Updates zu bezahlten Rechnungen und ausgegebenen Token von Mints zu erhalten. Dies erhöht die Reaktionsfähigkeit der Wallet, macht aber auch das Fingerprinting einfacher.\",\n      },\n      bitcoin_price: {\n        toggle: \"Wechselkurs von Coinbase abrufen\",\n        description:\n          \"Wenn aktiviert, wird der aktuelle Bitcoin-Wechselkurs von coinbase.com abgerufen und Ihr umgerechnetes Guthaben angezeigt.\",\n        currency: {\n          title: \"Fiat-Währung\",\n          description:\n            \"Wählen Sie die Fiat-Währung für die Bitcoin-Preisanzeige.\",\n        },\n      },\n    },\n    experimental: {\n      title: \"Experimentell\",\n      description: \"Diese Funktionen sind experimentell.\",\n      receive_swaps: {\n        toggle: \"Swaps empfangen\",\n        badge: \"Beta\",\n        description:\n          \"Option, empfangenen Ecash in Ihrer aktiven Mint im Dialog 'Ecash empfangen' zu tauschen.\",\n      },\n      auto_paste: {\n        toggle: \"Ecash automatisch einfügen\",\n        description:\n          \"Fügt Ecash automatisch aus Ihrer Zwischenablage ein, wenn Sie auf 'Empfangen', dann 'Ecash', dann 'Einfügen' drücken. Automatisches Einfügen kann auf iOS zu UI-Fehlern führen. Deaktivieren Sie dies, wenn Sie Probleme haben.\",\n      },\n      auditor: {\n        toggle: \"Auditor aktivieren\",\n        badge: \"Beta\",\n        description:\n          \"Wenn aktiviert, zeigt die Wallet Auditor-Informationen im Dialog 'Mint-Details' an. Der Auditor ist ein Drittanbieter-Service, der die Zuverlässigkeit von Mints überwacht.\",\n        url_label: \"Auditor URL\",\n        api_url_label: \"Auditor API URL\",\n      },\n      multinut: {\n        toggle: \"Multinut aktivieren\",\n        description:\n          \"Wenn aktiviert, verwendet die Wallet Multinut, um Rechnungen von mehreren Mints gleichzeitig zu bezahlen.\",\n      },\n      nostr_mint_backup: {\n        toggle: \"Mint-Liste auf Nostr sichern\",\n        description:\n          \"Wenn aktiviert, wird Ihre Mint-Liste automatisch auf Nostr-Relays mit Ihren konfigurierten Nostr-Schlüsseln gesichert. Dies ermöglicht es Ihnen, Ihre Mint-Liste auf verschiedenen Geräten wiederherzustellen.\",\n        notifications: {\n          enabled: \"Nostr-Mint-Backup aktiviert\",\n          disabled: \"Nostr-Mint-Backup deaktiviert\",\n          failed: \"Fehler beim Aktivieren des Nostr-Mint-Backups\",\n        },\n      },\n    },\n    appearance: {\n      keyboard: {\n        title: \"Bildschirmtastatur\",\n        description:\n          \"Verwenden Sie die Zifferntastatur zur Eingabe von Beträgen.\",\n        toggle: \"Numerische Tastatur verwenden\",\n        toggle_description:\n          \"Wenn aktiviert, wird die numerische Tastatur zur Eingabe von Beträgen verwendet.\",\n      },\n      theme: {\n        title: \"Aussehen\",\n        description: \"Ändern Sie das Aussehen Ihrer Wallet.\",\n        tooltips: {\n          mono: \"mono\",\n          cyber: \"cyber\",\n          freedom: \"freedom\",\n          nostr: \"nostr\",\n          bitcoin: \"bitcoin\",\n          mint: \"mint\",\n          nut: \"nut\",\n          blu: \"blu\",\n          flamingo: \"flamingo\",\n        },\n      },\n      bip177: {\n        title: \"Bitcoin-Symbol\",\n        description: \"Verwenden Sie das ₿-Symbol anstelle von sats.\",\n        toggle: \"₿-Symbol verwenden\",\n      },\n    },\n    advanced: {\n      title: \"Erweitert\",\n      developer: {\n        title: \"Entwickler-Einstellungen\",\n        description:\n          \"Die folgenden Einstellungen sind für Entwicklung und Debugging.\",\n        new_seed: {\n          button: \"Neue Seed-Phrase generieren\",\n          description:\n            \"Dies generiert eine neue Seed-Phrase. Sie müssen Ihr gesamtes Guthaben an sich selbst senden, um es mit einer neuen Seed wiederherstellen zu können.\",\n          confirm_question:\n            \"Sind Sie sicher, dass Sie eine neue Seed-Phrase generieren möchten?\",\n          cancel: \"Abbrechen\",\n          confirm: \"Bestätigen\",\n        },\n        remove_spent: {\n          button: \"Ausgegebene Nachweise entfernen\",\n          description:\n            \"Überprüfen Sie, ob die Ecash-Token von Ihren aktiven Mints ausgegeben wurden und entfernen Sie die ausgegebenen aus Ihrer Wallet. Verwenden Sie dies nur, wenn Ihre Wallet festhängt.\",\n        },\n        debug_console: {\n          button: \"Debug-Konsole umschalten\",\n          description:\n            \"Öffnen Sie das Javascript-Debug-Terminal. Fügen Sie niemals etwas in dieses Terminal ein, das Sie nicht verstehen. Ein Dieb könnte versuchen, Sie dazu zu bringen, bösartigen Code hier einzufügen.\",\n        },\n        export_proofs: {\n          button: \"Aktive Nachweise exportieren\",\n          description:\n            \"Kopieren Sie Ihr gesamtes Guthaben von der aktiven Mint als Cashu-Token in Ihre Zwischenablage. Dies exportiert nur die Token der ausgewählten Mint und Einheit. Für einen vollständigen Export wählen Sie eine andere Mint und Einheit und exportieren Sie erneut.\",\n        },\n        keyset_counters: {\n          title: \"Keyset-Zähler erhöhen\",\n          description:\n            'Klicken Sie auf die Keyset-ID, um die Ableitungspfad-Zähler für die Keysets in Ihrer Wallet zu erhöhen. Dies ist nützlich, wenn Sie die Fehlermeldung \"outputs have already been signed\" sehen.',\n          counter: \"Zähler: {count}\",\n        },\n        unset_reserved: {\n          button: \"Alle reservierten Token freigeben\",\n          description:\n            'Diese Wallet markiert ausstehenden ausgehenden Ecash als reserviert (und zieht es von Ihrem Guthaben ab), um Double-Spend-Versuche zu verhindern. Dieser Button gibt alle reservierten Token frei, damit sie wieder verwendet werden können. Wenn Sie dies tun, könnte Ihre Wallet ausgegebene Nachweise enthalten. Drücken Sie auf den Button \"Ausgegebene Nachweise entfernen\", um sie loszuwerden.',\n        },\n        show_onboarding: {\n          button: \"Onboarding anzeigen\",\n          description: \"Zeigen Sie den Onboarding-Bildschirm erneut an.\",\n        },\n        reset_wallet: {\n          button: \"Wallet-Daten zurücksetzen\",\n          description:\n            \"Setzen Sie Ihre Wallet-Daten zurück. Warnung: Dies löscht alles! Stellen Sie sicher, dass Sie vorher eine Sicherung erstellen.\",\n          confirm_question:\n            \"Sind Sie sicher, dass Sie Ihre Wallet-Daten löschen möchten?\",\n          cancel: \"Abbrechen\",\n          confirm: \"Wallet löschen\",\n        },\n        export_wallet: {\n          button: \"Wallet-Daten exportieren\",\n          description:\n            \"Laden Sie einen Dump Ihrer Wallet herunter. Sie können Ihre Wallet aus dieser Datei auf dem Willkommensbildschirm einer neuen Wallet wiederherstellen. Diese Datei ist nicht synchron, wenn Sie Ihre Wallet nach dem Export weiter verwenden.\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"Einer Mint beitreten\",\n    subtitle:\n      \"Sie sind noch keiner Cashu Mint beigetreten. Fügen Sie eine Mint URL in den Einstellungen hinzu oder empfangen Sie Ecash von einer neuen Mint, um zu beginnen.\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"Ecash empfangen\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"Verlauf\",\n      },\n      invoices: {\n        label: \"Rechnungen\",\n      },\n      mints: {\n        label: \"Mints\",\n      },\n    },\n    install: {\n      text: \"Installieren\",\n      tooltip: \"Cashu installieren\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"Nein.\",\n    text: \"Ein anderer Tab läuft bereits. Schließen Sie diesen Tab und versuchen Sie es erneut.\",\n    actions: {\n      retry: {\n        label: \"Erneut versuchen\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"Ups. Nichts gefunden…\",\n    actions: {\n      home: {\n        label: \"Zurück zur Startseite\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Mint\",\n    },\n    mintBalance: {\n      label: \"Guthaben\",\n    },\n    mintError: {\n      label: \"Mint-Fehler\",\n    },\n    pending: {\n      label: \"Ausstehend\",\n      tooltip: \"Alle ausstehenden Token prüfen\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"Zurück\",\n      },\n      next: {\n        label: \"Weiter\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"Willkommen bei Cashu\",\n    text: \"Cashu.me ist eine kostenlose und quelloffene Bitcoin-Wallet, die Ecash verwendet, um Ihre Gelder sicher und privat zu halten.\",\n    actions: {\n      more: {\n        label: \"Klicken, um mehr zu erfahren\",\n      },\n    },\n    p1: {\n      text: \"Cashu ist ein kostenloses und quelloffenes Ecash-Protokoll für Bitcoin. Mehr dazu erfahren Sie unter { link }.\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"Diese Wallet ist nicht mit einer Mint affiliiert. Um diese Wallet zu nutzen, müssen Sie sich mit einer oder mehreren Cashu Mints verbinden, denen Sie vertrauen.\",\n    },\n    p3: {\n      text: \"Diese Wallet speichert Ecash, auf das nur Sie Zugriff haben. Wenn Sie Ihre Browserdaten ohne Seed-Phrase-Sicherung löschen, verlieren Sie Ihre Token.\",\n    },\n    p4: {\n      text: \"Diese Wallet ist in Beta. Wir übernehmen keine Verantwortung für den Verlust des Zugangs zu Geldern. Nutzung auf eigenes Risiko! Dieser Code ist quelloffen und unter der MIT-Lizenz lizenziert.\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"PWA installieren\",\n    alt: { pwa_example: \"PWA Installationsbeispiel\" },\n    installing: \"Installiere…\",\n    instruction: {\n      intro: {\n        text: \"Für die beste Erfahrung verwenden Sie diese Wallet mit dem nativen Webbrowser Ihres Geräts, um sie als Progressive Web App zu installieren. Machen Sie dies jetzt.\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Tippen Sie auf das Menü (oben rechts)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Drücken Sie { buttonText }\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Tippen Sie auf Teilen (unten)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Drücken Sie { buttonText }\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"Nachdem Sie diese App auf Ihrem Gerät installiert haben, schließen Sie dieses Browserfenster und verwenden Sie die App von Ihrem Startbildschirm aus.\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"Erfolg!\",\n        text: \"Sie verwenden Cashu als PWA. Schließen Sie alle anderen geöffneten Browserfenster und verwenden Sie die App von Ihrem Startbildschirm aus.\",\n        nextSteps:\n          \"Sie können nun diesen Tab schließen und die App vom Startbildschirm öffnen.\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"Tippen Sie auf { icon } und { buttonText }\",\n    buttonText: \"Zum Home-Bildschirm\",\n  },\n  AndroidPWAPrompt: {\n    text: \"Tippen Sie auf { icon } und { buttonText }\",\n    buttonText: \"Zum Startbildschirm hinzufügen\",\n  },\n  WelcomeSlide3: {\n    title: \"Ihre Seed-Phrase\",\n    text: \"Speichern Sie Ihre Seed-Phrase in einem Passwortmanager oder auf Papier. Ihre Seed-Phrase ist der einzige Weg, Ihre Gelder wiederherzustellen, wenn Sie den Zugriff auf dieses Gerät verlieren.\",\n    inputs: {\n      seed_phrase: {\n        label: \"Seed-Phrase\",\n        caption: \"Sie können Ihre Seed-Phrase in den Einstellungen sehen.\",\n      },\n      checkbox: {\n        label: \"Ich habe sie aufgeschrieben\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"Bedingungen\",\n    actions: {\n      more: {\n        label: \"Nutzungsbedingungen lesen\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"Ich habe diese Bedingungen gelesen und akzeptiere sie\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"Richten Sie Ihre Wallet ein\",\n    text: \"Möchten Sie aus einer Seed-Phrase wiederherstellen oder eine neue Wallet erstellen?\",\n    options: {\n      new: {\n        title: \"Neue Wallet erstellen\",\n        subtitle: \"Neue Seed erzeugen und Mints hinzufügen.\",\n      },\n      recover: {\n        title: \"Wallet wiederherstellen\",\n        subtitle: \"Seed-Phrase eingeben, Mints und Ecash wiederherstellen.\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"Mints hinzufügen\",\n    text: \"Mints sind Server, die beim Senden und Empfangen von Ecash helfen. Wählen Sie eine gefundene Mint oder fügen Sie manuell eine hinzu. Sie können dies auch später tun.\",\n    sections: { your_mints: \"Ihre Mints\" },\n    restoring: \"Mints werden wiederhergestellt…\",\n    placeholder: { mint_url: \"https://\" },\n  },\n  WelcomeRecoverSeed: {\n    title: \"Seed-Phrase eingeben\",\n    text: \"Fügen Sie Ihre 12 Wörter ein oder tippen Sie sie ein, um wiederherzustellen.\",\n    inputs: { word: \"Wort { index }\" },\n    actions: { paste_all: \"Alle einfügen\" },\n    disclaimer:\n      \"Ihre Seed-Phrase wird nur lokal verwendet, um Ihre Wallet-Schlüssel abzuleiten.\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"Ihr Ecash wiederherstellen\",\n    text: \"Scannen Sie nach nicht ausgegebenen Nachweisen auf Ihren konfigurierten Mints und fügen Sie sie Ihrer Wallet hinzu.\",\n  },\n  MintRatings: {\n    title: \"Mint-Bewertungen\",\n    reviews: \"Bewertungen\",\n    ratings: \"Bewertungen\",\n    no_reviews: \"Keine Bewertungen gefunden\",\n    your_review: \"Ihre Bewertung\",\n    no_reviews_to_display: \"Keine Bewertungen anzuzeigen.\",\n    no_rating: \"Keine Bewertung\",\n    out_of: \"von\",\n    rows: \"Reviews\",\n    sort: \"Sortieren\",\n    sort_options: {\n      newest: \"Neueste\",\n      oldest: \"Älteste\",\n      highest: \"Höchste\",\n      lowest: \"Niedrigste\",\n    },\n    actions: { write_review: \"Bewertung schreiben\" },\n    empty_state_subtitle:\n      \"Helfen Sie, indem Sie eine Bewertung hinterlassen. Teilen Sie Ihre Erfahrungen mit diesem Mint und helfen Sie anderen, indem Sie eine Bewertung hinterlassen.\",\n  },\n  CreateMintReview: {\n    title: \"Mint bewerten\",\n    publishing_as: \"Veröffentlichen als\",\n    inputs: {\n      rating: { label: \"Bewertung\" },\n      review: { label: \"Rezension (optional)\" },\n    },\n    actions: {\n      publish: { label: \"Veröffentlichen\", in_progress: \"Veröffentlichen…\" },\n    },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"Aus Seed-Phrase wiederherstellen\",\n      caption:\n        \"Geben Sie Ihre Seed-Phrase ein, um Ihre Wallet wiederherzustellen. Stellen Sie vor der Wiederherstellung sicher, dass Sie alle Mints hinzugefügt haben, die Sie zuvor verwendet haben.\",\n      inputs: {\n        seed_phrase: {\n          label: \"Seed-Phrase\",\n          caption: \"Sie können Ihre Seed-Phrase in den Einstellungen sehen.\",\n        },\n      },\n    },\n    information: {\n      label: \"Information\",\n      caption:\n        \"Der Assistent stellt nur Ecash von einer anderen Seed-Phrase wieder her. Sie können diese Seed-Phrase nicht verwenden oder die Seed-Phrase der aktuell verwendeten Wallet ändern. Das bedeutet, dass wiederhergestellter Ecash nicht durch Ihre aktuelle Seed-Phrase geschützt ist, solange Sie den Ecash nicht einmal an sich selbst senden.\",\n    },\n    restore_mints: {\n      label: \"Mints wiederherstellen\",\n      caption:\n        'Wählen Sie die Mint zur Wiederherstellung aus. Sie können weitere Mints im Hauptbildschirm unter \"Mints\" hinzufügen und sie hier wiederherstellen.',\n    },\n    actions: {\n      paste: {\n        error: \"Zwischenablage-Inhalt konnte nicht gelesen werden.\",\n      },\n      validate: {\n        error: \"Mnemonisch muss mindestens 12 Wörter enthalten.\",\n      },\n      select_all: {\n        label: \"Alle auswählen\",\n      },\n      deselect_all: {\n        label: \"Alle abwählen\",\n      },\n      restore: {\n        label: \"Wiederherstellen\",\n        in_progress: \"Mint wird wiederhergestellt…\",\n        error: \"Fehler beim Wiederherstellen der Mint: { error }\",\n      },\n      restore_all_mints: {\n        label: \"Alle Mints wiederherstellen\",\n        in_progress: \"Mint { index } von { length } wird wiederhergestellt…\",\n        success: \"Wiederherstellung erfolgreich abgeschlossen\",\n        error: \"Fehler beim Wiederherstellen der Mints: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"Ausgewählte Mints wiederherstellen ({count})\",\n        in_progress: \"Mint { index } von { length } wird wiederhergestellt…\",\n        success: \"{count} Mint(s) erfolgreich wiederhergestellt\",\n        error: \"Fehler beim Wiederherstellen ausgewählter Mints: { error }\",\n      },\n    },\n    nostr_mints: {\n      label: \"Mints von Nostr wiederherstellen\",\n      caption:\n        \"Suchen Sie nach Mint-Backups, die auf Nostr-Relays mit Ihrer Seed-Phrase gespeichert sind. Dies hilft Ihnen, Mints zu entdecken, die Sie zuvor verwendet haben.\",\n      search_button: \"Nach Mint-Backups suchen\",\n      select_all: \"Alle auswählen\",\n      deselect_all: \"Alle abwählen\",\n      backed_up: \"Gesichert\",\n      already_added: \"Bereits hinzugefügt\",\n      add_selected: \"Ausgewählte hinzufügen ({count})\",\n      no_backups_found: \"Keine Mint-Backups gefunden\",\n      no_backups_hint:\n        \"Stellen Sie sicher, dass das Nostr-Mint-Backup in den Einstellungen aktiviert ist, um Ihre Mint-Liste automatisch zu sichern.\",\n      invalid_mnemonic:\n        \"Bitte geben Sie eine gültige Seed-Phrase ein, bevor Sie suchen.\",\n      search_error: \"Fehler bei der Suche nach Mint-Backups.\",\n      add_error: \"Fehler beim Hinzufügen ausgewählter Mints.\",\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"Mint hinzufügen\",\n      description:\n        \"Geben Sie die URL einer Cashu Mint ein, um sich mit ihr zu verbinden. Diese Wallet ist nicht mit einer Mint affiliiert.\",\n      inputs: {\n        nickname: {\n          placeholder: \"Spitzname (z.B. Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"Ungültige URL\",\n        },\n        scan: {\n          label: \"QR-Code scannen\",\n        },\n      },\n    },\n    discover: {\n      title: \"Mints entdecken\",\n      overline: \"Entdecken\",\n      caption:\n        \"Entdecken Sie Mints, die andere Benutzer auf Nostr empfohlen haben.\",\n      actions: {\n        discover: {\n          label: \"Mints entdecken\",\n          in_progress: \"Lädt…\",\n          error_no_mints: \"Keine Mints gefunden\",\n          success: \"{ length } Mints gefunden\",\n        },\n      },\n      recommendations: {\n        overline: \"{ length } Mints gefunden\",\n        caption:\n          \"Diese Mints wurden von anderen Nostr-Benutzern empfohlen. Seien Sie vorsichtig und recherchieren Sie selbst, bevor Sie eine Mint verwenden.\",\n        actions: {\n          browse: {\n            label: \"Klicken, um Mints zu durchsuchen\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"Tauschen\",\n      overline: \"Multimint-Swaps\",\n      caption:\n        \"Tauschen Sie Gelder zwischen Mints über Lightning. Hinweis: Lassen Sie Platz für potenzielle Lightning-Gebühren. Wenn die eingehende Zahlung nicht erfolgreich ist, überprüfen Sie die Rechnung manuell.\",\n      inputs: {\n        from: {\n          label: \"Von\",\n        },\n        to: {\n          label: \"Nach\",\n        },\n        amount: {\n          label: \"Betrag ({ ticker }))\",\n        },\n      },\n      actions: {\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n    },\n    error_badge: \"Fehler\",\n    reviews_text: \"Bewertungen\",\n    no_reviews_yet: \"Noch keine Bewertungen\",\n    discover_mints_button: \"Mints entdecken\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - Weiter scannen\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"Lightning empfangen\",\n    create_invoice_title: \"Rechnung erstellen\",\n    inputs: {\n      amount: {\n        label: \"Betrag ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"Rechnung erstellen\",\n        label_blocked: \"Rechnung wird erstellt…\",\n        in_progress: \"Erstellt\",\n      },\n    },\n    invoice: {\n      caption: \"Lightning Rechnung\",\n      status_paid_text: \"Bezahlt!\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"Senden\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Keine Mints verfügbar\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"Keine Mints verfügbar\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"Ecash senden\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"Offline\",\n    inputs: {\n      amount: {\n        label: \"Betrag ({ ticker }) *\",\n        invalid_too_much_error_text: \"Zu viel\",\n      },\n      p2pk_pubkey: {\n        label: \"Öffentlicher Schlüssel des Empfängers\",\n        label_invalid: \"Öffentlicher Schlüssel des Empfängers\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"Emoji kopieren\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"Link kopieren\",\n      },\n      share: {\n        tooltip_text: \"Ecash teilen\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"Aus Verlauf löschen\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"Auf NFC-Karte schreiben\",\n          ndef_unsupported_text: \"NDEF nicht unterstützt\",\n        },\n      },\n    },\n  },\n  ReceiveDialog: {\n    title: \"Empfangen\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Keine Mints verfügbar\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints:\n          \"Sie müssen sich mit einer Mint verbinden, um über Lightning zu empfangen\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"Ecash empfangen\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"Anfordern\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"Scannt…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"Ecash empfangen\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"Cashu Token einfügen\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"Ungültiger Token\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"Kann nicht empfangen werden. Die P2PK-Sperre dieses Tokens stimmt nicht mit Ihrem öffentlichen Schlüssel überein.\",\n      },\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"Mint wird hinzugefügt…\",\n      },\n      swap: {\n        label: \"@:global.actions.swap.label\",\n        tooltip_text: \"Zu einer vertrauenswürdigen Mint tauschen\",\n        caption: \"Tauschen { value }\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"Swap abbrechen\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      later: {\n        label: \"Später empfangen\",\n        tooltip_text: \"Zum Verlauf hinzufügen, um später zu empfangen\",\n        already_in_history_success_text: \"Ecash bereits im Verlauf\",\n        added_to_history_success_text: \"Ecash zum Verlauf hinzugefügt\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"Von NFC-Karte lesen\",\n          ndef_unsupported_text: \"NDEF nicht unterstützt\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"P2PK Schlüssel\",\n      description: \"Ecash empfangen, der auf diesen Schlüssel gesperrt ist\",\n      used_warning_text:\n        \"Warnung: Dieser Schlüssel wurde bereits verwendet. Verwenden Sie einen neuen Schlüssel für besseren Datenschutz.\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"Neuen Schlüssel generieren\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"Zahlungsanforderung\",\n      description: \"Zahlungen über Nostr empfangen\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"Neue Anforderung\",\n      },\n      add_amount: {\n        label: \"Betrag hinzufügen\",\n      },\n      use_active_mint: {\n        label: \"Beliebige Mint\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"Betrag eingeben\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"Tastatur deaktiviert. Sie können die Tastatur in den Einstellungen wieder aktivieren.\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostr Wallet Connect\",\n      description:\n        \"Steuern Sie Ihre Wallet per Fernzugriff mit NWC. Tippen Sie auf den QR-Code, um Ihre Wallet mit einer kompatiblen App zu verknüpfen.\",\n      warning_text:\n        \"Warnung: Jeder, der Zugriff auf diesen Verbindungsstring hat, kann Zahlungen von Ihrer Wallet initiieren. Nicht teilen!\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"Mint Nachricht\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"Kontakt\",\n    },\n    details: {\n      title: \"Mint Details\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"Alle anzeigen\",\n          },\n          hide: {\n            label: \"Ausblenden\",\n          },\n        },\n      },\n      currency: {\n        label: \"Währung\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"Version\",\n      },\n    },\n    actions: {\n      title: \"Aktionen\",\n      copy_mint_url: {\n        label: \"Mint URL kopieren\",\n      },\n      delete: {\n        label: \"Mint löschen\",\n      },\n      edit: {\n        label: \"Mint bearbeiten\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"Wählen Sie eine Mint\",\n    badge_mint_error_text: \"Fehler\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"Noch kein Verlauf vorhanden\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"Vor { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Status prüfen\",\n      },\n      receive: {\n        tooltip_text: \"Empfangen\",\n      },\n      filter_pending: {\n        label: \"Ausstehende filtern\",\n      },\n      show_all: {\n        label: \"Alle anzeigen\",\n      },\n    },\n    old_token_not_found_error_text: \"Alter Token nicht gefunden\",\n  },\n  InvoiceTable: {\n    empty_text: \"Noch keine Rechnungen vorhanden\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"Zum Kopieren klicken\",\n      date_label: \"Vor { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Status prüfen\",\n      },\n      filter_pending: {\n        label: \"Ausstehende filtern\",\n      },\n      show_all: {\n        label: \"Alle anzeigen\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"Sind Sie sicher, dass Sie diese Mint löschen möchten?\",\n    nickname: {\n      label: \"Spitzname\",\n    },\n    balances: {\n      label: \"Guthaben\",\n    },\n    warning_text:\n      \"Hinweis: Da diese Wallet paranoid ist, wird Ihr Ecash von dieser Mint nicht wirklich gelöscht, sondern auf Ihrem Gerät gespeichert bleiben. Sie werden ihn wieder sehen, wenn Sie diese Mint später erneut hinzufügen.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"Mint entfernen\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"Cashu Token oder Lightning-Adresse\",\n      receive: \"Cashu Token\",\n      pay: \"Lightning-Adresse oder Rechnung\",\n    },\n    qr_scanner: {\n      title: \"QR-Code scannen\",\n      description: \"Tippen Sie, um eine Adresse zu scannen\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"Lightning bezahlen\",\n      inputs: {\n        invoice_data: {\n          label: \"Lightning-Rechnung oder Adresse\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"{ payee } fordert { value } { ticker } an\",\n      amount_range_label:\n        \"{ payee } fordert{br}zwischen { min } und { max } { ticker } an\",\n      sending_to_lightning_address: \"Senden an { address }\",\n      inputs: {\n        amount: {\n          label: \"Betrag ({ ticker }) *\",\n        },\n        comment: {\n          label: \"Kommentar (optional)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"{ value } bezahlen\",\n      paying: \"Wird bezahlt\",\n      paid: \"Bezahlt\",\n      fee: \"Gebühr\",\n      memo: {\n        label: \"Memo\",\n      },\n      processing_info_text: \"Wird verarbeitet…\",\n      balance_too_low_warning_text: \"Guthaben zu niedrig\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"Bezahlen\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"Fehler\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"Mint bearbeiten\",\n    inputs: {\n      nickname: {\n        label: \"Spitzname\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"Vertrauen Sie dieser Mint?\",\n    description:\n      \"Bevor Sie diese Mint verwenden, stellen Sie sicher, dass Sie ihr vertrauen. Mints könnten bösartig werden oder jederzeit den Betrieb einstellen.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"Mint wird hinzugefügt\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"Bitte geben Sie ein Mnemonisch ein\",\n    restore_mint_error_text: \"Fehler beim Wiederherstellen der Mint: { error }\",\n    prepare_info_text: \"Wiederherstellungsprozess wird vorbereitet…\",\n    restored_proofs_for_keyset_info_text:\n      \"{ restoreCounter } Nachweise für Keyset { keysetId } wiederhergestellt\",\n    checking_proofs_for_keyset_info_text:\n      \"Prüfe Nachweise { startIndex } bis { endIndex } für Keyset { keysetId }\",\n    no_proofs_info_text: \"Keine Nachweise zum Wiederherstellen gefunden\",\n    restored_amount_success_text: \"{ amount } wiederhergestellt\",\n  },\n  swap: {\n    in_progress_warning_text: \"Swap läuft\",\n    invalid_swap_data_error_text: \"Ungültige Swap-Daten\",\n    swap_error_text: \"Fehler beim Tauschen\",\n  },\n  TokenInformation: {\n    fee: \"Gebühr\",\n    unit: \"Einheit\",\n    fiat: \"Fiat\",\n    p2pk: \"P2PK\",\n    locked: \"Gesperrt\",\n    locked_to_you: \"An dich gesperrt\",\n    mint: \"Münzstätte\",\n    memo: \"Notiz\",\n    payment_request: \"Zahlungsanforderung\",\n    nostr: \"Nostr\",\n    token_copied: \"Token in Zwischenablage kopiert\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/el-GR/index.ts",
    "content": "export default {\n  MultinutPicker: {\n    payment: \"Πληρωμή Multinut\",\n    selectMints:\n      \"Επιλέξτε ένα ή περισσότερα mints για να εκτελέσετε μια πληρωμή.\",\n    totalSelectedBalance: \"Συνολικό επιλεγμένο υπόλοιπο\",\n    multiMintPay: \"Πληρωμή Multi-Mint\",\n    balanceNotEnough:\n      \"Το υπόλοιπο πολλών νομισματοκοπείων δεν επαρκεί για την κάλυψη αυτού του τιμολογίου\",\n    failed: \"Η επεξεργασία απέτυχε: {error}\",\n    paid: \"Πληρώθηκε {amount} μέσω Lightning\",\n  },\n  global: {\n    copy_to_clipboard: {\n      success: \"Αντιγράφηκε στο πρόχειρο!\",\n    },\n    actions: {\n      add_mint: {\n        label: \"Προσθήκη mint\",\n      },\n      cancel: {\n        label: \"Ακύρωση\",\n      },\n      copy: {\n        label: \"Αντιγραφή\",\n      },\n      close: {\n        label: \"Κλείσιμο\",\n      },\n      enter: {\n        label: \"Είσοδος\",\n      },\n      lock: {\n        label: \"Κλείδωμα\",\n      },\n      paste: {\n        label: \"Επικόλληση\",\n      },\n      receive: {\n        label: \"Λήψη\",\n      },\n      scan: {\n        label: \"Σάρωση\",\n      },\n      send: {\n        label: \"Αποστολή\",\n      },\n      swap: {\n        label: \"Ανταλλαγή\",\n      },\n      update: {\n        label: \"Ενημέρωση\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"URL Mint\",\n      },\n    },\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"Το υπόλοιπο είναι πολύ χαμηλό\",\n      received: \"Λήφθηκε {amount}\",\n      fee: \" (προμήθεια: {fee})\",\n      could_not_request_mint: \"Δεν ήταν δυνατή η αίτηση στο mint\",\n      invoice_still_pending: \"Το τιμολόγιο εκκρεμεί ακόμα\",\n      paid_lightning: \"Πληρώθηκε {amount} μέσω Lightning\",\n      payment_pending_refresh:\n        \"Πληρωμή σε εκκρεμότητα. Ανανεώστε το τιμολόγιο χειροκίνητα.\",\n      sent: \"Στάλθηκε {amount}\",\n      token_still_pending: \"Το token εκκρεμεί ακόμα\",\n      received_lightning: \"Λήφθηκε {amount} μέσω Lightning\",\n      lightning_payment_failed: \"Η πληρωμή Lightning απέτυχε\",\n      failed_to_decode_invoice: \"Αποτυχία αποκωδικοποίησης τιμολογίου\",\n      invalid_lnurl: \"Μη έγκυρο LNURL\",\n      lnurl_error: \"Σφάλμα LNURL\",\n      no_amount: \"Δεν υπάρχει ποσό\",\n      no_lnurl_data: \"Δεν υπάρχουν δεδομένα LNURL\",\n      no_price_data: \"Δεν υπάρχουν δεδομένα τιμής.\",\n      please_try_again: \"Παρακαλώ προσπαθήστε ξανά.\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"Το mint έχει ήδη προστεθεί\",\n        added: \"Το mint προστέθηκε\",\n        not_found: \"Το mint δεν βρέθηκε\",\n        activation_failed: \"Η ενεργοποίηση του mint απέτυχε\",\n        no_active_mint: \"Δεν υπάρχει ενεργό mint\",\n        unit_activation_failed: \"Η ενεργοποίηση μονάδας απέτυχε\",\n        unit_not_supported: \"Η μονάδα δεν υποστηρίζεται από το mint\",\n        activated: \"Το mint ενεργοποιήθηκε\",\n        could_not_connect: \"Δεν ήταν δυνατή η σύνδεση στο mint\",\n        could_not_get_info: \"Δεν ήταν δυνατή η λήψη πληροφοριών mint\",\n        could_not_get_keys: \"Δεν ήταν δυνατή η λήψη κλειδιών mint\",\n        could_not_get_keysets: \"Δεν ήταν δυνατή η λήψη συνόλων κλειδιών mint\",\n        mint_validation_error: \"Σφάλμα επικύρωσης Mint\",\n        removed: \"Το mint αφαιρέθηκε\",\n        error: \"Σφάλμα mint\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"Ρυθμίσεις\",\n        settings: {\n          title: \"Ρυθμίσεις\",\n          caption: \"Διαμόρφωση πορτοφολιού\",\n        },\n      },\n      terms: {\n        title: \"Όροι\",\n        terms: {\n          title: \"Όροι\",\n          caption: \"Όροι Παροχής Υπηρεσιών\",\n        },\n      },\n      links: {\n        title: \"Σύνδεσμοι\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"Δωρεά\",\n          caption: \"Υποστήριξη Cashu\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"Εκτός σύνδεσης\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"Επαναφόρτωση σε { countdown }\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"Staging – μην το χρησιμοποιείτε με πραγματικά κεφάλαια!\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"Πορτοφόλι\",\n      },\n    },\n  },\n  Settings: {\n    language: {\n      title: \"Γλώσσα\",\n      description:\n        \"Παρακαλώ επιλέξτε την προτιμώμενη γλώσσα σας από την παρακάτω λίστα.\",\n    },\n    sections: {\n      backup_restore: \"ΑΝΤΙΓΡΑΦΟ ΑΣΦΑΛΕΙΑΣ & ΕΠΑΝΑΦΟΡΑ\",\n      lightning_address: \"ΔΙΕΥΘΥΝΣΗ LIGHTNING\",\n      nostr_keys: \"ΚΛΕΙΔΙΑ NOSTR\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"Κάντε κλικ για να επεξεργαστείτε τα relays\",\n          add: {\n            title: \"Προσθήκη relay\",\n            description:\n              \"Το πορτοφόλι σας χρησιμοποιεί αυτά τα relays για λειτουργίες nostr όπως αιτήματα πληρωμής, nostr wallet connect και αντίγραφα ασφαλείας.\",\n          },\n          list: {\n            title: \"Relays\",\n            description: \"Το πορτοφόλι σας θα συνδεθεί σε αυτά τα relays.\",\n            copy_tooltip: \"Αντιγραφή relay\",\n            remove_tooltip: \"Κατάργηση relay\",\n          },\n        },\n      },\n      payment_requests: \"ΑΙΤΗΜΑΤΑ ΠΛΗΡΩΜΗΣ\",\n      nostr_wallet_connect: \"NOSTR WALLET CONNECT\",\n      hardware_features: \"ΧΑΡΑΚΤΗΡΙΣΤΙΚΑ ΥΛΙΚΟΥ\",\n      p2pk_features: \"ΧΑΡΑΚΤΗΡΙΣΤΙΚΑ P2PK\",\n      privacy: \"ΑΠΟΡΡΗΤΟ\",\n      experimental: \"ΠΕΙΡΑΜΑΤΙΚΟ\",\n      appearance: \"ΕΜΦΑΝΙΣΗ\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"Φράση seed αντιγράφου ασφαλείας\",\n        description:\n          \"Η φράση seed σας μπορεί να επαναφέρει το πορτοφόλι σας. Κρατήστε την ασφαλή και ιδιωτική.\",\n        seed_phrase_label: \"Φράση seed\",\n      },\n      restore_ecash: {\n        title: \"Επαναφορά ecash\",\n        description:\n          \"Ο οδηγός επαναφοράς σάς επιτρέπει να ανακτήσετε χαμένο ecash από μια μνημονική φράση seed. Η φράση seed του τρέχοντος πορτοφολιού σας θα παραμείνει ανεπηρέαστη, ο οδηγός θα σας επιτρέψει μόνο να επαναφέρετε ecash από μια άλλη φράση seed.\",\n        button: \"Επαναφορά\",\n      },\n    },\n    lightning_address: {\n      title: \"Διεύθυνση Lightning\",\n      description: \"Λάβετε πληρωμές στη διεύθυνση Lightning σας.\",\n      enable: {\n        toggle: \"Ενεργοποίηση\",\n        description: \"Διεύθυνση Lightning με npub.cash\",\n      },\n      address: {\n        copy_tooltip: \"Αντιγραφή διεύθυνσης Lightning\",\n      },\n      automatic_claim: {\n        toggle: \"Αυτόματη διεκδίκηση\",\n        description: \"Λήψη εισερχόμενων πληρωμών αυτόματα.\",\n      },\n      npc_v2: {\n        choose_mint_title: \"Επιλέξτε mint για npub.cash v2\",\n        choose_mint_placeholder: \"Επιλέξτε ένα mint...\",\n      },\n    },\n    nostr_keys: {\n      title: \"Τα κλειδιά σας nostr\",\n      description: \"Ορίστε τα κλειδιά nostr για τη διεύθυνση Lightning σας.\",\n      wallet_seed: {\n        title: \"Φράση seed πορτοφολιού\",\n        description:\n          \"Δημιουργία ζεύγους κλειδιών nostr από τη seed του πορτοφολιού\",\n        copy_nsec: \"Αντιγραφή nsec\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"Χρήση bunker NIP-46\",\n        delete_tooltip: \"Διαγραφή σύνδεσης\",\n      },\n      use_nsec: {\n        title: \"Χρησιμοποιήστε το nsec σας\",\n        description: \"Αυτή η μέθοδος είναι επικίνδυνη και δεν συνιστάται\",\n        delete_tooltip: \"Διαγραφή nsec\",\n      },\n      signing_extension: {\n        title: \"Επέκταση υπογραφής\",\n        description: \"Χρήση επέκτασης υπογραφής NIP-07\",\n        not_found: \"Δεν βρέθηκε επέκταση υπογραφής NIP-07\",\n      },\n    },\n    payment_requests: {\n      title: \"Αιτήματα πληρωμής\",\n      description:\n        \"Τα αιτήματα πληρωμής σάς επιτρέπουν να λαμβάνετε πληρωμές μέσω nostr. Εάν το ενεργοποιήσετε, το πορτοφόλι σας θα εγγραφεί στα relays nostr σας.\",\n      enable_toggle: \"Ενεργοποίηση Αιτημάτων Πληρωμής\",\n      claim_automatically: {\n        toggle: \"Αυτόματη διεκδίκηση\",\n        description: \"Λήψη εισερχόμενων πληρωμών αυτόματα.\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"Nostr Wallet Connect (NWC)\",\n      description:\n        \"Χρησιμοποιήστε το NWC για να ελέγξετε το πορτοφόλι σας από οποιαδήποτε άλλη εφαρμογή.\",\n      enable_toggle: \"Ενεργοποίηση NWC\",\n      payments_note:\n        \"Μπορείτε να χρησιμοποιήσετε το NWC μόνο για πληρωμές από το υπόλοιπο Bitcoin σας. Οι πληρωμές θα γίνονται από το ενεργό σας mint.\",\n      connection: {\n        copy_tooltip: \"Αντιγραφή συμβολοσειράς σύνδεσης\",\n        qr_tooltip: \"Εμφάνιση κωδικού QR\",\n        allowance_label: \"Υπολειπόμενο όριο (sat)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"Επιλέξτε την κωδικοποίηση για εγγραφή σε κάρτες NFC\",\n        text: {\n          title: \"Κείμενο\",\n          description: \"Αποθήκευση token σε απλό κείμενο\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"Αποθήκευση URL σε αυτό το πορτοφόλι με token\",\n        },\n        binary: {\n          title: \"Δυαδικό\",\n          description: \"Αποθήκευση tokens ως δυαδικά δεδομένα\",\n        },\n        quick_access: {\n          toggle: \"Γρήγορη πρόσβαση σε NFC\",\n          description:\n            \"Γρήγορη σάρωση καρτών NFC στο μενού Λήψη Ecash. Αυτή η επιλογή προσθέτει ένα κουμπί NFC στο μενού Λήψη Ecash.\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"Δημιουργία ζεύγους κλειδιών για λήψη ecash κλειδωμένου με P2PK. Προειδοποίηση: Αυτή η δυνατότητα είναι πειραματική. Χρησιμοποιήστε μόνο με μικρά ποσά. Εάν χάσετε τα ιδιωτικά σας κλειδιά, κανείς δεν θα μπορεί πλέον να ξεκλειδώσει το ecash που είναι κλειδωμένο σε αυτά.\",\n      generate_button: \"Δημιουργία κλειδιού\",\n      import_button: \"Εισαγωγή nsec\",\n      quick_access: {\n        toggle: \"Γρήγορη πρόσβαση στο κλείδωμα\",\n        description:\n          \"Χρησιμοποιήστε το για να εμφανίσετε γρήγορα το κλειδί κλειδώματος P2PK στο μενού λήψης ecash.\",\n      },\n      keys_expansion: {\n        label: \"Κάντε κλικ για περιήγηση σε {count} κλειδιά\",\n        used_badge: \"χρησιμοποιημένο\",\n      },\n    },\n    privacy: {\n      title: \"Απόρρητο\",\n      description: \"Αυτές οι ρυθμίσεις επηρεάζουν το απόρρητό σας.\",\n      check_incoming: {\n        toggle: \"Έλεγχος εισερχόμενου τιμολογίου\",\n        description:\n          \"Εάν είναι ενεργοποιημένο, το πορτοφόλι θα ελέγχει το τελευταίο τιμολόγιο στο παρασκήνιο. Αυτό αυξάνει την απόκριση του πορτοφολιού, καθιστώντας ευκολότερο το fingerprinting. Μπορείτε να ελέγξετε μη αυτόματα τα απλήρωτα τιμολόγια στην καρτέλα Τιμολόγια.\",\n      },\n      check_startup: {\n        toggle: \"Έλεγχος εκκρεμών τιμολογίων κατά την εκκίνηση\",\n        description:\n          \"Εάν είναι ενεργοποιημένο, το πορτοφόλι θα ελέγχει τα εκκρεμή τιμολόγια των τελευταίων 24 ωρών κατά την εκκίνηση.\",\n      },\n      check_all: {\n        toggle: \"Έλεγχος όλων των τιμολογίων\",\n        description:\n          \"Εάν είναι ενεργοποιημένο, το πορτοφόλι θα ελέγχει περιοδικά τα απλήρωτα τιμολόγια στο παρασκήνιο για έως και δύο εβδομάδες. Αυτό αυξάνει τη διαδικτυακή δραστηριότητα του πορτοφολιού, καθιστώντας ευκολότερο το fingerprinting. Μπορείτε να ελέγξετε μη αυτόματα τα απλήρωτα τιμολόγια στην καρτέλα Τιμολόγια.\",\n      },\n      check_sent: {\n        toggle: \"Έλεγχος απεσταλμένου ecash\",\n        description:\n          \"Εάν είναι ενεργοποιημένο, το πορτοφόλι θα χρησιμοποιεί περιοδικούς ελέγχους παρασκηνίου για να προσδιορίσει εάν τα απεσταλμένα token έχουν εξαργυρωθεί. Αυτό αυξάνει τη διαδικτυακή δραστηριότητα του πορτοφολιού, καθιστώντας ευκολότερο το fingerprinting.\",\n      },\n      websockets: {\n        toggle: \"Χρήση WebSockets\",\n        description:\n          \"Εάν είναι ενεργοποιημένο, το πορτοφόλι θα χρησιμοποιεί μακροχρόνιες συνδέσεις WebSocket για λήψη ενημερώσεων σχετικά με πληρωμένα τιμολόγια και δαπανημένα token από τα mints. Αυτό αυξάνει την απόκριση του πορτοφολιού αλλά καθιστά επίσης ευκολότερο το fingerprinting.\",\n      },\n      bitcoin_price: {\n        toggle: \"Λήψη συναλλαγματικής ισοτιμίας από Coinbase\",\n        description:\n          \"Εάν είναι ενεργοποιημένο, η τρέχουσα συναλλαγματική ισοτιμία Bitcoin θα ληφθεί από το coinbase.com και θα εμφανιστεί το μετατραπέν υπόλοιπό σας.\",\n        currency: {\n          title: \"Νόμισμα Fiat\",\n          description:\n            \"Επιλέξτε το νόμισμα fiat για την εμφάνιση της τιμής Bitcoin.\",\n        },\n      },\n    },\n    experimental: {\n      title: \"Πειραματικό\",\n      description: \"Αυτές οι δυνατότητες είναι πειραματικές.\",\n      receive_swaps: {\n        toggle: \"Λήψη ανταλλαγών\",\n        badge: \"Beta\",\n        description:\n          \"Επιλογή ανταλλαγής του ληφθέντος Ecash στο ενεργό σας mint στον διάλογο Λήψη Ecash.\",\n      },\n      auto_paste: {\n        toggle: \"Αυτόματη επικόλληση Ecash\",\n        description:\n          \"Αυτόματη επικόλληση του ecash στο πρόχειρό σας όταν πατάτε Λήψη, μετά Ecash, μετά Επικόλληση. Η αυτόματη επικόλληση μπορεί να προκαλέσει δυσλειτουργίες στο UI στο iOS, απενεργοποιήστε την εάν αντιμετωπίζετε προβλήματα.\",\n      },\n      auditor: {\n        toggle: \"Ενεργοποίηση ελεγκτή\",\n        badge: \"Beta\",\n        description:\n          \"Εάν είναι ενεργοποιημένο, το πορτοφόλι θα εμφανίζει πληροφορίες ελεγκτή στον διάλογο λεπτομερειών του mint. Ο ελεγκτής είναι μια υπηρεσία τρίτου μέρους που παρακολουθεί την αξιοπιστία των mints.\",\n        url_label: \"URL Ελεγκτή\",\n        api_url_label: \"URL API Ελεγκτή\",\n      },\n      multinut: {\n        toggle: \"Ενεργοποίηση Multinut\",\n        description:\n          \"Εάν είναι ενεργοποιημένο, το πορτοφόλι θα χρησιμοποιεί το Multinut για την πληρωμή τιμολογίων από πολλαπλά mints ταυτόχρονα.\",\n      },\n      nostr_mint_backup: {\n        toggle: \"Δημιουργία αντιγράφων ασφαλείας της λίστας mint στο Nostr\",\n        description:\n          \"Εάν είναι ενεργοποιημένο, η λίστα mint σας θα δημιουργείται αυτόματα αντίγραφα ασφαλείας στα ρελέ Nostr χρησιμοποιώντας τα διαμορφωμένα κλειδιά Nostr. Αυτό σας επιτρέπει να επαναφέρετε τη λίστα mint σας σε όλες τις συσκευές.\",\n        notifications: {\n          enabled: \"Ενεργοποιήθηκε το αντίγραφο ασφαλείας Nostr mint\",\n          disabled: \"Απενεργοποιήθηκε το αντίγραφο ασφαλείας Nostr mint\",\n          failed: \"Αποτυχία ενεργοποίησης του αντιγράφου ασφαλείας Nostr mint\",\n        },\n      },\n    },\n    appearance: {\n      keyboard: {\n        title: \"Πληκτρολόγιο οθόνης\",\n        description:\n          \"Χρησιμοποιήστε το αριθμητικό πληκτρολόγιο για την εισαγωγή ποσών.\",\n        toggle: \"Χρήση αριθμητικού πληκτρολογίου\",\n        toggle_description:\n          \"Εάν είναι ενεργοποιημένο, θα χρησιμοποιείται το αριθμητικό πληκτρολόγιο για την εισαγωγή ποσών.\",\n      },\n      theme: {\n        title: \"Εμφάνιση\",\n        description: \"Αλλάξτε την εμφάνιση του πορτοφολιού σας.\",\n        tooltips: {\n          mono: \"mono\",\n          cyber: \"cyber\",\n          freedom: \"freedom\",\n          nostr: \"nostr\",\n          bitcoin: \"bitcoin\",\n          mint: \"mint\",\n          nut: \"nut\",\n          blu: \"blu\",\n          flamingo: \"flamingo\",\n        },\n      },\n      bip177: {\n        title: \"Σύμβολο Bitcoin\",\n        description: \"Χρησιμοποιήστε το σύμβολο ₿ αντί για sats.\",\n        toggle: \"Χρήση συμβόλου ₿\",\n      },\n    },\n    web_of_trust: {\n      title: \"Δίκτυο εμπιστοσύνης\",\n      known_pubkeys: \"Γνωστά pubkeys: {wotCount}\",\n      continue_crawl: \"Συνέχιση ανίχνευσης\",\n      crawl_odell: \"Ανίχνευση ODELL'S WEB OF TRUST\",\n      crawl_wot: \"Ανίχνευση web of trust\",\n      pause: \"Παύση\",\n      reset: \"Επαναφορά\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"Χρήση npubx.cash\",\n      copy_lightning_address: \"Αντιγραφή διεύθυνσης Lightning\",\n      v2_mint: \"npub.cash v2 mint\",\n    },\n    multinut: {\n      use_multinut: \"Χρήση Multinut\",\n    },\n    advanced: {\n      title: \"Για προχωρημένους\",\n      developer: {\n        title: \"Ρυθμίσεις προγραμματιστή\",\n        description:\n          \"Οι ακόλουθες ρυθμίσεις είναι για ανάπτυξη και εντοπισμό σφαλμάτων.\",\n        new_seed: {\n          button: \"Δημιουργία νέας φράσης seed\",\n          description:\n            \"Αυτό θα δημιουργήσει μια νέα φράση seed. Πρέπει να στείλετε ολόκληρο το υπόλοιπό σας στον εαυτό σας για να μπορέσετε να το επαναφέρετε με μια νέα seed.\",\n          confirm_question:\n            \"Είστε βέβαιοι ότι θέλετε να δημιουργήσετε μια νέα φράση seed;\",\n          cancel: \"Ακύρωση\",\n          confirm: \"Επιβεβαίωση\",\n        },\n        remove_spent: {\n          button: \"Αφαίρεση δαπανημένων αποδείξεων\",\n          description:\n            \"Ελέγξτε εάν τα token ecash από τα ενεργά σας mints έχουν δαπανηθεί και αφαιρέστε τα δαπανημένα από το πορτοφόλι σας. Χρησιμοποιήστε το μόνο εάν το πορτοφόλι σας έχει κολλήσει.\",\n        },\n        debug_console: {\n          button: \"Εναλλαγή Κονσόλας Εντοπισμού Σφαλμάτων\",\n          description:\n            \"Ανοίξτε το τερματικό εντοπισμού σφαλμάτων Javascript. Ποτέ μην επικολλάτε τίποτα σε αυτό το τερματικό που δεν καταλαβαίνετε. Ένας κλέφτης μπορεί να προσπαθήσει να σας εξαπατήσει για να επικολλήσετε κακόβουλο κώδικα εδώ.\",\n        },\n        export_proofs: {\n          button: \"Εξαγωγή ενεργών αποδείξεων\",\n          description:\n            \"Αντιγράψτε ολόκληρο το υπόλοιπό σας από το ενεργό mint ως token Cashu στο πρόχειρό σας. Αυτό θα εξάγει μόνο τα token από το επιλεγμένο mint και μονάδα. Για πλήρη εξαγωγή, επιλέξτε διαφορετικό mint και μονάδα και εξάγετε ξανά.\",\n        },\n        keyset_counters: {\n          title: \"Αύξηση μετρητών keyset\",\n          description:\n            'Κάντε κλικ στο ID του keyset για να αυξήσετε τους μετρητές διαδρομής παραγωγής για τα keysets στο πορτοφόλι σας. Αυτό είναι χρήσιμο εάν βλέπετε το σφάλμα \"οι εξόδοι έχουν ήδη υπογραφεί\".',\n          counter: \"μετρητής: {count}\",\n        },\n        unset_reserved: {\n          button: \"Κατάργηση δέσμευσης όλων των δεσμευμένων token\",\n          description:\n            'Αυτό το πορτοφόλι επισημαίνει το εκκρεμές εξερχόμενο ecash ως δεσμευμένο (και το αφαιρεί από το υπόλοιπό σας) για να αποτρέψει προσπάθειες διπλής δαπάνης. Αυτό το κουμπί θα καταργήσει τη δέσμευση όλων των δεσμευμένων token ώστε να μπορούν να χρησιμοποιηθούν ξανά. Εάν το κάνετε αυτό, το πορτοφόλι σας μπορεί να περιλαμβάνει δαπανημένες αποδείξεις. Πατήστε το κουμπί \"Αφαίρεση δαπανημένων αποδείξεων\" για να τις αφαιρέσετε.',\n        },\n        show_onboarding: {\n          button: \"Εμφάνιση onboarding\",\n          description: \"Εμφάνιση ξανά της οθόνης onboarding.\",\n        },\n        reset_wallet: {\n          button: \"Επαναφορά δεδομένων πορτοφολιού\",\n          description:\n            \"Επαναφορά των δεδομένων του πορτοφολιού σας. Προειδοποίηση: Αυτό θα διαγράψει τα πάντα! Βεβαιωθείτε ότι έχετε δημιουργήσει πρώτα ένα αντίγραφο ασφαλείας.\",\n          confirm_question:\n            \"Είστε βέβαιοι ότι θέλετε να διαγράψετε τα δεδομένα του πορτοφολιού σας;\",\n          cancel: \"Ακύρωση\",\n          confirm: \"Διαγραφή πορτοφολιού\",\n        },\n        export_wallet: {\n          button: \"Εξαγωγή δεδομένων πορτοφολιού\",\n          description:\n            \"Λήψη ενός dump του πορτοφολιού σας. Μπορείτε να επαναφέρετε το πορτοφόλι σας από αυτό το αρχείο στην οθόνη καλωσορίσματος ενός νέου πορτοφολιού. Αυτό το αρχείο θα είναι εκτός συγχρονισμού εάν συνεχίσετε να χρησιμοποιείτε το πορτοφόλι σας μετά την εξαγωγή του.\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"Συμμετοχή σε mint\",\n    subtitle:\n      \"Δεν έχετε συνδεθεί ακόμα σε κανένα Cashu mint. Προσθέστε ένα URL mint στις ρυθμίσεις ή λάβετε ecash από ένα νέο mint για να ξεκινήσετε.\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"Λήψη Ecash\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"Ιστορικό\",\n      },\n      invoices: {\n        label: \"Τιμολόγια\",\n      },\n      mints: {\n        label: \"Mints\",\n      },\n    },\n    install: {\n      text: \"Εγκατάσταση\",\n      tooltip: \"Εγκατάσταση Cashu\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"Όχι.\",\n    text: \"Μια άλλη καρτέλα εκτελείται ήδη. Κλείστε αυτήν την καρτέλα και δοκιμάστε ξανά.\",\n    actions: {\n      retry: {\n        label: \"Επανάληψη\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"Ωχ. Τίποτα εδώ…\",\n    actions: {\n      home: {\n        label: \"Επιστροφή στην αρχική σελίδα\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Mint\",\n    },\n    mintBalance: {\n      label: \"Υπόλοιπο\",\n    },\n    mintError: {\n      label: \"Σφάλμα mint\",\n    },\n    pending: {\n      label: \"Εκκρεμεί\",\n      tooltip: \"Έλεγχος όλων των εκκρεμών token\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"Προηγούμενο\",\n      },\n      next: {\n        label: \"Επόμενο\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"Καλώς ήρθατε στο Cashu\",\n    text: \"Το Cashu.me είναι ένα δωρεάν και ανοιχτού κώδικα πορτοφόλι Bitcoin που χρησιμοποιεί ecash για να διατηρεί τα κεφάλαιά σας ασφαλή και ιδιωτικά.\",\n    actions: {\n      more: {\n        label: \"Κάντε κλικ για να μάθετε περισσότερα\",\n      },\n    },\n    p1: {\n      text: \"Το Cashu είναι ένα δωρεάν και ανοιχτού κώδικα πρωτόκολλο ecash για Bitcoin. Μπορείτε να μάθετε περισσότερα γι' αυτό στο { link }.\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"Αυτό το πορτοφόλι δεν είναι συνδεδεμένο με κανένα mint. Για να χρησιμοποιήσετε αυτό το πορτοφόλι, πρέπει να συνδεθείτε σε ένα ή περισσότερα Cashu mints που εμπιστεύεστε.\",\n    },\n    p3: {\n      text: \"Αυτό το πορτοφόλι αποθηκεύει ecash στο οποίο έχετε πρόσβαση μόνο εσείς. Εάν διαγράψετε τα δεδομένα του προγράμματος περιήγησής σας χωρίς αντίγραφο ασφαλείας της φράσης seed, θα χάσετε τα token σας.\",\n    },\n    p4: {\n      text: \"Αυτό το πορτοφόλι βρίσκεται σε έκδοση beta. Δεν φέρουμε καμία ευθύνη για άτομα που χάνουν την πρόσβαση στα κεφάλαιά τους. Χρησιμοποιήστε το με δική σας ευθύνη! Αυτός ο κώδικας είναι ανοιχτού κώδικα και διατίθεται με άδεια MIT.\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"Εγκατάσταση PWA\",\n    alt: { pwa_example: \"Παράδειγμα εγκατάστασης PWA\" },\n    installing: \"Γίνεται εγκατάσταση…\",\n    instruction: {\n      intro: {\n        text: \"Για την καλύτερη εμπειρία, χρησιμοποιήστε αυτό το πορτοφόλι με το εγγενές πρόγραμμα περιήγησης ιστού της συσκευής σας για να το εγκαταστήσετε ως Προοδευτική Εφαρμογή Ιστού. Κάντε το αυτό τώρα.\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Πατήστε το μενού (πάνω δεξιά)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Πατήστε { buttonText }\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Πατήστε κοινοποίηση (κάτω)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Πατήστε { buttonText }\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"Μόλις εγκαταστήσετε αυτήν την εφαρμογή στη συσκευή σας, κλείστε αυτό το παράθυρο του προγράμματος περιήγησης και χρησιμοποιήστε την εφαρμογή από την αρχική σας οθόνη.\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"Επιτυχία!\",\n        text: \"Χρησιμοποιείτε το Cashu ως PWA. Κλείστε τυχόν άλλα ανοιχτά παράθυρα προγράμματος περιήγησης και χρησιμοποιήστε την εφαρμογή από την αρχική σας οθόνη.\",\n        nextSteps:\n          \"Τώρα μπορείτε να κλείσετε αυτήν την καρτέλα και να ανοίξετε την εφαρμογή από την αρχική οθόνη.\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"Πατήστε { icon } και { buttonText }\",\n    buttonText: \"Προσθήκη στην αρχική οθόνη\",\n  },\n  AndroidPWAPrompt: {\n    text: \"Πατήστε { icon } και { buttonText }\",\n    buttonText: \"Προσθήκη στην αρχική οθόνη\",\n  },\n  WelcomeSlide3: {\n    title: \"Η Φράση Seed σας\",\n    text: \"Αποθηκεύστε τη φράση seed σας σε έναν διαχειριστή κωδικών πρόσβασης ή σε χαρτί. Η φράση seed σας είναι ο μόνος τρόπος για να ανακτήσετε τα κεφάλαιά σας εάν χάσετε την πρόσβαση σε αυτήν τη συσκευή.\",\n    inputs: {\n      seed_phrase: {\n        label: \"Φράση Seed\",\n        caption: \"Μπορείτε να δείτε τη φράση seed σας στις ρυθμίσεις.\",\n      },\n      checkbox: {\n        label: \"Την έχω γράψει\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"Όροι\",\n    actions: {\n      more: {\n        label: \"Διαβάστε τους Όρους Παροχής Υπηρεσιών\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"Έχω διαβάσει και αποδέχομαι αυτούς τους όρους και προϋποθέσεις\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"Ρύθμιση πορτοφολιού\",\n    text: \"Θέλετε να επαναφέρετε από φράση seed ή να δημιουργήσετε νέο πορτοφόλι;\",\n    options: {\n      new: {\n        title: \"Δημιουργία νέου πορτοφολιού\",\n        subtitle: \"Δημιουργήστε νέα seed και προσθέστε mints.\",\n      },\n      recover: {\n        title: \"Επαναφορά πορτοφολιού\",\n        subtitle: \"Εισαγάγετε τη φράση seed, επαναφέρετε mints και ecash.\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"Προσθήκη mints\",\n    text: \"Τα mints είναι διακομιστές που βοηθούν στην αποστολή και λήψη ecash. Επιλέξτε ένα εντοπισμένο mint ή προσθέστε ένα χειροκίνητα. Μπορείτε να παραλείψετε και να προσθέσετε αργότερα.\",\n    sections: { your_mints: \"Τα mints σας\" },\n    restoring: \"Επαναφορά mints…\",\n    placeholder: { mint_url: \"https://\" },\n  },\n  WelcomeRecoverSeed: {\n    title: \"Εισαγάγετε τη φράση seed\",\n    text: \"Επικολλήστε ή πληκτρολογήστε τη φράση 12 λέξεων για επαναφορά.\",\n    inputs: { word: \"Λέξη { index }\" },\n    actions: { paste_all: \"Επικόλληση όλων\" },\n    disclaimer:\n      \"Η φράση seed χρησιμοποιείται μόνο τοπικά για παραγωγή κλειδιών πορτοφολιού.\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"Επαναφορά ecash\",\n    text: \"Σαρώστε για μη δαπανημένες αποδείξεις (proofs) στα ρυθμισμένα mints και προσθέστε τες στο πορτοφόλι σας.\",\n  },\n  MintRatings: {\n    title: \"Κριτικές mint\",\n    reviews: \"κριτικές\",\n    ratings: \"Βαθμολογίες\",\n    no_reviews: \"Δεν βρέθηκαν κριτικές\",\n    your_review: \"Η κριτική σας\",\n    no_reviews_to_display: \"Καμία κριτική προς εμφάνιση.\",\n    no_rating: \"Χωρίς βαθμολογία\",\n    out_of: \"από\",\n    rows: \"Reviews\",\n    sort: \"Ταξινόμηση\",\n    sort_options: {\n      newest: \"Νεότερες\",\n      oldest: \"Παλαιότερες\",\n      highest: \"Υψηλότερες\",\n      lowest: \"Χαμηλότερες\",\n    },\n    actions: { write_review: \"Γράψτε κριτική\" },\n    empty_state_subtitle:\n      \"Βοηθήστε αφήνοντας μια κριτική. Μοιραστείτε την εμπειρία σας με αυτό το mint και βοηθήστε άλλους αφήνοντας μια κριτική.\",\n  },\n  CreateMintReview: {\n    title: \"Κριτική mint\",\n    publishing_as: \"Δημοσίευση ως\",\n    inputs: {\n      rating: { label: \"Βαθμολογία\" },\n      review: { label: \"Κριτική (προαιρετικά)\" },\n    },\n    actions: {\n      publish: { label: \"Δημοσίευση\", in_progress: \"Γίνεται δημοσίευση…\" },\n    },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"Επαναφορά από Φράση Seed\",\n      caption:\n        \"Εισαγάγετε τη φράση seed σας για να επαναφέρετε το πορτοφόλι σας. Πριν την επαναφορά, βεβαιωθείτε ότι έχετε προσθέσει όλα τα mints που έχετε χρησιμοποιήσει στο παρελθόν.\",\n      inputs: {\n        seed_phrase: {\n          label: \"Φράση seed\",\n          caption: \"Μπορείτε να δείτε τη φράση seed σας στις ρυθμίσεις.\",\n        },\n      },\n    },\n    information: {\n      label: \"Πληροφορίες\",\n      caption:\n        \"Ο οδηγός θα επαναφέρει μόνο ecash από μια άλλη φράση seed, δεν θα μπορείτε να χρησιμοποιήσετε αυτήν τη φράση seed ή να αλλάξετε τη φράση seed του πορτοφολιού που χρησιμοποιείτε αυτήν τη στιγμή. Αυτό σημαίνει ότι το επαναφερμένο ecash δεν θα προστατεύεται από την τρέχουσα φράση seed σας εφόσον δεν στείλετε το ecash στον εαυτό σας μία φορά.\",\n    },\n    restore_mints: {\n      label: \"Επαναφορά Mints\",\n      caption:\n        'Επιλέξτε το mint για επαναφορά. Μπορείτε να προσθέσετε περισσότερα mints στην κύρια οθόνη κάτω από το \"Mints\" και να τα επαναφέρετε εδώ.',\n    },\n    actions: {\n      paste: {\n        error: \"Αποτυχία ανάγνωσης περιεχομένων προχείρου.\",\n      },\n      validate: {\n        error: \"Το μνημονικό πρέπει να είναι τουλάχιστον 12 λέξεις.\",\n      },\n      select_all: {\n        label: \"Επιλογή όλων\",\n      },\n      deselect_all: {\n        label: \"Αποεπιλογή όλων\",\n      },\n      restore: {\n        label: \"Επαναφορά\",\n        in_progress: \"Επαναφορά mint…\",\n        error: \"Σφάλμα κατά την επαναφορά του mint: { error }\",\n      },\n      restore_all_mints: {\n        label: \"Επαναφορά Όλων των Mints\",\n        in_progress: \"Επαναφορά mint { index } από { length }…\",\n        success: \"Η επαναφορά ολοκληρώθηκε με επιτυχία\",\n        error: \"Σφάλμα κατά την επαναφορά των mints: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"Επαναφορά επιλεγμένων Mints ({count})\",\n        in_progress: \"Επαναφορά mint { index } από { length }…\",\n        success: \"Επιτυχής επαναφορά {count} mint(s)\",\n        error: \"Σφάλμα κατά την επαναφορά των mints: { error }\",\n      },\n    },\n    nostr_mints: {\n      label: \"Επαναφορά Mints από το Nostr\",\n      caption:\n        \"Αναζητήστε αντίγραφα ασφαλείας mint που είναι αποθηκευμένα σε ρελέ Nostr χρησιμοποιώντας τη φράση-κλειδί σας. Αυτό θα σας βοηθήσει να ανακαλύψετε νομισματοκοπεία που χρησιμοποιήσατε προηγουμένως.\",\n      search_button: \"Αναζήτηση για αντίγραφα ασφαλείας Mint\",\n      select_all: \"Επιλογή όλων\",\n      deselect_all: \"Αποεπιλογή όλων\",\n      backed_up: \"Δημιουργήθηκαν αντίγραφα ασφαλείας\",\n      already_added: \"Έχει ήδη προστεθεί\",\n      add_selected: \"Προσθήκη επιλεγμένων ({count})\",\n      no_backups_found: \"Δεν βρέθηκαν αντίγραφα ασφαλείας mint\",\n      no_backups_hint:\n        \"Βεβαιωθείτε ότι η δημιουργία αντιγράφων ασφαλείας του Nostr mint είναι ενεργοποιημένη στις ρυθμίσεις για την αυτόματη δημιουργία αντιγράφων ασφαλείας της λίστας mint σας.\",\n      invalid_mnemonic:\n        \"Εισαγάγετε μια έγκυρη φράση-κλειδί πριν από την αναζήτηση.\",\n      search_error: \"Αποτυχία αναζήτησης αντιγράφων ασφαλείας mint.\",\n      add_error: \"Αποτυχία προσθήκης επιλεγμένων mints.\",\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"Προσθήκη mint\",\n      description:\n        \"Εισαγάγετε το URL ενός Cashu mint για να συνδεθείτε σε αυτό. Αυτό το πορτοφόλι δεν είναι συνδεδεμένο με κανένα mint.\",\n      inputs: {\n        nickname: {\n          placeholder: \"Ψευδώνυμο (π.χ. Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"Μη έγκυρο URL\",\n        },\n        scan: {\n          label: \"Σάρωση Κωδικού QR\",\n        },\n      },\n    },\n    discover: {\n      title: \"Ανακάλυψη mints\",\n      overline: \"Ανακάλυψη\",\n      caption: \"Ανακαλύψτε mints που άλλοι χρήστες έχουν προτείνει στο nostr.\",\n      actions: {\n        discover: {\n          label: \"Ανακάλυψη mints\",\n          in_progress: \"Φόρτωση…\",\n          error_no_mints: \"Δεν βρέθηκαν mints\",\n          success: \"Βρέθηκαν { length } mints\",\n        },\n      },\n      recommendations: {\n        overline: \"Βρέθηκαν { length } mints\",\n        caption:\n          \"Αυτά τα mints προτάθηκαν από άλλους χρήστες Nostr. Να είστε προσεκτικοί και κάντε τη δική σας έρευνα πριν χρησιμοποιήσετε ένα mint.\",\n        actions: {\n          browse: {\n            label: \"Κάντε κλικ για περιήγηση στα mints\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"Ανταλλαγή\",\n      overline: \"Ανταλλαγές Multimint\",\n      caption:\n        \"Ανταλλάξτε κεφάλαια μεταξύ mints μέσω Lightning. Σημείωση: Αφήστε χώρο για πιθανές χρεώσεις Lightning. Εάν η εισερχόμενη πληρωμή δεν επιτύχει, ελέγξτε το τιμολόγιο μη αυτόματα.\",\n      inputs: {\n        from: {\n          label: \"Από\",\n        },\n        to: {\n          label: \"Προς\",\n        },\n        amount: {\n          label: \"Ποσό ({ ticker })\",\n        },\n      },\n      actions: {\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n    },\n    error_badge: \"Σφάλμα\",\n    reviews_text: \"κριτικές\",\n    no_reviews_yet: \"Δεν υπάρχουν κριτικές ακόμα\",\n    discover_mints_button: \"Ανακαλύψτε mints\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - Συνέχεια σάρωσης\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"Λήψη Lightning\",\n    create_invoice_title: \"Δημιουργία Τιμολογίου\",\n    inputs: {\n      amount: {\n        label: \"Ποσό ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"Δημιουργία Τιμολογίου\",\n        label_blocked: \"Δημιουργία τιμολογίου…\",\n        in_progress: \"Δημιουργία\",\n      },\n    },\n    invoice: {\n      caption: \"Τιμολόγιο Lightning\",\n      status_paid_text: \"Πληρώθηκε!\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"Αποστολή\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Δεν υπάρχουν διαθέσιμα mints\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"Δεν υπάρχουν διαθέσιμα mints\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"Αποστολή Ecash\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"Εκτός σύνδεσης\",\n    inputs: {\n      amount: {\n        label: \"Ποσό ({ ticker }) *\",\n        invalid_too_much_error_text: \"Πάρα πολύ\",\n      },\n      p2pk_pubkey: {\n        label: \"Δημόσιο κλειδί παραλήπτη\",\n        label_invalid: \"Μη έγκυρο δημόσιο κλειδί παραλήπτη\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"Αντιγραφή Emoji\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"Αντιγραφή συνδέσμου\",\n      },\n      share: {\n        tooltip_text: \"Κοινοποίηση ecash\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"Διαγραφή από το ιστορικό\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"Εγγραφή στην κάρτα NFC\",\n          ndef_unsupported_text: \"Το NDEF δεν υποστηρίζεται\",\n        },\n      },\n    },\n  },\n  ReceiveDialog: {\n    title: \"Λήψη\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Δεν υπάρχουν διαθέσιμα mints\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints:\n          \"Πρέπει να συνδεθείτε σε ένα mint για λήψη μέσω Lightning\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"Λήψη Ecash\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"Αίτημα\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"Σάρωση…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"Λήψη Ecash\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"Επικολλήστε το token Cashu\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"Μη έγκυρο token\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"Δεν είναι δυνατή η λήψη. Το κλείδωμα P2PK αυτού του token δεν ταιριάζει με το δημόσιο κλειδί σας.\",\n      },\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"Προσθήκη mint…\",\n      },\n      swap: {\n        label: \"@:global.actions.swap.label\",\n        tooltip_text: \"Ανταλλαγή σε αξιόπιστο mint\",\n        caption: \"Ανταλλαγή { value }\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"Ακύρωση ανταλλαγής\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      later: {\n        label: \"Λήψη αργότερα\",\n        tooltip_text: \"Προσθήκη στο ιστορικό για λήψη αργότερα\",\n        already_in_history_success_text: \"Το Ecash είναι ήδη στο Ιστορικό\",\n        added_to_history_success_text: \"Το Ecash προστέθηκε στο Ιστορικό\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"Ανάγνωση από κάρτα NFC\",\n          ndef_unsupported_text: \"Το NDEF δεν υποστηρίζεται\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"Κλειδί P2PK\",\n      description: \"Λήψη ecash κλειδωμένο σε αυτό το κλειδί\",\n      used_warning_text:\n        \"Προειδοποίηση: Αυτό το κλειδί χρησιμοποιήθηκε στο παρελθόν. Χρησιμοποιήστε ένα νέο κλειδί για καλύτερο απόρρητο.\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"Δημιουργία νέου κλειδιού\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"Αίτημα Πληρωμής\",\n      description: \"Λήψη πληρωμών μέσω Nostr\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"Νέο αίτημα\",\n      },\n      add_amount: {\n        label: \"Προσθήκη ποσού\",\n      },\n      use_active_mint: {\n        label: \"Οποιοδήποτε mint\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"Εισαγωγή ποσού\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"Πληκτρολόγιο απενεργοποιημένο. Μπορείτε να ενεργοποιήσετε ξανά το πληκτρολόγιο στις ρυθμίσεις.\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostr Wallet Connect\",\n      description:\n        \"Ελέγξτε το πορτοφόλι σας απομακρυσμένα με το NWC. Πατήστε τον κωδικό QR για να συνδέσετε το πορτοφόλι σας με μια συμβατή εφαρμογή.\",\n      warning_text:\n        \"Προειδοποίηση: οποιοσδήποτε με πρόσβαση σε αυτήν τη συμβολοσειρά σύνδεσης μπορεί να ξεκινήσει πληρωμές από το πορτοφόλι σας. Μην την κοινοποιείτε!\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"Μήνυμα Mint\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"Επαφή\",\n    },\n    details: {\n      title: \"Λεπτομέρειες mint\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"Προβολή όλων\",\n          },\n          hide: {\n            label: \"Απόκρυψη\",\n          },\n        },\n      },\n      currency: {\n        label: \"Νόμισμα\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"Έκδοση\",\n      },\n    },\n    actions: {\n      title: \"Ενέργειες\",\n      copy_mint_url: {\n        label: \"Αντιγραφή URL mint\",\n      },\n      delete: {\n        label: \"Διαγραφή mint\",\n      },\n      edit: {\n        label: \"Επεξεργασία mint\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"Επιλέξτε ένα mint\",\n    badge_mint_error_text: \"Σφάλμα\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"Δεν υπάρχει ακόμη ιστορικό\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"πριν από { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Έλεγχος κατάστασης\",\n      },\n      receive: {\n        tooltip_text: \"Λήψη\",\n      },\n      filter_pending: {\n        label: \"Φιλτράρισμα εκκρεμών\",\n      },\n      show_all: {\n        label: \"Εμφάνιση όλων\",\n      },\n    },\n    old_token_not_found_error_text: \"Παλιό token δεν βρέθηκε\",\n  },\n  InvoiceTable: {\n    empty_text: \"Δεν υπάρχουν ακόμη τιμολόγια\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"Κάντε κλικ για αντιγραφή\",\n      date_label: \"πριν από { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Έλεγχος κατάστασης\",\n      },\n      filter_pending: {\n        label: \"Φιλτράρισμα εκκρεμών\",\n      },\n      show_all: {\n        label: \"Εμφάνιση όλων\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"Είστε βέβαιοι ότι θέλετε να διαγράψετε αυτό το mint?\",\n    nickname: {\n      label: \"Ψευδώνυμο\",\n    },\n    balances: {\n      label: \"Υπόλοιπα\",\n    },\n    warning_text:\n      \"Σημείωση: Επειδή αυτό το πορτοφόλι είναι παρανοϊκό, το ecash σας από αυτό το mint δεν θα διαγραφεί στην πραγματικότητα, αλλά θα παραμείνει αποθηκευμένο στη συσκευή σας. Θα το δείτε να εμφανίζεται ξανά εάν προσθέσετε ξανά αυτό το mint αργότερα.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"Αφαίρεση mint\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"Token Cashu ή διεύθυνση Lightning\",\n      receive: \"Token Cashu\",\n      pay: \"Διεύθυνση Lightning ή τιμολόγιο\",\n    },\n    qr_scanner: {\n      title: \"Σάρωση Κωδικού QR\",\n      description: \"Πατήστε για σάρωση διεύθυνσης\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"Πληρωμή με Lightning\",\n      inputs: {\n        invoice_data: {\n          label: \"Τιμολόγιο ή διεύθυνση Lightning\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"Ο { payee } ζητά { value } { ticker }\",\n      amount_range_label:\n        \"Ο { payee } ζητά{br}μεταξύ { min } και { max } { ticker }\",\n      sending_to_lightning_address: \"Αποστολή σε { address }\",\n      inputs: {\n        amount: {\n          label: \"Ποσό ({ ticker }) *\",\n        },\n        comment: {\n          label: \"Σχόλιο (προαιρετικό)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"Πληρωμή { value }\",\n      paying: \"Πληρώνεται\",\n      paid: \"Πληρώθηκε\",\n      fee: \"Χρέωση\",\n      memo: {\n        label: \"Σημείωμα\",\n      },\n      processing_info_text: \"Επεξεργασία…\",\n      balance_too_low_warning_text: \"Το υπόλοιπο είναι πολύ χαμηλό\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"Πληρωμή\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"Σφάλμα\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"Επεξεργασία mint\",\n    inputs: {\n      nickname: {\n        label: \"Ψευδώνυμο\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"Εμπιστεύεστε αυτό το mint;\",\n    description:\n      \"Πριν χρησιμοποιήσετε αυτό το mint, βεβαιωθείτε ότι το εμπιστεύεστε. Τα mints μπορεί να γίνουν κακόβουλα ή να σταματήσουν τη λειτουργία τους ανά πάσα στιγμή.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"Προσθήκη mint\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"Παρακαλώ εισαγάγετε ένα μνημονικό\",\n    restore_mint_error_text: \"Σφάλμα κατά την επαναφορά του mint: { error }\",\n    prepare_info_text: \"Προετοιμασία διαδικασίας επαναφοράς…\",\n    restored_proofs_for_keyset_info_text:\n      \"Επαναφέρθηκαν { restoreCounter } αποδείξεις για το keyset { keysetId }\",\n    checking_proofs_for_keyset_info_text:\n      \"Έλεγχος αποδείξεων { startIndex } έως { endIndex } για το keyset { keysetId }\",\n    no_proofs_info_text: \"Δεν βρέθηκαν αποδείξεις για επαναφορά\",\n    restored_amount_success_text: \"Επαναφέρθηκε { amount }\",\n  },\n  swap: {\n    in_progress_warning_text: \"Η ανταλλαγή βρίσκεται σε εξέλιξη\",\n    invalid_swap_data_error_text: \"Μη έγκυρα δεδομένα ανταλλαγής\",\n    swap_error_text: \"Σφάλμα κατά την ανταλλαγή\",\n  },\n  TokenInformation: {\n    fee: \"Χρέωση\",\n    unit: \"Μονάδα\",\n    fiat: \"Fiat\",\n    p2pk: \"P2PK\",\n    locked: \"Κλειδωμένο\",\n    locked_to_you: \"Κλειδωμένο για εσάς\",\n    mint: \"Νομισματοκοπείο\",\n    memo: \"Σημείωση\",\n    payment_request: \"Αίτημα πληρωμής\",\n    nostr: \"Nostr\",\n    token_copied: \"Το token αντιγράφηκε στο πρόχειρο\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/en-US/index.ts",
    "content": "export default {\n  global: {\n    copy_to_clipboard: {\n      success: \"Copied to clipboard!\",\n    },\n    actions: {\n      add_mint: {\n        label: \"Add mint\",\n      },\n      cancel: {\n        label: \"Cancel\",\n      },\n      copy: {\n        label: \"Copy\",\n      },\n      close: {\n        label: \"Close\",\n      },\n      enter: {\n        label: \"Enter\",\n      },\n      lock: {\n        label: \"Lock\",\n      },\n      paste: {\n        label: \"Paste\",\n      },\n      receive: {\n        label: \"Receive\",\n      },\n      scan: {\n        label: \"Scan\",\n      },\n      send: {\n        label: \"Send\",\n      },\n      pay: {\n        label: \"Pay\",\n      },\n      swap: {\n        label: \"Swap\",\n      },\n      update: {\n        label: \"Update\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"Mint URL\",\n      },\n    },\n  },\n  common: {\n    fee: \"Fee\",\n  },\n  MultinutPicker: {\n    payment: \"Multinut payment\",\n    selectMints: \"Select one or multiple mints to execute a payment from.\",\n    totalSelectedBalance: \"Total Selected Balance\",\n    multiMintPay: \"Multi-Mint Pay\",\n    balanceNotEnough: \"Multi-mint balance not enough to satisfy this invoice\",\n    failed: \"Failed to process: {error}\",\n    paid: \"Paid {amount} via Lightning\",\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"Balance is too low\",\n      received: \"Received {amount}\",\n      fee: \" (fee: {fee})\",\n      could_not_request_mint: \"Could not request mint\",\n      invoice_still_pending: \"Invoice still pending\",\n      paid_lightning: \"Paid {amount} via Lightning\",\n      payment_pending_refresh: \"Payment pending. Refresh invoice manually.\",\n      sent: \"Sent {amount}\",\n      token_still_pending: \"Token still pending\",\n      received_lightning: \"Received {amount} via Lightning\",\n      lightning_payment_failed: \"Lightning payment failed\",\n      failed_to_decode_invoice: \"Failed to decode invoice\",\n      unsupported_legacy_qr: \"Unsupported Legacy QR code\",\n      legacy_qr_not_supported:\n        \"This Legacy QR code is not from a supported merchant\",\n      invalid_lnurl: \"Invalid LNURL\",\n      lnurl_error: \"LNURL Error\",\n      no_amount: \"No amount\",\n      no_lnurl_data: \"No LNURL data\",\n      no_price_data: \"No price data.\",\n      please_try_again: \"Please try again.\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"Mint already added\",\n        added: \"Mint added\",\n        not_found: \"Mint not found\",\n        activation_failed: \"Mint activation failed\",\n        no_active_mint: \"No active mint\",\n        unit_activation_failed: \"Unit activation failed\",\n        unit_not_supported: \"Unit not supported by mint\",\n        activated: \"Mint activated\",\n        could_not_connect: \"Could not connect to mint\",\n        could_not_get_info: \"Could not get mint info\",\n        could_not_get_keys: \"Could not get mint keys\",\n        could_not_get_keysets: \"Could not get mint keysets\",\n        mint_validation_error: \"Mint validation error\",\n        removed: \"Mint removed\",\n        error: \"Mint error\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"Settings\",\n        settings: {\n          title: \"Settings\",\n          caption: \"Wallet configuration\",\n        },\n      },\n      terms: {\n        title: \"Terms\",\n        terms: {\n          title: \"Terms\",\n          caption: \"Terms of Service\",\n        },\n      },\n      links: {\n        title: \"Links\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"Donate\",\n          caption: \"Support Cashu\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"Offline\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"Reload in { countdown }\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"Staging – don't use with real funds!\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"Wallet\",\n      },\n    },\n  },\n  Settings: {\n    language: {\n      title: \"Language\",\n      description: \"Please choose your preferred language from the list below.\",\n    },\n    sections: {\n      backup_restore: \"BACKUP & RESTORE\",\n      lightning_address: \"LIGHTNING ADDRESS\",\n      nostr_keys: \"NOSTR KEYS\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"Click to edit relays\",\n          add: {\n            title: \"Add relay\",\n            description:\n              \"Your wallet uses these relays for nostr operations such as payment requests, nostr wallet connect, and backups.\",\n          },\n          list: {\n            title: \"Relays\",\n            description: \"Your wallet will connect to these relays.\",\n            copy_tooltip: \"Copy relay\",\n            remove_tooltip: \"Remove relay\",\n          },\n        },\n      },\n      payment_requests: \"PAYMENT REQUESTS\",\n      nostr_wallet_connect: \"NOSTR WALLET CONNECT\",\n      hardware_features: \"HARDWARE FEATURES\",\n      p2pk_features: \"P2PK FEATURES\",\n      privacy: \"PRIVACY\",\n      experimental: \"EXPERIMENTAL\",\n      appearance: \"APPEARANCE\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"Backup seed phrase\",\n        description:\n          \"Your seed phrase can restore your wallet. Keep it safe and private.\",\n        seed_phrase_label: \"Seed phrase\",\n      },\n      restore_ecash: {\n        title: \"Restore ecash\",\n        description:\n          \"The restore wizard lets you recover lost ecash from a mnemonic seed phrase. The seed phrase of your current wallet will remain unaffected, the wizard will only allow you to restore ecash from another seed phrase.\",\n        button: \"Restore\",\n      },\n    },\n    lightning_address: {\n      title: \"Lightning address\",\n      description: \"Receive payments to your Lightning address.\",\n      enable: {\n        toggle: \"Enable\",\n        description: \"Lightning address with npub.cash\",\n      },\n      address: {\n        copy_tooltip: \"Copy Lightning address\",\n      },\n      automatic_claim: {\n        toggle: \"Claim automatically\",\n        description: \"Receive incoming payments automatically.\",\n      },\n      npc_v2: {\n        choose_mint_title: \"Choose mint for npub.cash v2\",\n        choose_mint_placeholder: \"Select a mint...\",\n      },\n    },\n    nostr_keys: {\n      title: \"Your nostr keys\",\n      description:\n        \"Your nostr keys will be used to determine your Lightning address.\",\n      wallet_seed: {\n        title: \"Wallet seed phrase\",\n        description: \"Generate nostr key pair from wallet seed\",\n        copy_nsec: \"Copy nsec\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"Use a NIP-46 bunker\",\n        delete_tooltip: \"Delete connection\",\n      },\n      use_nsec: {\n        title: \"Use your nsec\",\n        description: \"This method is dangerous and not recommended\",\n        delete_tooltip: \"Delete nsec\",\n      },\n      signing_extension: {\n        title: \"Signing extension\",\n        description: \"Use a NIP-07 signing extension\",\n        not_found: \"No NIP-07 signing extension found\",\n      },\n    },\n    payment_requests: {\n      title: \"Payment requests\",\n      description:\n        \"Payment requests allow you to receive payments via nostr. If you enable this, your wallet will subscribe to your nostr relays.\",\n      enable_toggle: \"Enable Payment Requests\",\n      claim_automatically: {\n        toggle: \"Claim automatically\",\n        description: \"Receive incoming payments automatically.\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"Nostr Wallet Connect (NWC)\",\n      description: \"Use NWC to control your wallet from any other application.\",\n      enable_toggle: \"Enable NWC\",\n      payments_note:\n        \"You can only use NWC for payments from your Bitcoin balance. Payments will be made from your active mint.\",\n      connection: {\n        copy_tooltip: \"Copy connection string\",\n        qr_tooltip: \"Show QR code\",\n        allowance_label: \"Allowance left (sat)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"Choose the encoding for writing to NFC cards\",\n        text: {\n          title: \"Text\",\n          description: \"Store token in plain text\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"Store URL to this wallet with token\",\n        },\n        binary: {\n          title: \"Binary\",\n          description: \"Store tokens as binary data\",\n        },\n        quick_access: {\n          toggle: \"Quick access to NFC\",\n          description:\n            \"Quickly scan NFC cards in the Receive Ecash menu. This option adds an NFC button the Receive Ecash menu.\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"Generate a key pair to receive P2PK-locked ecash. Warning: This feature is experimental. Only use with small amounts. If you lose your private keys, nobody will be able to unlock the ecash locked to it anymore.\",\n      generate_button: \"Generate key\",\n      import_button: \"Import nsec\",\n      quick_access: {\n        toggle: \"Quick access to lock\",\n        description:\n          \"Use this to quickly show your P2PK locking key in the receive ecash menu.\",\n      },\n      keys_expansion: {\n        label: \"Click to browse {count} keys\",\n        used_badge: \"used\",\n      },\n    },\n    privacy: {\n      title: \"Privacy\",\n      description: \"These settings affect your privacy.\",\n      check_incoming: {\n        toggle: \"Check incoming invoice\",\n        description:\n          \"If enabled, the wallet will check the latest invoice in the background. This increases the wallet's responsiveness which makes fingerprinting easier. You can manually check unpaid invoices in the Invoices tab.\",\n      },\n      check_startup: {\n        toggle: \"Check pending invoices on startup\",\n        description:\n          \"If enabled, the wallet will check pending invoices from the last 24 hours on startup.\",\n      },\n      check_all: {\n        toggle: \"Check all invoices\",\n        description:\n          \"If enabled, the wallet will periodically check unpaid invoices in the background for up to two weeks. This increases the wallet's online activity which makes fingerprinting easier. You can manually check unpaid invoices in the Invoices tab.\",\n      },\n      check_sent: {\n        toggle: \"Check sent ecash\",\n        description:\n          \"If enabled, the wallet will use periodic background checks to determine if sent tokens have been redeemed. This increases the wallet's online activity which makes fingerprinting easier.\",\n      },\n      websockets: {\n        toggle: \"Use WebSockets\",\n        description:\n          \"If enabled, the wallet will use long-lived WebSocket connections to receive updates on paid invoices and spent tokens from mints. This increases the wallet's responsiveness but also makes fingerprinting easier.\",\n      },\n      bitcoin_price: {\n        toggle: \"Get exchange rate from Coinbase\",\n        description:\n          \"If enabled, the current Bitcoin exchange rate will be fetched from coinbase.com and your converted balance will be displayed.\",\n        currency: {\n          title: \"Fiat Currency\",\n          description: \"Choose the fiat currency for Bitcoin price display.\",\n        },\n      },\n    },\n    experimental: {\n      title: \"Experimental\",\n      description: \"These features are experimental.\",\n      receive_swaps: {\n        toggle: \"Receive swaps\",\n        badge: \"Beta\",\n        description:\n          \"Option to swap received Ecash to your active mint in the Receive Ecash dialog.\",\n      },\n      auto_paste: {\n        toggle: \"Paste Ecash automatically\",\n        description:\n          \"Automatically paste ecash in your clipboard when you press Receive, then Ecash, then Paste. Automatic pasting can cause UI glitches in iOS, turn it off if you experience issues.\",\n      },\n      auditor: {\n        toggle: \"Enable auditor\",\n        badge: \"Beta\",\n        description:\n          \"If enabled, the wallet will display auditor information in the mint details dialog. The auditor is a third party service that monitors the reliability of mints.\",\n        url_label: \"Auditor URL\",\n        api_url_label: \"Auditor API URL\",\n      },\n      multinut: {\n        toggle: \"Enable Multinut\",\n        description:\n          \"If enabled, the wallet will use Multinut to pay invoices from multiple mints at once.\",\n      },\n      nostr_mint_backup: {\n        toggle: \"Backup mint list on Nostr\",\n        description:\n          \"If enabled, your mint list will be automatically backed up to Nostr relays using your configured Nostr keys. This allows you to restore your mint list across devices.\",\n        notifications: {\n          enabled: \"Nostr mint backup enabled\",\n          disabled: \"Nostr mint backup disabled\",\n          failed: \"Failed to enable Nostr mint backup\",\n        },\n      },\n    },\n    appearance: {\n      keyboard: {\n        title: \"On-screen keyboard\",\n        description: \"Use the numeric keyboard for entering amounts.\",\n        toggle: \"Use numeric keyboard\",\n        toggle_description:\n          \"If enabled, the numeric keyboard will be used for entering amounts.\",\n      },\n      theme: {\n        title: \"Appearance\",\n        description: \"Change how your wallet looks.\",\n        tooltips: {\n          mono: \"mono\",\n          cyber: \"cyber\",\n          freedom: \"freedom\",\n          nostr: \"nostr\",\n          bitcoin: \"bitcoin\",\n          mint: \"mint\",\n          nut: \"nut\",\n          blu: \"blu\",\n          flamingo: \"flamingo\",\n        },\n      },\n      bip177: {\n        title: \"Bitcoin symbol\",\n        description: \"Use ₿ symbol instead of sats.\",\n        toggle: \"Use ₿ symbol\",\n      },\n    },\n    web_of_trust: {\n      title: \"Web of trust\",\n      known_pubkeys: \"Known pubkeys: {wotCount}\",\n      continue_crawl: \"Continue crawl\",\n      crawl_odell: \"Crawl ODELL'S WEB OF TRUST\",\n      crawl_wot: \"Crawl web of trust\",\n      pause: \"Pause\",\n      reset: \"Reset\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"Use npubx.cash\",\n      copy_lightning_address: \"Copy Lightning address\",\n      v2_mint: \"npub.cash v2 mint\",\n    },\n    multinut: {\n      use_multinut: \"Use Multinut\",\n    },\n    advanced: {\n      title: \"Advanced\",\n      developer: {\n        title: \"Developer settings\",\n        description:\n          \"The following settings are for development and debugging.\",\n        new_seed: {\n          button: \"Generate new seed phrase\",\n          description:\n            \"This will generate a new seed phrase. You must send your entire balance to yourself in order to be able to restore it with a new seed.\",\n          confirm_question:\n            \"Are you sure you want to generate a new seed phrase?\",\n          cancel: \"Cancel\",\n          confirm: \"Confirm\",\n        },\n        remove_spent: {\n          button: \"Remove spent proofs\",\n          description:\n            \"Check if the ecash tokens from your active mints are spent and remove the spent ones from your wallet. Only use this if your wallet is stuck.\",\n        },\n        debug_console: {\n          button: \"Toggle Debug Console\",\n          description:\n            \"Open the Javascript debug terminal. Never paste anything into this terminal that you don't understand. A thief might try to trick you into pasting malicious code here.\",\n        },\n        export_proofs: {\n          button: \"Export active proofs\",\n          description:\n            \"Copy your entire balance from the active mint as a Cashu token into your clipboard. This will only export the tokens from the selected mint and unit. For a full export, select a different mint and unit and export again.\",\n        },\n        keyset_counters: {\n          title: \"Increment keyset counters\",\n          description:\n            'Click the keyset ID to increment the derivation path counters for the keysets in your wallet. This is useful if you see the \"outputs have already been signed\" error.',\n          counter: \"counter: {count}\",\n        },\n        unset_reserved: {\n          button: \"Unset all reserved tokens\",\n          description:\n            'This wallet marks pending outgoing ecash as reserved (and subtracts it from your balance) to prevent double-spend attempts. This button will unset all reserved tokens so they can be used again. If you do this, your wallet might include spent proofs. Press the \"Remove spent proofs\" button to get rid of them.',\n        },\n        show_onboarding: {\n          button: \"Show onboarding\",\n          description: \"Show the onboarding screen again.\",\n        },\n        reset_wallet: {\n          button: \"Reset wallet data\",\n          description:\n            \"Reset your wallet data. Warning: This will delete everything! Make sure you create a backup first.\",\n          confirm_question: \"Are you sure you want to delete your wallet data?\",\n          cancel: \"Cancel\",\n          confirm: \"Delete wallet\",\n        },\n        export_wallet: {\n          button: \"Export wallet data\",\n          description:\n            \"Download a dump of your wallet. You can restore your wallet from this file in the welcome screen of a new wallet. This file will be out of sync if you keep using your wallet after exporting it.\",\n        },\n        import_wallet: {\n          button: \"Import wallet backup\",\n          description:\n            \"Restore your wallet from a previously exported backup file. This will replace your current wallet data with the backup.\",\n          confirm_question:\n            \"Are you sure you want to restore your wallet data?\",\n          cancel: \"Cancel\",\n          confirm: \"IMPORT WALLET BACKUP\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"Join a mint\",\n    subtitle:\n      \"You haven't joined any Cashu mint yet. Add a mint URL in the settings or receive ecash from a new mint to get started.\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"Receive Ecash\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"History\",\n      },\n      invoices: {\n        label: \"Invoices\",\n      },\n      mints: {\n        label: \"Mints\",\n      },\n    },\n    install: {\n      text: \"Install\",\n      tooltip: \"Install Cashu\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"Nope.\",\n    text: \"Another tab is already running. Close this tab and try again.\",\n    actions: {\n      retry: {\n        label: \"Retry\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"Oops. Nothing here…\",\n    actions: {\n      home: {\n        label: \"Go back home\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Mint\",\n    },\n    mintBalance: {\n      label: \"Balance\",\n    },\n    mintError: {\n      label: \"Mint error\",\n    },\n    pending: {\n      label: \"Pending\",\n      tooltip: \"Check all pending tokens\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"Previous\",\n      },\n      next: {\n        label: \"Next\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"Welcome to Cashu\",\n    text: \"Cashu.me is a free and open-source Bitcoin wallet that uses ecash to keep your funds secure and private.\",\n    actions: {\n      more: {\n        label: \"Click to learn more\",\n      },\n    },\n    p1: {\n      text: \"Cashu is a free and open-source ecash protocol for Bitcoin. You can learn more about it at { link }.\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"This wallet is not affiliated with any mint. To use this wallet, you need to connect to one or more Cashu mints that you trust.\",\n    },\n    p3: {\n      text: \"This wallet stores ecash that only you have access to. If you delete your browser data without a seed phrase backup, you will lose your tokens.\",\n    },\n    p4: {\n      text: \"This wallet is in beta. We hold no responsibility for people losing access to funds. Use at your own risk! This code is open-source and licensed under the MIT license.\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"Install PWA\",\n    alt: {\n      pwa_example: \"PWA Installation Example\",\n    },\n    installing: \"Installing…\",\n    instruction: {\n      intro: {\n        text: \"For the best experience, use this wallet with your device's native web browser to install it as a Progressive Web App.\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Tap the menu (top right)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Press { buttonText }\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Tap share (bottom)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Press { buttonText }\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"Once you installed this app on your device, close this browser window and use the app from your home screen.\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"Success!\",\n        text: \"You are using Cashu as a PWA. Close any other open browser windows and use the app from your home screen.\",\n        nextSteps:\n          \"You can now close this browser tab and open the app from your home screen.\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"Tap { icon } and { buttonText }\",\n    buttonText: \"Add to Home Screen\",\n  },\n  AndroidPWAPrompt: {\n    text: \"Tap { icon } and { buttonText }\",\n    buttonText: \"Add to Home Screen\",\n  },\n  WelcomeSlide3: {\n    title: \"Your Seed Phrase\",\n    text: \"Store your seed phrase in a password manager or on paper. Your seed phrase is the only way to recover your funds if you lose access to this device.\",\n    inputs: {\n      seed_phrase: {\n        label: \"Seed Phrase\",\n        caption: \"You can see your seed phrase in the settings.\",\n      },\n      checkbox: {\n        label: \"I have written it down\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"Terms\",\n    actions: {\n      more: {\n        label: \"Read Terms of Service\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"I've read and accept these terms and conditions\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"Set up your wallet\",\n    text: \"Do you want to recover from a seed phrase or create a new wallet?\",\n    options: {\n      new: {\n        title: \"Create new wallet\",\n        subtitle: \"Generate a new seed and add mints.\",\n      },\n      recover: {\n        title: \"Recover wallet\",\n        subtitle: \"Enter your seed phrase, restore mints and ecash.\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"Add mints\",\n    text: \"Mints are servers that help you send and receive ecash. Choose a discovered mint or add one manually. Skip to add mints later.\",\n    sections: {\n      your_mints: \"Your mints\",\n    },\n    restoring: \"Restoring mints…\",\n    placeholder: {\n      mint_url: \"https://\",\n    },\n  },\n  WelcomeRecoverSeed: {\n    title: \"Enter your seed phrase\",\n    text: \"Paste or type your 12 word seed phrase to recover.\",\n    inputs: {\n      word: \"Word { index }\",\n    },\n    actions: {\n      paste_all: \"Paste all\",\n    },\n    disclaimer:\n      \"Your seed phrase is only used locally to derive your wallet keys.\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"Restore your ecash\",\n    text: \"Scan for unspent proofs on your configured mints and add them to your wallet.\",\n  },\n  MintRatings: {\n    title: \"Mint Reviews\",\n    reviews: \"reviews\",\n    ratings: \"Ratings\",\n    no_reviews: \"No reviews found\",\n    your_review: \"Your review\",\n    no_reviews_to_display: \"No reviews to display.\",\n    no_rating: \"No rating\",\n    out_of: \"out of\",\n    rows: \"Reviews\",\n    sort: \"Sort\",\n    sort_options: {\n      newest: \"Newest\",\n      oldest: \"Oldest\",\n      highest: \"Highest\",\n      lowest: \"Lowest\",\n    },\n    actions: {\n      write_review: \"Write a review\",\n    },\n    empty_state_subtitle:\n      \"Help by leaving a review. Share your experience with this mint and help others by leaving a review.\",\n  },\n  CreateMintReview: {\n    title: \"Review Mint\",\n    publishing_as: \"Publishing as\",\n    inputs: {\n      rating: { label: \"Rating\" },\n      review: { label: \"Review (optional)\" },\n    },\n    actions: {\n      publish: { label: \"Submit Review\", in_progress: \"Submitting…\" },\n    },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"Restore from Seed Phrase\",\n      caption:\n        \"Enter your seed phrase to restore your wallet. Before you restore, make sure you have added all the mints that you have used before.\",\n      inputs: {\n        seed_phrase: {\n          label: \"Seed phrase\",\n          caption: \"You can see your seed phrase in the settings.\",\n        },\n      },\n    },\n    information: {\n      label: \"Information\",\n      caption:\n        \"The wizard will only restore ecash from another seed phrase, you will not be able to use this seed phrase or change the seed phrase of the wallet that you're currently using. This means that restored ecash will not be protected by your current seed phrase as long as you don't send the ecash to yourself once.\",\n    },\n    restore_mints: {\n      label: \"Restore Mints\",\n      caption:\n        'Select the mint to restore. You can add more mints in the main screen under \"Mints\" and restore them here.',\n    },\n    actions: {\n      paste: {\n        error: \"Failed to read clipboard contents.\",\n      },\n      validate: {\n        error: \"Mnemonic is not a valid BIP39 seed phrase.\",\n      },\n      select_all: {\n        label: \"Select All\",\n      },\n      deselect_all: {\n        label: \"Deselect All\",\n      },\n      restore: {\n        label: \"Restore\",\n        in_progress: \"Restoring mint …\",\n        error: \"Error restoring mint: { error }\",\n      },\n      restore_all_mints: {\n        label: \"Restore All Mints\",\n        in_progress: \"Restoring mint { index } of { length } …\",\n        success: \"Restore finished successfully\",\n        error: \"Error restoring mints: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"Restore Selected Mints ({count})\",\n        in_progress: \"Restoring mint { index } of { length } …\",\n        success: \"Successfully restored {count} mint(s)\",\n        error: \"Error restoring selected mints: { error }\",\n      },\n    },\n    nostr_mints: {\n      label: \"Restore Mints from Nostr\",\n      caption:\n        \"Search for mint backups stored on Nostr relays using your seed phrase. This will help you discover mints you previously used.\",\n      search_button: \"Search for Mint Backups\",\n      select_all: \"Select All\",\n      deselect_all: \"Deselect All\",\n      backed_up: \"Backed up\",\n      already_added: \"Already Added\",\n      add_selected: \"Add Selected ({count})\",\n      no_backups_found: \"No mint backups found\",\n      no_backups_hint:\n        \"Make sure Nostr mint backup is enabled in settings to automatically backup your mint list.\",\n      invalid_mnemonic: \"Please enter a valid seed phrase before searching.\",\n      search_error: \"Failed to search for mint backups.\",\n      add_error: \"Failed to add selected mints.\",\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"Add mint\",\n      description:\n        \"Enter the URL of a Cashu mint to connect to it. This wallet is not affiliated with any mint.\",\n      inputs: {\n        nickname: {\n          placeholder: \"Nickname (e.g. Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"Invalid URL\",\n        },\n        scan: {\n          label: \"Scan QR Code\",\n        },\n      },\n    },\n    discover: {\n      title: \"Discover mints\",\n      overline: \"Discover\",\n      caption: \"Discover mints other users have recommended on nostr.\",\n      actions: {\n        discover: {\n          label: \"Discover mints\",\n          in_progress: \"Loading…\",\n          error_no_mints: \"No mints found\",\n          success: \"Found { length } mints\",\n        },\n      },\n      recommendations: {\n        overline: \"Found { length } mints\",\n        caption:\n          \"These mints were recommended by other Nostr users. Be careful and do your own research before using a mint.\",\n        actions: {\n          browse: {\n            label: \"Click to browse mints\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"Swap\",\n      overline: \"Multimint Swaps\",\n      actions: {\n        receove_to_trusted_mint: {\n          label: \"Receive to trusted mint\",\n        },\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n      caption:\n        \"Swap funds between mints via Lightning. Note: Leave room for potential Lightning fees. If the incoming payment does not succeed, check the invoice manually.\",\n      inputs: {\n        from: {\n          label: \"From\",\n        },\n        to: {\n          label: \"To\",\n        },\n        amount: {\n          label: \"Amount ({ ticker })\",\n        },\n      },\n    },\n    error_badge: \"Error\",\n    reviews_text: \"reviews\",\n    no_reviews_yet: \"No reviews yet\",\n    discover_mints_button: \"Discover mints\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - Keep scanning\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"Receive Lightning\",\n    create_invoice_title: \"Create Invoice\",\n    inputs: {\n      amount: {\n        label: \"Amount ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"Create Invoice\",\n        label_blocked: \"Creating invoice…\",\n        in_progress: \"Creating\",\n      },\n    },\n    invoice: {\n      caption: \"Lightning invoice\",\n      status_paid_text: \"Paid!\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"Send\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"No mints available\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"No mints available\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"Send Ecash\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"Offline\",\n    inputs: {\n      amount: {\n        label: \"Amount ({ ticker }) *\",\n        invalid_too_much_error_text: \"Too much\",\n      },\n      p2pk_pubkey: {\n        label: \"Receiver public key\",\n        label_invalid: \"Receiver public key\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"Copy Emoji\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"Copy link\",\n      },\n      share: {\n        tooltip_text: \"Share ecash\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      pay: {\n        label: \"@:global.actions.pay.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"Delete from history\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"Flash to NFC card\",\n          ndef_unsupported_text: \"NDEF unsupported\",\n        },\n      },\n    },\n    errors: {\n      amount_required: \"Enter an amount first.\",\n      serialization_failed: \"Could not prepare ecash token.\",\n    },\n  },\n  SendPaymentRequest: {\n    actions: {\n      pay: {\n        label: \"Pay\",\n      },\n      pay_via: {\n        label: \"Pay via {transport}\",\n      },\n    },\n    info: {\n      pay_to: \"Pay to {target}\",\n      invalid_url: \"Invalid URL\",\n    },\n  },\n  PaymentRequestInfo: {\n    title_with_transport: \"Payment request via {transport}\",\n    title: \"Payment request\",\n    subtitle: \"Pay to {target}\",\n    subtitle_fallback: \"Payment request\",\n    invalid_url: \"Invalid URL\",\n  },\n  ReceiveDialog: {\n    title: \"Receive\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"No mints available\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints:\n          \"You need to connect to a mint to receive via Lightning\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"Receive Ecash\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"Request\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"Scanning…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"Receive Ecash\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"Paste Cashu token\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"Invalid token\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"Unable to receive. This token's P2PK lock doesn't match your public key.\",\n      },\n    },\n    unknown_mint_info_text:\n      \"Unknown mint. It will be added after you receive this token.\",\n    swap_section: {\n      title: \"Swap\",\n      source_label: \"From\",\n      destination_label: \"To\",\n      fee_info: \"This swap will incur Lightning network fees.\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"Adding mint…\",\n      },\n      swap: {\n        label: \"Receive to trusted mint\",\n        tooltip_text: \"Swap to a trusted mint\",\n        caption: \"Swap { value }\",\n        processing: \"Processing swap...\",\n        failed: \"Swap failed\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"Cancel swap\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      receive_to_selected_mint: {\n        label: \"Receive to selected mint\",\n      },\n      later: {\n        label: \"Receive later\",\n        tooltip_text: \"Add to history to receive later\",\n        already_in_history_success_text: \"Ecash already in History\",\n        added_to_history_success_text: \"Ecash added to History\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"Read from NFC card\",\n          ndef_unsupported_text: \"NDEF unsupported\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"P2PK Key\",\n      description: \"Receive ecash locked to this key\",\n      used_warning_text:\n        \"Warning: This key was used before. Use a new key for better privacy.\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"Generate new key\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"Payment Request\",\n      description: \"Receive payments via Nostr\",\n    },\n    received_total: \"Received total\",\n    no_payments_yet: \"No payments yet\",\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"New request\",\n      },\n      add_amount: {\n        label: \"Add amount\",\n      },\n      use_active_mint: {\n        label: \"Any mint\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"Enter amount\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"Keyboard disabled. You can re-enable the keyboard in the settings.\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostr Wallet Connect\",\n      description:\n        \"Control your wallet remotely with NWC. Press the QR code to link your wallet with a compatible app.\",\n      warning_text:\n        \"Warning: anyone with access to this connection string can initiate payments from your wallet. Do not share!\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"Mint Message\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"Contact\",\n    },\n    details: {\n      title: \"Mint details\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"View all\",\n          },\n          hide: {\n            label: \"Hide\",\n          },\n        },\n      },\n      currency: {\n        label: \"Currency\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"Version\",\n      },\n    },\n    actions: {\n      title: \"Actions\",\n      copy_mint_url: {\n        label: \"Copy mint URL\",\n      },\n      delete: {\n        label: \"Delete mint\",\n      },\n      edit: {\n        label: \"Edit mint\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"Select a mint\",\n    placeholder: \"Select a mint\",\n    available_text: \"available\",\n    sheet_title: \"Select Mint\",\n    badge_mint_error_text: \"Error\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"No history yet\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"{ value } ago\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Check status\",\n      },\n      receive: {\n        tooltip_text: \"Receive\",\n      },\n      filter_pending: {\n        label: \"Filter pending\",\n      },\n      show_all: {\n        label: \"Show all\",\n      },\n    },\n    old_token_not_found_error_text: \"Old token not found\",\n  },\n  InvoiceTable: {\n    empty_text: \"No invoices yet\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"Click to copy\",\n      date_label: \"{ value } ago\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Check status\",\n      },\n      filter_pending: {\n        label: \"Filter pending\",\n      },\n      show_all: {\n        label: \"Show all\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"Are you sure you want to delete this mint?\",\n    nickname: {\n      label: \"Nickname\",\n    },\n    balances: {\n      label: \"Balances\",\n    },\n    warning_text:\n      \"Note: Because this wallet is paranoid, your ecash from this mint will not be actually deleted but will remain stored on your device. You will see it reappear if you re-add this mint later again.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"Remove mint\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"Cashu token or Lightning address\",\n      receive: \"Cashu token\",\n      pay: \"Lightning address or invoice\",\n    },\n    qr_scanner: {\n      title: \"Scan QR Code\",\n      description: \"Tap to scan an address\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"Pay Lightning\",\n      inputs: {\n        invoice_data: {\n          label: \"Lightning invoice or address\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"{ payee } is requesting { value } { ticker }\",\n      amount_range_label:\n        \"{ payee } is requesting{br}between { min } and { max } { ticker }\",\n      sending_to_lightning_address: \"Sending to { address }\",\n      inputs: {\n        amount: {\n          label: \"Amount ({ ticker }) *\",\n        },\n        comment: {\n          label: \"Comment (optional)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"Pay { value }\",\n      paying: \"Paying\",\n      paid: \"Paid\",\n      fee: \"Fee\",\n      memo: {\n        label: \"Memo\",\n      },\n      processing_info_text: \"Processing…\",\n      balance_too_low_warning_text: \"Balance too low\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"Pay\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"Error\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"Edit mint\",\n    inputs: {\n      nickname: {\n        label: \"Nickname\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"Do you trust this mint?\",\n    description:\n      \"Before using this mint, make sure you trust it. Mints could become malicious or cease operation at any time.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"Adding mint\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"Please enter a mnemonic\",\n    restore_mint_error_text: \"Error restoring mint: { error }\",\n    prepare_info_text: \"Preparing restore process …\",\n    restored_proofs_for_keyset_info_text:\n      \"Restored { restoreCounter } proofs for keyset { keysetId }\",\n    checking_proofs_for_keyset_info_text:\n      \"Checking proofs { startIndex } to { endIndex } for keyset { keysetId }\",\n    no_proofs_info_text: \"No proofs found to restore\",\n    restored_amount_success_text: \"Restored { amount }\",\n  },\n  swap: {\n    in_progress_warning_text: \"Swap in progress\",\n    invalid_swap_data_error_text: \"Invalid swap data\",\n    swap_error_text: \"Error swapping\",\n  },\n  TokenInformation: {\n    fee: \"Fee\",\n    unit: \"Unit\",\n    fiat: \"Fiat\",\n    p2pk: \"P2PK\",\n    locked: \"Locked\",\n    locked_to_you: \"Locked to you\",\n    mint: \"Mint\",\n    memo: \"Memo\",\n    payment_request: \"Payment request\",\n    nostr: \"Nostr\",\n    token_copied: \"Token copied to clipboard\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/es-ES/index.ts",
    "content": "export default {\n  global: {\n    copy_to_clipboard: {\n      success: \"¡Copiado al portapapeles!\",\n    },\n    actions: {\n      add_mint: {\n        label: \"Añadir mint\",\n      },\n      cancel: {\n        label: \"Cancelar\",\n      },\n      copy: {\n        label: \"Copiar\",\n      },\n      close: {\n        label: \"Cerrar\",\n      },\n      enter: {\n        label: \"Entrar\",\n      },\n      lock: {\n        label: \"Bloquear\",\n      },\n      paste: {\n        label: \"Pegar\",\n      },\n      receive: {\n        label: \"Recibir\",\n      },\n      scan: {\n        label: \"Escanear\",\n      },\n      send: {\n        label: \"Enviar\",\n      },\n      swap: {\n        label: \"Intercambiar\",\n      },\n      update: {\n        label: \"Actualizar\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"URL del mint\",\n      },\n    },\n  },\n  MultinutPicker: {\n    payment: \"Pago Multinut\",\n    selectMints:\n      \"Seleccione una o varias casas de moneda para ejecutar un pago.\",\n    totalSelectedBalance: \"Saldo Total Seleccionado\",\n    multiMintPay: \"Pago Multi-Mint\",\n    balanceNotEnough:\n      \"El saldo de multi-mint no es suficiente para satisfacer esta factura\",\n    failed: \"Error al procesar: {error}\",\n    paid: \"Pagado {amount} a través de Lightning\",\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"El saldo es demasiado bajo\",\n      received: \"Recibido {amount}\",\n      fee: \" (comisión: {fee})\",\n      could_not_request_mint: \"No se pudo solicitar acuñación\",\n      invoice_still_pending: \"Factura aún pendiente\",\n      paid_lightning: \"Pagado {amount} a través de Lightning\",\n      payment_pending_refresh:\n        \"Pago pendiente. Actualice la factura manualmente.\",\n      sent: \"Enviado {amount}\",\n      token_still_pending: \"Token aún pendiente\",\n      received_lightning: \"Recibido {amount} a través de Lightning\",\n      lightning_payment_failed: \"Pago Lightning fallido\",\n      failed_to_decode_invoice: \"No se pudo decodificar la factura\",\n      invalid_lnurl: \"LNURL inválido\",\n      lnurl_error: \"Error LNURL\",\n      no_amount: \"Sin cantidad\",\n      no_lnurl_data: \"Sin datos LNURL\",\n      no_price_data: \"Sin datos de precio.\",\n      please_try_again: \"Por favor, inténtelo de nuevo.\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"Mint ya añadido\",\n        added: \"Mint añadido\",\n        not_found: \"Mint no encontrado\",\n        activation_failed: \"Fallo en la activación del mint\",\n        no_active_mint: \"No hay mint activo\",\n        unit_activation_failed: \"Fallo en la activación de la unidad\",\n        unit_not_supported: \"Unidad no soportada por el mint\",\n        activated: \"Mint activado\",\n        could_not_connect: \"No se pudo conectar al mint\",\n        could_not_get_info: \"No se pudo obtener información del mint\",\n        could_not_get_keys: \"No se pudieron obtener las claves del mint\",\n        could_not_get_keysets: \"No se pudieron obtener los keysets del mint\",\n        mint_validation_error: \"Error de validación de Mint\",\n        removed: \"Mint eliminado\",\n        error: \"Error del mint\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"Configuración\",\n        settings: {\n          title: \"Configuración\",\n          caption: \"Configuración de la billetera\",\n        },\n      },\n      terms: {\n        title: \"Términos\",\n        terms: {\n          title: \"Términos\",\n          caption: \"Términos de Servicio\",\n        },\n      },\n      links: {\n        title: \"Enlaces\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"Donar\",\n          caption: \"Apoyar a Cashu\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"Sin conexión\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"Recargar en { countdown }\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"Staging – ¡no usar con fondos reales!\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"Billetera\",\n      },\n    },\n  },\n  Settings: {\n    language: {\n      title: \"Idioma\",\n      description: \"Por favor, elige tu idioma preferido de la lista de abajo.\",\n    },\n    sections: {\n      backup_restore: \"COPIA DE SEGURIDAD Y RESTAURACIÓN\",\n      lightning_address: \"DIRECCIÓN LIGHTNING\",\n      nostr_keys: \"CLAVES NOSTR\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"Haga clic para editar relays\",\n          add: {\n            title: \"Añadir relay\",\n            description:\n              \"Su billetera usa estos relays para operaciones de nostr como solicitudes de pago, NWC y copias de seguridad.\",\n          },\n          list: {\n            title: \"Relays\",\n            description: \"Su billetera se conectará a estos relays.\",\n            copy_tooltip: \"Copiar relay\",\n            remove_tooltip: \"Eliminar relay\",\n          },\n        },\n      },\n      payment_requests: \"SOLICITUDES DE PAGO\",\n      nostr_wallet_connect: \"NOSTR WALLET CONNECT\",\n      hardware_features: \"CARACTERÍSTICAS DE HARDWARE\",\n      p2pk_features: \"CARACTERÍSTICAS P2PK\",\n      privacy: \"PRIVACIDAD\",\n      experimental: \"EXPERIMENTAL\",\n      appearance: \"APARIENCIA\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"Frase semilla de respaldo\",\n        description:\n          \"Tu frase semilla puede restaurar tu billetera. Mantenla segura y privada.\",\n        seed_phrase_label: \"Frase semilla\",\n      },\n      restore_ecash: {\n        title: \"Restaurar ecash\",\n        description:\n          \"El asistente de restauración te permite recuperar ecash perdido desde una frase semilla mnemónica. La frase semilla de tu billetera actual no se verá afectada, el asistente solo te permitirá restaurar ecash desde otra frase semilla.\",\n        button: \"Restaurar\",\n      },\n    },\n    lightning_address: {\n      title: \"Dirección Lightning\",\n      description: \"Recibe pagos a tu dirección Lightning.\",\n      enable: {\n        toggle: \"Habilitar\",\n        description: \"Dirección Lightning con npub.cash\",\n      },\n      address: {\n        copy_tooltip: \"Copiar dirección Lightning\",\n      },\n      automatic_claim: {\n        toggle: \"Reclamar automáticamente\",\n        description: \"Recibir pagos entrantes automáticamente.\",\n      },\n      npc_v2: {\n        choose_mint_title: \"Elegir mint para npub.cash v2\",\n        choose_mint_placeholder: \"Seleccionar un mint...\",\n      },\n    },\n    nostr_keys: {\n      title: \"Tus claves nostr\",\n      description: \"Establece las claves nostr para tu dirección Lightning.\",\n      wallet_seed: {\n        title: \"Frase semilla de la billetera\",\n        description:\n          \"Generar par de claves nostr desde la semilla de la billetera\",\n        copy_nsec: \"Copiar nsec\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"Usar un bunker NIP-46\",\n        delete_tooltip: \"Eliminar conexión\",\n      },\n      use_nsec: {\n        title: \"Usa tu nsec\",\n        description: \"Este método es peligroso y no se recomienda\",\n        delete_tooltip: \"Eliminar nsec\",\n      },\n      signing_extension: {\n        title: \"Extensión de firma\",\n        description: \"Usar una extensión de firma NIP-07\",\n        not_found: \"No se encontró ninguna extensión de firma NIP-07\",\n      },\n    },\n    payment_requests: {\n      title: \"Solicitudes de pago\",\n      description:\n        \"Las solicitudes de pago te permiten recibir pagos vía nostr. Si habilitas esto, tu billetera se suscribirá a tus relays nostr.\",\n      enable_toggle: \"Habilitar Solicitudes de Pago\",\n      claim_automatically: {\n        toggle: \"Reclamar automáticamente\",\n        description: \"Recibir pagos entrantes automáticamente.\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"Nostr Wallet Connect (NWC)\",\n      description:\n        \"Usa NWC para controlar tu billetera desde cualquier otra aplicación.\",\n      enable_toggle: \"Habilitar NWC\",\n      payments_note:\n        \"Solo puedes usar NWC para pagos desde tu saldo de Bitcoin. Los pagos se realizarán desde tu mint activo.\",\n      connection: {\n        copy_tooltip: \"Copiar cadena de conexión\",\n        qr_tooltip: \"Mostrar código QR\",\n        allowance_label: \"Límite restante (sat)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"Elige la codificación para escribir en tarjetas NFC\",\n        text: {\n          title: \"Texto\",\n          description: \"Almacenar token en texto plano\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"Almacenar URL a esta billetera con el token\",\n        },\n        binary: {\n          title: \"Binario\",\n          description: \"Almacenar tokens como datos binarios\",\n        },\n        quick_access: {\n          toggle: \"Acceso rápido a NFC\",\n          description:\n            \"Escanea rápidamente tarjetas NFC en el menú Recibir Ecash. Esta opción añade un botón NFC al menú Recibir Ecash.\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"Genera un par de claves para recibir ecash bloqueado con P2PK. Advertencia: Esta característica es experimental. Úsala solo con cantidades pequeñas. Si pierdes tus claves privadas, nadie podrá desbloquear el ecash bloqueado con ellas.\",\n      generate_button: \"Generar clave\",\n      import_button: \"Importar nsec\",\n      quick_access: {\n        toggle: \"Acceso rápido para bloquear\",\n        description:\n          \"Usa esto para mostrar rápidamente tu clave de bloqueo P2PK en el menú recibir ecash.\",\n      },\n      keys_expansion: {\n        label: \"Haz clic para ver {count} claves\",\n        used_badge: \"usada\",\n      },\n    },\n    privacy: {\n      title: \"Privacidad\",\n      description: \"Estas configuraciones afectan tu privacidad.\",\n      check_incoming: {\n        toggle: \"Verificar factura entrante\",\n        description:\n          \"Si está habilitado, la billetera verificará la última factura en segundo plano. Esto aumenta la capacidad de respuesta de la billetera, lo que facilita la toma de huellas digitales. Puedes verificar manualmente las facturas no pagadas en la pestaña Facturas.\",\n      },\n      check_startup: {\n        toggle: \"Verificar facturas pendientes al inicio\",\n        description:\n          \"Si está habilitado, la billetera verificará las facturas pendientes de las últimas 24 horas al inicio.\",\n      },\n      check_all: {\n        toggle: \"Verificar todas las facturas\",\n        description:\n          \"Si está habilitado, la billetera verificará periódicamente las facturas no pagadas en segundo plano durante hasta dos semanas. Esto aumenta la actividad en línea de la billetera, lo que facilita la toma de huellas digitales. Puedes verificar manualmente las facturas no pagadas en la pestaña Facturas.\",\n      },\n      check_sent: {\n        toggle: \"Verificar ecash enviado\",\n        description:\n          \"Si está habilitado, la billetera usará verificaciones periódicas en segundo plano para determinar si los tokens enviados han sido canjeados. Esto aumenta la actividad en línea de la billetera, lo que facilita la toma de huellas digitales.\",\n      },\n      websockets: {\n        toggle: \"Usar WebSockets\",\n        description:\n          \"Si está habilitado, la billetera usará conexiones WebSocket de larga duración para recibir actualizaciones sobre facturas pagadas y tokens gastados de los mints. Esto aumenta la capacidad de respuesta de la billetera pero también facilita la toma de huellas digitales.\",\n      },\n      bitcoin_price: {\n        toggle: \"Obtener tasa de cambio de Coinbase\",\n        description:\n          \"Si está habilitado, se obtendrá la tasa de cambio actual de Bitcoin de coinbase.com y se mostrará tu saldo convertido.\",\n        currency: {\n          title: \"Moneda Fiat\",\n          description:\n            \"Elija la moneda fiat para mostrar el precio de Bitcoin.\",\n        },\n      },\n    },\n    experimental: {\n      title: \"Experimental\",\n      description: \"Estas características son experimentales.\",\n      receive_swaps: {\n        toggle: \"Recibir intercambios\",\n        badge: \"Beta\",\n        description:\n          \"Opción para intercambiar Ecash recibido a tu mint activo en el diálogo Recibir Ecash.\",\n      },\n      auto_paste: {\n        toggle: \"Pegar Ecash automáticamente\",\n        description:\n          \"Pega automáticamente ecash de tu portapapeles cuando presionas Recibir, luego Ecash, luego Pegar. El pegado automático puede causar fallos en la interfaz de usuario en iOS, desactívalo si experimentas problemas.\",\n      },\n      auditor: {\n        toggle: \"Habilitar auditor\",\n        badge: \"Beta\",\n        description:\n          \"Si está habilitado, la billetera mostrará información del auditor en el diálogo de detalles del mint. El auditor es un servicio de terceros que monitorea la fiabilidad de los mints.\",\n        url_label: \"URL del Auditor\",\n        api_url_label: \"URL API del Auditor\",\n      },\n      multinut: {\n        toggle: \"Habilitar Multinut\",\n        description:\n          \"Si está habilitado, el monedero utilizará Multinut para pagar facturas de varias cecas a la vez.\",\n      },\n      nostr_mint_backup: {\n        toggle: \"Copia de seguridad de la lista de cecas en Nostr\",\n        description:\n          \"Si está activada, se realizará una copia de seguridad automática de su lista de cecas en los relés de Nostr utilizando sus claves de Nostr configuradas. Esto le permite restaurar su lista de cecas en todos los dispositivos.\",\n        notifications: {\n          enabled: \"Copia de seguridad de la ceca de Nostr activada\",\n          disabled: \"Copia de seguridad de la ceca de Nostr desactivada\",\n          failed: \"Error al activar la copia de seguridad de la ceca de Nostr\",\n        },\n      },\n    },\n    appearance: {\n      keyboard: {\n        title: \"Teclado en pantalla\",\n        description: \"Usa el teclado numérico para ingresar cantidades.\",\n        toggle: \"Usar teclado numérico\",\n        toggle_description:\n          \"Si está habilitado, se usará el teclado numérico para ingresar cantidades.\",\n      },\n      theme: {\n        title: \"Apariencia\",\n        description: \"Cambia cómo se ve tu billetera.\",\n        tooltips: {\n          mono: \"mono\",\n          cyber: \"ciber\",\n          freedom: \"libertad\",\n          nostr: \"nostr\",\n          bitcoin: \"bitcoin\",\n          mint: \"mint\",\n          nut: \"nuez\",\n          blu: \"azul\",\n          flamingo: \"flamenco\",\n        },\n      },\n      bip177: {\n        title: \"Símbolo de Bitcoin\",\n        description: \"Utilice el símbolo ₿ en lugar de sats.\",\n        toggle: \"Utilice el símbolo ₿\",\n      },\n    },\n    web_of_trust: {\n      title: \"Red de confianza\",\n      known_pubkeys: \"Claves públicas conocidas: {wotCount}\",\n      continue_crawl: \"Continuar rastreo\",\n      crawl_odell: \"RASTREAR LA RED DE CONFIANZA DE ODELL\",\n      crawl_wot: \"Rastrear red de confianza\",\n      pause: \"Pausa\",\n      reset: \"Reiniciar\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"Utilice npubx.cash\",\n      copy_lightning_address: \"Copiar dirección Lightning\",\n      v2_mint: \"npub.cash v2 mint\",\n    },\n    multinut: {\n      use_multinut: \"Usar Multinut\",\n    },\n    advanced: {\n      title: \"Avanzado\",\n      developer: {\n        title: \"Configuración de desarrollador\",\n        description:\n          \"Las siguientes configuraciones son para desarrollo y depuración.\",\n\n        new_seed: {\n          button: \"Generar nueva frase semilla\",\n          description:\n            \"Esto generará una nueva frase semilla. Debes enviar tu saldo completo a ti mismo para poder restaurarlo con una nueva semilla.\",\n          confirm_question:\n            \"¿Estás seguro de que quieres generar una nueva frase semilla?\",\n          cancel: \"Cancelar\",\n          confirm: \"Confirmar\",\n        },\n        remove_spent: {\n          button: \"Eliminar pruebas gastadas\",\n          description:\n            \"Verifica si los tokens ecash de tus mints activos están gastados y elimina los gastados de tu billetera. Solo usa esto si tu billetera está bloqueada.\",\n        },\n        debug_console: {\n          button: \"Alternar Consola de Depuración\",\n          description:\n            \"Abre la terminal de depuración de Javascript. Nunca pegues nada en esta terminal que no entiendas. Un ladrón podría intentar engañarte para que pegues código malicioso aquí.\",\n        },\n        export_proofs: {\n          button: \"Exportar pruebas activas\",\n          description:\n            \"Copia tu saldo completo del mint activo como un token Cashu en tu portapapeles. Esto solo exportará los tokens del mint y unidad seleccionados. Para una exportación completa, selecciona un mint y unidad diferente y exporta de nuevo.\",\n        },\n        keyset_counters: {\n          title: \"Incrementar contadores de keyset\",\n          description:\n            'Haz clic en el ID del keyset para incrementar los contadores de la ruta de derivación para los keysets en tu billetera. Esto es útil si ves el error \"las salidas ya han sido firmadas\".',\n          counter: \"contador: {count}\",\n        },\n        unset_reserved: {\n          button: \"Desmarcar todos los tokens reservados\",\n          description:\n            'Esta billetera marca el ecash saliente pendiente como reservado (y lo resta de tu saldo) para prevenir intentos de doble gasto. Este botón desmarcará todos los tokens reservados para que puedan usarse de nuevo. Si haces esto, tu billetera podría incluir pruebas gastadas. Presiona el botón \"Eliminar pruebas gastadas\" para deshacerte de ellas.',\n        },\n        show_onboarding: {\n          button: \"Mostrar bienvenida\",\n          description: \"Mostrar la pantalla de bienvenida de nuevo.\",\n        },\n        reset_wallet: {\n          button: \"Restablecer datos de la billetera\",\n          description:\n            \"Restablece los datos de tu billetera. Advertencia: ¡Esto borrará todo! Asegúrate de crear una copia de seguridad primero.\",\n          confirm_question:\n            \"¿Estás seguro de que quieres eliminar los datos de tu billetera?\",\n          cancel: \"Cancelar\",\n          confirm: \"Eliminar billetera\",\n        },\n        export_wallet: {\n          button: \"Exportar datos de la billetera\",\n          description:\n            \"Descarga un volcado de tu billetera. Puedes restaurar tu billetera desde este archivo en la pantalla de bienvenida de una nueva billetera. Este archivo estará desactualizado si sigues usando tu billetera después de exportarlo.\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"Únete a un mint\",\n    subtitle:\n      \"Todavía no te has unido a ningún mint de Cashu. Añade una URL de mint en la configuración o recibe ecash de un nuevo mint para empezar.\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"Recibir Ecash\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"Historial\",\n      },\n      invoices: {\n        label: \"Facturas\",\n      },\n      mints: {\n        label: \"Mints\",\n      },\n    },\n    install: {\n      text: \"Instalar\",\n      tooltip: \"Instalar Cashu\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"Nop.\",\n    text: \"Otra pestaña ya está en ejecución. Cierra esta pestaña e inténtalo de nuevo.\",\n    actions: {\n      retry: {\n        label: \"Reintentar\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"Oops. Nada por aquí…\",\n    actions: {\n      home: {\n        label: \"Volver al inicio\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Mint\",\n    },\n    mintBalance: {\n      label: \"Saldo\",\n    },\n    mintError: {\n      label: \"Error del mint\",\n    },\n    pending: {\n      label: \"Pendiente\",\n      tooltip: \"Verificar todos los tokens pendientes\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"Anterior\",\n      },\n      next: {\n        label: \"Siguiente\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"Bienvenido a Cashu\",\n    text: \"Cashu.me es una billetera de Bitcoin gratuita y de código abierto que utiliza ecash para mantener tus fondos seguros y privados.\",\n    actions: {\n      more: {\n        label: \"Haz clic para saber más\",\n      },\n    },\n    p1: {\n      text: \"Cashu es un protocolo ecash gratuito y de código abierto para Bitcoin. Puedes aprender más en { link }.\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"Esta billetera no está afiliada a ningún mint. Para usar esta billetera, necesitas conectarte a uno o más mints de Cashu en los que confíes.\",\n    },\n    p3: {\n      text: \"Esta billetera almacena ecash al que solo tú tienes acceso. Si eliminas los datos de tu navegador sin una copia de seguridad de la frase semilla, perderás tus tokens.\",\n    },\n    p4: {\n      text: \"Esta billetera está en beta. No nos hacemos responsables de las personas que pierdan el acceso a sus fondos. ¡Úsala bajo tu propio riesgo! Este código es de código abierto y está licenciado bajo la licencia MIT.\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"Instalar PWA\",\n    alt: { pwa_example: \"Ejemplo de instalación PWA\" },\n    installing: \"Instalando…\",\n    instruction: {\n      intro: {\n        text: \"Para la mejor experiencia, usa esta billetera con el navegador web nativo de tu dispositivo para instalarla como una Aplicación Web Progresiva. Haz esto ahora mismo.\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Toca el menú (arriba a la derecha)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Presiona { buttonText }\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Toca compartir (abajo)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Presiona { buttonText }\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"Una vez que hayas instalado esta aplicación en tu dispositivo, cierra esta ventana del navegador y usa la aplicación desde tu pantalla de inicio.\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"¡Éxito!\",\n        text: \"Estás usando Cashu como una PWA. Cierra cualquier otra ventana del navegador abierta y usa la aplicación desde tu pantalla de inicio.\",\n        nextSteps:\n          \"Ahora puedes cerrar esta pestaña del navegador y abrir la app desde tu pantalla de inicio.\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"Toca { icon } y { buttonText }\",\n    buttonText: \"Añadir a pantalla de inicio\",\n  },\n  AndroidPWAPrompt: {\n    text: \"Toca { icon } y { buttonText }\",\n    buttonText: \"Añadir a pantalla de inicio\",\n  },\n  WelcomeSlide3: {\n    title: \"Tu Frase Semilla\",\n    text: \"Guarda tu frase semilla en un gestor de contraseñas o en papel. Tu frase semilla es la única forma de recuperar tus fondos si pierdes el acceso a este dispositivo.\",\n    inputs: {\n      seed_phrase: {\n        label: \"Frase Semilla\",\n        caption: \"Puedes ver tu frase semilla en la configuración.\",\n      },\n      checkbox: {\n        label: \"La he anotado\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"Términos\",\n    actions: {\n      more: {\n        label: \"Leer Términos de Servicio\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"He leído y acepto estos términos y condiciones\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"Configura tu billetera\",\n    text: \"¿Quieres recuperar desde una frase semilla o crear una billetera nueva?\",\n    options: {\n      new: {\n        title: \"Crear billetera nueva\",\n        subtitle: \"Genera una semilla nueva y añade mints.\",\n      },\n      recover: {\n        title: \"Recuperar billetera\",\n        subtitle: \"Ingresa tu frase semilla, restaura mints y ecash.\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"Añadir mints\",\n    text: \"Los mints son servidores que te ayudan a enviar y recibir ecash. Elige un mint descubierto o añade uno manualmente. Puedes hacerlo más tarde.\",\n    sections: { your_mints: \"Tus mints\" },\n    restoring: \"Restaurando mints…\",\n    placeholder: { mint_url: \"https://\" },\n  },\n  WelcomeRecoverSeed: {\n    title: \"Ingresa tu frase semilla\",\n    text: \"Pega o escribe tu frase semilla de 12 palabras para recuperar.\",\n    inputs: { word: \"Palabra { index }\" },\n    actions: { paste_all: \"Pegar todo\" },\n    disclaimer:\n      \"Tu frase semilla solo se usa localmente para derivar las claves de tu billetera.\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"Restaura tu ecash\",\n    text: \"Busca comprobantes no gastados en tus mints configurados y agrégalos a tu billetera.\",\n  },\n  MintRatings: {\n    title: \"Reseñas del mint\",\n    reviews: \"reseñas\",\n    ratings: \"Calificaciones\",\n    no_reviews: \"No se encontraron reseñas\",\n    your_review: \"Tu reseña\",\n    no_reviews_to_display: \"No hay reseñas para mostrar.\",\n    no_rating: \"Sin calificación\",\n    out_of: \"de\",\n    rows: \"Reviews\",\n    sort: \"Ordenar\",\n    sort_options: {\n      newest: \"Más recientes\",\n      oldest: \"Más antiguas\",\n      highest: \"Más altas\",\n      lowest: \"Más bajas\",\n    },\n    actions: { write_review: \"Escribir una reseña\" },\n    empty_state_subtitle:\n      \"Ayuda dejando una reseña. Comparte tu experiencia con este mint y ayuda a otros dejando una reseña.\",\n  },\n  CreateMintReview: {\n    title: \"Reseñar mint\",\n    publishing_as: \"Publicando como\",\n    inputs: {\n      rating: { label: \"Calificación\" },\n      review: { label: \"Reseña (opcional)\" },\n    },\n    actions: { publish: { label: \"Publicar\", in_progress: \"Publicando…\" } },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"Restaurar desde Frase Semilla\",\n      caption:\n        \"Ingresa tu frase semilla para restaurar tu billetera. Antes de restaurar, asegúrate de haber añadido todos los mints que has usado antes.\",\n      inputs: {\n        seed_phrase: {\n          label: \"Frase semilla\",\n          caption: \"Puedes ver tu frase semilla en la configuración.\",\n        },\n      },\n    },\n    information: {\n      label: \"Información\",\n      caption:\n        \"El asistente solo restaurará ecash desde otra frase semilla, no podrás usar esta frase semilla ni cambiar la frase semilla de la billetera que estás usando actualmente. Esto significa que el ecash restaurado no estará protegido por tu frase semilla actual mientras no te envíes el ecash a ti mismo una vez.\",\n    },\n    restore_mints: {\n      label: \"Restaurar Mints\",\n      caption:\n        'Selecciona el mint para restaurar. Puedes añadir más mints en la pantalla principal bajo \"Mints\" y restaurarlos aquí.',\n    },\n    nostr_mints: {\n      label: \"Restaurar Mints de Nostr\",\n      caption:\n        \"Busque copias de seguridad de mints almacenadas en relés de Nostr utilizando su frase semilla. Esto le ayudará a descubrir mints que utilizó anteriormente.\",\n      search_button: \"Buscar copias de seguridad de Mint\",\n      select_all: \"Seleccionar todo\",\n      deselect_all: \"Deseleccionar todo\",\n      backed_up: \"Copia de seguridad realizada\",\n      already_added: \"Ya añadido\",\n      add_selected: \"Añadir seleccionados ({count})\",\n      no_backups_found: \"No se han encontrado copias de seguridad de mints\",\n      no_backups_hint:\n        \"Asegúrese de que la copia de seguridad de Nostr mint está activada en los ajustes para hacer una copia de seguridad automática de su lista de mints.\",\n      invalid_mnemonic:\n        \"Por favor, introduzca una frase semilla válida antes de buscar.\",\n      search_error: \"Error al buscar copias de seguridad de mints.\",\n      add_error: \"Error al añadir los mints seleccionados.\",\n    },\n    actions: {\n      paste: {\n        error: \"Error al leer el contenido del portapapeles.\",\n      },\n      validate: {\n        error: \"El mnemónico debe tener al menos 12 palabras.\",\n      },\n      select_all: {\n        label: \"Seleccionar todo\",\n      },\n      deselect_all: {\n        label: \"Deseleccionar todo\",\n      },\n      restore: {\n        label: \"Restaurar\",\n        in_progress: \"Restaurando mint…\",\n        error: \"Error restaurando mint: { error }\",\n      },\n      restore_all_mints: {\n        label: \"Restaurar Todos los Mints\",\n        in_progress: \"Restaurando mint { index } de { length }…\",\n        success: \"Restauración finalizada con éxito\",\n        error: \"Error restaurando mints: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"Restaurar Mints seleccionados ({count})\",\n        in_progress: \"Restaurando mint { index } de { length }…\",\n        success: \"Se han restaurado correctamente {count} mint(s)\",\n        error: \"Error al restaurar los mints seleccionados: { error }\",\n      },\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"Añadir mint\",\n      description:\n        \"Ingresa la URL de un mint de Cashu para conectarte a él. Esta billetera no está afiliada a ningún mint.\",\n      inputs: {\n        nickname: {\n          placeholder: \"Apodo (ej. Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"URL inválida\",\n        },\n        scan: {\n          label: \"Escanear Código QR\",\n        },\n      },\n    },\n    discover: {\n      title: \"Descubrir mints\",\n      overline: \"Descubrir\",\n      caption: \"Descubre mints que otros usuarios han recomendado en nostr.\",\n      actions: {\n        discover: {\n          label: \"Descubrir mints\",\n          in_progress: \"Cargando…\",\n          error_no_mints: \"No se encontraron mints\",\n          success: \"Encontrados { length } mints\",\n        },\n      },\n      recommendations: {\n        overline: \"Encontrados { length } mints\",\n        caption:\n          \"Estos mints fueron recomendados por otros usuarios de Nostr. Ten cuidado y haz tu propia investigación antes de usar un mint.\",\n        actions: {\n          browse: {\n            label: \"Haz clic para explorar mints\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"Intercambiar\",\n      overline: \"Intercambios Multimint\",\n      caption:\n        \"Intercambia fondos entre mints vía Lightning. Nota: Deja espacio para posibles comisiones de Lightning. Si el pago entrante no tiene éxito, verifica la factura manualmente.\",\n      inputs: {\n        from: {\n          label: \"Desde\",\n        },\n        to: {\n          label: \"Hasta\",\n        },\n        amount: {\n          label: \"Cantidad ({ ticker })\",\n        },\n      },\n      actions: {\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n    },\n    error_badge: \"Error\",\n    reviews_text: \"reseñas\",\n    no_reviews_yet: \"Aún no hay reseñas\",\n    discover_mints_button: \"Descubrir mints\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - Sigue escaneando\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"Recibir Lightning\",\n    create_invoice_title: \"Crear Factura\",\n    inputs: {\n      amount: {\n        label: \"Cantidad ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"Crear Factura\",\n        label_blocked: \"Creando factura…\",\n        in_progress: \"Creando\",\n      },\n    },\n    invoice: {\n      caption: \"Factura Lightning\",\n      status_paid_text: \"¡Pagada!\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"Enviar\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"No hay mints disponibles\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"No hay mints disponibles\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"Enviar Ecash\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"Sin conexión\",\n    inputs: {\n      amount: {\n        label: \"Cantidad ({ ticker }) *\",\n        invalid_too_much_error_text: \"Demasiado\",\n      },\n      p2pk_pubkey: {\n        label: \"Clave pública del receptor\",\n        label_invalid: \"Clave pública del receptor inválida\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"Copiar Emoji\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"Copiar enlace\",\n      },\n      share: {\n        tooltip_text: \"Compartir ecash\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"Eliminar del historial\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"Grabar en tarjeta NFC\",\n          ndef_unsupported_text: \"NDEF no soportado\",\n        },\n      },\n    },\n  },\n  ReceiveDialog: {\n    title: \"Recibir\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"No hay mints disponibles\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints:\n          \"Necesitas conectarte a un mint para recibir vía Lightning\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"Recibir Ecash\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"Solicitar\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"Escaneando…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"Recibir Ecash\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"Pegar token Cashu\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"Token inválido\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"No se puede recibir. El bloqueo P2PK de este token no coincide con su clave pública.\",\n      },\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"Añadiendo mint…\",\n      },\n      swap: {\n        label: \"@:global.actions.swap.label\",\n        tooltip_text: \"Intercambiar a un mint de confianza\",\n        caption: \"Intercambiar { value }\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"Cancelar intercambio\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      later: {\n        label: \"Recibir más tarde\",\n        tooltip_text: \"Añadir al historial para recibir más tarde\",\n        already_in_history_success_text: \"Ecash ya está en el Historial\",\n        added_to_history_success_text: \"Ecash añadido al Historial\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"Leer desde tarjeta NFC\",\n          ndef_unsupported_text: \"NDEF no soportado\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"Clave P2PK\",\n      description: \"Recibir ecash bloqueado con esta clave\",\n      used_warning_text:\n        \"Advertencia: Esta clave se usó antes. Usa una clave nueva para mayor privacidad.\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"Generar nueva clave\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"Solicitud de Pago\",\n      description: \"Recibir pagos vía Nostr\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"Nueva solicitud\",\n      },\n      add_amount: {\n        label: \"Añadir cantidad\",\n      },\n      use_active_mint: {\n        label: \"Cualquier mint\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"Ingresar cantidad\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"Teclado desactivado. Puedes reactivar el teclado en la configuración.\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostr Wallet Connect\",\n      description:\n        \"Controla tu billetera remotamente con NWC. Presiona el código QR para vincular tu billetera con una aplicación compatible.\",\n      warning_text:\n        \"Advertencia: cualquiera con acceso a esta cadena de conexión puede iniciar pagos desde tu billetera. ¡No la compartas!\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"Mensaje del Mint\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"Contacto\",\n    },\n    details: {\n      title: \"Detalles del mint\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"Ver todo\",\n          },\n          hide: {\n            label: \"Ocultar\",\n          },\n        },\n      },\n      currency: {\n        label: \"Moneda\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"Versión\",\n      },\n    },\n    actions: {\n      title: \"Acciones\",\n      copy_mint_url: {\n        label: \"Copiar URL del mint\",\n      },\n      delete: {\n        label: \"Eliminar mint\",\n      },\n      edit: {\n        label: \"Editar mint\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"Selecciona un mint\",\n    badge_mint_error_text: \"Error\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"Aún no hay historial\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"hace { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Verificar estado\",\n      },\n      receive: {\n        tooltip_text: \"Recibir\",\n      },\n      filter_pending: {\n        label: \"Filtrar pendientes\",\n      },\n      show_all: {\n        label: \"Mostrar todo\",\n      },\n    },\n    old_token_not_found_error_text: \"Token antiguo no encontrado\",\n  },\n  InvoiceTable: {\n    empty_text: \"Aún no hay facturas\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"Haz clic para copiar\",\n      date_label: \"hace { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Verificar estado\",\n      },\n      filter_pending: {\n        label: \"Filtrar pendientes\",\n      },\n      show_all: {\n        label: \"Mostrar todo\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"¿Estás seguro de que quieres eliminar este mint?\",\n    nickname: {\n      label: \"Apodo\",\n    },\n    balances: {\n      label: \"Saldos\",\n    },\n    warning_text:\n      \"Nota: Debido a que esta billetera es paranoica, tu ecash de este mint no se eliminará realmente, sino que permanecerá almacenado en tu dispositivo. Lo verás reaparecer si vuelves a añadir este mint más tarde.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"Eliminar mint\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"Token Cashu o dirección Lightning\",\n      receive: \"Token Cashu\",\n      pay: \"Dirección Lightning o factura\",\n    },\n    qr_scanner: {\n      title: \"Escanear Código QR\",\n      description: \"Toca para escanear una dirección\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"Pagar con Lightning\",\n      inputs: {\n        invoice_data: {\n          label: \"Factura o dirección Lightning\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"{ payee } está solicitando { value } { ticker }\",\n      amount_range_label:\n        \"{ payee } está solicitando{br}entre { min } y { max } { ticker }\",\n      sending_to_lightning_address: \"Enviando a { address }\",\n      inputs: {\n        amount: {\n          label: \"Cantidad ({ ticker }) *\",\n        },\n        comment: {\n          label: \"Comentario (opcional)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"Pagar { value }\",\n      paying: \"Pagando\",\n      paid: \"Pagado\",\n      fee: \"Tarifa\",\n      memo: {\n        label: \"Memo\",\n      },\n      processing_info_text: \"Procesando…\",\n      balance_too_low_warning_text: \"Saldo demasiado bajo\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"Pagar\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"Error\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"Editar mint\",\n    inputs: {\n      nickname: {\n        label: \"Apodo\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"¿Confías en este mint?\",\n    description:\n      \"Antes de usar este mint, asegúrate de confiar en él. Los mints podrían volverse maliciosos o dejar de operar en cualquier momento.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"Añadiendo mint\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"Por favor, ingresa un mnemónico\",\n    restore_mint_error_text: \"Error restaurando mint: { error }\",\n    prepare_info_text: \"Preparando proceso de restauración…\",\n    restored_proofs_for_keyset_info_text:\n      \"Restauradas { restoreCounter } pruebas para el keyset { keysetId }\",\n    checking_proofs_for_keyset_info_text:\n      \"Verificando pruebas { startIndex } a { endIndex } para el keyset { keysetId }\",\n    no_proofs_info_text: \"No se encontraron pruebas para restaurar\",\n    restored_amount_success_text: \"Restaurado { amount }\",\n  },\n  swap: {\n    in_progress_warning_text: \"Intercambio en progreso\",\n    invalid_swap_data_error_text: \"Datos de intercambio inválidos\",\n    swap_error_text: \"Error al intercambiar\",\n  },\n  TokenInformation: {\n    fee: \"Tarifa\",\n    unit: \"Unidad\",\n    fiat: \"Fiat\",\n    p2pk: \"P2PK\",\n    locked: \"Bloqueado\",\n    locked_to_you: \"Bloqueado para ti\",\n    mint: \"Casa de moneda\",\n    memo: \"Memo\",\n    payment_request: \"Solicitud de pago\",\n    nostr: \"Nostr\",\n    token_copied: \"Token copiado al portapapeles\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/fr-FR/index.ts",
    "content": "export default {\n  MultinutPicker: {\n    payment: \"Paiement Multinut\",\n    selectMints:\n      \"Sélectionnez une ou plusieurs mints pour exécuter un paiement.\",\n    totalSelectedBalance: \"Solde total sélectionné\",\n    multiMintPay: \"Paiement Multi-Mint\",\n    balanceNotEnough:\n      \"Le solde multi-mint n’est pas suffisant pour satisfaire cette facture\",\n    failed: \"Échec du traitement: {error}\",\n    paid: \"Payé {amount} via Lightning\",\n  },\n  global: {\n    copy_to_clipboard: {\n      success: \"Copié dans le presse-papiers !\",\n    },\n    actions: {\n      add_mint: {\n        label: \"Ajouter une mint\",\n      },\n      cancel: {\n        label: \"Annuler\",\n      },\n      copy: {\n        label: \"Copier\",\n      },\n      close: {\n        label: \"Fermer\",\n      },\n      enter: {\n        label: \"Entrer\",\n      },\n      lock: {\n        label: \"Verrouiller\",\n      },\n      paste: {\n        label: \"Coller\",\n      },\n      receive: {\n        label: \"Recevoir\",\n      },\n      scan: {\n        label: \"Scanner\",\n      },\n      send: {\n        label: \"Envoyer\",\n      },\n      swap: {\n        label: \"Échanger\",\n      },\n      update: {\n        label: \"Mettre à jour\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"URL de la mint\",\n      },\n    },\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"Le solde est trop bas\",\n      received: \"Reçu {amount}\",\n      fee: \" (frais: {fee})\",\n      could_not_request_mint: \"Impossible de demander à la Mint\",\n      invoice_still_pending: \"Facture toujours en attente\",\n      paid_lightning: \"Payé {amount} via Lightning\",\n      payment_pending_refresh:\n        \"Paiement en attente. Rafraîchissez la facture manuellement.\",\n      sent: \"Envoyé {amount}\",\n      token_still_pending: \"Token toujours en attente\",\n      received_lightning: \"Reçu {amount} via Lightning\",\n      lightning_payment_failed: \"Paiement Lightning échoué\",\n      failed_to_decode_invoice: \"Impossible de décoder la facture\",\n      invalid_lnurl: \"LNURL invalide\",\n      lnurl_error: \"Erreur LNURL\",\n      no_amount: \"Pas de montant\",\n      no_lnurl_data: \"Pas de données LNURL\",\n      no_price_data: \"Pas de données de prix.\",\n      please_try_again: \"Veuillez réessayer.\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"Mint déjà ajoutée\",\n        added: \"Mint ajoutée\",\n        not_found: \"Mint introuvable\",\n        activation_failed: \"L'activation de la mint a échoué\",\n        no_active_mint: \"Aucune mint active\",\n        unit_activation_failed: \"L'activation de l'unité a échoué\",\n        unit_not_supported: \"Unité non prise en charge par la mint\",\n        activated: \"Mint activée\",\n        could_not_connect: \"Impossible de se connecter à la mint\",\n        could_not_get_info: \"Impossible d'obtenir les informations de la mint\",\n        could_not_get_keys: \"Impossible d'obtenir les clés de la mint\",\n        could_not_get_keysets: \"Impossible d'obtenir les keysets de la mint\",\n        mint_validation_error: \"Erreur de validation de la menthe\",\n        removed: \"Mint supprimée\",\n        error: \"Erreur de la mint\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"Paramètres\",\n        settings: {\n          title: \"Paramètres\",\n          caption: \"Configuration du portefeuille\",\n        },\n      },\n      terms: {\n        title: \"Conditions\",\n        terms: {\n          title: \"Conditions\",\n          caption: \"Conditions de Service\",\n        },\n      },\n      links: {\n        title: \"Liens\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"Faire un don\",\n          caption: \"Soutenir Cashu\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"Hors ligne\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"Recharger dans { countdown }\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"Staging – ne pas utiliser avec de vrais fonds !\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"Portefeuille\",\n      },\n    },\n  },\n  Settings: {\n    sections: {\n      backup_restore: \"SAUVEGARDE & RESTAURATION\",\n      lightning_address: \"ADRESSE LIGHTNING\",\n      nostr_keys: \"CLÉS NOSTR\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"Cliquer pour modifier les relais\",\n          add: {\n            title: \"Ajouter un relais\",\n            description:\n              \"Votre portefeuille utilise ces relais pour les opérations Nostr comme les demandes de paiement, NWC et les sauvegardes.\",\n          },\n          list: {\n            title: \"Relais\",\n            description: \"Votre portefeuille se connectera à ces relais.\",\n            copy_tooltip: \"Copier le relais\",\n            remove_tooltip: \"Supprimer le relais\",\n          },\n        },\n      },\n      payment_requests: \"DEMANDES DE PAIEMENT\",\n      nostr_wallet_connect: \"NOSTR WALLET CONNECT\",\n      hardware_features: \"FONCTIONNALITÉS MATÉRIELLES\",\n      p2pk_features: \"FONCTIONNALITÉS P2PK\",\n      privacy: \"CONFIDENTIALITÉ\",\n      experimental: \"EXPÉRIMENTAL\",\n      appearance: \"APPARENCE\",\n    },\n    language: {\n      title: \"Langue\",\n      description:\n        \"Veuillez choisir votre langue préférée dans la liste ci-dessous.\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"Sauvegarder la phrase de départ\",\n        description:\n          \"Votre phrase de départ peut restaurer votre portefeuille. Gardez-la en sécurité et privée.\",\n        seed_phrase_label: \"Phrase de départ\",\n      },\n      restore_ecash: {\n        title: \"Restaurer ecash\",\n        description:\n          \"L'assistant de restauration vous permet de récupérer l'ecash perdu à partir d'une phrase mnémonique. La phrase de départ de votre portefeuille actuel ne sera pas affectée, l'assistant vous permettra uniquement de restaurer l'ecash à partir d'une autre phrase de départ.\",\n        button: \"Restaurer\",\n      },\n    },\n    lightning_address: {\n      title: \"Adresse Lightning\",\n      description: \"Recevez des paiements sur votre adresse Lightning.\",\n      enable: {\n        toggle: \"Activer\",\n        description: \"Adresse Lightning avec npub.cash\",\n      },\n      address: {\n        copy_tooltip: \"Copier l'adresse Lightning\",\n      },\n      automatic_claim: {\n        toggle: \"Réclamer automatiquement\",\n        description: \"Recevez les paiements entrants automatiquement.\",\n      },\n      npc_v2: {\n        choose_mint_title: \"Choisissez une menthe pour npub.cash v2\",\n        choose_mint_placeholder: \"Sélectionnez une menthe...\",\n      },\n    },\n    nostr_keys: {\n      title: \"Vos clés Nostr\",\n      description: \"Définissez les clés nostr pour votre adresse Lightning.\",\n      wallet_seed: {\n        title: \"Phrase de départ du portefeuille\",\n        description:\n          \"Générer une paire de clés nostr à partir de la phrase de départ du portefeuille\",\n        copy_nsec: \"Copier nsec\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"Utiliser un bunker NIP-46\",\n        delete_tooltip: \"Supprimer la connexion\",\n      },\n      use_nsec: {\n        title: \"Utiliser votre nsec\",\n        description: \"Cette méthode est dangereuse et non recommandée\",\n        delete_tooltip: \"Supprimer nsec\",\n      },\n      signing_extension: {\n        title: \"Extension de signature\",\n        description: \"Utiliser une extension de signature NIP-07\",\n        not_found: \"Aucune extension de signature NIP-07 trouvée\",\n      },\n    },\n    payment_requests: {\n      title: \"Demandes de paiement\",\n      description:\n        \"Les demandes de paiement vous permettent de recevoir des paiements via nostr. Si vous activez cela, votre portefeuille s'abonnera à vos relais nostr.\",\n      enable_toggle: \"Activer les demandes de paiement\",\n      claim_automatically: {\n        toggle: \"Réclamer automatiquement\",\n        description: \"Recevez les paiements entrants automatiquement.\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"Nostr Wallet Connect (NWC)\",\n      description:\n        \"Utilisez NWC pour contrôler votre portefeuille depuis n'importe quelle autre application.\",\n      enable_toggle: \"Activer NWC\",\n      payments_note:\n        \"Vous ne pouvez utiliser NWC que pour les paiements à partir de votre solde Bitcoin. Les paiements seront effectués à partir de votre mint active.\",\n      connection: {\n        copy_tooltip: \"Copier la chaîne de connexion\",\n        qr_tooltip: \"Afficher le code QR\",\n        allowance_label: \"Montant restant (sat)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"Choisissez l'encodage pour l'écriture sur les cartes NFC\",\n        text: {\n          title: \"Texte\",\n          description: \"Stocker le jeton en texte brut\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"Stocker l'URL de ce portefeuille avec le jeton\",\n        },\n        binary: {\n          title: \"Binaire\",\n          description: \"Stocker les jetons comme données binaires\",\n        },\n        quick_access: {\n          toggle: \"Accès rapide à la NFC\",\n          description:\n            \"Scannez rapidement les cartes NFC dans le menu Recevoir Ecash. Cette option ajoute un bouton NFC au menu Recevoir Ecash.\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"Générez une paire de clés pour recevoir de l'ecash verrouillé P2PK. Attention : Cette fonctionnalité est expérimentale. N'utilisez qu'avec de petits montants. Si vous perdez vos clés privées, personne ne pourra plus déverrouiller l'ecash qui y est verrouillé.\",\n      generate_button: \"Générer une clé\",\n      import_button: \"Importer nsec\",\n      quick_access: {\n        toggle: \"Accès rapide au verrouillage\",\n        description:\n          \"Utilisez ceci pour afficher rapidement votre clé de verrouillage P2PK dans le menu Recevoir Ecash.\",\n      },\n      keys_expansion: {\n        label: \"Cliquez pour parcourir {count} clés\",\n        used_badge: \"utilisée\",\n      },\n    },\n    privacy: {\n      title: \"Confidentialité\",\n      description: \"Ces paramètres affectent votre confidentialité.\",\n      check_incoming: {\n        toggle: \"Vérifier la facture entrante\",\n        description:\n          \"Si activé, le portefeuille vérifiera la dernière facture en arrière-plan. Cela augmente la réactivité du portefeuille, ce qui rend le fingerprinting plus facile. Vous pouvez vérifier manuellement les factures impayées dans l'onglet Factures.\",\n      },\n      check_startup: {\n        toggle: \"Vérifier les factures en attente au démarrage\",\n        description:\n          \"Si activé, le portefeuille vérifiera les factures en attente des dernières 24 heures au démarrage.\",\n      },\n      check_all: {\n        toggle: \"Vérifier toutes les factures\",\n        description:\n          \"Si activé, le portefeuille vérifiera périodiquement les factures impayées en arrière-plan pendant jusqu'à deux semaines. Cela augmente l'activité en ligne du portefeuille, ce qui rend le fingerprinting plus facile. Vous pouvez vérifier manuellement les factures impayées dans l'onglet Factures.\",\n      },\n      check_sent: {\n        toggle: \"Vérifier l'ecash envoyé\",\n        description:\n          \"Si activé, le portefeuille utilisera des vérifications périodiques en arrière-plan pour déterminer si les jetons envoyés ont été utilisés. Cela augmente l'activité en ligne du portefeuille, ce qui rend le fingerprinting plus facile.\",\n      },\n      websockets: {\n        toggle: \"Utiliser les WebSockets\",\n        description:\n          \"Si activé, le portefeuille utilisera des connexions WebSocket de longue durée pour recevoir des mises à jour sur les factures payées et les jetons dépensés des mints. Cela augmente la réactivité du portefeuille mais rend également le fingerprinting plus facile.\",\n      },\n      bitcoin_price: {\n        toggle: \"Obtenir le taux de change de Coinbase\",\n        description:\n          \"Si activé, le taux de change actuel du Bitcoin sera récupéré de coinbase.com et votre solde converti sera affiché.\",\n        currency: {\n          title: \"Devise Fiat\",\n          description:\n            \"Choisissez la devise fiat pour l'affichage du prix Bitcoin.\",\n        },\n      },\n    },\n    experimental: {\n      title: \"Expérimental\",\n      description: \"Ces fonctionnalités sont expérimentales.\",\n      receive_swaps: {\n        toggle: \"Recevoir des échanges\",\n        badge: \"Bêta\",\n        description:\n          \"Option pour échanger l'Ecash reçu vers votre mint active dans la boîte de dialogue Recevoir Ecash.\",\n      },\n      auto_paste: {\n        toggle: \"Coller l'Ecash automatiquement\",\n        description:\n          \"Coller automatiquement l'ecash dans votre presse-papiers lorsque vous appuyez sur Recevoir, puis Ecash, puis Coller. Le collage automatique peut causer des problèmes d'interface utilisateur dans iOS, désactivez-le si vous rencontrez des problèmes.\",\n      },\n      auditor: {\n        toggle: \"Activer l'auditeur\",\n        badge: \"Bêta\",\n        description:\n          \"Si activé, le portefeuille affichera les informations de l'auditeur dans la boîte de dialogue des détails de la mint. L'auditeur est un service tiers qui surveille la fiabilité des mints.\",\n        url_label: \"URL de l'auditeur\",\n        api_url_label: \"URL de l'API de l'auditeur\",\n      },\n      multinut: {\n        toggle: \"Activer Multinut\",\n        description:\n          \"Si cette option est activée, le portefeuille utilisera Multinut pour payer les factures de plusieurs टकसाल à la fois.\",\n      },\n      nostr_mint_backup: {\n        toggle: \"Sauvegarder la liste des टकसाल sur Nostr\",\n        description:\n          \"Si cette option est activée, votre liste de टकसाल sera automatiquement sauvegardée sur les relais Nostr à l'aide de vos clés Nostr configurées. Cela vous permet de restaurer votre liste de टकसाल sur plusieurs appareils.\",\n        notifications: {\n          enabled: \"Sauvegarde de la टकसाल Nostr activée\",\n          disabled: \"Sauvegarde de la टकसाल Nostr désactivée\",\n          failed: \"Échec de l'activation de la sauvegarde de la टकसाल Nostr\",\n        },\n      },\n    },\n    multinut: {\n      use_multinut: \"Utiliser Multinut\",\n    },\n    appearance: {\n      keyboard: {\n        title: \"Clavier à l'écran\",\n        description: \"Utilisez le clavier numérique pour saisir les montants.\",\n        toggle: \"Utiliser le clavier numérique\",\n        toggle_description:\n          \"Si activé, le clavier numérique sera utilisé pour saisir les montants.\",\n      },\n      theme: {\n        title: \"Apparence\",\n        description: \"Changez l'apparence de votre portefeuille.\",\n        tooltips: {\n          mono: \"mono\",\n          cyber: \"cyber\",\n          freedom: \"liberté\",\n          nostr: \"nostr\",\n          bitcoin: \"bitcoin\",\n          mint: \"mint\",\n          nut: \"nut\",\n          blu: \"blu\",\n          flamingo: \"flamingo\",\n        },\n      },\n      bip177: {\n        title: \"Symbole Bitcoin\",\n        description: \"Utilisez le symbole ₿ au lieu de sats.\",\n        toggle: \"Utiliser le symbole ₿\",\n      },\n    },\n    web_of_trust: {\n      title: \"Réseau de confiance\",\n      known_pubkeys: \"Clés publiques connues: {wotCount}\",\n      continue_crawl: \"Poursuivre l'exploration\",\n      crawl_odell: \"EXPLORER LE RÉSEAU DE CONFIANCE D'ODELL\",\n      crawl_wot: \"Explorer le réseau de confiance\",\n      pause: \"Pause\",\n      reset: \"Réinitialiser\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"Utiliser npubx.cash\",\n      copy_lightning_address: \"Copier l'adresse Lightning\",\n      v2_mint: \"npub.cash v2 mint\",\n    },\n    advanced: {\n      title: \"Avancé\",\n      developer: {\n        title: \"Paramètres développeur\",\n        description:\n          \"Les paramètres suivants sont pour le développement et le débogage.\",\n        keyset_counters: {\n          title: \"Incrémenter les compteurs de keyset\",\n          description:\n            \"Cliquez sur l'ID du keyset pour incrémenter les compteurs de chemin de dérivation pour les keysets de votre portefeuille. Ceci est utile si vous voyez l'erreur \\\"les sorties ont déjà été signées\\\".\",\n          counter: \"compteur : {count}\",\n        },\n        new_seed: {\n          button: \"Générer une nouvelle phrase de départ\",\n          description:\n            \"Cela générera une nouvelle phrase de départ. Vous devez vous envoyer votre solde entier pour pouvoir le restaurer avec une nouvelle phrase de départ.\",\n          confirm_question:\n            \"Êtes-vous sûr de vouloir générer une nouvelle phrase de départ ?\",\n          cancel: \"Annuler\",\n          confirm: \"Confirmer\",\n        },\n        remove_spent: {\n          button: \"Supprimer les preuves dépensées\",\n          description:\n            \"Vérifiez si les jetons ecash de vos mints actives sont dépensés et supprimez les jetons dépensés de votre portefeuille. N'utilisez ceci que si votre portefeuille est bloqué.\",\n        },\n        debug_console: {\n          button: \"Basculer la console de débogage\",\n          description:\n            \"Ouvrez le terminal de débogage Javascript. Ne collez jamais rien dans ce terminal que vous ne comprenez pas. Un voleur pourrait essayer de vous piéger en y collant du code malveillant.\",\n        },\n        export_proofs: {\n          button: \"Exporter les preuves actives\",\n          description:\n            \"Copiez votre solde entier de la mint active en tant que jeton Cashu dans votre presse-papiers. Cela n'exportera que les jetons de la mint et de l'unité sélectionnées. Pour un export complet, sélectionnez une mint et une unité différentes et exportez à nouveau.\",\n        },\n        unset_reserved: {\n          button: \"Annuler la réservation de tous les jetons réservés\",\n          description:\n            \"Ce portefeuille marque l'ecash sortant en attente comme réservé (et le soustrait de votre solde) pour éviter les tentatives de double dépense. Ce bouton annulera la réservation de tous les jetons réservés afin qu'ils puissent être utilisés à nouveau. Si vous faites cela, votre portefeuille pourrait inclure des preuves dépensées. Appuyez sur le bouton \\\"Supprimer les preuves dépensées\\\" pour vous en débarrasser.\",\n        },\n        show_onboarding: {\n          button: \"Afficher l'accueil\",\n          description: \"Afficher à nouveau l'écran d'accueil.\",\n        },\n        reset_wallet: {\n          button: \"Réinitialiser les données du portefeuille\",\n          description:\n            \"Réinitialisez les données de votre portefeuille. Attention : Cela supprimera tout ! Assurez-vous de faire une sauvegarde d'abord.\",\n          confirm_question:\n            \"Êtes-vous sûr de vouloir supprimer les données de votre portefeuille ?\",\n          cancel: \"Annuler\",\n          confirm: \"Supprimer le portefeuille\",\n        },\n        export_wallet: {\n          button: \"Exporter les données du portefeuille\",\n          description:\n            \"Téléchargez un dump de votre portefeuille. Vous pouvez restaurer votre portefeuille à partir de ce fichier sur l'écran d'accueil d'un nouveau portefeuille. Ce fichier sera désynchronisé si vous continuez à utiliser votre portefeuille après l'exportation.\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"Rejoindre une mint\",\n    subtitle:\n      \"Vous n'avez pas encore rejoint de mint Cashu. Ajoutez une URL de mint dans les paramètres ou recevez de l'ecash d'une nouvelle mint pour commencer.\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"Recevoir Ecash\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"Historique\",\n      },\n      invoices: {\n        label: \"Factures\",\n      },\n      mints: {\n        label: \"Mints\",\n      },\n    },\n    install: {\n      text: \"Installer\",\n      tooltip: \"Installer Cashu\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"Non.\",\n    text: \"Un autre onglet est déjà en cours d'exécution. Fermez cet onglet et réessayez.\",\n    actions: {\n      retry: {\n        label: \"Réessayer\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"Oups. Rien ici…\",\n    actions: {\n      home: {\n        label: \"Retour à l'accueil\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Mint\",\n    },\n    mintBalance: {\n      label: \"Solde\",\n    },\n    mintError: {\n      label: \"Erreur de la mint\",\n    },\n    pending: {\n      label: \"En attente\",\n      tooltip: \"Vérifier tous les jetons en attente\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"Précédent\",\n      },\n      next: {\n        label: \"Suivant\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"Bienvenue sur Cashu\",\n    text: \"Cashu.me est un portefeuille Bitcoin gratuit et open-source qui utilise l'ecash pour garder vos fonds sécurisés et privés.\",\n    actions: {\n      more: {\n        label: \"Cliquez pour en savoir plus\",\n      },\n    },\n    p1: {\n      text: \"Cashu est un protocole ecash gratuit et open-source pour Bitcoin. Vous pouvez en savoir plus sur { link }.\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"Ce portefeuille n'est affilié à aucune mint. Pour utiliser ce portefeuille, vous devez vous connecter à une ou plusieurs mints Cashu auxquelles vous faites confiance.\",\n    },\n    p3: {\n      text: \"Ce portefeuille stocke l'ecash auquel vous seul avez accès. Si vous supprimez les données de votre navigateur sans sauvegarde de la phrase de départ, vous perdrez vos jetons.\",\n    },\n    p4: {\n      text: \"Ce portefeuille est en version bêta. Nous déclinons toute responsabilité en cas de perte d'accès aux fonds. Utilisez à vos propres risques ! Ce code est open-source et sous licence MIT.\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"Installer la PWA\",\n    alt: { pwa_example: \"Exemple d’installation PWA\" },\n    installing: \"Installation…\",\n    instruction: {\n      intro: {\n        text: \"Pour la meilleure expérience, utilisez ce portefeuille avec le navigateur web natif de votre appareil pour l'installer en tant que Progressive Web App. Faites-le maintenant.\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Appuyez sur le menu (en haut à droite)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Appuyez sur { buttonText }\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Appuyez sur partager (en bas)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Appuyez sur { buttonText }\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"Une fois que vous avez installé cette application sur votre appareil, fermez cette fenêtre de navigateur et utilisez l'application depuis votre écran d'accueil.\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"Succès !\",\n        text: \"Vous utilisez Cashu comme PWA. Fermez les autres fenêtres de navigateur ouvertes et utilisez l'application depuis votre écran d'accueil.\",\n        nextSteps:\n          \"Vous pouvez maintenant fermer cet onglet et ouvrir l’application depuis votre écran d’accueil.\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"Appuyez sur { icon } et { buttonText }\",\n    buttonText: \"Sur l'écran d'accueil\",\n  },\n  AndroidPWAPrompt: {\n    text: \"Appuyez sur { icon } et { buttonText }\",\n    buttonText: \"Ajouter à l'écran d'accueil\",\n  },\n  WelcomeSlide3: {\n    title: \"Votre Phrase de Départ\",\n    text: \"Stockez votre phrase de départ dans un gestionnaire de mots de passe ou sur papier. Votre phrase de départ est le seul moyen de récupérer vos fonds si vous perdez l'accès à cet appareil.\",\n    inputs: {\n      seed_phrase: {\n        label: \"Phrase de départ\",\n        caption: \"Vous pouvez voir votre phrase de départ dans les paramètres.\",\n      },\n      checkbox: {\n        label: \"Je l'ai écrite\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"Conditions\",\n    actions: {\n      more: {\n        label: \"Lire les Conditions de Service\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"J'ai lu et j'accepte ces termes et conditions\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"Configurez votre portefeuille\",\n    text: \"Souhaitez-vous restaurer à partir d’une phrase de récupération ou créer un nouveau portefeuille ?\",\n    options: {\n      new: {\n        title: \"Créer un nouveau portefeuille\",\n        subtitle: \"Générez une nouvelle seed et ajoutez des mints.\",\n      },\n      recover: {\n        title: \"Restaurer le portefeuille\",\n        subtitle:\n          \"Saisissez votre phrase de récupération, restaurez les mints et l’ecash.\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"Ajouter des mints\",\n    text: \"Les mints sont des serveurs qui vous aident à envoyer et recevoir de l’ecash. Choisissez un mint découvert ou ajoutez-en un manuellement. Vous pouvez passer pour ajouter des mints plus tard.\",\n    sections: { your_mints: \"Vos mints\" },\n    restoring: \"Restauration des mints…\",\n    placeholder: { mint_url: \"https://\" },\n  },\n  WelcomeRecoverSeed: {\n    title: \"Saisissez votre phrase de récupération\",\n    text: \"Collez ou saisissez votre phrase de 12 mots pour restaurer.\",\n    inputs: { word: \"Mot { index }\" },\n    actions: { paste_all: \"Tout coller\" },\n    disclaimer:\n      \"Votre phrase de récupération est uniquement utilisée localement pour dériver les clés de votre portefeuille.\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"Restaurez votre ecash\",\n    text: \"Recherchez les preuves non dépensées sur vos mints configurés et ajoutez-les à votre portefeuille.\",\n  },\n  MintRatings: {\n    title: \"Avis sur le mint\",\n    reviews: \"avis\",\n    ratings: \"Notes\",\n    no_reviews: \"Aucun avis trouvé\",\n    your_review: \"Votre avis\",\n    no_reviews_to_display: \"Aucun avis à afficher.\",\n    no_rating: \"Aucune note\",\n    out_of: \"sur\",\n    rows: \"Reviews\",\n    sort: \"Trier\",\n    sort_options: {\n      newest: \"Plus récents\",\n      oldest: \"Plus anciens\",\n      highest: \"Plus élevées\",\n      lowest: \"Plus basses\",\n    },\n    actions: { write_review: \"Rédiger un avis\" },\n    empty_state_subtitle:\n      \"Aidez en laissant un avis. Partagez votre expérience avec ce mint et aidez les autres en laissant un avis.\",\n  },\n  CreateMintReview: {\n    title: \"Évaluer le mint\",\n    publishing_as: \"Publier en tant que\",\n    inputs: {\n      rating: { label: \"Note\" },\n      review: { label: \"Avis (facultatif)\" },\n    },\n    actions: {\n      publish: { label: \"Publier\", in_progress: \"Publication…\" },\n    },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"Restaurer à partir de la Phrase de Départ\",\n      caption:\n        \"Entrez votre phrase de départ pour restaurer votre portefeuille. Avant de restaurer, assurez-vous d'avoir ajouté toutes les mints que vous avez utilisées auparavant.\",\n      inputs: {\n        seed_phrase: {\n          label: \"Phrase de départ\",\n          caption:\n            \"Vous pouvez voir votre phrase de départ dans les paramètres.\",\n        },\n      },\n    },\n    information: {\n      label: \"Information\",\n      caption:\n        \"L'assistant ne restaurera que l'ecash d'une autre phrase de départ, vous ne pourrez pas utiliser cette phrase de départ ni changer la phrase de départ du portefeuille que vous utilisez actuellement. Cela signifie que l'ecash restauré ne sera pas protégé par votre phrase de départ actuelle tant que vous ne vous enverrez pas l'ecash une fois.\",\n    },\n    restore_mints: {\n      label: \"Restaurer les Mints\",\n      caption:\n        \"Sélectionnez la mint à restaurer. Vous pouvez ajouter d'autres mints dans l'écran principal sous \\\"Mints\\\" et les restaurer ici.\",\n    },\n    actions: {\n      paste: {\n        error: \"Échec de la lecture du contenu du presse-papiers.\",\n      },\n      validate: {\n        error: \"Le mnémonique doit comporter au moins 12 mots.\",\n      },\n      select_all: {\n        label: \"Tout sélectionner\",\n      },\n      deselect_all: {\n        label: \"Tout désélectionner\",\n      },\n      restore: {\n        label: \"Restaurer\",\n        in_progress: \"Restauration de la mint…\",\n        error: \"Erreur lors de la restauration de la mint : { error }\",\n      },\n      restore_all_mints: {\n        label: \"Restaurer toutes les mints\",\n        in_progress: \"Restauration de la mint { index } sur { length }…\",\n        success: \"Restauration terminée avec succès\",\n        error: \"Erreur lors de la restauration des mints : { error }\",\n      },\n      restore_selected_mints: {\n        label: \"Restaurer les Mints sélectionnées ({count})\",\n        in_progress: \"Restauration de la menthe {index} de {length}…\",\n        success: \"{count} mint(s) restaurée(s) avec succès\",\n        error:\n          \"Erreur lors de la restauration des mints sélectionnées: {error}\",\n      },\n    },\n    nostr_mints: {\n      label: \"Restaurer les Mints de Nostr\",\n      caption:\n        \"Recherchez les sauvegardes de mints stockées sur les relais Nostr en utilisant votre phrase de départ. Cela vous aidera à découvrir les mints que vous avez précédemment utilisées.\",\n      search_button: \"Rechercher les sauvegardes de Mint\",\n      select_all: \"Tout sélectionner\",\n      deselect_all: \"Tout désélectionner\",\n      backed_up: \"Sauvegardé\",\n      already_added: \"Déjà ajouté\",\n      add_selected: \"Ajouter la sélection ({count})\",\n      no_backups_found: \"Aucune sauvegarde de mint trouvée\",\n      no_backups_hint:\n        \"Assurez-vous que la sauvegarde de la liste des mints Nostr est activée dans les paramètres pour sauvegarder automatiquement votre liste de mints.\",\n      invalid_mnemonic:\n        \"Veuillez entrer une phrase de départ valide avant de rechercher.\",\n      search_error: \"Échec de la recherche des sauvegardes de mints.\",\n      add_error: \"Échec de l'ajout des mints sélectionnées.\",\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"Ajouter une mint\",\n      description:\n        \"Entrez l'URL d'une mint Cashu pour vous y connecter. Ce portefeuille n'est affilié à aucune mint.\",\n      inputs: {\n        nickname: {\n          placeholder: \"Surnom (ex. Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"URL invalide\",\n        },\n        scan: {\n          label: \"Scanner le Code QR\",\n        },\n      },\n    },\n    discover: {\n      title: \"Découvrir les mints\",\n      overline: \"Découvrir\",\n      caption:\n        \"Découvrez les mints que d'autres utilisateurs ont recommandées sur nostr.\",\n      actions: {\n        discover: {\n          label: \"Découvrir les mints\",\n          in_progress: \"Chargement…\",\n          error_no_mints: \"Aucune mint trouvée\",\n          success: \"{ length } mints trouvées\",\n        },\n      },\n      recommendations: {\n        overline: \"{ length } mints trouvées\",\n        caption:\n          \"Ces mints ont été recommandées par d'autres utilisateurs Nostr. Soyez prudent et faites vos propres recherches avant d'utiliser une mint.\",\n        actions: {\n          browse: {\n            label: \"Cliquez pour parcourir les mints\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"Échanger\",\n      overline: \"Échanges Multi-mints\",\n      caption:\n        \"Échangez des fonds entre mints via Lightning. Note : Prévoyez de la place pour les frais Lightning potentiels. Si le paiement entrant ne réussit pas, vérifiez la facture manuellement.\",\n      inputs: {\n        from: {\n          label: \"De\",\n        },\n        to: {\n          label: \"À\",\n        },\n        amount: {\n          label: \"Montant ({ ticker }) )\",\n        },\n      },\n      actions: {\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n    },\n    error_badge: \"Erreur\",\n    reviews_text: \"avis\",\n    no_reviews_yet: \"Aucun avis pour l'instant\",\n    discover_mints_button: \"Découvrir les mints\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - Continuer à scanner\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"Recevoir Lightning\",\n    create_invoice_title: \"Créer une Facture\",\n    inputs: {\n      amount: {\n        label: \"Montant ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"Créer une Facture\",\n        label_blocked: \"Création en cours…\",\n        in_progress: \"Création\",\n      },\n    },\n    invoice: {\n      caption: \"Facture Lightning\",\n      status_paid_text: \"Payée !\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"Envoyer\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Aucune mint disponible\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"Aucune mint disponible\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"Envoyer Ecash\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"Hors ligne\",\n    inputs: {\n      amount: {\n        label: \"Montant ({ ticker }) *\",\n        invalid_too_much_error_text: \"Trop élevé\",\n      },\n      p2pk_pubkey: {\n        label: \"Clé publique du destinataire\",\n        label_invalid: \"Clé publique du destinataire\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"Copier l'Emoji\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"Copier le lien\",\n      },\n      share: {\n        tooltip_text: \"Partager ecash\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"Supprimer de l'historique\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"Flasher sur carte NFC\",\n          ndef_unsupported_text: \"NDEF non pris en charge\",\n        },\n      },\n    },\n  },\n  ReceiveDialog: {\n    title: \"Recevoir\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Aucune mint disponible\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints:\n          \"Vous devez vous connecter à une mint pour recevoir via Lightning\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"Recevoir Ecash\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"Demander\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"Scan en cours…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"Recevoir Ecash\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"Coller le jeton Cashu\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"Jeton invalide\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"Impossible de recevoir. Le verrouillage P2PK de ce jeton ne correspond pas à votre clé publique.\",\n      },\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"Ajout de la mint…\",\n      },\n      swap: {\n        label: \"@:global.actions.swap.label\",\n        tooltip_text: \"Échanger vers une mint de confiance\",\n        caption: \"Échanger { value }\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"Annuler l'échange\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      later: {\n        label: \"Recevoir plus tard\",\n        tooltip_text: \"Ajouter à l'historique pour recevoir plus tard\",\n        already_in_history_success_text: \"Ecash déjà dans l'historique\",\n        added_to_history_success_text: \"Ecash ajouté à l'historique\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"Lire à partir de la carte NFC\",\n          ndef_unsupported_text: \"NDEF non pris en charge\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"Clé P2PK\",\n      description: \"Recevoir de l'ecash verrouillé sur cette clé\",\n      used_warning_text:\n        \"Attention : Cette clé a déjà été utilisée. Utilisez une nouvelle clé pour une meilleure confidentialité.\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"Générer une nouvelle clé\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"Demande de Paiement\",\n      description: \"Recevoir des paiements via Nostr\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"Nouvelle demande\",\n      },\n      add_amount: {\n        label: \"Ajouter un montant\",\n      },\n      use_active_mint: {\n        label: \"N'importe quelle mint\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"Entrez le montant\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"Clavier désactivé. Vous pouvez réactiver le clavier dans les paramètres.\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostr Wallet Connect\",\n      description:\n        \"Contrôlez votre portefeuille à distance avec NWC. Appuyez sur le code QR pour lier votre portefeuille à une application compatible.\",\n      warning_text:\n        \"Attention : toute personne ayant accès à cette chaîne de connexion peut initier des paiements depuis votre portefeuille. Ne la partagez pas !\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"Message de la Mint\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"Contact\",\n    },\n    details: {\n      title: \"Détails de la mint\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"Tout afficher\",\n          },\n          hide: {\n            label: \"Masquer\",\n          },\n        },\n      },\n      currency: {\n        label: \"Devise\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"Version\",\n      },\n    },\n    actions: {\n      title: \"Actions\",\n      copy_mint_url: {\n        label: \"Copier l'URL de la mint\",\n      },\n      delete: {\n        label: \"Supprimer la mint\",\n      },\n      edit: {\n        label: \"Modifier la mint\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"Sélectionnez une mint\",\n    badge_mint_error_text: \"Erreur\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"Aucun historique pour l'instant\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"Il y a { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Vérifier le statut\",\n      },\n      receive: {\n        tooltip_text: \"Recevoir\",\n      },\n      filter_pending: {\n        label: \"Filtrer en attente\",\n      },\n      show_all: {\n        label: \"Tout afficher\",\n      },\n    },\n    old_token_not_found_error_text: \"Ancien jeton introuvable\",\n  },\n  InvoiceTable: {\n    empty_text: \"Aucune facture pour l'instant\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"Cliquez pour copier\",\n      date_label: \"Il y a { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Vérifier le statut\",\n      },\n      filter_pending: {\n        label: \"Filtrer en attente\",\n      },\n      show_all: {\n        label: \"Tout afficher\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"Êtes-vous sûr de vouloir supprimer cette mint ?\",\n    nickname: {\n      label: \"Surnom\",\n    },\n    balances: {\n      label: \"Soldes\",\n    },\n    warning_text:\n      \"Note : Comme ce portefeuille est paranoïaque, votre ecash de cette mint ne sera pas réellement supprimé mais restera stocké sur votre appareil. Vous le verrez réapparaître si vous réajoutez cette mint plus tard.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"Supprimer la mint\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"Token Cashu ou adresse Lightning\",\n      receive: \"Token Cashu\",\n      pay: \"Adresse Lightning ou facture\",\n    },\n    qr_scanner: {\n      title: \"Scanner le Code QR\",\n      description: \"Appuyez pour scanner une adresse\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"Payer Lightning\",\n      inputs: {\n        invoice_data: {\n          label: \"Facture ou adresse Lightning\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"{ payee } demande { value } { ticker }\",\n      amount_range_label:\n        \"{ payee } demande{br}entre { min } et { max } { ticker }\",\n      sending_to_lightning_address: \"Envoi à { address }\",\n      inputs: {\n        amount: {\n          label: \"Montant ({ ticker }) *\",\n        },\n        comment: {\n          label: \"Commentaire (optionnel)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"Payer { value }\",\n      paying: \"Paiement en cours\",\n      paid: \"Payé\",\n      fee: \"Frais\",\n      memo: {\n        label: \"Mémo\",\n      },\n      processing_info_text: \"Traitement…\",\n      balance_too_low_warning_text: \"Solde trop faible\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"Payer\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"Erreur\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"Modifier la mint\",\n    inputs: {\n      nickname: {\n        label: \"Surnom\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"Faites-vous confiance à cette mint ?\",\n    description:\n      \"Avant d'utiliser cette mint, assurez-vous de lui faire confiance. Les mints pourraient devenir malveillantes ou cesser leurs opérations à tout moment.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"Ajout de la mint\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"Veuillez entrer un mnémonique\",\n    restore_mint_error_text:\n      \"Erreur lors de la restauration de la mint : { error }\",\n    prepare_info_text: \"Préparation du processus de restauration…\",\n    restored_proofs_for_keyset_info_text:\n      \"{ restoreCounter } preuves restaurées pour le keyset { keysetId }\",\n    checking_proofs_for_keyset_info_text:\n      \"Vérification des preuves { startIndex } à { endIndex } pour le keyset { keysetId }\",\n    no_proofs_info_text: \"Aucune preuve trouvée à restaurer\",\n    restored_amount_success_text: \"{ amount } restauré\",\n  },\n  swap: {\n    in_progress_warning_text: \"Échange en cours\",\n    invalid_swap_data_error_text: \"Données d'échange invalides\",\n    swap_error_text: \"Erreur lors de l'échange\",\n  },\n  TokenInformation: {\n    fee: \"Frais\",\n    unit: \"Unité\",\n    fiat: \"Fiat\",\n    p2pk: \"P2PK\",\n    locked: \"Verrouillé\",\n    locked_to_you: \"Verrouillé pour vous\",\n    mint: \"Monnaie\",\n    memo: \"Mémo\",\n    payment_request: \"Demande de paiement\",\n    nostr: \"Nostr\",\n    token_copied: \"Token copié dans le presse-papiers\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/index.ts",
    "content": "import enUS from \"./en-US\";\nimport esES from \"./es-ES\";\nimport itIT from \"./it-IT\";\nimport deDE from \"./de-DE\";\nimport frFR from \"./fr-FR\";\nimport csCZ from \"./cs-CZ\";\nimport svSE from \"./sv-SE\";\nimport elGR from \"./el-GR\";\nimport trTR from \"./tr-TR\";\nimport thTH from \"./th-TH\";\nimport arSA from \"./ar-SA\";\nimport zhCN from \"./zh-CN\";\nimport jaJP from \"./ja-JP\";\nimport ptBR from \"./pt-BR\";\n\nexport default {\n  \"en-US\": enUS,\n  \"es-ES\": esES,\n  \"it-IT\": itIT,\n  \"de-DE\": deDE,\n  \"fr-FR\": frFR,\n  \"cs-CZ\": csCZ,\n  \"sv-SE\": svSE,\n  \"el-GR\": elGR,\n  \"tr-TR\": trTR,\n  \"th-TH\": thTH,\n  \"ar-SA\": arSA,\n  \"zh-CN\": zhCN,\n  \"ja-JP\": jaJP,\n  \"pt-BR\": ptBR,\n};\n"
  },
  {
    "path": "src/i18n/it-IT/index.ts",
    "content": "export default {\n  MultinutPicker: {\n    payment: \"Pagamento Multinut\",\n    selectMints: \"Seleziona una o più mint da cui eseguire un pagamento.\",\n    totalSelectedBalance: \"Saldo totale selezionato\",\n    multiMintPay: \"Pagamento Multi-Mint\",\n    balanceNotEnough:\n      \"Il saldo multi-mint non è sufficiente per questa fattura\",\n    failed: \"Impossibile elaborare: {error}\",\n    paid: \"Pagato {amount} via Lightning\",\n  },\n  global: {\n    // merged global keys\n    copy_to_clipboard: {\n      success: \"Copiato negli appunti!\",\n    },\n    actions: {\n      add_mint: { label: \"Aggiungi mint\" },\n      cancel: { label: \"Annulla\" },\n      copy: { label: \"Copia\" },\n      close: { label: \"Chiudi\" },\n      enter: { label: \"Invio\" },\n      lock: { label: \"Blocca\" },\n      paste: { label: \"Incolla\" },\n      receive: { label: \"Ricevi\" },\n      scan: { label: \"Scansiona\" },\n      send: { label: \"Invia\" },\n      swap: { label: \"Scambia\" },\n      update: { label: \"Aggiorna\" },\n    },\n    inputs: { mint_url: { label: \"URL Mint\" } },\n  },\n\n  wallet: {\n    notifications: {\n      balance_too_low: \"Il saldo è troppo basso\",\n      received: \"Ricevuto {amount}\",\n      fee: \" (commissione: {fee})\",\n      could_not_request_mint: \"Impossibile richiedere coniazione\",\n      invoice_still_pending: \"Fattura ancora in attesa\",\n      paid_lightning: \"Pagato {amount} tramite Lightning\",\n      payment_pending_refresh:\n        \"Pagamento in attesa. Aggiorna la fattura manualmente.\",\n      sent: \"Inviato {amount}\",\n      token_still_pending: \"Token ancora in attesa\",\n      received_lightning: \"Ricevuto {amount} tramite Lightning\",\n      lightning_payment_failed: \"Pagamento Lightning fallito\",\n      failed_to_decode_invoice: \"Impossibile decodificare la fattura\",\n      invalid_lnurl: \"LNURL non valido\",\n      lnurl_error: \"Errore LNURL\",\n      no_amount: \"Nessun importo\",\n      no_lnurl_data: \"Nessun dato LNURL\",\n      no_price_data: \"Nessun dato di prezzo.\",\n      please_try_again: \"Si prega di riprovare.\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"Mint già aggiunto\",\n        added: \"Mint aggiunto\",\n        not_found: \"Mint non trovato\",\n        activation_failed: \"Attivazione mint fallita\",\n        no_active_mint: \"Nessun mint attivo\",\n        unit_activation_failed: \"Attivazione unità fallita\",\n        unit_not_supported: \"Unità non supportata dal mint\",\n        activated: \"Mint attivato\",\n        could_not_connect: \"Impossibile connettersi al mint\",\n        could_not_get_info: \"Impossibile ottenere informazioni sul mint\",\n        could_not_get_keys: \"Impossibile ottenere le chiavi del mint\",\n        could_not_get_keysets: \"Impossibile ottenere i keysets del mint\",\n        mint_validation_error: \"Errore di validazione del mint\",\n        removed: \"Mint rimosso\",\n        error: \"Errore mint\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"Impostazioni\",\n        settings: {\n          title: \"Impostazioni\",\n          caption: \"Configurazione portafoglio\",\n        },\n      },\n      terms: {\n        title: \"Termini\",\n        terms: {\n          title: \"Termini\",\n          caption: \"Termini di Servizio\",\n        },\n      },\n      links: {\n        title: \"Link\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"Dona\",\n          caption: \"Supporta Cashu\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"Offline\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"Ricarica tra { countdown }\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"Staging – non usare con fondi reali!\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"Portafoglio\",\n      },\n    },\n  },\n  Settings: {\n    language: {\n      title: \"Lingua\",\n      description: \"Seleziona la tua lingua preferita dall'elenco sottostante.\",\n    },\n    sections: {\n      backup_restore: \"BACKUP E RIPRISTINO\",\n      lightning_address: \"INDIRIZZO LIGHTNING\",\n      nostr_keys: \"CHIAVI NOSTR\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"Clicca per modificare i relay\",\n          add: {\n            title: \"Aggiungi relay\",\n            description:\n              \"Il tuo portafoglio usa questi relay per operazioni nostr come richieste di pagamento, NWC e backup.\",\n          },\n          list: {\n            title: \"Relay\",\n            description: \"Il tuo portafoglio si connetterà a questi relay.\",\n            copy_tooltip: \"Copia relay\",\n            remove_tooltip: \"Rimuovi relay\",\n          },\n        },\n      },\n      payment_requests: \"RICHIESTE DI PAGAMENTO\",\n      nostr_wallet_connect: \"NOSTR WALLET CONNECT\",\n      hardware_features: \"FUNZIONALITÀ HARDWARE\",\n      p2pk_features: \"FUNZIONALITÀ P2PK\",\n      privacy: \"PRIVACY\",\n      experimental: \"SPERIMENTALE\",\n      appearance: \"ASPETTO\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"Backup frase seed\",\n        description:\n          \"La tua frase seed può ripristinare il tuo portafoglio. Conservala al sicuro e privata.\",\n        seed_phrase_label: \"Frase seed\",\n      },\n      restore_ecash: {\n        title: \"Ripristina ecash\",\n        description:\n          \"La procedura guidata di ripristino consente di recuperare ecash perso da una frase mnemonica. La frase seed del tuo portafoglio attuale rimarrà inalterata, la procedura guidata ti consentirà solo di ripristinare ecash da un'altra frase seed.\",\n        button: \"Ripristina\",\n      },\n    },\n    lightning_address: {\n      title: \"Indirizzo Lightning\",\n      description: \"Ricevi pagamenti al tuo indirizzo Lightning.\",\n      enable: {\n        toggle: \"Abilita\",\n        description: \"Indirizzo Lightning con npub.cash\",\n      },\n      address: {\n        copy_tooltip: \"Copia indirizzo Lightning\",\n      },\n      automatic_claim: {\n        toggle: \"Richiedi automaticamente\",\n        description: \"Ricevi pagamenti in entrata automaticamente.\",\n      },\n      npc_v2: {\n        choose_mint_title: \"Scegli mint per npub.cash v2\",\n        choose_mint_placeholder: \"Seleziona un mint...\",\n      },\n    },\n    nostr_keys: {\n      title: \"Le tue chiavi nostr\",\n      description: \"Imposta le chiavi nostr per il tuo indirizzo Lightning.\",\n      wallet_seed: {\n        title: \"Frase seed del portafoglio\",\n        description: \"Genera coppia di chiavi nostr dalla seed del portafoglio\",\n        copy_nsec: \"Copia nsec\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"Usa un bunker NIP-46\",\n        delete_tooltip: \"Elimina connessione\",\n      },\n      use_nsec: {\n        title: \"Usa la tua nsec\",\n        description: \"Questo metodo è pericoloso e non raccomandato\",\n        delete_tooltip: \"Elimina nsec\",\n      },\n      signing_extension: {\n        title: \"Estensione di firma\",\n        description: \"Usa un'estensione di firma NIP-07\",\n        not_found: \"Nessuna estensione di firma NIP-07 trovata\",\n      },\n    },\n    payment_requests: {\n      title: \"Richieste di pagamento\",\n      description:\n        \"Le richieste di pagamento ti permettono di ricevere pagamenti via nostr. Se abiliti questa opzione, il tuo portafoglio si iscriverà ai tuoi relay nostr.\",\n      enable_toggle: \"Abilita Richieste di Pagamento\",\n      claim_automatically: {\n        toggle: \"Richiedi automaticamente\",\n        description: \"Ricevi pagamenti in entrata automaticamente.\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"Nostr Wallet Connect (NWC)\",\n      description:\n        \"Usa NWC per controllare il tuo portafoglio da qualsiasi altra applicazione.\",\n      enable_toggle: \"Abilita NWC\",\n      payments_note:\n        \"Puoi usare NWC solo per pagamenti dal tuo saldo Bitcoin. I pagamenti verranno effettuati dal tuo mint attivo.\",\n      connection: {\n        copy_tooltip: \"Copia stringa di connessione\",\n        qr_tooltip: \"Mostra codice QR\",\n        allowance_label: \"Limite rimasto (sat)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"Scegli la codifica per scrivere su schede NFC\",\n        text: {\n          title: \"Testo\",\n          description: \"Memorizza token in testo semplice\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"Memorizza URL a questo portafoglio con token\",\n        },\n        binary: {\n          title: \"Binario\",\n          description: \"Memorizza i token come dati binari\",\n        },\n        quick_access: {\n          toggle: \"Accesso rapido NFC\",\n          description:\n            \"Scansiona rapidamente schede NFC nel menu Ricevi Ecash. Questa opzione aggiunge un pulsante NFC al menu Ricevi Ecash.\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"Genera una coppia di chiavi per ricevere ecash bloccato con P2PK. Attenzione: Questa funzionalità è sperimentale. Usare solo con piccole somme. Se perdi le tue chiavi private, nessuno sarà più in grado di sbloccare l'ecash ad esse associato.\",\n      generate_button: \"Genera chiave\",\n      import_button: \"Importa nsec\",\n      quick_access: {\n        toggle: \"Accesso rapido al blocco\",\n        description:\n          \"Usa questo per mostrare rapidamente la tua chiave di blocco P2PK nel menu ricevi ecash.\",\n      },\n      keys_expansion: {\n        label: \"Clicca per sfogliare {count} chiavi\",\n        used_badge: \"usata\",\n      },\n    },\n    privacy: {\n      title: \"Privacy\",\n      description: \"Queste impostazioni influenzano la tua privacy.\",\n      check_incoming: {\n        toggle: \"Verifica fattura in entrata\",\n        description:\n          \"Se abilitato, il portafoglio verificherà l'ultima fattura in background. Questo aumenta la reattività del portafoglio, rendendo più facile il fingerprinting. Puoi verificare manualmente le fatture non pagate nella scheda Fatture.\",\n      },\n      check_startup: {\n        toggle: \"Verifica fatture pendenti all'avvio\",\n        description:\n          \"Se abilitato, il portafoglio verificherà le fatture pendenti delle ultime 24 ore all'avvio.\",\n      },\n      check_all: {\n        toggle: \"Verifica tutte le fatture\",\n        description:\n          \"Se abilitato, il portafoglio verificherà periodicamente le fatture non pagate in background per un massimo di due settimane. Questo aumenta l'attività online del portafoglio, rendendo più facile il fingerprinting. Puoi verificare manualmente le fatture non pagate nella scheda Fatture.\",\n      },\n      check_sent: {\n        toggle: \"Verifica ecash inviato\",\n        description:\n          \"Se abilitato, il portafoglio userà controlli periodici in background per determinare se i token inviati sono stati riscattati. Questo aumenta l'attività online del portafoglio, rendendo più facile il fingerprinting.\",\n      },\n      websockets: {\n        toggle: \"Usa WebSockets\",\n        description:\n          \"Se abilitato, il portafoglio userà connessioni WebSocket a lunga durata per ricevere aggiornamenti su fatture pagate e token spesi dai mints. Questo aumenta la reattività del portafoglio ma rende anche più facile il fingerprinting.\",\n      },\n      bitcoin_price: {\n        toggle: \"Ottieni tasso di cambio da Coinbase\",\n        description:\n          \"Se abilitato, il tasso di cambio attuale di Bitcoin verrà recuperato da coinbase.com e verrà visualizzato il tuo saldo convertito.\",\n        currency: {\n          title: \"Valuta Fiat\",\n          description:\n            \"Scegli la valuta fiat per la visualizzazione del prezzo Bitcoin.\",\n        },\n      },\n    },\n    experimental: {\n      title: \"Sperimentale\",\n      description: \"Queste funzionalità sono sperimentali.\",\n      receive_swaps: {\n        toggle: \"Ricevi scambi\",\n        badge: \"Beta\",\n        description:\n          \"Opzione per scambiare Ecash ricevuto al tuo mint attivo nella finestra di dialogo Ricevi Ecash.\",\n      },\n      auto_paste: {\n        toggle: \"Incolla Ecash automaticamente\",\n        description:\n          \"Incolla automaticamente ecash nei tuoi appunti quando premi Ricevi, poi Ecash, poi Incolla. L'incollaggio automatico può causare problemi all'interfaccia utente su iOS, disattivalo se riscontri problemi.\",\n      },\n      auditor: {\n        toggle: \"Abilita revisore\",\n        badge: \"Beta\",\n        description:\n          \"Se abilitato, il portafoglio mostrerà le informazioni del revisore nella finestra di dialogo dei dettagli del mint. Il revisore è un servizio di terze parti che monitora l'affidabilità dei mints.\",\n        url_label: \"URL Revisore\",\n        api_url_label: \"URL API Revisore\",\n      },\n      multinut: {\n        toggle: \"Abilita Multinut\",\n        description:\n          \"Se abilitato, il portafoglio userà Multinut per pagare le fatture da più mint contemporaneamente.\",\n      },\n      nostr_mint_backup: {\n        toggle: \"Backup lista mint su Nostr\",\n        description:\n          \"Se abilitato, la tua lista mint verrà automaticamente sottoposta a backup sui relay Nostr usando le tue chiavi Nostr configurate. Questo ti permette di ripristinare la tua lista mint su tutti i dispositivi.\",\n        notifications: {\n          enabled: \"Backup mint Nostr abilitato\",\n          disabled: \"Backup mint Nostr disabilitato\",\n          failed: \"Impossibile abilitare il backup mint Nostr\",\n        },\n      },\n    },\n    appearance: {\n      keyboard: {\n        title: \"Tastiera su schermo\",\n        description: \"Usa la tastiera numerica per inserire importi.\",\n        toggle: \"Usa tastiera numerica\",\n        toggle_description:\n          \"Se abilitato, verrà utilizzata la tastiera numerica per inserire gli importi.\",\n      },\n      theme: {\n        title: \"Aspetto\",\n        description: \"Cambia l'aspetto del tuo portafoglio.\",\n        tooltips: {\n          mono: \"mono\",\n          cyber: \"cyber\",\n          freedom: \"freedom\",\n          nostr: \"nostr\",\n          bitcoin: \"bitcoin\",\n          mint: \"mint\",\n          nut: \"nut\",\n          blu: \"blu\",\n          flamingo: \"flamingo\",\n        },\n      },\n      bip177: {\n        title: \"Simbolo Bitcoin\",\n        description: \"Usa il simbolo ₿ invece di sats.\",\n        toggle: \"Usa il simbolo ₿\",\n      },\n    },\n    web_of_trust: {\n      title: \"Rete di fiducia\",\n      known_pubkeys: \"Chiavi pubbliche conosciute: {wotCount}\",\n      continue_crawl: \"Continua scansione\",\n      crawl_odell: \"Scansiona WEB OF TRUST DI ODELL\",\n      crawl_wot: \"Scansiona web of trust\",\n      pause: \"Pausa\",\n      reset: \"Reset\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"Usa npubx.cash\",\n      copy_lightning_address: \"Copia indirizzo Lightning\",\n      v2_mint: \"npub.cash v2 mint\",\n    },\n    multinut: {\n      use_multinut: \"Usa Multinut\",\n    },\n    advanced: {\n      title: \"Avanzato\",\n      developer: {\n        title: \"Impostazioni sviluppatore\",\n        description: \"Le seguenti impostazioni sono per sviluppo e debug.\",\n        new_seed: {\n          button: \"Genera nuova frase seed\",\n          description:\n            \"Questo genererà una nuova frase seed. Devi inviare l'intero saldo a te stesso per poterlo ripristinare con una nuova seed.\",\n          confirm_question:\n            \"Sei sicuro di voler generare una nuova frase seed?\",\n          cancel: \"Annulla\",\n          confirm: \"Conferma\",\n        },\n        remove_spent: {\n          button: \"Rimuovi prove spese\",\n          description:\n            \"Verifica se i token ecash dai tuoi mints attivi sono spesi e rimuovi quelli spesi dal tuo portafoglio. Usalo solo se il tuo portafoglio è bloccato.\",\n        },\n        debug_console: {\n          button: \"Attiva/disattiva Console di Debug\",\n          description:\n            \"Apri il terminale di debug Javascript. Non incollare mai nulla in questo terminale che non capisci. Un ladro potrebbe provare a ingannarti facendoti incollare codice malevolo qui.\",\n        },\n        export_proofs: {\n          button: \"Esporta prove attive\",\n          description:\n            \"Copia l'intero saldo dal mint attivo come token Cashu nei tuoi appunti. Questo esporterà solo i token dal mint e unità selezionati. Per un'esportazione completa, seleziona un mint e un'unità diversi ed esporta di nuovo.\",\n        },\n        keyset_counters: {\n          title: \"Incrementa contatori keyset\",\n          description:\n            \"Clicca l'ID del keyset per incrementare i contatori del percorso di derivazione per i keyset nel tuo portafoglio. Questo è utile se vedi l'errore \\\"le uscite sono già state firmate\\\".\",\n          counter: \"contatore: {count}\",\n        },\n        unset_reserved: {\n          button: \"Annulla prenotazione tutti i token riservati\",\n          description:\n            'Questo portafoglio marca l\\'ecash in uscita pendente come riservato (e lo sottrae dal tuo saldo) per prevenire tentativi di doppia spesa. Questo pulsante annullerà la prenotazione di tutti i token riservati così potranno essere usati di nuovo. Se fai questo, il tuo portafoglio potrebbe includere prove spese. Premi il pulsante \"Rimuovi prove spese\" per eliminarle.',\n        },\n        show_onboarding: {\n          button: \"Mostra onboarding\",\n          description: \"Mostra di nuovo la schermata di onboarding.\",\n        },\n        reset_wallet: {\n          button: \"Resetta dati portafoglio\",\n          description:\n            \"Resetta i dati del tuo portafoglio. Attenzione: Questo eliminerà tutto! Assicurati di creare prima un backup.\",\n          confirm_question:\n            \"Sei sicuro di voler eliminare i dati del tuo portafoglio?\",\n          cancel: \"Annulla\",\n          confirm: \"Elimina portafoglio\",\n        },\n        export_wallet: {\n          button: \"Esporta dati portafoglio\",\n          description:\n            \"Scarica un dump del tuo portafoglio. Puoi ripristinare il tuo portafoglio da questo file nella schermata di benvenuto di un nuovo portafoglio. Questo file non sarà sincronizzato se continui a usare il tuo portafoglio dopo averlo esportato.\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"Unisciti a un mint\",\n    subtitle:\n      \"Non ti sei ancora unito a nessun mint Cashu. Aggiungi un URL mint nelle impostazioni o ricevi ecash da un nuovo mint per iniziare.\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"Ricevi Ecash\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"Cronologia\",\n      },\n      invoices: {\n        label: \"Fatture\",\n      },\n      mints: {\n        label: \"Mints\",\n      },\n    },\n    install: {\n      text: \"Installa\",\n      tooltip: \"Installa Cashu\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"Nope.\",\n    text: \"Un'altra scheda è già in esecuzione. Chiudi questa scheda e riprova.\",\n    actions: {\n      retry: {\n        label: \"Riprova\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"Oops. Niente qui…\",\n    actions: {\n      home: {\n        label: \"Torna alla home\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Mint\",\n    },\n    mintBalance: {\n      label: \"Saldo\",\n    },\n    mintError: {\n      label: \"Errore mint\",\n    },\n    pending: {\n      label: \"In attesa\",\n      tooltip: \"Verifica tutti i token in attesa\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"Precedente\",\n      },\n      next: {\n        label: \"Successivo\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"Benvenuto in Cashu\",\n    text: \"Cashu.me è un portafoglio Bitcoin gratuito e open-source che utilizza ecash per mantenere i tuoi fondi sicuri e privati.\",\n    actions: {\n      more: {\n        label: \"Clicca per saperne di più\",\n      },\n    },\n    p1: {\n      text: \"Cashu è un protocollo ecash gratuito e open-source per Bitcoin. Puoi saperne di più su { link }.\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"Questo portafoglio non è affiliato a nessun mint. Per usare questo portafoglio, devi connetterti a uno o più mints Cashu di cui ti fidi.\",\n    },\n    p3: {\n      text: \"Questo portafoglio conserva ecash a cui solo tu hai accesso. Se elimini i dati del tuo browser senza un backup della frase seed, perderai i tuoi token.\",\n    },\n    p4: {\n      text: \"Questo portafoglio è in beta. Non ci assumiamo alcuna responsabilità per le persone che perdono l'accesso ai fondi. Usalo a tuo rischio! Questo codice è open-source e licenziato sotto la licenza MIT.\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"Installa PWA\",\n    alt: { pwa_example: \"Esempio installazione PWA\" },\n    installing: \"Installazione…\",\n    instruction: {\n      intro: {\n        text: \"Per la migliore esperienza, usa questo portafoglio con il browser web nativo del tuo dispositivo per installarlo come App Web Progressiva. Fallo subito.\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Tocca il menu (in alto a destra)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Premi { buttonText }\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Tocca condividi (in basso)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Premi { buttonText }\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"Una volta installata questa app sul tuo dispositivo, chiudi questa finestra del browser e usa l'app dalla tua schermata home.\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"Successo!\",\n        text: \"Stai usando Cashu come PWA. Chiudi qualsiasi altra finestra del browser aperta e usa l'app dalla tua schermata home.\",\n        nextSteps:\n          \"Ora puoi chiudere questa scheda del browser e aprire l’app dalla schermata Home.\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"Tocca { icon } e { buttonText }\",\n    buttonText: \"Aggiungi alla schermata Home\",\n  },\n  AndroidPWAPrompt: {\n    text: \"Tocca { icon } e { buttonText }\",\n    buttonText: \"Aggiungi alla schermata Home\",\n  },\n  WelcomeSlide3: {\n    title: \"La tua Frase Seed\",\n    text: \"Conserva la tua frase seed in un gestore di password o su carta. La tua frase seed è l'unico modo per recuperare i tuoi fondi se perdi l'accesso a questo dispositivo.\",\n    inputs: {\n      seed_phrase: {\n        label: \"Frase Seed\",\n        caption: \"Puoi vedere la tua frase seed nelle impostazioni.\",\n      },\n      checkbox: {\n        label: \"L'ho scritta\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"Termini\",\n    actions: {\n      more: {\n        label: \"Leggi i Termini di Servizio\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"Ho letto e accetto questi termini e condizioni\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"Configura il tuo portafoglio\",\n    text: \"Vuoi recuperare da una frase seed o creare un nuovo portafoglio?\",\n    options: {\n      new: {\n        title: \"Crea nuovo portafoglio\",\n        subtitle: \"Genera una nuova seed e aggiungi mints.\",\n      },\n      recover: {\n        title: \"Recupera portafoglio\",\n        subtitle: \"Inserisci la tua frase seed, ripristina mints ed ecash.\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"Aggiungi mints\",\n    text: \"I mints sono server che ti aiutano a inviare e ricevere ecash. Scegli un mint scoperto o aggiungine uno manualmente. Puoi saltare per aggiungere mints più tardi.\",\n    sections: { your_mints: \"I tuoi mints\" },\n    restoring: \"Ripristino mints…\",\n    placeholder: { mint_url: \"https://\" },\n  },\n  WelcomeRecoverSeed: {\n    title: \"Inserisci la tua frase seed\",\n    text: \"Incolla o digita la tua frase seed di 12 parole per recuperare.\",\n    inputs: { word: \"Parola { index }\" },\n    actions: { paste_all: \"Incolla tutto\" },\n    disclaimer:\n      \"La tua frase seed è usata solo localmente per derivare le chiavi del portafoglio.\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"Ripristina il tuo ecash\",\n    text: \"Scansiona le proofs non spese sui mints configurati e aggiungile al tuo portafoglio.\",\n  },\n  MintRatings: {\n    title: \"Recensioni del mint\",\n    reviews: \"recensioni\",\n    ratings: \"Valutazioni\",\n    no_reviews: \"Nessuna recensione trovata\",\n    your_review: \"La tua recensione\",\n    no_reviews_to_display: \"Nessuna recensione da mostrare.\",\n    no_rating: \"Nessuna valutazione\",\n    out_of: \"su\",\n    rows: \"Reviews\",\n    sort: \"Ordina\",\n    sort_options: {\n      newest: \"Più recenti\",\n      oldest: \"Più vecchie\",\n      highest: \"Più alte\",\n      lowest: \"Più basse\",\n    },\n    actions: { write_review: \"Scrivi una recensione\" },\n    empty_state_subtitle:\n      \"Aiuta lasciando una recensione. Condividi la tua esperienza con questo mint e aiuta gli altri lasciando una recensione.\",\n  },\n  CreateMintReview: {\n    title: \"Recensisci il mint\",\n    publishing_as: \"Pubblicazione come\",\n    inputs: {\n      rating: { label: \"Valutazione\" },\n      review: { label: \"Recensione (opzionale)\" },\n    },\n    actions: {\n      publish: { label: \"Pubblica\", in_progress: \"Pubblicazione…\" },\n    },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"Ripristina da Frase Seed\",\n      caption:\n        \"Inserisci la tua frase seed per ripristinare il tuo portafoglio. Prima di ripristinare, assicurati di aver aggiunto tutti i mints che hai usato in precedenza.\",\n      inputs: {\n        seed_phrase: {\n          label: \"Frase seed\",\n          caption: \"Puoi vedere la tua frase seed nelle impostazioni.\",\n        },\n      },\n    },\n    information: {\n      label: \"Informazioni\",\n      caption:\n        \"L'assistente ripristinerà solo ecash da un'altra frase seed, non potrai usare questa frase seed o cambiare la frase seed del portafoglio che stai usando attualmente. Ciò significa che l'ecash ripristinato non sarà protetto dalla tua frase seed attuale finché non invii l'ecash a te stesso una volta.\",\n    },\n    restore_mints: {\n      label: \"Ripristina Mints\",\n      caption:\n        'Seleziona il mint da ripristinare. Puoi aggiungere altri mints nella schermata principale sotto \"Mints\" e ripristinarli qui.',\n    },\n    actions: {\n      paste: {\n        error: \"Impossibile leggere il contenuto degli appunti.\",\n      },\n      validate: {\n        error: \"Il mnemonico dovrebbe essere di almeno 12 parole.\",\n      },\n      select_all: {\n        label: \"Seleziona tutto\",\n      },\n      deselect_all: {\n        label: \"Deseleziona tutto\",\n      },\n      restore: {\n        label: \"Ripristina\",\n        in_progress: \"Ripristino mint in corso…\",\n        error: \"Errore nel ripristino del mint: { error }\",\n      },\n      restore_all_mints: {\n        label: \"Ripristina Tutti i Mints\",\n        in_progress: \"Ripristino mint { index } di { length }…\",\n        success: \"Ripristino completato con successo\",\n        error: \"Errore nel ripristino dei mints: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"Ripristina Mints selezionati ({count})\",\n        in_progress: \"Ripristino mint { index } di { length }…\",\n        success: \"{count} mint(s) ripristinati con successo\",\n        error: \"Errore nel ripristino dei mints selezionati: { error }\",\n      },\n    },\n    nostr_mints: {\n      label: \"Ripristina Mints da Nostr\",\n      caption:\n        \"Cerca i backup dei mints memorizzati sui relay Nostr usando la tua frase seed. Questo ti aiuterà a scoprire i mints che hai usato in precedenza.\",\n      search_button: \"Cerca backup Mint\",\n      select_all: \"Seleziona tutto\",\n      deselect_all: \"Deseleziona tutto\",\n      backed_up: \"Backup effettuato\",\n      already_added: \"Già aggiunto\",\n      add_selected: \"Aggiungi selezionati ({count})\",\n      no_backups_found: \"Nessun backup mint trovato\",\n      no_backups_hint:\n        \"Assicurati che il backup mint Nostr sia abilitato nelle impostazioni per eseguire automaticamente il backup della tua lista mint.\",\n      invalid_mnemonic: \"Inserisci una frase seed valida prima di cercare.\",\n      search_error: \"Impossibile cercare i backup dei mints.\",\n      add_error: \"Impossibile aggiungere i mints selezionati.\",\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"Aggiungi mint\",\n      description:\n        \"Inserisci l'URL di un mint Cashu per connetterti ad esso. Questo portafoglio non è affiliato a nessun mint.\",\n      inputs: {\n        nickname: {\n          placeholder: \"Nickname (es. Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"URL non valido\",\n        },\n        scan: {\n          label: \"Scansiona Codice QR\",\n        },\n      },\n    },\n    discover: {\n      title: \"Scopri mints\",\n      overline: \"Scopri\",\n      caption: \"Scopri mints che altri utenti hanno raccomandato su nostr.\",\n      actions: {\n        discover: {\n          label: \"Scopri mints\",\n          in_progress: \"Caricamento…\",\n          error_no_mints: \"Nessun mint trovato\",\n          success: \"Trovati { length } mints\",\n        },\n      },\n      recommendations: {\n        overline: \"Trovati { length } mints\",\n        caption:\n          \"Questi mints sono stati raccomandati da altri utenti Nostr. Sii cauto e fai le tue ricerche prima di usare un mint.\",\n        actions: {\n          browse: {\n            label: \"Clicca per sfogliare i mints\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"Scambia\",\n      overline: \"Scambi Multimint\",\n      caption:\n        \"Scambia fondi tra mints via Lightning. Nota: Lascia spazio per potenziali commissioni Lightning. Se il pagamento in entrata non riesce, controlla manualmente la fattura.\",\n      inputs: {\n        from: {\n          label: \"Da\",\n        },\n        to: {\n          label: \"A\",\n        },\n        amount: {\n          label: \"Importo ({ ticker })\",\n        },\n      },\n      actions: {\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n    },\n    error_badge: \"Errore\",\n    reviews_text: \"recensioni\",\n    no_reviews_yet: \"Nessuna recensione ancora\",\n    discover_mints_button: \"Scopri mints\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - Continua a scansionare\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"Ricevi Lightning\",\n    create_invoice_title: \"Crea Fattura\",\n    inputs: {\n      amount: {\n        label: \"Importo ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"Crea Fattura\",\n        label_blocked: \"Creazione fattura…\",\n        in_progress: \"Creazione\",\n      },\n    },\n    invoice: {\n      caption: \"Fattura Lightning\",\n      status_paid_text: \"Pagata!\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"Invia\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Nessun mint disponibile\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"Nessun mint disponibile\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"Invia Ecash\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"Offline\",\n    inputs: {\n      amount: {\n        label: \"Importo ({ ticker }) *\",\n        invalid_too_much_error_text: \"Troppo\",\n      },\n      p2pk_pubkey: {\n        label: \"Chiave pubblica ricevitore\",\n        label_invalid: \"Chiave pubblica ricevitore non valida\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"Copia Emoji\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"Copia link\",\n      },\n      share: {\n        tooltip_text: \"Condividi ecash\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"Elimina dalla cronologia\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"Scrivi su scheda NFC\",\n          ndef_unsupported_text: \"NDEF non supportato\",\n        },\n      },\n    },\n  },\n  ReceiveDialog: {\n    title: \"Ricevi\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Nessun mint disponibile\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"Devi connetterti a un mint per ricevere via Lightning\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"Ricevi Ecash\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"Richiedi\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"Scansione…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"Ricevi Ecash\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"Incolla token Cashu\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"Token non valido\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"Impossibile ricevere. Il blocco P2PK di questo token non corrisponde alla tua chiave pubblica.\",\n      },\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"Aggiunta mint…\",\n      },\n      swap: {\n        label: \"@:global.actions.swap.label\",\n        tooltip_text: \"Scambia verso un mint fidato\",\n        caption: \"Scambia { value }\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"Annulla scambio\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      later: {\n        label: \"Ricevi più tardi\",\n        tooltip_text: \"Aggiungi alla cronologia per ricevere dopo\",\n        already_in_history_success_text: \"Ecash già nella Cronologia\",\n        added_to_history_success_text: \"Ecash aggiunto alla Cronologia\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"Leggi da scheda NFC\",\n          ndef_unsupported_text: \"NDEF non supportato\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"Chiave P2PK\",\n      description: \"Ricevi ecash bloccato su questa chiave\",\n      used_warning_text:\n        \"Attenzione: Questa chiave è stata usata prima. Usa una chiave nuova per maggiore privacy.\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"Genera nuova chiave\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"Richiesta di Pagamento\",\n      description: \"Ricevi pagamenti via Nostr\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"Nuova richiesta\",\n      },\n      add_amount: {\n        label: \"Aggiungi importo\",\n      },\n      use_active_mint: {\n        label: \"Qualsiasi mint\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"Inserisci importo\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"Tastiera disabilitata. Puoi riabilitare la tastiera nelle impostazioni.\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostr Wallet Connect\",\n      description:\n        \"Controlla il tuo portafoglio remotamente con NWC. Premi il codice QR per collegare il tuo portafoglio con un'app compatibile.\",\n      warning_text:\n        \"Attenzione: chiunque abbia accesso a questa stringa di connessione può avviare pagamenti dal tuo portafoglio. Non condividerla!\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"Messaggio del Mint\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"Contatto\",\n    },\n    details: {\n      title: \"Dettagli mint\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"Vedi tutto\",\n          },\n          hide: {\n            label: \"Nascondi\",\n          },\n        },\n      },\n      currency: {\n        label: \"Valuta\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"Versione\",\n      },\n    },\n    actions: {\n      title: \"Azioni\",\n      copy_mint_url: {\n        label: \"Copia URL mint\",\n      },\n      delete: {\n        label: \"Elimina mint\",\n      },\n      edit: {\n        label: \"Modifica mint\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"Seleziona un mint\",\n    badge_mint_error_text: \"Errore\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"Nessuna cronologia ancora\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"{ value } fa\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Verifica stato\",\n      },\n      receive: {\n        tooltip_text: \"Ricevi\",\n      },\n      filter_pending: {\n        label: \"Filtra pendenti\",\n      },\n      show_all: {\n        label: \"Mostra tutto\",\n      },\n    },\n    old_token_not_found_error_text: \"Vecchio token non trovato\",\n  },\n  InvoiceTable: {\n    empty_text: \"Nessuna fattura ancora\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"Clicca per copiare\",\n      date_label: \"{ value } fa\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Verifica stato\",\n      },\n      filter_pending: {\n        label: \"Filtra pendenti\",\n      },\n      show_all: {\n        label: \"Mostra tutto\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"Sei sicuro di voler eliminare questo mint?\",\n    nickname: {\n      label: \"Nickname\",\n    },\n    balances: {\n      label: \"Saldi\",\n    },\n    warning_text:\n      \"Nota: Poiché questo portafoglio è paranoico, il tuo ecash da questo mint non verrà effettivamente eliminato ma rimarrà memorizzato sul tuo dispositivo. Lo vedrai riapparire se aggiungerai nuovamente questo mint in seguito.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"Rimuovi mint\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"Token Cashu o indirizzo Lightning\",\n      receive: \"Token Cashu\",\n      pay: \"Indirizzo Lightning o fattura\",\n    },\n    qr_scanner: {\n      title: \"Scansiona Codice QR\",\n      description: \"Tocca per scansionare un indirizzo\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"Paga con Lightning\",\n      inputs: {\n        invoice_data: {\n          label: \"Fattura o indirizzo Lightning\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"{ payee } richiede { value } { ticker }\",\n      amount_range_label:\n        \"{ payee } richiede{br}tra { min } e { max } { ticker }\",\n      sending_to_lightning_address: \"Invio a { address }\",\n      inputs: {\n        amount: {\n          label: \"Importo ({ ticker }) *\",\n        },\n        comment: {\n          label: \"Commento (opzionale)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"Paga { value }\",\n      paying: \"Pagamento in corso\",\n      paid: \"Pagato\",\n      fee: \"Commissione\",\n      memo: {\n        label: \"Memo\",\n      },\n      processing_info_text: \"Elaborazione…\",\n      balance_too_low_warning_text: \"Saldo troppo basso\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"Paga\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"Errore\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"Modifica mint\",\n    inputs: {\n      nickname: {\n        label: \"Nickname\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"Ti fidi di questo mint?\",\n    description:\n      \"Prima di usare questo mint, assicurati di fidarti. I mints potrebbero diventare malevoli o cessare l'attività in qualsiasi momento.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"Aggiunta mint\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"Inserisci un mnemonico\",\n    restore_mint_error_text: \"Errore nel ripristino del mint: { error }\",\n    prepare_info_text: \"Preparazione processo di ripristino…\",\n    restored_proofs_for_keyset_info_text:\n      \"Ripristinate { restoreCounter } prove per keyset { keysetId }\",\n    checking_proofs_for_keyset_info_text:\n      \"Verifica prove da { startIndex } a { endIndex } per keyset { keysetId }\",\n    no_proofs_info_text: \"Nessuna prova trovata da ripristinare\",\n    restored_amount_success_text: \"Ripristinato { amount }\",\n  },\n  swap: {\n    in_progress_warning_text: \"Scambio in corso\",\n    invalid_swap_data_error_text: \"Dati di scambio non validi\",\n    swap_error_text: \"Errore durante lo scambio\",\n  },\n  TokenInformation: {\n    fee: \"Commissione\",\n    unit: \"Unità\",\n    fiat: \"Fiat\",\n    p2pk: \"P2PK\",\n    locked: \"Bloccato\",\n    locked_to_you: \"Bloccato per te\",\n    mint: \"Zecca\",\n    memo: \"Memo\",\n    payment_request: \"Richiesta di pagamento\",\n    nostr: \"Nostr\",\n    token_copied: \"Token copiato negli appunti\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/ja-JP/index.ts",
    "content": "export default {\n  MultinutPicker: {\n    payment: \"マルチナット支払い\",\n    selectMints: \"支払いに使用するミントを一つ以上選択してください。\",\n    totalSelectedBalance: \"選択した合計残高\",\n    multiMintPay: \"マルチミント支払い\",\n    balanceNotEnough: \"複数ミントの残高がこの請求書を満たすには不十分です\",\n    failed: \"処理に失敗しました: {error}\",\n    paid: \"Lightningで{amount}を支払いました\",\n  },\n  global: {\n    copy_to_clipboard: {\n      success: \"クリップボードにコピーしました！\",\n    },\n    actions: {\n      add_mint: {\n        label: \"ミントを追加\",\n      },\n      cancel: {\n        label: \"キャンセル\",\n      },\n      copy: {\n        label: \"コピー\",\n      },\n      close: {\n        label: \"閉じる\",\n      },\n      enter: {\n        label: \"入力\",\n      },\n      lock: {\n        label: \"ロック\",\n      },\n      paste: {\n        label: \"貼り付け\",\n      },\n      receive: {\n        label: \"受け取る\",\n      },\n      scan: {\n        label: \"スキャン\",\n      },\n      send: {\n        label: \"送る\",\n      },\n      swap: {\n        label: \"スワップ\",\n      },\n      update: {\n        label: \"更新\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"ミントURL\",\n      },\n    },\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"残高が不足しています\",\n      received: \"{amount}を受け取りました\",\n      fee: \" (手数料: {fee})\",\n      could_not_request_mint: \"ミントをリクエストできませんでした\",\n      invoice_still_pending: \"請求書はまだ処理中です\",\n      paid_lightning: \"Lightningで{amount}を支払いました\",\n      payment_pending_refresh:\n        \"支払いは保留中です。請求書を手動で更新してください。\",\n      sent: \"{amount}を送信しました\",\n      token_still_pending: \"トークンはまだ処理中です\",\n      received_lightning: \"Lightningで{amount}を受け取りました\",\n      lightning_payment_failed: \"Lightning支払いに失敗しました\",\n      failed_to_decode_invoice: \"請求書をデコードできませんでした\",\n      invalid_lnurl: \"無効なLNURL\",\n      lnurl_error: \"LNURLエラー\",\n      no_amount: \"金額がありません\",\n      no_lnurl_data: \"LNURLデータがありません\",\n      no_price_data: \"価格データがありません。\",\n      please_try_again: \"もう一度お試しください。\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"ミントはすでに追加されています\",\n        added: \"ミントが追加されました\",\n        not_found: \"ミントが見つかりません\",\n        activation_failed: \"ミントの有効化に失敗しました\",\n        no_active_mint: \"アクティブなミントがありません\",\n        unit_activation_failed: \"単位の有効化に失敗しました\",\n        unit_not_supported: \"この単位はミントでサポートされていません\",\n        activated: \"ミントが有効化されました\",\n        could_not_connect: \"ミントに接続できませんでした\",\n        could_not_get_info: \"ミント情報を取得できませんでした\",\n        could_not_get_keys: \"ミントキーを取得できませんでした\",\n        could_not_get_keysets: \"ミントキーセットを取得できませんでした\",\n        mint_validation_error: \"ミントの検証エラー\",\n        removed: \"ミントが削除されました\",\n        error: \"ミントエラー\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"設定\",\n        settings: {\n          title: \"設定\",\n          caption: \"ウォレット構成\",\n        },\n      },\n      terms: {\n        title: \"規約\",\n        terms: {\n          title: \"規約\",\n          caption: \"利用規約\",\n        },\n      },\n      links: {\n        title: \"リンク集\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"寄付する\",\n          caption: \"Cashuをサポートする\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"オフライン\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"{ countdown }後に再読み込み\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"ステージング環境 – 実際の資金では使用しないでください！\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"ウォレット\",\n      },\n    },\n  },\n  Settings: {\n    language: {\n      title: \"言語\",\n      description: \"以下のリストから希望の言語を選択してください。\",\n    },\n    sections: {\n      backup_restore: \"バックアップと復元\",\n      lightning_address: \"ライトニングアドレス\",\n      nostr_keys: \"ノストルキー\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"クリックしてリレーを編集\",\n          add: {\n            title: \"リレーを追加\",\n            description:\n              \"あなたのウォレットは、支払い要求、nostr wallet connect、バックアップなどのnostr操作にこれらのリレーを使用します。\",\n          },\n          list: {\n            title: \"リレー\",\n            description: \"あなたのウォレットはこれらのリレーに接続します。\",\n            copy_tooltip: \"リレーをコピー\",\n            remove_tooltip: \"リレーを削除\",\n          },\n        },\n      },\n      payment_requests: \"支払いリクエスト\",\n      nostr_wallet_connect: \"ノストルウォレットコネクト\",\n      hardware_features: \"ハードウェア機能\",\n      p2pk_features: \"P2PK機能\",\n      privacy: \"プライバシー\",\n      experimental: \"実験的な機能\",\n      appearance: \"外観\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"シードフレーズをバックアップ\",\n        description:\n          \"シードフレーズでウォレットを復元できます。安全に保管してください。\",\n        seed_phrase_label: \"シードフレーズ\",\n      },\n      restore_ecash: {\n        title: \"ecashを復元\",\n        description:\n          \"復元ウィザードを使用すると、ニーモニックシードフレーズから失われたecashを回復できます。現在のウォレットのシードフレーズは影響を受けず、ウィザードでは別のシードフレーズからecashを復元することのみが可能です。\",\n        button: \"復元\",\n      },\n    },\n    lightning_address: {\n      title: \"ライトニングアドレス\",\n      description: \"Lightningアドレスで支払いを受け取ります。\",\n      enable: {\n        toggle: \"有効にする\",\n        description: \"npub.cash付きLightningアドレス\",\n      },\n      address: {\n        copy_tooltip: \"Lightningアドレスをコピー\",\n      },\n      automatic_claim: {\n        toggle: \"自動的に請求\",\n        description: \"着信支払いを自動的に受け取ります。\",\n      },\n      npc_v2: {\n        choose_mint_title: \"npub.cash v2のミントを選択\",\n        choose_mint_placeholder: \"ミントを選択...\",\n      },\n    },\n    nostr_keys: {\n      title: \"あなたのnostrキー\",\n      description: \"Lightningアドレスのnostrキーを設定します。\",\n      wallet_seed: {\n        title: \"ウォレットシードフレーズ\",\n        description: \"ウォレットシードからnostrキーペアを生成\",\n        copy_nsec: \"nsecをコピー\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"NIP-46バンカーを使用\",\n        delete_tooltip: \"接続を削除\",\n      },\n      use_nsec: {\n        title: \"nsecを使用\",\n        description: \"この方法は危険であり推奨されません\",\n        delete_tooltip: \"nsecを削除\",\n      },\n      signing_extension: {\n        title: \"署名拡張機能\",\n        description: \"NIP-07署名拡張機能を使用\",\n        not_found: \"NIP-07署名拡張機能が見つかりません\",\n      },\n    },\n    payment_requests: {\n      title: \"支払いリクエスト\",\n      description:\n        \"支払いリクエストを使用すると、nostr経由で支払いを受け取ることができます。これを有効にすると、ウォレットはあなたのnostrリレーに購読します。\",\n      enable_toggle: \"支払いリクエストを有効にする\",\n      claim_automatically: {\n        toggle: \"自動的に請求\",\n        description: \"着信支払いを自動的に受け取ります。\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"Nostrウォレットコネクト (NWC)\",\n      description:\n        \"NWCを使用して、他のどのアプリケーションからでもウォレットを制御できます。\",\n      enable_toggle: \"NWCを有効にする\",\n      payments_note:\n        \"NWCはBitcoin残高からの支払いにのみ使用できます。支払いはアクティブなミントから行われます。\",\n      connection: {\n        copy_tooltip: \"接続文字列をコピー\",\n        qr_tooltip: \"QRコードを表示\",\n        allowance_label: \"残りアローワンス (sat)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"NFCカードへの書き込みエンコーディングを選択\",\n        text: {\n          title: \"テキスト\",\n          description: \"トークンをプレーンテキストで保存\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"トークン付きでこのウォレットへのURLを保存\",\n        },\n        binary: {\n          title: \"バイナリ\",\n          description: \"トークンをバイナリデータとして保存\",\n        },\n        quick_access: {\n          toggle: \"NFCへのクイックアクセス\",\n          description:\n            \"Ecash受信メニューでNFCカードをすばやくスキャンします。このオプションは、Ecash受信メニューにNFCボタンを追加します。\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"このキーにロックされたecashを受け取るためのキーペアを生成します。警告: この機能は実験的です。少額のみに使用してください。秘密鍵を紛失した場合、誰にもそれにロックされたecashのロックを解除できなくなります。\",\n      generate_button: \"キーを生成\",\n      import_button: \"nsecをインポート\",\n      quick_access: {\n        toggle: \"ロックへのクイックアクセス\",\n        description:\n          \"これを使用して、ecash受信メニューでP2PKロックキーをすばやく表示します。\",\n      },\n      keys_expansion: {\n        label: \"{count}個のキーをブラウズするにはクリック\",\n        used_badge: \"使用済み\",\n      },\n    },\n    privacy: {\n      title: \"プライバシー\",\n      description: \"これらの設定はプライバシーに影響します。\",\n      check_incoming: {\n        toggle: \"着信請求書をチェック\",\n        description:\n          \"有効にすると、ウォレットはバックグラウンドで最新の請求書をチェックします。これによりウォレットの応答性が向上し、フィンガープリンティングが容易になります。未払い請求書は請求書タブで手動で確認できます。\",\n      },\n      check_startup: {\n        toggle: \"起動時に保留中の請求書をチェック\",\n        description:\n          \"有効にすると、ウォレットは起動時に過去24時間の保留中の請求書をチェックします。\",\n      },\n      check_all: {\n        toggle: \"すべての請求書をチェック\",\n        description:\n          \"有効にすると、ウォレットは最大2週間、未払い請求書をバックグラウンドで定期的にチェックします。これによりウォレットのオンラインアクティビティが増加し、フィンガープリンティングが容易になります。未払い請求書は請求書タブで手動で確認できます。\",\n      },\n      check_sent: {\n        toggle: \"送信されたecashをチェック\",\n        description:\n          \"有効にすると、ウォレットは定期的なバックグラウンドチェックを使用して、送信されたトークンが償還されたかどうかを判断します。これによりウォレットのオンラインアクティビティが増加し、フィンガープリンティングが容易になります。\",\n      },\n      websockets: {\n        toggle: \"WebSocketsを使用\",\n        description:\n          \"有効にすると、ウォレットは長期間のWebSocket接続を使用して、ミントから支払われた請求書や使用済みトークンに関する更新を受け取ります。これによりウォレットの応答性は向上しますが、フィンガープリンティングも容易になります。\",\n      },\n      bitcoin_price: {\n        toggle: \"Coinbaseから為替レートを取得\",\n        description:\n          \"有効にすると、現在のBitcoin為替レートがcoinbase.comから取得され、換算された残高が表示されます。\",\n        currency: {\n          title: \"法定通貨\",\n          description: \"Bitcoin価格表示用の法定通貨を選択してください。\",\n        },\n      },\n    },\n    experimental: {\n      title: \"実験的な機能\",\n      description: \"これらの機能は実験的です。\",\n      receive_swaps: {\n        toggle: \"スワップを受け取る\",\n        badge: \"ベータ\",\n        description:\n          \"Ecash受信ダイアログで、受信したEcashをアクティブなミントにスワップするオプション。\",\n      },\n      auto_paste: {\n        toggle: \"Ecashを自動的に貼り付け\",\n        description:\n          \"受信、Ecash、貼り付けを押すと、クリップボードのecashを自動的に貼り付けます。自動貼り付けはiOSでUIグリッチを引き起こす可能性があります。問題が発生した場合はオフにしてください。\",\n      },\n      auditor: {\n        toggle: \"監査人を有効にする\",\n        badge: \"ベータ\",\n        description:\n          \"有効にすると、ウォレットはミントの詳細ダイアログに監査人情報を表示します。監査人はミントの信頼性を監視するサードパーティサービスです。\",\n        url_label: \"監査人URL\",\n        api_url_label: \"監査人API URL\",\n      },\n      multinut: {\n        toggle: \"マルチナットを有効にする\",\n        description:\n          \"有効にすると、ウォレットはマルチナットを使用して複数のミントから一度に請求書を支払います。\",\n      },\n      nostr_mint_backup: {\n        toggle: \"Nostrでミントリストをバックアップ\",\n        description:\n          \"有効にすると、設定されたNostrキーを使用して、ミントリストがNostrリレーに自動的にバックアップされます。これにより、デバイス間でミントリストを復元できます。\",\n        notifications: {\n          enabled: \"Nostrミントのバックアップが有効になりました\",\n          disabled: \"Nostrミントのバックアップが無効になりました\",\n          failed: \"Nostrミントのバックアップを有効にできませんでした\",\n        },\n      },\n    },\n    appearance: {\n      keyboard: {\n        title: \"オンスクリーンキーボード\",\n        description: \"金額入力に数字キーボードを使用します。\",\n        toggle: \"数字キーボードを使用\",\n        toggle_description:\n          \"有効にすると、金額入力に数字キーボードが使用されます。\",\n      },\n      theme: {\n        title: \"外観\",\n        description: \"ウォレットの外観を変更します。\",\n        tooltips: {\n          mono: \"モノラル\",\n          cyber: \"サイバー\",\n          freedom: \"自由\",\n          nostr: \"ノストル\",\n          bitcoin: \"ビットコイン\",\n          mint: \"ミント\",\n          nut: \"ナッツ\",\n          blu: \"ブルー\",\n          flamingo: \"フラミンゴ\",\n        },\n      },\n      bip177: {\n        title: \"ビットコインシンボル\",\n        description: \"satsの代わりに₿シンボルを使用します。\",\n        toggle: \"₿シンボルを使用\",\n      },\n    },\n    web_of_trust: {\n      title: \"信頼のウェブ\",\n      known_pubkeys: \"既知の公開鍵: {wotCount}\",\n      continue_crawl: \"クロールを続行\",\n      crawl_odell: \"ODELLの信頼のウェブをクロール\",\n      crawl_wot: \"信頼のウェブをクロール\",\n      pause: \"一時停止\",\n      reset: \"リセット\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"npubx.cashを使用\",\n      copy_lightning_address: \"Lightningアドレスをコピー\",\n      v2_mint: \"npub.cash v2ミント\",\n    },\n    multinut: {\n      use_multinut: \"マルチナットを使用\",\n    },\n    advanced: {\n      title: \"高度な設定\",\n      developer: {\n        title: \"開発者設定\",\n        description: \"以下の設定は開発およびデバッグ用です。\",\n        new_seed: {\n          button: \"新しいシードフレーズを生成\",\n          description:\n            \"これにより新しいシードフレーズが生成されます。新しいシードで復元できるように、すべての残高を自分自身に送金する必要があります。\",\n          confirm_question: \"新しいシードフレーズを生成してもよろしいですか？\",\n          cancel: \"キャンセル\",\n          confirm: \"確認\",\n        },\n        remove_spent: {\n          button: \"使用済み証明書を削除\",\n          description:\n            \"アクティブなミントからのecashトークンが使用されているかチェックし、使用済みのものをウォレットから削除します。ウォレットが詰まった場合にのみ使用してください。\",\n        },\n        debug_console: {\n          button: \"デバッグコンソールを切り替え\",\n          description:\n            \"Javascriptデバッグターミナルを開きます。理解できないものをこのターミナルに貼り付けないでください。泥棒が悪意のあるコードを貼り付けさせようとする可能性があります。\",\n        },\n        export_proofs: {\n          button: \"アクティブな証明書をエクスポート\",\n          description:\n            \"アクティブなミントからの全残高をCashuトークンとしてクリップボードにコピーします。これは選択したミントと単位のトークンのみをエクスポートします。完全なエクスポートを行うには、別のミントと単位を選択して再度エクスポートしてください。\",\n        },\n        keyset_counters: {\n          title: \"キーセットカウンターをインクリメント\",\n          description:\n            \"キーセットIDをクリックして、ウォレット内のキーセットの導出パスカウンターをインクリメントします。「出力はすでに署名されています」というエラーが表示される場合に便利です。\",\n          counter: \"カウンター: {count}\",\n        },\n        unset_reserved: {\n          button: \"すべての予約済みトークンを解除\",\n          description:\n            \"このウォレットは、二重支払いを防ぐために、保留中の出金ecashを予約済みとしてマークします（そして残高から差し引きます）。このボタンはすべての予約済みトークンを解除し、再び使用できるようにします。これを行うと、ウォレットに消費済みの証明書が含まれる可能性があります。「消費済み証明書を削除」ボタンを押してそれらを取り除いてください。\",\n        },\n        show_onboarding: {\n          button: \"オンボーディングを表示\",\n          description: \"オンボーディング画面を再度表示します。\",\n        },\n        reset_wallet: {\n          button: \"ウォレットデータをリセット\",\n          description:\n            \"ウォレットデータをリセットします。警告: これによりすべてが削除されます！まずバックアップを作成してください。\",\n          confirm_question: \"ウォレットデータを削除してもよろしいですか？\",\n          cancel: \"キャンセル\",\n          confirm: \"ウォレットを削除\",\n        },\n        export_wallet: {\n          button: \"ウォレットデータをエクスポート\",\n          description:\n            \"ウォレットのダンプをダウンロードします。新しいウォレットのウェルカム画面でこのファイルからウォレットを復元できます。このファイルは、エクスポート後にウォレットを使い続けると同期がずれます。\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"ミントに参加する\",\n    subtitle:\n      \"まだCashuミントに参加していません。始めるには、設定でミントURLを追加するか、新しいミントからecashを受け取ります。\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"Ecashを受け取る\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"履歴\",\n      },\n      invoices: {\n        label: \"請求書\",\n      },\n      mints: {\n        label: \"ミント\",\n      },\n    },\n    install: {\n      text: \"インストール\",\n      tooltip: \"Cashuをインストール\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"ダメです。\",\n    text: \"別のタブがすでに実行中です。このタブを閉じて再試行してください。\",\n    actions: {\n      retry: {\n        label: \"再試行\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"おっと、何もありません…\",\n    actions: {\n      home: {\n        label: \"ホームに戻る\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"ミント\",\n    },\n    mintBalance: {\n      label: \"残高\",\n    },\n    mintError: {\n      label: \"ミントエラー\",\n    },\n    pending: {\n      label: \"保留中\",\n      tooltip: \"すべての保留中のトークンを確認\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"前へ\",\n      },\n      next: {\n        label: \"次へ\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"Cashuへようこそ\",\n    text: \"Cashu.meは、ecashを使用して資金を安全かつプライベートに保つための無料のオープンソースBitcoinウォレットです。\",\n    actions: {\n      more: {\n        label: \"もっと詳しく\",\n      },\n    },\n    p1: {\n      text: \"CashuはBitcoinのための無料のオープンソースecashプロトコルです。{ link }で詳しく知ることができます。\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"このウォレットはどのミントにも関連付けられていません。このウォレットを使用するには、信頼する1つ以上のCashuミントに接続する必要があります。\",\n    },\n    p3: {\n      text: \"このウォレットは、あなただけがアクセスできるecashを保存します。シードフレーズのバックアップなしにブラウザデータを削除すると、トークンを失います。\",\n    },\n    p4: {\n      text: \"このウォレットはベータ版です。資金へのアクセスを失うことについて、当社は一切の責任を負いません。ご自身の責任で使用してください！このコードはオープンソースであり、MITライセンスの下でライセンスされています。\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"PWAをインストール\",\n    alt: { pwa_example: \"PWA インストール例\" },\n    installing: \"インストール中…\",\n    instruction: {\n      intro: {\n        text: \"最高の体験のため、このウォレットをデバイスのネイティブWebブラウザでProgressive Web Appとしてインストールしてください。今すぐこれを行ってください。\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"メニューをタップ (右上)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"{ buttonText }を押す\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"共有をタップ (下)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"{ buttonText }を押す\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"このアプリをデバイスにインストールしたら、このブラウザウィンドウを閉じてホーム画面からアプリを使用してください。\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"成功！\",\n        text: \"CashuをPWAとして使用しています。他の開いているブラウザウィンドウをすべて閉じ、ホーム画面からアプリを使用してください。\",\n        nextSteps: \"このブラウザタブを閉じて、ホーム画面からアプリを開けます。\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"{ icon }と{ buttonText }をタップ\",\n    buttonText: \"ホーム画面に追加\",\n  },\n  AndroidPWAPrompt: {\n    text: \"{ icon }と{ buttonText }をタップ\",\n    buttonText: \"ホーム画面に追加\",\n  },\n  WelcomeSlide3: {\n    title: \"あなたのシードフレーズ\",\n    text: \"シードフレーズをパスワードマネージャーまたは紙に保管してください。シードフレーズは、このデバイスへのアクセスを失った場合に資金を回復する唯一の方法です。\",\n    inputs: {\n      seed_phrase: {\n        label: \"シードフレーズ\",\n        caption: \"設定でシードフレーズを確認できます。\",\n      },\n      checkbox: {\n        label: \"書き留めました\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"規約\",\n    actions: {\n      more: {\n        label: \"利用規約を読む\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"これらの規約と条件を読み、同意します\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"ウォレットをセットアップ\",\n    text: \"シードフレーズから復元しますか？ それとも新しいウォレットを作成しますか？\",\n    options: {\n      new: {\n        title: \"新しいウォレットを作成\",\n        subtitle: \"新しいシードを生成してミントを追加します。\",\n      },\n      recover: {\n        title: \"ウォレットを復元\",\n        subtitle: \"シードフレーズを入力し、ミントとecashを復元します。\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"ミントを追加\",\n    text: \"ミントはecashの送受信を助けるサーバーです。検出されたミントを選ぶか、手動で追加できます。後で追加することもできます。\",\n    sections: { your_mints: \"あなたのミント\" },\n    restoring: \"ミントを復元中…\",\n    placeholder: { mint_url: \"https://\" },\n  },\n  WelcomeRecoverSeed: {\n    title: \"シードフレーズを入力\",\n    text: \"復元のために12語のシードフレーズを貼り付けるか入力してください。\",\n    inputs: { word: \"単語 { index }\" },\n    actions: { paste_all: \"すべて貼り付け\" },\n    disclaimer:\n      \"シードフレーズはローカルでのみ使用され、ウォレットの鍵を導出します。\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"ecash を復元\",\n    text: \"設定済みミントで未使用のproofをスキャンし、ウォレットに追加します。\",\n  },\n  MintRatings: {\n    title: \"ミントのレビュー\",\n    reviews: \"レビュー\",\n    ratings: \"評価\",\n    no_reviews: \"レビューが見つかりません\",\n    your_review: \"あなたのレビュー\",\n    no_reviews_to_display: \"表示するレビューはありません。\",\n    no_rating: \"評価なし\",\n    out_of: \"のうち\",\n    rows: \"Reviews\",\n    sort: \"並び替え\",\n    sort_options: {\n      newest: \"新しい順\",\n      oldest: \"古い順\",\n      highest: \"高い順\",\n      lowest: \"低い順\",\n    },\n    actions: { write_review: \"レビューを書く\" },\n    empty_state_subtitle:\n      \"レビューを残して助けてください。このミントでの経験を共有し、レビューを残して他の人を助けてください。\",\n  },\n  CreateMintReview: {\n    title: \"ミントをレビュー\",\n    publishing_as: \"次として公開\",\n    inputs: {\n      rating: { label: \"評価\" },\n      review: { label: \"レビュー（任意）\" },\n    },\n    actions: {\n      publish: { label: \"公開\", in_progress: \"公開中…\" },\n    },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"シードフレーズから復元\",\n      caption:\n        \"ウォレットを復元するには、シードフレーズを入力してください。復元する前に、以前に使用したすべてのミントを追加したことを確認してください。\",\n      inputs: {\n        seed_phrase: {\n          label: \"シードフレーズ\",\n          caption: \"設定でシードフレーズを確認できます。\",\n        },\n      },\n    },\n    information: {\n      label: \"情報\",\n      caption:\n        \"ウィザードは別のシードフレーズからのみecashを復元し、現在使用しているウォレットのシードフレーズを使用したり変更したりすることはできません。これは、一度ecashを自分自身に送金しない限り、復元されたecashが現在のシードフレーズによって保護されないことを意味します。\",\n    },\n    restore_mints: {\n      label: \"ミントを復元\",\n      caption:\n        \"復元するミントを選択します。メイン画面の「ミント」でさらにミントを追加し、ここで復元できます。\",\n    },\n    actions: {\n      paste: {\n        error: \"クリップボードの内容の読み取りに失敗しました。\",\n      },\n      validate: {\n        error: \"ニーモニックは少なくとも12単語である必要があります。\",\n      },\n      select_all: {\n        label: \"すべて選択\",\n      },\n      deselect_all: {\n        label: \"すべて解除\",\n      },\n      restore: {\n        label: \"復元\",\n        in_progress: \"ミントを復元中…\",\n        error: \"ミントの復元エラー: { error }\",\n      },\n      restore_all_mints: {\n        label: \"すべてのミントを復元\",\n        in_progress: \"{ length }個のミントのうち{ index }個目を復元中…\",\n        success: \"復元が正常に完了しました\",\n        error: \"ミントの復元エラー: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"選択したミントを復元 ({count})\",\n        in_progress: \"{length}個のミントのうち{index}個を復元しています…\",\n        success: \"{count}個のミントを正常に復元しました\",\n        error: \"選択したミントの復元中にエラーが発生しました: {error}\",\n      },\n    },\n    nostr_mints: {\n      label: \"Nostrからミントを復元\",\n      caption:\n        \"シードフレーズを使用してNostrリレーに保存されているミントのバックアップを検索します。これにより、以前使用したミントを発見できます。\",\n      search_button: \"ミントのバックアップを検索\",\n      select_all: \"すべて選択\",\n      deselect_all: \"すべて選択解除\",\n      backed_up: \"バックアップ済み\",\n      already_added: \"既に追加済み\",\n      add_selected: \"選択したものを追加 ({count})\",\n      no_backups_found: \"ミントのバックアップが見つかりません\",\n      no_backups_hint:\n        \"ミントリストを自動的にバックアップするには、設定でNostrミントのバックアップが有効になっていることを確認してください。\",\n      invalid_mnemonic: \"検索する前に有効なシードフレーズを入力してください。\",\n      search_error: \"ミントのバックアップの検索に失敗しました。\",\n      add_error: \"選択したミントの追加に失敗しました。\",\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"ミントを追加\",\n      description:\n        \"接続するCashuミントのURLを入力します。このウォレットはどのミントにも関連付けられていません。\",\n      inputs: {\n        nickname: {\n          placeholder: \"ニックネーム（例：Testnet）\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"無効なURL\",\n        },\n        scan: {\n          label: \"QRコードをスキャン\",\n        },\n      },\n    },\n    discover: {\n      title: \"ミントを発見\",\n      overline: \"発見\",\n      caption: \"他のユーザーがnostrで推奨したミントを発見します。\",\n      actions: {\n        discover: {\n          label: \"ミントを発見\",\n          in_progress: \"読み込み中…\",\n          error_no_mints: \"ミントが見つかりませんでした\",\n          success: \"{ length }個のミントが見つかりました\",\n        },\n      },\n      recommendations: {\n        overline: \"{ length }個のミントが見つかりました\",\n        caption:\n          \"これらのミントは他のNostrユーザーによって推奨されました。ミントを使用する前に注意し、ご自身の調査を行ってください。\",\n        actions: {\n          browse: {\n            label: \"ミントをブラウズするにはクリック\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"スワップ\",\n      overline: \"マルチミントスワップ\",\n      caption:\n        \"Lightning経由でミント間で資金をスワップします。注意：Lightningの手数料の可能性に備えて余裕を持たせてください。着信支払いが成功しない場合は、手動で請求書を確認してください。\",\n      inputs: {\n        from: {\n          label: \"から\",\n        },\n        to: {\n          label: \"へ\",\n        },\n        amount: {\n          label: \"金額 ({ ticker })\",\n        },\n      },\n      actions: {\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n    },\n    error_badge: \"エラー\",\n    reviews_text: \"レビュー\",\n    no_reviews_yet: \"まだレビューはありません\",\n    discover_mints_button: \"ミントを発見\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - スキャンを続行\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"Lightningを受け取る\",\n    create_invoice_title: \"請求書の作成\",\n    inputs: {\n      amount: {\n        label: \"金額 ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"請求書を作成\",\n        label_blocked: \"請求書作成中…\",\n        in_progress: \"作成中\",\n      },\n    },\n    invoice: {\n      caption: \"Lightning請求書\",\n      status_paid_text: \"支払い済み！\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"送る\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"利用可能なミントがありません\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"利用可能なミントがありません\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"Ecashを送る\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"オフライン\",\n    inputs: {\n      amount: {\n        label: \"金額 ({ ticker }) *\",\n        invalid_too_much_error_text: \"多すぎます\",\n      },\n      p2pk_pubkey: {\n        label: \"受信者の公開鍵\",\n        label_invalid: \"受信者の公開鍵\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"絵文字をコピー\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"リンクをコピー\",\n      },\n      share: {\n        tooltip_text: \"ecashトークンを共有\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"履歴から削除\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"NFCカードに書き込み\",\n          ndef_unsupported_text: \"NDEF非対応\",\n        },\n      },\n    },\n  },\n  ReceiveDialog: {\n    title: \"受け取る\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"利用可能なミントがありません\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints:\n          \"Lightning経由で受け取るにはミントに接続する必要があります\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"Ecashを受け取る\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"リクエスト\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"スキャン中…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"Ecashを受け取る\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"Cashuトークンを貼り付け\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"無効なトークン\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"受信できません。このトークンのP2PKロックがあなたの公開鍵と一致しません。\",\n      },\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"ミント追加中…\",\n      },\n      swap: {\n        label: \"@:global.actions.swap.label\",\n        tooltip_text: \"信頼できるミントにスワップ\",\n        caption: \"{ value }をスワップ\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"スワップをキャンセル\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      later: {\n        label: \"後で受け取る\",\n        tooltip_text: \"後で受け取るために履歴に追加\",\n        already_in_history_success_text: \"Ecashはすでに履歴にあります\",\n        added_to_history_success_text: \"Ecashを履歴に追加しました\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"NFCカードから読み取り\",\n          ndef_unsupported_text: \"NDEF非対応\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"P2PKキー\",\n      description: \"このキーにロックされたecashを受け取る\",\n      used_warning_text:\n        \"警告: このキーは以前に使用されています。プライバシー向上のため新しいキーを使用してください。\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"新しいキーを生成\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"支払いリクエスト\",\n      description: \"Nostr経由で支払いを受け取る\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"新しいリクエスト\",\n      },\n      add_amount: {\n        label: \"金額を追加\",\n      },\n      use_active_mint: {\n        label: \"任意のミント\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"金額を入力\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"キーボードが無効になりました。設定でキーボードを再度有効にできます。\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostrウォレットコネクト\",\n      description:\n        \"NWCを使用してリモートでウォレットを制御します。互換性のあるアプリでウォレットをリンクするには、QRコードを押してください。\",\n      warning_text:\n        \"警告: この接続文字列にアクセスできる人は誰でもウォレットから支払いを開始できます。共有しないでください！\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"ミントメッセージ\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"連絡先\",\n    },\n    details: {\n      title: \"ミント詳細\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"すべて表示\",\n          },\n          hide: {\n            label: \"隠す\",\n          },\n        },\n      },\n      currency: {\n        label: \"通貨\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"バージョン\",\n      },\n    },\n    actions: {\n      title: \"アクション\",\n      copy_mint_url: {\n        label: \"ミントURLをコピー\",\n      },\n      delete: {\n        label: \"ミントを削除\",\n      },\n      edit: {\n        label: \"ミントを編集\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"ミントを選択\",\n    badge_mint_error_text: \"エラー\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"履歴はまだありません\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"{ value }前\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"ステータスを確認\",\n      },\n      receive: {\n        tooltip_text: \"受け取る\",\n      },\n      filter_pending: {\n        label: \"保留中をフィルタリング\",\n      },\n      show_all: {\n        label: \"すべて表示\",\n      },\n    },\n    old_token_not_found_error_text: \"古いトークンが見つかりません\",\n  },\n  InvoiceTable: {\n    empty_text: \"請求書はまだありません\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"クリックしてコピー\",\n      date_label: \"{ value }前\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"ステータスを確認\",\n      },\n      filter_pending: {\n        label: \"保留中をフィルタリング\",\n      },\n      show_all: {\n        label: \"すべて表示\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"このミントを削除してもよろしいですか？\",\n    nickname: {\n      label: \"ニックネーム\",\n    },\n    balances: {\n      label: \"残高\",\n    },\n    warning_text:\n      \"注: このウォレットは偏執的であるため、このミントからのecashは実際には削除されず、デバイスに保存されたままになります。後でこのミントを再度追加すると、再び表示されます。\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"ミントを削除\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"CashuトークンまたはLightningアドレス\",\n      receive: \"Cashuトークン\",\n      pay: \"Lightningアドレスまたは請求書\",\n    },\n    qr_scanner: {\n      title: \"QRコードをスキャン\",\n      description: \"タップしてアドレスをスキャン\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"Lightningで支払う\",\n      inputs: {\n        invoice_data: {\n          label: \"Lightning請求書またはアドレス\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label:\n        \"{ payee }が{ value } { ticker }をリクエストしています\",\n      amount_range_label:\n        \"{ payee }が{ min }から{ max } { ticker }の間をリクエストしています\",\n      sending_to_lightning_address: \"{ address }に送信中\",\n      inputs: {\n        amount: {\n          label: \"金額 ({ ticker }) *\",\n        },\n        comment: {\n          label: \"コメント (任意)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"{ value }を支払う\",\n      paying: \"支払い中\",\n      paid: \"支払い済み\",\n      fee: \"手数料\",\n      memo: {\n        label: \"メモ\",\n      },\n      processing_info_text: \"処理中…\",\n      balance_too_low_warning_text: \"残高不足\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"支払う\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"エラー\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"ミントを編集\",\n    inputs: {\n      nickname: {\n        label: \"ニックネーム\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"このミントを信頼しますか？\",\n    description:\n      \"このミントを使用する前に、信頼できることを確認してください。ミントはいつでも悪意のあるものになるか、運営を停止する可能性があります。\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"ミントを追加中\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"ニーモニックを入力してください\",\n    restore_mint_error_text: \"ミントの復元エラー: { error }\",\n    prepare_info_text: \"復元処理を準備中…\",\n    restored_proofs_for_keyset_info_text:\n      \"{ keysetId }キーセットの{ restoreCounter }個の証明書を復元しました\",\n    checking_proofs_for_keyset_info_text:\n      \"{ keysetId }キーセットの{ startIndex }から{ endIndex }までの証明書を確認中\",\n    no_proofs_info_text: \"復元する証明書が見つかりませんでした\",\n    restored_amount_success_text: \"{ amount }復元しました\",\n  },\n  swap: {\n    in_progress_warning_text: \"スワップ進行中\",\n    invalid_swap_data_error_text: \"無効なスワップデータ\",\n    swap_error_text: \"スワップエラー\",\n  },\n  TokenInformation: {\n    fee: \"手数料\",\n    unit: \"単位\",\n    fiat: \"フィアット\",\n    p2pk: \"P2PK\",\n    locked: \"ロック済み\",\n    locked_to_you: \"あなたにロック済み\",\n    mint: \"ミント\",\n    memo: \"メモ\",\n    payment_request: \"支払いリクエスト\",\n    nostr: \"Nostr\",\n    token_copied: \"トークンをクリップボードにコピーしました\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/pt-BR/index.ts",
    "content": "export default {\n  global: {\n    copy_to_clipboard: {\n      success: \"Copiado para a área de transferência!\",\n    },\n    actions: {\n      add_mint: {\n        label: \"Adicionar mint\",\n      },\n      cancel: {\n        label: \"Cancelar\",\n      },\n      copy: {\n        label: \"Copiar\",\n      },\n      close: {\n        label: \"Fechar\",\n      },\n      enter: {\n        label: \"Confirmar\",\n      },\n      lock: {\n        label: \"Bloquear\",\n      },\n      paste: {\n        label: \"Colar\",\n      },\n      receive: {\n        label: \"Receber\",\n      },\n      scan: {\n        label: \"Escanear\",\n      },\n      send: {\n        label: \"Enviar\",\n      },\n      pay: {\n        label: \"Pagar\",\n      },\n      swap: {\n        label: \"Swap\",\n      },\n      update: {\n        label: \"Atualizar\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"URL do mint\",\n      },\n    },\n  },\n  common: {\n    fee: \"Taxa\",\n  },\n  MultinutPicker: {\n    payment: \"Pagamento multi-mint\",\n    selectMints: \"Selecione um ou mais mints para realizar um pagamento.\",\n    totalSelectedBalance: \"Saldo total selecionado\",\n    multiMintPay: \"Pagamento multi-mint\",\n    balanceNotEnough: \"Saldo multi-mint insuficiente para cobrir esta fatura\",\n    failed: \"Falha ao processar: {error}\",\n    paid: \"Pago {amount} via Lightning\",\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"Saldo insuficiente\",\n      received: \"Recebido {amount}\",\n      fee: \" (taxa: {fee})\",\n      could_not_request_mint: \"Não foi possível solicitar ao mint\",\n      invoice_still_pending: \"Fatura ainda pendente\",\n      paid_lightning: \"Pago {amount} via Lightning\",\n      payment_pending_refresh:\n        \"Pagamento pendente. Atualize a fatura manualmente.\",\n      sent: \"Enviado {amount}\",\n      token_still_pending: \"Token ainda pendente\",\n      received_lightning: \"Recebido {amount} via Lightning\",\n      lightning_payment_failed: \"Pagamento Lightning falhou\",\n      failed_to_decode_invoice: \"Falha ao decodificar fatura\",\n      unsupported_legacy_qr: \"QR code legado não suportado\",\n      legacy_qr_not_supported:\n        \"Este QR code legado não é de um comerciante suportado\",\n      invalid_lnurl: \"LNURL inválido\",\n      lnurl_error: \"Erro de LNURL\",\n      no_amount: \"Sem valor\",\n      no_lnurl_data: \"Sem dados LNURL\",\n      no_price_data: \"Sem dados de preço.\",\n      please_try_again: \"Por favor, tente novamente.\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"Mint já adicionado\",\n        added: \"Mint adicionado\",\n        not_found: \"Mint não encontrado\",\n        activation_failed: \"Falha ao ativar mint\",\n        no_active_mint: \"Nenhum mint ativo\",\n        unit_activation_failed: \"Falha ao ativar unidade\",\n        unit_not_supported: \"Unidade não suportada pelo mint\",\n        activated: \"Mint ativado\",\n        could_not_connect: \"Não foi possível conectar ao mint\",\n        could_not_get_info: \"Não foi possível obter informações do mint\",\n        could_not_get_keys: \"Não foi possível obter chaves do mint\",\n        could_not_get_keysets: \"Não foi possível obter keysets do mint\",\n        mint_validation_error: \"Erro de validação do mint\",\n        removed: \"Mint removido\",\n        error: \"Erro no mint\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"Configurações\",\n        settings: {\n          title: \"Configurações\",\n          caption: \"Configuração da carteira\",\n        },\n      },\n      terms: {\n        title: \"Termos\",\n        terms: {\n          title: \"Termos\",\n          caption: \"Termos de Serviço\",\n        },\n      },\n      links: {\n        title: \"Links\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"Doar\",\n          caption: \"Apoie o Cashu\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"Offline\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"Recarregando em { countdown }\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"Staging – não use com fundos reais!\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"Carteira\",\n      },\n    },\n  },\n  Settings: {\n    language: {\n      title: \"Idioma\",\n      description:\n        \"Por favor, escolha o idioma de sua preferência na lista abaixo.\",\n    },\n    sections: {\n      backup_restore: \"BACKUP E RESTAURAÇÃO\",\n      lightning_address: \"ENDEREÇO LIGHTNING\",\n      nostr_keys: \"CHAVES NOSTR\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"Clique para editar os relays\",\n          add: {\n            title: \"Adicionar relay\",\n            description:\n              \"Sua carteira usa esses relays para operações Nostr como solicitações de pagamento, Nostr Wallet Connect e backups.\",\n          },\n          list: {\n            title: \"Relays\",\n            description: \"Sua carteira se conectará a esses relays.\",\n            copy_tooltip: \"Copiar relay\",\n            remove_tooltip: \"Remover relay\",\n          },\n        },\n      },\n      payment_requests: \"SOLICITAÇÕES DE PAGAMENTO\",\n      nostr_wallet_connect: \"NOSTR WALLET CONNECT\",\n      hardware_features: \"RECURSOS DE HARDWARE\",\n      p2pk_features: \"RECURSOS P2PK\",\n      privacy: \"PRIVACIDADE\",\n      experimental: \"EXPERIMENTAL\",\n      appearance: \"APARÊNCIA\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"Fazer backup da frase de recuperação\",\n        description:\n          \"Sua frase de recuperação pode restaurar sua carteira. Mantenha-a segura e privada.\",\n        seed_phrase_label: \"Frase de recuperação\",\n      },\n      restore_ecash: {\n        title: \"Restaurar ecash\",\n        description:\n          \"O assistente de restauração permite recuperar ecash perdido a partir de uma frase de recuperação mnemônica. A frase de recuperação da sua carteira atual permanecerá inalterada; o assistente só permitirá restaurar ecash de outra frase de recuperação.\",\n        button: \"Restaurar\",\n      },\n    },\n    lightning_address: {\n      title: \"Endereço Lightning\",\n      description: \"Receba pagamentos no seu endereço Lightning.\",\n      enable: {\n        toggle: \"Ativar\",\n        description: \"Endereço Lightning com npub.cash\",\n      },\n      address: {\n        copy_tooltip: \"Copiar endereço Lightning\",\n      },\n      automatic_claim: {\n        toggle: \"Reivindicar automaticamente\",\n        description: \"Receba pagamentos recebidos automaticamente.\",\n      },\n      npc_v2: {\n        choose_mint_title: \"Escolha o mint para npub.cash v2\",\n        choose_mint_placeholder: \"Selecione um mint...\",\n      },\n    },\n    nostr_keys: {\n      title: \"Suas chaves Nostr\",\n      description:\n        \"Suas chaves Nostr serão usadas para determinar seu endereço Lightning.\",\n      wallet_seed: {\n        title: \"Frase de recuperação da carteira\",\n        description:\n          \"Gerar par de chaves Nostr a partir da frase de recuperação\",\n        copy_nsec: \"Copiar nsec\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"Usar um bunker NIP-46\",\n        delete_tooltip: \"Excluir conexão\",\n      },\n      use_nsec: {\n        title: \"Usar seu nsec\",\n        description: \"Este método é perigoso e não recomendado\",\n        delete_tooltip: \"Excluir nsec\",\n      },\n      signing_extension: {\n        title: \"Extensão de assinatura\",\n        description: \"Usar uma extensão de assinatura NIP-07\",\n        not_found: \"Nenhuma extensão de assinatura NIP-07 encontrada\",\n      },\n    },\n    payment_requests: {\n      title: \"Solicitações de pagamento\",\n      description:\n        \"As solicitações de pagamento permitem receber pagamentos via Nostr. Se ativado, sua carteira se inscreverá nos seus relays Nostr.\",\n      enable_toggle: \"Ativar Solicitações de Pagamento\",\n      claim_automatically: {\n        toggle: \"Reivindicar automaticamente\",\n        description: \"Receba pagamentos recebidos automaticamente.\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"Nostr Wallet Connect (NWC)\",\n      description:\n        \"Use NWC para controlar sua carteira a partir de qualquer outro aplicativo.\",\n      enable_toggle: \"Ativar NWC\",\n      payments_note:\n        \"Você só pode usar NWC para pagamentos do seu saldo em Bitcoin. Os pagamentos serão feitos a partir do seu mint ativo.\",\n      connection: {\n        copy_tooltip: \"Copiar string de conexão\",\n        qr_tooltip: \"Mostrar QR code\",\n        allowance_label: \"Limite restante (sat)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"Escolha a codificação para gravar em cartões NFC\",\n        text: {\n          title: \"Texto\",\n          description: \"Armazenar token em texto simples\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"Armazenar URL desta carteira com o token\",\n        },\n        binary: {\n          title: \"Binário\",\n          description: \"Armazenar tokens como dados binários\",\n        },\n        quick_access: {\n          toggle: \"Acesso rápido ao NFC\",\n          description:\n            \"Escaneie rapidamente cartões NFC no menu Receber Ecash. Esta opção adiciona um botão NFC ao menu Receber Ecash.\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"Gere um par de chaves para receber ecash bloqueado por P2PK. Aviso: Este recurso é experimental. Use apenas com pequenas quantias. Se perder suas chaves privadas, ninguém poderá desbloquear o ecash vinculado a elas.\",\n      generate_button: \"Gerar chave\",\n      import_button: \"Importar nsec\",\n      quick_access: {\n        toggle: \"Acesso rápido ao bloqueio\",\n        description:\n          \"Use isto para mostrar rapidamente sua chave de bloqueio P2PK no menu de recebimento de ecash.\",\n      },\n      keys_expansion: {\n        label: \"Clique para ver {count} chaves\",\n        used_badge: \"usada\",\n      },\n    },\n    privacy: {\n      title: \"Privacidade\",\n      description: \"Estas configurações afetam sua privacidade.\",\n      check_incoming: {\n        toggle: \"Verificar fatura recebida\",\n        description:\n          \"Se ativado, a carteira verificará a última fatura em segundo plano. Isso aumenta a responsividade da carteira, facilitando a identificação. Você pode verificar manualmente as faturas não pagas na aba Faturas.\",\n      },\n      check_startup: {\n        toggle: \"Verificar faturas pendentes na inicialização\",\n        description:\n          \"Se ativado, a carteira verificará faturas pendentes das últimas 24 horas na inicialização.\",\n      },\n      check_all: {\n        toggle: \"Verificar todas as faturas\",\n        description:\n          \"Se ativado, a carteira verificará periodicamente em segundo plano faturas não pagas por até duas semanas. Isso aumenta a atividade online da carteira, facilitando a identificação. Você pode verificar manualmente as faturas não pagas na aba Faturas.\",\n      },\n      check_sent: {\n        toggle: \"Verificar ecash enviado\",\n        description:\n          \"Se ativado, a carteira usará verificações periódicas em segundo plano para determinar se os tokens enviados foram resgatados. Isso aumenta a atividade online da carteira, facilitando a identificação.\",\n      },\n      websockets: {\n        toggle: \"Usar WebSockets\",\n        description:\n          \"Se ativado, a carteira usará conexões WebSocket de longa duração para receber atualizações sobre faturas pagas e tokens gastos dos mints. Isso aumenta a responsividade da carteira, mas também facilita a identificação.\",\n      },\n      bitcoin_price: {\n        toggle: \"Obter taxa de câmbio da Coinbase\",\n        description:\n          \"Se ativado, a taxa de câmbio atual do Bitcoin será obtida de coinbase.com e seu saldo convertido será exibido.\",\n        currency: {\n          title: \"Moeda Fiduciária\",\n          description:\n            \"Escolha a moeda fiduciária para exibição do preço do Bitcoin.\",\n        },\n      },\n    },\n    experimental: {\n      title: \"Experimental\",\n      description: \"Estes recursos são experimentais.\",\n      receive_swaps: {\n        toggle: \"Receber swaps\",\n        badge: \"Beta\",\n        description:\n          \"Opção para fazer swap do ecash recebido para o seu mint ativo no diálogo Receber Ecash.\",\n      },\n      auto_paste: {\n        toggle: \"Colar ecash automaticamente\",\n        description:\n          \"Colar automaticamente o ecash da sua área de transferência ao pressionar Receber, depois Ecash, depois Colar. A colagem automática pode causar falhas na interface no iOS; desative se tiver problemas.\",\n      },\n      auditor: {\n        toggle: \"Ativar auditor\",\n        badge: \"Beta\",\n        description:\n          \"Se ativado, a carteira exibirá informações do auditor no diálogo de detalhes do mint. O auditor é um serviço de terceiros que monitora a confiabilidade dos mints.\",\n        url_label: \"URL do auditor\",\n        api_url_label: \"URL da API do auditor\",\n      },\n      multinut: {\n        toggle: \"Ativar Multinut\",\n        description:\n          \"Se ativado, a carteira usará o Multinut para pagar faturas de múltiplos mints simultaneamente.\",\n      },\n      nostr_mint_backup: {\n        toggle: \"Fazer backup da lista de mints no Nostr\",\n        description:\n          \"Se ativado, sua lista de mints será automaticamente salva nos relays Nostr usando suas chaves Nostr configuradas. Isso permite restaurar sua lista de mints em outros dispositivos.\",\n        notifications: {\n          enabled: \"Backup de mints no Nostr ativado\",\n          disabled: \"Backup de mints no Nostr desativado\",\n          failed: \"Falha ao ativar backup de mints no Nostr\",\n        },\n      },\n    },\n    appearance: {\n      keyboard: {\n        title: \"Teclado na tela\",\n        description: \"Use o teclado numérico para inserir valores.\",\n        toggle: \"Usar teclado numérico\",\n        toggle_description:\n          \"Se ativado, o teclado numérico será usado para inserir valores.\",\n      },\n      theme: {\n        title: \"Aparência\",\n        description: \"Altere a aparência da sua carteira.\",\n        tooltips: {\n          mono: \"mono\",\n          cyber: \"cyber\",\n          freedom: \"freedom\",\n          nostr: \"nostr\",\n          bitcoin: \"bitcoin\",\n          mint: \"mint\",\n          nut: \"nut\",\n          blu: \"blu\",\n          flamingo: \"flamingo\",\n        },\n      },\n      bip177: {\n        title: \"Símbolo do Bitcoin\",\n        description: \"Usar o símbolo ₿ em vez de sats.\",\n        toggle: \"Usar símbolo ₿\",\n      },\n    },\n    web_of_trust: {\n      title: \"Rede de confiança\",\n      known_pubkeys: \"Chaves públicas conhecidas: {wotCount}\",\n      continue_crawl: \"Continuar varredura\",\n      crawl_odell: \"Varrer a TEIA DE CONFIANÇA DO ODELL\",\n      crawl_wot: \"Varrer teia de confiança\",\n      pause: \"Pausar\",\n      reset: \"Redefinir\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"Usar npubx.cash\",\n      copy_lightning_address: \"Copiar endereço Lightning\",\n      v2_mint: \"Mint npub.cash v2\",\n    },\n    multinut: {\n      use_multinut: \"Usar Multinut\",\n    },\n    advanced: {\n      title: \"Avançado\",\n      developer: {\n        title: \"Configurações de desenvolvedor\",\n        description:\n          \"As configurações a seguir são para desenvolvimento e depuração.\",\n        new_seed: {\n          button: \"Gerar nova frase de recuperação\",\n          description:\n            \"Isso gerará uma nova frase de recuperação. Você deve enviar todo o seu saldo para si mesmo para poder restaurá-lo com uma nova frase de recuperação.\",\n          confirm_question:\n            \"Tem certeza de que deseja gerar uma nova frase de recuperação?\",\n          cancel: \"Cancelar\",\n          confirm: \"Confirmar\",\n        },\n        remove_spent: {\n          button: \"Remover provas gastas\",\n          description:\n            \"Verifique se os tokens ecash dos seus mints ativos foram gastos e remova os gastos da sua carteira. Use isso somente se sua carteira estiver travada.\",\n        },\n        debug_console: {\n          button: \"Alternar Console de Depuração\",\n          description:\n            \"Abra o terminal de depuração JavaScript. Nunca cole nada neste terminal que você não entenda. Um ladrão pode tentar induzi-lo a colar código malicioso aqui.\",\n        },\n        export_proofs: {\n          button: \"Exportar provas ativas\",\n          description:\n            \"Copie seu saldo completo do mint ativo como um token Cashu para sua área de transferência. Isso exportará apenas os tokens do mint e unidade selecionados. Para uma exportação completa, selecione um mint e unidade diferentes e exporte novamente.\",\n        },\n        keyset_counters: {\n          title: \"Incrementar contadores de keyset\",\n          description:\n            'Clique no ID do keyset para incrementar os contadores de caminho de derivação dos keysets na sua carteira. Isso é útil se você ver o erro \"outputs have already been signed\".',\n          counter: \"contador: {count}\",\n        },\n        unset_reserved: {\n          button: \"Desmarcar todos os tokens reservados\",\n          description:\n            'Esta carteira marca o ecash sainte pendente como reservado (e o subtrai do seu saldo) para evitar tentativas de gasto duplo. Este botão desmarcará todos os tokens reservados para que possam ser usados novamente. Se fizer isso, sua carteira pode incluir provas gastas. Pressione o botão \"Remover provas gastas\" para eliminá-las.',\n        },\n        show_onboarding: {\n          button: \"Mostrar integração\",\n          description: \"Mostrar a tela de integração novamente.\",\n        },\n        reset_wallet: {\n          button: \"Redefinir dados da carteira\",\n          description:\n            \"Redefina os dados da sua carteira. Aviso: Isso apagará tudo! Certifique-se de criar um backup primeiro.\",\n          confirm_question:\n            \"Tem certeza de que deseja excluir os dados da sua carteira?\",\n          cancel: \"Cancelar\",\n          confirm: \"Excluir carteira\",\n        },\n        export_wallet: {\n          button: \"Exportar dados da carteira\",\n          description:\n            \"Baixe um dump da sua carteira. Você pode restaurar sua carteira a partir deste arquivo na tela de boas-vindas de uma nova carteira. Este arquivo ficará desatualizado se você continuar usando a carteira após a exportação.\",\n        },\n        import_wallet: {\n          button: \"Importar backup da carteira\",\n          description:\n            \"Restaure sua carteira a partir de um arquivo de backup exportado anteriormente. Isso substituirá os dados atuais da sua carteira pelo backup.\",\n          confirm_question:\n            \"Tem certeza de que deseja restaurar os dados da sua carteira?\",\n          cancel: \"Cancelar\",\n          confirm: \"IMPORTAR BACKUP DA CARTEIRA\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"Entre em um mint\",\n    subtitle:\n      \"Você ainda não entrou em nenhum mint Cashu. Adicione uma URL de mint nas configurações ou receba ecash de um novo mint para começar.\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"Receber Ecash\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"Histórico\",\n      },\n      invoices: {\n        label: \"Faturas\",\n      },\n      mints: {\n        label: \"Mints\",\n      },\n    },\n    install: {\n      text: \"Instalar\",\n      tooltip: \"Instalar Cashu\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"Ops.\",\n    text: \"Outra aba já está em execução. Feche esta aba e tente novamente.\",\n    actions: {\n      retry: {\n        label: \"Tentar novamente\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"Ops. Nada aqui…\",\n    actions: {\n      home: {\n        label: \"Voltar ao início\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Mint\",\n    },\n    mintBalance: {\n      label: \"Saldo\",\n    },\n    mintError: {\n      label: \"Erro no mint\",\n    },\n    pending: {\n      label: \"Pendente\",\n      tooltip: \"Verificar todos os tokens pendentes\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"Anterior\",\n      },\n      next: {\n        label: \"Próximo\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"Bem-vindo ao Cashu\",\n    text: \"Cashu.me é uma carteira Bitcoin gratuita e de código aberto que usa ecash para manter seus fundos seguros e privados.\",\n    actions: {\n      more: {\n        label: \"Clique para saber mais\",\n      },\n    },\n    p1: {\n      text: \"Cashu é um protocolo de ecash gratuito e de código aberto para Bitcoin. Saiba mais em { link }.\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"Esta carteira não é afiliada a nenhum mint. Para usá-la, você precisa se conectar a um ou mais mints Cashu em que confia.\",\n    },\n    p3: {\n      text: \"Esta carteira armazena ecash ao qual somente você tem acesso. Se você excluir os dados do navegador sem um backup da frase de recuperação, perderá seus tokens.\",\n    },\n    p4: {\n      text: \"Esta carteira está em beta. Não nos responsabilizamos por pessoas que perdem acesso a fundos. Use por sua conta e risco! Este código é de código aberto e licenciado sob a licença MIT.\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"Instalar PWA\",\n    alt: {\n      pwa_example: \"Exemplo de Instalação de PWA\",\n    },\n    installing: \"Instalando…\",\n    instruction: {\n      intro: {\n        text: \"Para a melhor experiência, use esta carteira com o navegador nativo do seu dispositivo para instalá-la como um Aplicativo Web Progressivo.\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Toque no menu (canto superior direito)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Pressione { buttonText }\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Toque em compartilhar (parte inferior)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Pressione { buttonText }\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"Após instalar o aplicativo no seu dispositivo, feche esta janela do navegador e use o aplicativo pela tela inicial.\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"Sucesso!\",\n        text: \"Você está usando o Cashu como PWA. Feche qualquer outra janela do navegador aberta e use o aplicativo pela tela inicial.\",\n        nextSteps:\n          \"Agora você pode fechar esta aba do navegador e abrir o aplicativo pela tela inicial.\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"Toque em { icon } e { buttonText }\",\n    buttonText: \"Adicionar à Tela de Início\",\n  },\n  AndroidPWAPrompt: {\n    text: \"Toque em { icon } e { buttonText }\",\n    buttonText: \"Adicionar à Tela de Início\",\n  },\n  WelcomeSlide3: {\n    title: \"Sua Frase de Recuperação\",\n    text: \"Guarde sua frase de recuperação em um gerenciador de senhas ou no papel. Sua frase de recuperação é a única forma de recuperar seus fundos caso perca o acesso a este dispositivo.\",\n    inputs: {\n      seed_phrase: {\n        label: \"Frase de Recuperação\",\n        caption: \"Você pode ver sua frase de recuperação nas configurações.\",\n      },\n      checkbox: {\n        label: \"Já a anotei\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"Termos\",\n    actions: {\n      more: {\n        label: \"Ler Termos de Serviço\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"Li e aceito estes termos e condições\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"Configure sua carteira\",\n    text: \"Você deseja recuperar a partir de uma frase de recuperação ou criar uma nova carteira?\",\n    options: {\n      new: {\n        title: \"Criar nova carteira\",\n        subtitle: \"Gerar uma nova frase de recuperação e adicionar mints.\",\n      },\n      recover: {\n        title: \"Recuperar carteira\",\n        subtitle: \"Digite sua frase de recuperação, restaure mints e ecash.\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"Adicionar mints\",\n    text: \"Mints são servidores que ajudam você a enviar e receber ecash. Escolha um mint descoberto ou adicione um manualmente. Pule para adicionar mints depois.\",\n    sections: {\n      your_mints: \"Seus mints\",\n    },\n    restoring: \"Restaurando mints…\",\n    placeholder: {\n      mint_url: \"https://\",\n    },\n  },\n  WelcomeRecoverSeed: {\n    title: \"Digite sua frase de recuperação\",\n    text: \"Cole ou digite sua frase de recuperação de 12 palavras para recuperar.\",\n    inputs: {\n      word: \"Palavra { index }\",\n    },\n    actions: {\n      paste_all: \"Colar tudo\",\n    },\n    disclaimer:\n      \"Sua frase de recuperação é usada apenas localmente para derivar as chaves da sua carteira.\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"Restaurar seu ecash\",\n    text: \"Busque provas não gastas nos seus mints configurados e adicione-as à sua carteira.\",\n  },\n  MintRatings: {\n    title: \"Avaliações do Mint\",\n    reviews: \"avaliações\",\n    ratings: \"Avaliações\",\n    no_reviews: \"Nenhuma avaliação encontrada\",\n    your_review: \"Sua avaliação\",\n    no_reviews_to_display: \"Nenhuma avaliação para exibir.\",\n    no_rating: \"Sem avaliação\",\n    out_of: \"de\",\n    rows: \"Avaliações\",\n    sort: \"Ordenar\",\n    sort_options: {\n      newest: \"Mais recente\",\n      oldest: \"Mais antiga\",\n      highest: \"Maior\",\n      lowest: \"Menor\",\n    },\n    actions: {\n      write_review: \"Escrever uma avaliação\",\n    },\n    empty_state_subtitle:\n      \"Ajude deixando uma avaliação. Compartilhe sua experiência com este mint e ajude outros deixando uma avaliação.\",\n  },\n  CreateMintReview: {\n    title: \"Avaliar Mint\",\n    publishing_as: \"Publicando como\",\n    inputs: {\n      rating: { label: \"Avaliação\" },\n      review: { label: \"Comentário (opcional)\" },\n    },\n    actions: {\n      publish: { label: \"Enviar Avaliação\", in_progress: \"Enviando…\" },\n    },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"Restaurar a partir da Frase de Recuperação\",\n      caption:\n        \"Digite sua frase de recuperação para restaurar sua carteira. Antes de restaurar, certifique-se de ter adicionado todos os mints que usou anteriormente.\",\n      inputs: {\n        seed_phrase: {\n          label: \"Frase de recuperação\",\n          caption: \"Você pode ver sua frase de recuperação nas configurações.\",\n        },\n      },\n    },\n    information: {\n      label: \"Informação\",\n      caption:\n        \"O assistente só restaurará ecash de outra frase de recuperação; você não poderá usar esta frase de recuperação nem alterar a frase de recuperação da carteira que está usando atualmente. Isso significa que o ecash restaurado não estará protegido pela sua frase de recuperação atual enquanto você não o enviar para si mesmo uma vez.\",\n    },\n    restore_mints: {\n      label: \"Restaurar Mints\",\n      caption:\n        'Selecione o mint para restaurar. Você pode adicionar mais mints na tela principal em \"Mints\" e restaurá-los aqui.',\n    },\n    actions: {\n      paste: {\n        error: \"Falha ao ler o conteúdo da área de transferência.\",\n      },\n      validate: {\n        error: \"O mnemônico não é uma frase de recuperação BIP39 válida.\",\n      },\n      select_all: {\n        label: \"Selecionar Todos\",\n      },\n      deselect_all: {\n        label: \"Desselecionar Todos\",\n      },\n      restore: {\n        label: \"Restaurar\",\n        in_progress: \"Restaurando mint …\",\n        error: \"Erro ao restaurar mint: { error }\",\n      },\n      restore_all_mints: {\n        label: \"Restaurar Todos os Mints\",\n        in_progress: \"Restaurando mint { index } de { length } …\",\n        success: \"Restauração concluída com sucesso\",\n        error: \"Erro ao restaurar mints: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"Restaurar Mints Selecionados ({count})\",\n        in_progress: \"Restaurando mint { index } de { length } …\",\n        success: \"Restaurado(s) com sucesso {count} mint(s)\",\n        error: \"Erro ao restaurar mints selecionados: { error }\",\n      },\n    },\n    nostr_mints: {\n      label: \"Restaurar Mints do Nostr\",\n      caption:\n        \"Busque backups de mints armazenados nos relays Nostr usando sua frase de recuperação. Isso ajudará você a buscar os mints que usou anteriormente.\",\n      search_button: \"Buscar Backups de Mints\",\n      select_all: \"Selecionar Todos\",\n      deselect_all: \"Desselecionar Todos\",\n      backed_up: \"Com backup\",\n      already_added: \"Já adicionado\",\n      add_selected: \"Adicionar Selecionados ({count})\",\n      no_backups_found: \"Nenhum backup de mint encontrado\",\n      no_backups_hint:\n        \"Certifique-se de que o backup de mints no Nostr está ativado nas configurações para fazer backup automático da sua lista de mints.\",\n      invalid_mnemonic:\n        \"Por favor, insira uma frase de recuperação válida antes de buscar.\",\n      search_error: \"Falha ao buscar backups de mints.\",\n      add_error: \"Falha ao adicionar os mints selecionados.\",\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"Adicionar mint\",\n      description:\n        \"Digite a URL de um mint Cashu para se conectar. Esta carteira não é afiliada a nenhum mint.\",\n      inputs: {\n        nickname: {\n          placeholder: \"Apelido (ex.: Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"URL inválida\",\n        },\n        scan: {\n          label: \"Escanear QR Code\",\n        },\n      },\n    },\n    discover: {\n      title: \"Buscar mints\",\n      overline: \"Buscar\",\n      caption: \"Descubra mints que outros usuários recomendaram no Nostr.\",\n      actions: {\n        discover: {\n          label: \"Buscar mints\",\n          in_progress: \"Carregando…\",\n          error_no_mints: \"Nenhum mint encontrado\",\n          success: \"Encontrado(s) { length } mint(s)\",\n        },\n      },\n      recommendations: {\n        overline: \"Encontrado(s) { length } mint(s)\",\n        caption:\n          \"Esses mints foram recomendados por outros usuários do Nostr. Tome cuidado e faça sua própria pesquisa antes de usar um mint.\",\n        actions: {\n          browse: {\n            label: \"Clique para ver os mints\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"Swap\",\n      overline: \"Swap Multi-mint\",\n      actions: {\n        receove_to_trusted_mint: {\n          label: \"Receber para mint confiável\",\n        },\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n      caption:\n        \"Faça swap de fundos entre mints via Lightning. Nota: Deixe margem para possíveis taxas Lightning. Se o pagamento recebido não for bem-sucedido, verifique a fatura manualmente.\",\n      inputs: {\n        from: {\n          label: \"De\",\n        },\n        to: {\n          label: \"Para\",\n        },\n        amount: {\n          label: \"Valor ({ ticker })\",\n        },\n      },\n    },\n    error_badge: \"Erro\",\n    reviews_text: \"avaliações\",\n    no_reviews_yet: \"Ainda sem avaliações\",\n    discover_mints_button: \"Buscar mints\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - Continue escaneando\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"Receber Lightning\",\n    create_invoice_title: \"Criar Fatura\",\n    inputs: {\n      amount: {\n        label: \"Valor ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"Criar Fatura\",\n        label_blocked: \"Criando fatura…\",\n        in_progress: \"Criando\",\n      },\n    },\n    invoice: {\n      caption: \"Fatura Lightning\",\n      status_paid_text: \"Pago!\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"Enviar\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Nenhum mint disponível\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"Nenhum mint disponível\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"Enviar Ecash\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"Offline\",\n    inputs: {\n      amount: {\n        label: \"Valor ({ ticker }) *\",\n        invalid_too_much_error_text: \"Valor muito alto\",\n      },\n      p2pk_pubkey: {\n        label: \"Chave pública do destinatário\",\n        label_invalid: \"Chave pública do destinatário\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"Copiar Emoji\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"Copiar link\",\n      },\n      share: {\n        tooltip_text: \"Compartilhar ecash\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      pay: {\n        label: \"@:global.actions.pay.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"Excluir do histórico\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"Gravar no cartão NFC\",\n          ndef_unsupported_text: \"NDEF não suportado\",\n        },\n      },\n    },\n    errors: {\n      amount_required: \"Insira um valor primeiro.\",\n      serialization_failed: \"Não foi possível preparar o token ecash.\",\n    },\n  },\n  SendPaymentRequest: {\n    actions: {\n      pay: {\n        label: \"Pagar\",\n      },\n      pay_via: {\n        label: \"Pagar via {transport}\",\n      },\n    },\n    info: {\n      pay_to: \"Pagar para {target}\",\n      invalid_url: \"URL inválida\",\n    },\n  },\n  PaymentRequestInfo: {\n    title_with_transport: \"Solicitação de pagamento via {transport}\",\n    title: \"Solicitação de pagamento\",\n    subtitle: \"Pagar para {target}\",\n    subtitle_fallback: \"Solicitação de pagamento\",\n    invalid_url: \"URL inválida\",\n  },\n  ReceiveDialog: {\n    title: \"Receber\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Nenhum mint disponível\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints:\n          \"Você precisa se conectar a um mint para receber via Lightning\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"Receber Ecash\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"Solicitar\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"Escaneando…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"Receber Ecash\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"Colar token Cashu\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"Token inválido\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"Não foi possível receber. O bloqueio P2PK deste token não corresponde à sua chave pública.\",\n      },\n    },\n    unknown_mint_info_text:\n      \"Mint desconhecido. Será adicionado após você receber este token.\",\n    swap_section: {\n      title: \"Swap\",\n      source_label: \"De\",\n      destination_label: \"Para\",\n      fee_info: \"Este swap incorrerá em taxas da rede Lightning.\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"Adicionando mint…\",\n      },\n      swap: {\n        label: \"Receber para mint confiável\",\n        tooltip_text: \"Swap para um mint confiável\",\n        caption: \"Swap { value }\",\n        processing: \"Processando swap...\",\n        failed: \"Swap falhou\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"Cancelar swap\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      receive_to_selected_mint: {\n        label: \"Receber para o mint selecionado\",\n      },\n      later: {\n        label: \"Receber depois\",\n        tooltip_text: \"Adicionar ao histórico para receber depois\",\n        already_in_history_success_text: \"Ecash já está no Histórico\",\n        added_to_history_success_text: \"Ecash adicionado ao Histórico\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"Ler do cartão NFC\",\n          ndef_unsupported_text: \"NDEF não suportado\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"Chave P2PK\",\n      description: \"Receber ecash bloqueado a esta chave\",\n      used_warning_text:\n        \"Aviso: Esta chave já foi usada. Use uma nova chave para melhor privacidade.\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"Gerar nova chave\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"Solicitação de Pagamento\",\n      description: \"Receba pagamentos via Nostr\",\n    },\n    received_total: \"Total recebido\",\n    no_payments_yet: \"Nenhum pagamento ainda\",\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"Nova solicitação\",\n      },\n      add_amount: {\n        label: \"Adicionar valor\",\n      },\n      use_active_mint: {\n        label: \"Qualquer mint\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"Digite o valor\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"Teclado desativado. Você pode reativar o teclado nas configurações.\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostr Wallet Connect\",\n      description:\n        \"Controle sua carteira remotamente com NWC. Pressione o QR code para vincular sua carteira a um aplicativo compatível.\",\n      warning_text:\n        \"Aviso: qualquer pessoa com acesso a esta string de conexão pode iniciar pagamentos da sua carteira. Não compartilhe!\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"Mensagem do Mint\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"Contato\",\n    },\n    details: {\n      title: \"Detalhes do mint\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"Ver todos\",\n          },\n          hide: {\n            label: \"Ocultar\",\n          },\n        },\n      },\n      currency: {\n        label: \"Moeda\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"Versão\",\n      },\n    },\n    actions: {\n      title: \"Ações\",\n      copy_mint_url: {\n        label: \"Copiar URL do mint\",\n      },\n      delete: {\n        label: \"Excluir mint\",\n      },\n      edit: {\n        label: \"Editar mint\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"Selecione um mint\",\n    placeholder: \"Selecione um mint\",\n    available_text: \"disponível\",\n    sheet_title: \"Selecionar Mint\",\n    badge_mint_error_text: \"Erro\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"Nenhum histórico ainda\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"há { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Verificar status\",\n      },\n      receive: {\n        tooltip_text: \"Receber\",\n      },\n      filter_pending: {\n        label: \"Filtrar pendentes\",\n      },\n      show_all: {\n        label: \"Mostrar todos\",\n      },\n    },\n    old_token_not_found_error_text: \"Token antigo não encontrado\",\n  },\n  InvoiceTable: {\n    empty_text: \"Nenhuma fatura ainda\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"Clique para copiar\",\n      date_label: \"há { value }\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Verificar status\",\n      },\n      filter_pending: {\n        label: \"Filtrar pendentes\",\n      },\n      show_all: {\n        label: \"Mostrar todos\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"Tem certeza de que deseja excluir este mint?\",\n    nickname: {\n      label: \"Apelido\",\n    },\n    balances: {\n      label: \"Saldos\",\n    },\n    warning_text:\n      \"Nota: Como esta carteira é precavida, seu ecash deste mint não será realmente excluído, mas permanecerá armazenado no seu dispositivo. Você o verá reaparecer se adicionar novamente este mint.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"Remover mint\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"Token Cashu ou endereço Lightning\",\n      receive: \"Token Cashu\",\n      pay: \"Endereço Lightning ou fatura\",\n    },\n    qr_scanner: {\n      title: \"Escanear QR Code\",\n      description: \"Toque para escanear um endereço\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"Pagar Lightning\",\n      inputs: {\n        invoice_data: {\n          label: \"Fatura ou endereço Lightning\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"{ payee } está solicitando { value } { ticker }\",\n      amount_range_label:\n        \"{ payee } está solicitando{br}entre { min } e { max } { ticker }\",\n      sending_to_lightning_address: \"Enviando para { address }\",\n      inputs: {\n        amount: {\n          label: \"Valor ({ ticker }) *\",\n        },\n        comment: {\n          label: \"Comentário (opcional)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"Pagar { value }\",\n      paying: \"Pagando\",\n      paid: \"Pago\",\n      fee: \"Taxa\",\n      memo: {\n        label: \"Memo\",\n      },\n      processing_info_text: \"Processando…\",\n      balance_too_low_warning_text: \"Saldo insuficiente\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"Pagar\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"Erro\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"Editar mint\",\n    inputs: {\n      nickname: {\n        label: \"Apelido\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"Você confia neste mint?\",\n    description:\n      \"Antes de usar este mint, certifique-se de que confia nele. Os mints podem se tornar maliciosos ou encerrar as operações a qualquer momento.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"Adicionando mint\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"Por favor, insira um mnemônico\",\n    restore_mint_error_text: \"Erro ao restaurar mint: { error }\",\n    prepare_info_text: \"Preparando processo de restauração …\",\n    restored_proofs_for_keyset_info_text:\n      \"Restaurado(s) { restoreCounter } prova(s) para o keyset { keysetId }\",\n    checking_proofs_for_keyset_info_text:\n      \"Verificando provas { startIndex } a { endIndex } para o keyset { keysetId }\",\n    no_proofs_info_text: \"Nenhuma prova encontrada para restaurar\",\n    restored_amount_success_text: \"Restaurado { amount }\",\n  },\n  swap: {\n    in_progress_warning_text: \"Swap em andamento\",\n    invalid_swap_data_error_text: \"Dados de swap inválidos\",\n    swap_error_text: \"Erro no swap\",\n  },\n  TokenInformation: {\n    fee: \"Taxa\",\n    unit: \"Unidade\",\n    fiat: \"Moeda fiduciária\",\n    p2pk: \"P2PK\",\n    locked: \"Bloqueado\",\n    locked_to_you: \"Bloqueado para você\",\n    mint: \"Mint\",\n    memo: \"Memo\",\n    payment_request: \"Solicitação de pagamento\",\n    nostr: \"Nostr\",\n    token_copied: \"Token copiado para a área de transferência\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/sv-SE/index.ts",
    "content": "export default {\n  MultinutPicker: {\n    payment: \"Multinut-betalning\",\n    selectMints: \"Välj en eller flera mints att betala från.\",\n    totalSelectedBalance: \"Totalt valt saldo\",\n    multiMintPay: \"Multi-Mint-betalning\",\n    balanceNotEnough: \"Multi-mint-saldo räcker inte för denna faktura\",\n    failed: \"Misslyckades att behandla: {error}\",\n    paid: \"Betalat {amount} via Lightning\",\n  },\n\n  global: {\n    copy_to_clipboard: {\n      success: \"Kopierat till urklipp!\",\n    },\n    actions: {\n      add_mint: {\n        label: \"Lägg till mint\",\n      },\n      cancel: {\n        label: \"Avbryt\",\n      },\n      copy: {\n        label: \"Kopiera\",\n      },\n      close: {\n        label: \"Stäng\",\n      },\n      enter: {\n        label: \"Ange\",\n      },\n      lock: {\n        label: \"Lås\",\n      },\n      paste: {\n        label: \"Klistra in\",\n      },\n      receive: {\n        label: \"Ta emot\",\n      },\n      scan: {\n        label: \"Skanna\",\n      },\n      send: {\n        label: \"Skicka\",\n      },\n      swap: {\n        label: \"Byt\",\n      },\n      update: {\n        label: \"Uppdatera\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"Mint URL\",\n      },\n    },\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"Saldot är för lågt\",\n      received: \"Mottaget {amount}\",\n      fee: \" (avgift: {fee})\",\n      could_not_request_mint: \"Kunde inte begära prägling\",\n      invoice_still_pending: \"Fakturan väntar fortfarande\",\n      paid_lightning: \"Betalat {amount} via Lightning\",\n      payment_pending_refresh: \"Betalning väntar. Uppdatera fakturan manuellt.\",\n      sent: \"Skickat {amount}\",\n      token_still_pending: \"Token väntar fortfarande\",\n      received_lightning: \"Mottaget {amount} via Lightning\",\n      lightning_payment_failed: \"Lightning-betalning misslyckades\",\n      failed_to_decode_invoice: \"Kunde inte avkoda fakturan\",\n      invalid_lnurl: \"Ogiltig LNURL\",\n      lnurl_error: \"LNURL-fel\",\n      no_amount: \"Inget belopp\",\n      no_lnurl_data: \"Ingen LNURL-data\",\n      no_price_data: \"Ingen prisdata.\",\n      please_try_again: \"Försök igen.\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"Mint redan tillagd\",\n        added: \"Mint tillagd\",\n        not_found: \"Mint hittades inte\",\n        activation_failed: \"Aktivering av mint misslyckades\",\n        no_active_mint: \"Ingen aktiv mint\",\n        unit_activation_failed: \"Aktivering av enhet misslyckades\",\n        unit_not_supported: \"Enheten stöds inte av mint\",\n        activated: \"Mint aktiverad\",\n        could_not_connect: \"Kunde inte ansluta till mint\",\n        could_not_get_info: \"Kunde inte hämta mint-information\",\n        could_not_get_keys: \"Kunde inte hämta mint-nycklar\",\n        could_not_get_keysets: \"Kunde inte hämta mint-nyckeluppsättningar\",\n        mint_validation_error: \"Mint-valideringsfel\",\n        removed: \"Mint borttagen\",\n        error: \"Mint-fel\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"Inställningar\",\n        settings: {\n          title: \"Inställningar\",\n          caption: \"Plånboksinställningar\",\n        },\n      },\n      terms: {\n        title: \"Villkor\",\n        terms: {\n          title: \"Villkor\",\n          caption: \"Användarvillkor\",\n        },\n      },\n      links: {\n        title: \"Länkar\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"Donera\",\n          caption: \"Stöd Cashu\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"Offline\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"Laddar om om { countdown }\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"Staging – använd inte med riktiga pengar!\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"Plånbok\",\n      },\n    },\n  },\n  Settings: {\n    web_of_trust: {\n      title: \"Förtroendenätverk\",\n      known_pubkeys: \"Kända pubkeys: {wotCount}\",\n      continue_crawl: \"Fortsätt genomsökning\",\n      crawl_odell: \"Genomsök ODELL'S WEB OF TRUST\",\n      crawl_wot: \"Genomsök web of trust\",\n      pause: \"Pausa\",\n      reset: \"Återställ\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"Använd npubx.cash\",\n      copy_lightning_address: \"Kopiera Lightning-adress\",\n      v2_mint: \"npub.cash v2 mint\",\n    },\n    multinut: {\n      use_multinut: \"Använd Multinut\",\n    },\n    language: {\n      title: \"Språk\",\n      description: \"Välj önskat språk från listan nedan.\",\n    },\n    sections: {\n      backup_restore: \"SÄKERHETSKOPIERING & ÅTERSTÄLLNING\",\n      lightning_address: \"LIGHTNING ADRESS\",\n      nostr_keys: \"NOSTR NYCKLAR\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"Klicka för att redigera reläer\",\n          add: {\n            title: \"Lägg till relä\",\n            description:\n              \"Din plånbok använder dessa reläer för nostr-operationer som betalningsförfrågningar, nostr wallet connect och säkerhetskopior.\",\n          },\n          list: {\n            title: \"Reläer\",\n            description: \"Din plånbok kommer att ansluta till dessa reläer.\",\n            copy_tooltip: \"Kopiera relä\",\n            remove_tooltip: \"Ta bort relä\",\n          },\n        },\n      },\n      payment_requests: \"BETALNINGSFÖRFRÅGNINGAR\",\n      nostr_wallet_connect: \"NOSTR PLÅNBOKSANSLUTNING\",\n      hardware_features: \"MASKINVARA FUNKTIONER\",\n      p2pk_features: \"P2PK FUNKTIONER\",\n      privacy: \"INTEGRITET\",\n      experimental: \"EXPERIMENTELLA\",\n      appearance: \"UTSEENDE\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"Säkerhetskopiera återställningsfras\",\n        description:\n          \"Din återställningsfras kan återställa din plånbok. Håll den säker och privat.\",\n        seed_phrase_label: \"Återställningsfras\",\n      },\n      restore_ecash: {\n        title: \"Återställ ecash\",\n        description:\n          \"Återställningsguiden låter dig återställa förlorad ecash från en mnemonisk återställningsfras. Din nuvarande plånboks återställningsfras kommer inte att påverkas, guiden tillåter dig endast att återställa ecash från en annan återställningsfras.\",\n        button: \"Återställ\",\n      },\n    },\n    lightning_address: {\n      title: \"Lightning-adress\",\n      description: \"Ta emot betalningar till din Lightning-adress.\",\n      enable: {\n        toggle: \"Aktivera\",\n        description: \"Lightning-adress med npub.cash\",\n      },\n      address: {\n        copy_tooltip: \"Kopiera Lightning-adress\",\n      },\n      automatic_claim: {\n        toggle: \"Hämta automatiskt\",\n        description: \"Ta emot inkommande betalningar automatiskt.\",\n      },\n      npc_v2: {\n        choose_mint_title: \"Välj mint för npub.cash v2\",\n        choose_mint_placeholder: \"Välj en mint...\",\n      },\n    },\n    nostr_keys: {\n      title: \"Dina nostr-nycklar\",\n      description: \"Ställ in nostr-nycklarna för din Lightning-adress.\",\n      wallet_seed: {\n        title: \"Plånbokens återställningsfras\",\n        description:\n          \"Generera nostr nyckelpar från plånbokens återställningsfras\",\n        copy_nsec: \"Kopiera nsec\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"Använd en NIP-46 bunker\",\n        delete_tooltip: \"Radera anslutning\",\n      },\n      use_nsec: {\n        title: \"Använd din nsec\",\n        description: \"Denna metod är farlig och rekommenderas inte\",\n        delete_tooltip: \"Radera nsec\",\n      },\n      signing_extension: {\n        title: \"Signeringsutökning\",\n        description: \"Använd en NIP-07 signeringsutökning\",\n        not_found: \"Ingen NIP-07 signeringsutökning hittades\",\n      },\n    },\n    payment_requests: {\n      title: \"Betalningsförfrågningar\",\n      description:\n        \"Betalningsförfrågningar gör det möjligt att ta emot betalningar via nostr. Om du aktiverar detta prenumererar din plånbok på dina nostr-reläer.\",\n      enable_toggle: \"Aktivera betalningsförfrågningar\",\n      claim_automatically: {\n        toggle: \"Hämta automatiskt\",\n        description: \"Ta emot inkommande betalningar automatiskt.\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"Nostr Plånboksanslutning (NWC)\",\n      description:\n        \"Använd NWC för att styra din plånbok från valfri annan applikation.\",\n      enable_toggle: \"Aktivera NWC\",\n      payments_note:\n        \"Du kan endast använda NWC för betalningar från ditt Bitcoin-saldo. Betalningar kommer att göras från din aktiva mint.\",\n      connection: {\n        copy_tooltip: \"Kopiera anslutningssträng\",\n        qr_tooltip: \"Visa QR-kod\",\n        allowance_label: \"Tillåtelse kvar (sat)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"Välj kodning för att skriva till NFC-kort\",\n        text: {\n          title: \"Text\",\n          description: \"Spara token i klartext\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"Spara URL till denna plånbok med token\",\n        },\n        binary: {\n          title: \"Binär\",\n          description: \"Lagra tokens som binärdata\",\n        },\n        quick_access: {\n          toggle: \"Snabb åtkomst till NFC\",\n          description:\n            \"Skanna snabbt NFC-kort i Ta emot Ecash-menyn. Detta alternativ lägger till en NFC-knapp i Ta emot Ecash-menyn.\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"Generera ett nyckelpar för att ta emot P2PK-låst ecash. Varning: Denna funktion är experimentell. Använd endast med små belopp. Om du förlorar dina privata nycklar kommer ingen att kunna låsa upp ecash som är låst till den längre.\",\n      generate_button: \"Generera nyckel\",\n      import_button: \"Importera nsec\",\n      quick_access: {\n        toggle: \"Snabb åtkomst till lås\",\n        description:\n          \"Använd detta för att snabbt visa din P2PK låsningsnyckel i menyn för att ta emot ecash.\",\n      },\n      keys_expansion: {\n        label: \"Klicka för att bläddra bland {count} nycklar\",\n        used_badge: \"använd\",\n      },\n    },\n    privacy: {\n      title: \"Integritet\",\n      description: \"Dessa inställningar påverkar din integritet.\",\n      check_incoming: {\n        toggle: \"Kontrollera inkommande faktura\",\n        description:\n          \"Om aktiverat kommer plånboken att kontrollera den senaste fakturan i bakgrunden. Detta ökar plånbokens responsivitet vilket gör fingeravtryckning enklare. Du kan manuellt kontrollera obetalda fakturor under fliken Fakturor.\",\n      },\n      check_startup: {\n        toggle: \"Kontrollera väntande fakturor vid start\",\n        description:\n          \"Om aktiverat kommer plånboken att kontrollera väntande fakturor från de senaste 24 timmarna vid start.\",\n      },\n      check_all: {\n        toggle: \"Kontrollera alla fakturor\",\n        description:\n          \"Om aktiverat kommer plånboken periodvis att kontrollera obetalda fakturor i bakgrunden i upp till två veckor. Detta ökar plånbokens online-aktivitet vilket gör fingeravtryckning enklare. Du kan manuellt kontrollera obetalda fakturor under fliken Fakturor.\",\n      },\n      check_sent: {\n        toggle: \"Kontrollera skickad ecash\",\n        description:\n          \"Om aktiverat kommer plånboken att använda periodiska bakgrundskontroller för att avgöra om skickade tokens har lösts in. Detta ökar plånbokens online-aktivitet vilket gör fingeravtryckning enklare.\",\n      },\n      websockets: {\n        toggle: \"Använd WebSockets\",\n        description:\n          \"Om aktiverat kommer plånboken att använda långlivade WebSocket-anslutningar för att ta emot uppdateringar om betalda fakturor och spenderade tokens från mints. Detta ökar plånbokens responsivitet men gör också fingeravtryckning enklare.\",\n      },\n      bitcoin_price: {\n        toggle: \"Hämta växelkurs från Coinbase\",\n        description:\n          \"Om aktiverat kommer aktuell Bitcoin-växelkurs att hämtas från coinbase.com och ditt konverterade saldo kommer att visas.\",\n        currency: {\n          title: \"Fiat-valuta\",\n          description: \"Välj fiat-valuta för Bitcoin-prisvisning.\",\n        },\n      },\n    },\n    experimental: {\n      title: \"Experimentella\",\n      description: \"Dessa funktioner är experimentella.\",\n      receive_swaps: {\n        toggle: \"Ta emot byten\",\n        badge: \"Beta\",\n        description:\n          \"Möjlighet att byta mottagen Ecash till din aktiva mint i dialogrutan Ta emot Ecash.\",\n      },\n      auto_paste: {\n        toggle: \"Klistra in Ecash automatiskt\",\n        description:\n          \"Klistra in ecash från ditt urklipp automatiskt när du trycker på Ta emot, sedan Ecash, sedan Klistra in. Automatisk inklistring kan orsaka UI-problem i iOS, stäng av det om du upplever problem.\",\n      },\n      auditor: {\n        toggle: \"Aktivera revisor\",\n        badge: \"Beta\",\n        description:\n          \"Om aktiverat kommer plånboken att visa revisorsinformation i dialogrutan för mintdetaljer. Revisorn är en tredjepartstjänst som övervakar mints pålitlighet.\",\n        url_label: \"Revisor URL\",\n        api_url_label: \"Revisor API URL\",\n      },\n      multinut: {\n        toggle: \"Aktivera Multinut\",\n        description:\n          \"Om aktiverat kommer plånboken att använda Multinut för att betala fakturor från flera mints samtidigt.\",\n      },\n      nostr_mint_backup: {\n        toggle: \"Säkerhetskopiera mintlista på Nostr\",\n        description:\n          \"Om aktiverat kommer din mintlista automatiskt att säkerhetskopieras till Nostr-reläer med dina konfigurerade Nostr-nycklar. Detta gör att du kan återställa din mintlista över enheter.\",\n        notifications: {\n          enabled: \"Nostr mint-säkerhetskopiering aktiverad\",\n          disabled: \"Nostr mint-säkerhetskopiering inaktiverad\",\n          failed: \"Misslyckades att aktivera Nostr mint-säkerhetskopiering\",\n        },\n      },\n    },\n    appearance: {\n      keyboard: {\n        title: \"Skärmtangentbord\",\n        description: \"Använd det numeriska tangentbordet för att ange belopp.\",\n        toggle: \"Använd numeriskt tangentbord\",\n        toggle_description:\n          \"Om aktiverat kommer det numeriska tangentbordet att användas för att ange belopp.\",\n      },\n      theme: {\n        title: \"Utseende\",\n        description: \"Ändra hur din plånbok ser ut.\",\n        tooltips: {\n          mono: \"mono\",\n          cyber: \"cyber\",\n          freedom: \"frihet\",\n          nostr: \"nostr\",\n          bitcoin: \"bitcoin\",\n          mint: \"mint\",\n          nut: \"nöt\",\n          blu: \"blå\",\n          flamingo: \"flamingo\",\n        },\n      },\n      bip177: {\n        title: \"Bitcoin-symbol\",\n        description: \"Använd ₿-symbolen istället för sats.\",\n        toggle: \"Använd ₿-symbolen\",\n      },\n    },\n    advanced: {\n      title: \"Avancerade\",\n      developer: {\n        title: \"Utvecklarinställningar\",\n        description: \"Följande inställningar är för utveckling och felsökning.\",\n        new_seed: {\n          button: \"Generera ny återställningsfras\",\n          description:\n            \"Detta kommer att generera en ny återställningsfras. Du måste skicka hela ditt saldo till dig själv för att kunna återställa det med en ny återställningsfras.\",\n          confirm_question:\n            \"Är du säker på att du vill generera en ny återställningsfras?\",\n          cancel: \"Avbryt\",\n          confirm: \"Bekräfta\",\n        },\n        remove_spent: {\n          button: \"Ta bort spenderade proofs\",\n          description:\n            \"Kontrollera om ecash-tokens från dina aktiva mints är spenderade och ta bort de spenderade från din plånbok. Använd detta endast om din plånbok har fastnat.\",\n        },\n        debug_console: {\n          button: \"Visa/dölj debugkonsol\",\n          description:\n            \"Öppna Javascript debugterminalen. Klistra aldrig in något i den här terminalen som du inte förstår. En tjuv kan försöka lura dig att klistra in skadlig kod här.\",\n        },\n        export_proofs: {\n          button: \"Exportera aktiva proofs\",\n          description:\n            \"Kopiera hela ditt saldo från den aktiva minten som en Cashu-token till ditt urklipp. Detta exporterar endast tokens från den valda minten och enheten. För en fullständig export, välj en annan mint och enhet och exportera igen.\",\n        },\n        keyset_counters: {\n          title: \"Öka keyset-räknare\",\n          description:\n            'Klicka på keyset-ID för att öka härledningsvägsräknarna för keysets i din plånbok. Detta är användbart om du ser felet \"outputs have already been signed\".',\n          counter: \"räknare: {count}\",\n        },\n        unset_reserved: {\n          button: \"Avboka alla reserverade tokens\",\n          description:\n            'Denna plånbok markerar väntande utgående ecash som reserverad (och drar av den från ditt saldo) för att förhindra försök till dubbelspending. Den här knappen kommer att avboka alla reserverade tokens så att de kan användas igen. Om du gör detta kan din plånbok inkludera spenderade proofs. Tryck på knappen \"Ta bort spenderade proofs\" för att bli av med dem.',\n        },\n        show_onboarding: {\n          button: \"Visa introduktion\",\n          description: \"Visa introduktionsskärmen igen.\",\n        },\n        reset_wallet: {\n          button: \"Återställ plånboksdata\",\n          description:\n            \"Återställ dina plånboksdata. Varning: Detta kommer att radera allt! Se till att du skapar en säkerhetskopia först.\",\n          confirm_question:\n            \"Är du säker på att du vill radera din plånboksdata?\",\n          cancel: \"Avbryt\",\n          confirm: \"Radera plånbok\",\n        },\n        export_wallet: {\n          button: \"Exportera plånboksdata\",\n          description:\n            \"Ladda ner en dump av din plånbok. Du kan återställa din plånbok från den här filen i välkomstskärmen på en ny plånbok. Den här filen kommer att vara osynkroniserad om du fortsätter att använda din plånbok efter att ha exporterat den.\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"Gå med i en mint\",\n    subtitle:\n      \"Du har inte gått med i någon Cashu mint ännu. Lägg till en mint URL i inställningarna eller ta emot ecash från en ny mint för att komma igång.\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"Ta emot Ecash\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"Historik\",\n      },\n      invoices: {\n        label: \"Fakturor\",\n      },\n      mints: {\n        label: \"Mints\",\n      },\n    },\n    install: {\n      text: \"Installera\",\n      tooltip: \"Installera Cashu\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"Nej.\",\n    text: \"En annan flik körs redan. Stäng den här fliken och försök igen.\",\n    actions: {\n      retry: {\n        label: \"Försök igen\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"Oops. Ingenting här…\",\n    actions: {\n      home: {\n        label: \"Gå tillbaka hem\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Mint\",\n    },\n    mintBalance: {\n      label: \"Saldo\",\n    },\n    mintError: {\n      label: \"Mint fel\",\n    },\n    pending: {\n      label: \"Väntande\",\n      tooltip: \"Kontrollera alla väntande tokens\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"Föregående\",\n      },\n      next: {\n        label: \"Nästa\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"Välkommen till Cashu\",\n    text: \"Cashu.me är en gratis och öppen källkods Bitcoin-plånbok som använder ecash för att hålla dina pengar säkra och privata.\",\n    actions: {\n      more: {\n        label: \"Klicka för att lära dig mer\",\n      },\n    },\n    p1: {\n      text: \"Cashu är ett gratis och öppet källkods ecash-protokoll för Bitcoin. Du kan lära dig mer om det på { link }.\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"Denna plånbok är inte ansluten till någon mint. För att använda denna plånbok måste du ansluta till en eller flera Cashu mints som du litar på.\",\n    },\n    p3: {\n      text: \"Denna plånbok lagrar ecash som endast du har åtkomst till. Om du raderar dina webbläsardata utan en säkerhetskopia av återställningsfrasen kommer du att förlora dina tokens.\",\n    },\n    p4: {\n      text: \"Denna plånbok är i beta. Vi tar inget ansvar för att personer förlorar åtkomst till medel. Använd på egen risk! Denna kod är öppen källkod och licensierad under MIT-licensen.\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"Installera PWA\",\n    alt: { pwa_example: \"Exempel på PWA-installation\" },\n    installing: \"Installerar…\",\n    instruction: {\n      intro: {\n        text: \"För bästa upplevelsen, använd denna plånbok med din enhets webbläsare för att installera den som en Progressive Web App. Gör detta nu.\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Tryck på menyn (uppe till höger)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Tryck på { buttonText }\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Tryck på dela (nere)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"Tryck på { buttonText }\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"När du har installerat denna app på din enhet, stäng detta webbläsarfönster och använd appen från din startskärm.\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"Klart!\",\n        text: \"Du använder Cashu som en PWA. Stäng alla andra öppna webbläsarfönster och använd appen från din startskärm.\",\n        nextSteps:\n          \"Du kan nu stänga denna flik och öppna appen från hemskärmen.\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"Tryck på { icon } och { buttonText }\",\n    buttonText: \"Lägg till på hemskärmen\",\n  },\n  AndroidPWAPrompt: {\n    text: \"Tryck på { icon } och { buttonText }\",\n    buttonText: \"Lägg till på hemskärmen\",\n  },\n  WelcomeSlide3: {\n    title: \"Din återställningsfras\",\n    text: \"Spara din återställningsfras i en lösenordshanterare eller på papper. Din återställningsfras är det enda sättet att återställa dina pengar om du förlorar åtkomst till denna enhet.\",\n    inputs: {\n      seed_phrase: {\n        label: \"Återställningsfras\",\n        caption: \"Du kan se din återställningsfras i inställningarna.\",\n      },\n      checkbox: {\n        label: \"Jag har skrivit ner den\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"Villkor\",\n    actions: {\n      more: {\n        label: \"Läs användarvillkoren\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"Jag har läst och accepterar dessa villkor\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"Ställ in din plånbok\",\n    text: \"Vill du återställa från en återställningsfras eller skapa en ny plånbok?\",\n    options: {\n      new: {\n        title: \"Skapa ny plånbok\",\n        subtitle: \"Generera en ny fras och lägg till mints.\",\n      },\n      recover: {\n        title: \"Återställ plånbok\",\n        subtitle: \"Ange din återställningsfras, återställ mints och ecash.\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"Lägg till mints\",\n    text: \"Mints är servrar som hjälper dig skicka och ta emot ecash. Välj en upptäckt mint eller lägg till en manuellt. Du kan hoppa över och lägga till senare.\",\n    sections: { your_mints: \"Dina mints\" },\n    restoring: \"Återställer mints…\",\n    placeholder: { mint_url: \"https://\" },\n  },\n  WelcomeRecoverSeed: {\n    title: \"Ange din återställningsfras\",\n    text: \"Klistra in eller skriv din 12 ord långa fras för att återställa.\",\n    inputs: { word: \"Ord { index }\" },\n    actions: { paste_all: \"Klistra in alla\" },\n    disclaimer:\n      \"Din fras används endast lokalt för att härleda dina plånboksnycklar.\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"Återställ ditt ecash\",\n    text: \"Sök efter ospenderade proofs på dina konfigurerade mints och lägg till dem i plånboken.\",\n  },\n  MintRatings: {\n    title: \"Mint-recensioner\",\n    reviews: \"recensioner\",\n    ratings: \"Betyg\",\n    no_reviews: \"Inga recensioner hittades\",\n    your_review: \"Din recension\",\n    no_reviews_to_display: \"Inga recensioner att visa.\",\n    no_rating: \"Ingen betygsättning\",\n    out_of: \"av\",\n    rows: \"Reviews\",\n    sort: \"Sortera\",\n    sort_options: {\n      newest: \"Nyaste\",\n      oldest: \"Äldsta\",\n      highest: \"Högsta\",\n      lowest: \"Lägsta\",\n    },\n    actions: { write_review: \"Skriv en recension\" },\n    empty_state_subtitle:\n      \"Hjälp genom att lämna en recension. Dela din upplevelse med denna mint och hjälp andra genom att lämna en recension.\",\n  },\n  CreateMintReview: {\n    title: \"Recensera mint\",\n    publishing_as: \"Publicerar som\",\n    inputs: {\n      rating: { label: \"Betyg\" },\n      review: { label: \"Recension (valfritt)\" },\n    },\n    actions: {\n      publish: { label: \"Publicera\", in_progress: \"Publicerar…\" },\n    },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"Återställ från återställningsfras\",\n      caption:\n        \"Ange din återställningsfras för att återställa din plånbok. Innan du återställer, se till att du har lagt till alla mints som du har använt tidigare.\",\n      inputs: {\n        seed_phrase: {\n          label: \"Återställningsfras\",\n          caption: \"Du kan se din återställningsfras i inställningarna.\",\n        },\n      },\n    },\n    information: {\n      label: \"Information\",\n      caption:\n        \"Guiden kommer endast att återställa ecash från en annan återställningsfras, du kommer inte att kunna använda denna återställningsfras eller ändra återställningsfrasen för plånboken du för närvarande använder. Detta innebär att återställd ecash inte kommer att skyddas av din nuvarande återställningsfras så länge du inte skickar ecash till dig själv en gång.\",\n    },\n    restore_mints: {\n      label: \"Återställ Mints\",\n      caption:\n        'Välj mint att återställa. Du kan lägga till fler mints på huvudskärmen under \"Mints\" och återställa dem här.',\n    },\n    actions: {\n      paste: {\n        error: \"Kunde inte läsa urklippsinnehåll.\",\n      },\n      validate: {\n        error: \"Mnemoniska frasen bör vara minst 12 ord.\",\n      },\n      select_all: {\n        label: \"Välj alla\",\n      },\n      deselect_all: {\n        label: \"Avmarkera alla\",\n      },\n      restore: {\n        label: \"Återställ\",\n        in_progress: \"Återställer mint…\",\n        error: \"Fel vid återställning av mint: { error }\",\n      },\n      restore_all_mints: {\n        label: \"Återställ Alla Mints\",\n        in_progress: \"Återställer mint { index } av { length } …\",\n        success: \"Återställning slutfördes framgångsrikt\",\n        error: \"Fel vid återställning av mints: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"Återställ valda mints ({count})\",\n        in_progress: \"Återställer mint {index} av {length} ...\",\n        success: \"Lyckades återställa {count} mint(s)\",\n        error: \"Fel vid återställning av valda mints: {error}\",\n      },\n    },\n    nostr_mints: {\n      label: \"Återställ Mints från Nostr\",\n      caption:\n        \"Sök efter mint-säkerhetskopior lagrade på Nostr-reläer med din återställningsfras. Detta hjälper dig att upptäcka mints du tidigare använt.\",\n      search_button: \"Sök efter Mint-säkerhetskopior\",\n      select_all: \"Välj alla\",\n      deselect_all: \"Avmarkera alla\",\n      backed_up: \"Säkerhetskopierad\",\n      already_added: \"Redan tillagd\",\n      add_selected: \"Lägg till valda ({count})\",\n      no_backups_found: \"Inga mint-säkerhetskopior hittades\",\n      no_backups_hint:\n        \"Se till att Nostr mint-säkerhetskopiering är aktiverat i inställningarna för att automatiskt säkerhetskopiera din mintlista.\",\n      invalid_mnemonic: \"Ange en giltig återställningsfras innan du söker.\",\n      search_error: \"Misslyckades att söka efter mint-säkerhetskopior.\",\n      add_error: \"Misslyckades att lägga till valda mints.\",\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"Lägg till mint\",\n      description:\n        \"Ange URL:en för en Cashu mint för att ansluta till den. Denna plånbok är inte ansluten till någon mint.\",\n      inputs: {\n        nickname: {\n          placeholder: \"Smeknamn (t.ex. Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"Ogiltig URL\",\n        },\n        scan: {\n          label: \"Skanna QR-kod\",\n        },\n      },\n    },\n    discover: {\n      title: \"Upptäck mints\",\n      overline: \"Upptäck\",\n      caption: \"Upptäck mints som andra användare har rekommenderat på nostr.\",\n      actions: {\n        discover: {\n          label: \"Upptäck mints\",\n          in_progress: \"Laddar…\",\n          error_no_mints: \"Inga mints hittades\",\n          success: \"{ length } mints hittades\",\n        },\n      },\n      recommendations: {\n        overline: \"{ length } mints hittades\",\n        caption:\n          \"Dessa mints rekommenderades av andra Nostr-användare. Var försiktig och gör din egen research innan du använder en mint.\",\n        actions: {\n          browse: {\n            label: \"Klicka för att bläddra bland mints\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"Byt\",\n      overline: \"Multimint-byten\",\n      caption:\n        \"Byt medel mellan mints via Lightning. Obs: Lämna utrymme för eventuella Lightning-avgifter. Om den inkommande betalningen inte lyckas, kontrollera fakturan manuellt.\",\n      inputs: {\n        from: {\n          label: \"Från\",\n        },\n        to: {\n          label: \"Till\",\n        },\n        amount: {\n          label: \"Belopp ({ ticker })\",\n        },\n      },\n      actions: {\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n    },\n    error_badge: \"Fel\",\n    reviews_text: \"recensioner\",\n    no_reviews_yet: \"Inga recensioner ännu\",\n    discover_mints_button: \"Upptäck mints\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - Fortsätt skanna\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"Ta emot Lightning\",\n    create_invoice_title: \"Skapa faktura\",\n    inputs: {\n      amount: {\n        label: \"Belopp ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"Skapa faktura\",\n        label_blocked: \"Skapar faktura…\",\n        in_progress: \"Skapar\",\n      },\n    },\n    invoice: {\n      caption: \"Lightning-faktura\",\n      status_paid_text: \"Betald!\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"Skicka\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Inga mints tillgängliga\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"Inga mints tillgängliga\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"Skicka Ecash\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"Offline\",\n    inputs: {\n      amount: {\n        label: \"Belopp ({ ticker }) *\",\n        invalid_too_much_error_text: \"För mycket\",\n      },\n      p2pk_pubkey: {\n        label: \"Mottagarens publika nyckel\",\n        label_invalid: \"Mottagarens publika nyckel\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"Kopiera Emoji\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"Kopiera länk\",\n      },\n      share: {\n        tooltip_text: \"Dela ecash\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"Ta bort från historik\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"Flash till NFC-kort\",\n          ndef_unsupported_text: \"NDEF stöds inte\",\n        },\n      },\n    },\n  },\n  ReceiveDialog: {\n    title: \"Ta emot\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Inga mints tillgängliga\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints:\n          \"Du måste ansluta till en mint för att ta emot via Lightning\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"Ta emot Ecash\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"Begär\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"Skannar…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"Ta emot Ecash\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"Klistra in Cashu token\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"Ogiltig token\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"Kan inte ta emot. Denna tokens P2PK-lås matchar inte din publika nyckel.\",\n      },\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"Lägger till mint…\",\n      },\n      swap: {\n        label: \"@:global.actions.swap.label\",\n        tooltip_text: \"Byt till en betrodd mint\",\n        caption: \"Byt { value }\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"Avbryt byte\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      later: {\n        label: \"Ta emot senare\",\n        tooltip_text: \"Lägg till i historik för att ta emot senare\",\n        already_in_history_success_text: \"Ecash redan i historik\",\n        added_to_history_success_text: \"Ecash lades till i historik\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"Läs från NFC-kort\",\n          ndef_unsupported_text: \"NDEF stöds inte\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"P2PK Nyckel\",\n      description: \"Ta emot ecash låst till denna nyckel\",\n      used_warning_text:\n        \"Varning: Denna nyckel användes tidigare. Använd en ny nyckel för bättre integritet.\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"Generera ny nyckel\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"Betalningsförfrågan\",\n      description: \"Ta emot betalningar via Nostr\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"Ny förfrågan\",\n      },\n      add_amount: {\n        label: \"Lägg till belopp\",\n      },\n      use_active_mint: {\n        label: \"Valfri mint\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"Ange belopp\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"Tangentbordet inaktiverat. Du kan återaktivera tangentbordet i inställningarna.\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostr Plånboksanslutning\",\n      description:\n        \"Styr din plånbok på distans med NWC. Tryck på QR-koden för att länka din plånbok med en kompatibel app.\",\n      warning_text:\n        \"Varning: den som har åtkomst till denna anslutningssträng kan initiera betalningar från din plånbok. Dela inte!\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"Mintmeddelande\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"Kontakt\",\n    },\n    details: {\n      title: \"Mintdetaljer\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"Visa alla\",\n          },\n          hide: {\n            label: \"Dölj\",\n          },\n        },\n      },\n      currency: {\n        label: \"Valuta\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"Version\",\n      },\n    },\n    actions: {\n      title: \"Åtgärder\",\n      copy_mint_url: {\n        label: \"Kopiera mint URL\",\n      },\n      delete: {\n        label: \"Radera mint\",\n      },\n      edit: {\n        label: \"Redigera mint\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"Välj en mint\",\n    badge_mint_error_text: \"Fel\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"Ingen historik än\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"{ value } sedan\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Kontrollera status\",\n      },\n      receive: {\n        tooltip_text: \"Ta emot\",\n      },\n      filter_pending: {\n        label: \"Filtrera väntande\",\n      },\n      show_all: {\n        label: \"Visa alla\",\n      },\n    },\n    old_token_not_found_error_text: \"Gammal token hittades inte\",\n  },\n  InvoiceTable: {\n    empty_text: \"Inga fakturor än\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"Klicka för att kopiera\",\n      date_label: \"{ value } sedan\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Kontrollera status\",\n      },\n      filter_pending: {\n        label: \"Filtrera väntande\",\n      },\n      show_all: {\n        label: \"Visa alla\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"Är du säker på att du vill radera denna mint?\",\n    nickname: {\n      label: \"Smeknamn\",\n    },\n    balances: {\n      label: \"Saldon\",\n    },\n    warning_text:\n      \"Obs: Eftersom denna plånbok är paranoid, kommer din ecash från denna mint inte att raderas helt utan förbli lagrad på din enhet. Du kommer att se den återkomma om du lägger till denna mint igen senare.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"Ta bort mint\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"Cashu token eller Lightning-adress\",\n      receive: \"Cashu token\",\n      pay: \"Lightning-adress eller faktura\",\n    },\n    qr_scanner: {\n      title: \"Skanna QR-kod\",\n      description: \"Tryck för att skanna en adress\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"Betala Lightning\",\n      inputs: {\n        invoice_data: {\n          label: \"Lightning-faktura eller adress\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"{ payee } begär { value } { ticker }\",\n      amount_range_label:\n        \"{ payee } begär{br}mellan { min } och { max } { ticker }\",\n      sending_to_lightning_address: \"Skickar till { address }\",\n      inputs: {\n        amount: {\n          label: \"Belopp ({ ticker }) *\",\n        },\n        comment: {\n          label: \"Kommentar (valfritt)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"Betala { value }\",\n      paying: \"Betalar\",\n      paid: \"Betald\",\n      fee: \"Avgift\",\n      memo: {\n        label: \"Memo\",\n      },\n      processing_info_text: \"Bearbetar…\",\n      balance_too_low_warning_text: \"Saldot för lågt\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"Betala\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"Fel\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"Redigera mint\",\n    inputs: {\n      nickname: {\n        label: \"Smeknamn\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"Litar du på denna mint?\",\n    description:\n      \"Innan du använder denna mint, se till att du litar på den. Mints kan bli skadliga eller upphöra med sin verksamhet när som helst.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"Lägger till mint\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"Ange en mnemonisk fras\",\n    restore_mint_error_text: \"Fel vid återställning av mint: { error }\",\n    prepare_info_text: \"Förbereder återställningsprocessen…\",\n    restored_proofs_for_keyset_info_text:\n      \"Återställde { restoreCounter } proofs för keyset { keysetId }\",\n    checking_proofs_for_keyset_info_text:\n      \"Kontrollerar proofs { startIndex } till { endIndex } för keyset { keysetId }\",\n    no_proofs_info_text: \"Inga proofs hittades att återställa\",\n    restored_amount_success_text: \"Återställde { amount }\",\n  },\n  swap: {\n    in_progress_warning_text: \"Byte pågår\",\n    invalid_swap_data_error_text: \"Ogiltig bytesdata\",\n    swap_error_text: \"Fel vid byte\",\n  },\n  TokenInformation: {\n    fee: \"Avgift\",\n    unit: \"Enhet\",\n    fiat: \"Fiat\",\n    p2pk: \"P2PK\",\n    locked: \"Låst\",\n    locked_to_you: \"Låst till dig\",\n    mint: \"Myntverk\",\n    memo: \"Memo\",\n    payment_request: \"Betalningsförfrågan\",\n    nostr: \"Nostr\",\n    token_copied: \"Token kopierad till urklipp\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/th-TH/index.ts",
    "content": "export default {\n  MultinutPicker: {\n    payment: \"การชำระเงิน Multinut\",\n    selectMints: \"เลือกหนึ่งหรือหลาย mint เพื่อทำการชำระเงิน\",\n    totalSelectedBalance: \"ยอดคงเหลือที่เลือกทั้งหมด\",\n    multiMintPay: \"จ่ายแบบหลาย Mint\",\n    balanceNotEnough: \"ยอดหลาย mint ไม่เพียงพอสำหรับใบแจ้งหนี้นี้\",\n    failed: \"ไม่สามารถประมวลผล: {error}\",\n    paid: \"จ่าย {amount} ผ่าน Lightning\",\n  },\n\n  global: {\n    copy_to_clipboard: {\n      success: \"คัดลอกไปยังคลิปบอร์ดแล้ว!\",\n    },\n    actions: {\n      add_mint: {\n        label: \"เพิ่ม Mint\",\n      },\n      cancel: {\n        label: \"ยกเลิก\",\n      },\n      copy: {\n        label: \"คัดลอก\",\n      },\n      close: {\n        label: \"ปิด\",\n      },\n      enter: {\n        label: \"ป้อน\",\n      },\n      lock: {\n        label: \"ล็อก\",\n      },\n      paste: {\n        label: \"วาง\",\n      },\n      receive: {\n        label: \"รับ\",\n      },\n      scan: {\n        label: \"สแกน\",\n      },\n      send: {\n        label: \"ส่ง\",\n      },\n      swap: {\n        label: \"แลกเปลี่ยน\",\n      },\n      update: {\n        label: \"อัปเดต\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"Mint URL\",\n      },\n    },\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"ยอดคงเหลือน้อยเกินไป\",\n      received: \"ได้รับ {amount}\",\n      fee: \" (ค่าธรรมเนียม: {fee})\",\n      could_not_request_mint: \"ไม่สามารถขอ mint ได้\",\n      invoice_still_pending: \"ใบแจ้งหนี้ยังอยู่ระหว่างดำเนินการ\",\n      paid_lightning: \"จ่าย {amount} ผ่าน Lightning\",\n      payment_pending_refresh:\n        \"การชำระเงินอยู่ระหว่างดำเนินการ รีเฟรชใบแจ้งหนี้ด้วยตนเอง\",\n      sent: \"ส่ง {amount}\",\n      token_still_pending: \"โทเค็นยังอยู่ระหว่างดำเนินการ\",\n      received_lightning: \"ได้รับ {amount} ผ่าน Lightning\",\n      lightning_payment_failed: \"การชำระเงิน Lightning ล้มเหลว\",\n      failed_to_decode_invoice: \"ไม่สามารถถอดรหัสใบแจ้งหนี้\",\n      invalid_lnurl: \"LNURL ไม่ถูกต้อง\",\n      lnurl_error: \"ข้อผิดพลาด LNURL\",\n      no_amount: \"ไม่มียอดเงิน\",\n      no_lnurl_data: \"ไม่มีข้อมูล LNURL\",\n      no_price_data: \"ไม่มีข้อมูลราคา\",\n      please_try_again: \"โปรดลองอีกครั้ง\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"เพิ่ม Mint แล้ว\",\n        added: \"เพิ่ม Mint แล้ว\",\n        not_found: \"ไม่พบ Mint\",\n        activation_failed: \"การเปิดใช้งาน Mint ล้มเหลว\",\n        no_active_mint: \"ไม่มี Mint ที่ใช้งานอยู่\",\n        unit_activation_failed: \"การเปิดใช้งานหน่วยล้มเหลว\",\n        unit_not_supported: \"หน่วยไม่รองรับโดย Mint\",\n        activated: \"เปิดใช้งาน Mint แล้ว\",\n        could_not_connect: \"ไม่สามารถเชื่อมต่อกับ Mint ได้\",\n        could_not_get_info: \"ไม่สามารถดึงข้อมูล Mint ได้\",\n        could_not_get_keys: \"ไม่สามารถดึงคีย์ Mint ได้\",\n        could_not_get_keysets: \"ไม่สามารถดึงชุดคีย์ Mint ได้\",\n        mint_validation_error: \"ข้อผิดพลาดในการตรวจสอบ mint\",\n        removed: \"ลบ Mint แล้ว\",\n        error: \"ข้อผิดพลาด Mint\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"การตั้งค่า\",\n        settings: {\n          title: \"การตั้งค่า\",\n          caption: \"การกำหนดค่า Wallet\",\n        },\n      },\n      terms: {\n        title: \"เงื่อนไข\",\n        terms: {\n          title: \"เงื่อนไข\",\n          caption: \"ข้อกำหนดในการให้บริการ\",\n        },\n      },\n      links: {\n        title: \"ลิงก์\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"บริจาค\",\n          caption: \"สนับสนุน Cashu\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"ออฟไลน์\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"โหลดใหม่ใน { countdown }\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"กำลังทดสอบ – ห้ามใช้กับเงินจริง!\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"Wallet\",\n      },\n    },\n  },\n  Settings: {\n    web_of_trust: {\n      title: \"เครือข่ายที่เชื่อถือได้\",\n      known_pubkeys: \"Pubkey ที่รู้จัก: {wotCount}\",\n      continue_crawl: \"ดำเนินการสำรวจต่อ\",\n      crawl_odell: \"สำรวจ ODELL'S WEB OF TRUST\",\n      crawl_wot: \"สำรวจ web of trust\",\n      pause: \"หยุดชั่วคราว\",\n      reset: \"รีเซ็ต\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"ใช้ npubx.cash\",\n      copy_lightning_address: \"คัดลอกที่อยู่ Lightning\",\n      v2_mint: \"npub.cash v2 mint\",\n    },\n    multinut: {\n      use_multinut: \"ใช้ Multinut\",\n    },\n    language: {\n      title: \"ภาษา\",\n      description: \"โปรดเลือกภาษาที่คุณต้องการจากรายการด้านล่าง\",\n    },\n    sections: {\n      backup_restore: \"สำรองข้อมูล & กู้คืน\",\n      lightning_address: \"ที่อยู่ LIGHTNING\",\n      nostr_keys: \"คีย์ NOSTR\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"คลิกเพื่อแก้ไขรีเลย์\",\n          add: {\n            title: \"เพิ่มรีเลย์\",\n            description:\n              \"กระเป๋าเงินของคุณใช้รีเลย์เหล่านี้สำหรับการดำเนินงาน nostr เช่น คำขอชำระเงิน nostr wallet connect และการสำรองข้อมูล\",\n          },\n          list: {\n            title: \"รีเลย์\",\n            description: \"กระเป๋าเงินของคุณจะเชื่อมต่อกับรีเลย์เหล่านี้\",\n            copy_tooltip: \"คัดลอกรีเลย์\",\n            remove_tooltip: \"ลบรีเลย์\",\n          },\n        },\n      },\n      payment_requests: \"คำขอชำระเงิน\",\n      nostr_wallet_connect: \"NOSTR WALLET CONNECT\",\n      hardware_features: \"คุณสมบัติฮาร์ดแวร์\",\n      p2pk_features: \"คุณสมบัติ P2PK\",\n      privacy: \"ความเป็นส่วนตัว\",\n      experimental: \"ทดลอง\",\n      appearance: \"รูปลักษณ์\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"สำรองวลีกู้คืน\",\n        description:\n          \"วลีกู้คืนของคุณสามารถกู้คืน Wallet ของคุณได้ เก็บไว้ให้ปลอดภัยและเป็นส่วนตัว\",\n        seed_phrase_label: \"วลีกู้คืน\",\n      },\n      restore_ecash: {\n        title: \"กู้คืน ecash\",\n        description:\n          \"วิซาร์ดการกู้คืนช่วยให้คุณกู้คืน ecash ที่สูญหายจากวลีกู้คืนแบบ Mnemonic ได้ วลีกู้คืนของ Wallet ปัจจุบันของคุณจะไม่ได้รับผลกระทบ วิซาร์ดจะอนุญาตให้คุณ <i>กู้คืน</i> ecash จากวลีกู้คืนอื่นเท่านั้น\",\n        button: \"กู้คืน\",\n      },\n    },\n    lightning_address: {\n      title: \"ที่อยู่ Lightning\",\n      description: \"รับการชำระเงินไปยังที่อยู่ Lightning ของคุณ\",\n      enable: {\n        toggle: \"เปิดใช้งาน\",\n        description: \"ที่อยู่ Lightning กับ npub.cash\",\n      },\n      address: {\n        copy_tooltip: \"คัดลอกที่อยู่ Lightning\",\n      },\n      automatic_claim: {\n        toggle: \"รับอัตโนมัติ\",\n        description: \"รับการชำระเงินขาเข้าโดยอัตโนมัติ\",\n      },\n      npc_v2: {\n        choose_mint_title: \"เลือก mint สำหรับ npub.cash v2\",\n        choose_mint_placeholder: \"เลือก mint...\",\n      },\n    },\n    nostr_keys: {\n      title: \"คีย์ Nostr ของคุณ\",\n      description: \"ตั้งค่าคีย์ nostr สำหรับที่อยู่ Lightning ของคุณ\",\n      wallet_seed: {\n        title: \"วลีสำหรับกู้คืน Wallet\",\n        description: \"สร้างคู่คีย์ nostr จากวลีสำหรับกู้คืน Wallet\",\n        copy_nsec: \"คัดลอก nsec\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"ใช้ NIP-46 bunker\",\n        delete_tooltip: \"ลบการเชื่อมต่อ\",\n      },\n      use_nsec: {\n        title: \"ใช้ nsec ของคุณ\",\n        description: \"วิธีนี้อันตรายและไม่แนะนำ\",\n        delete_tooltip: \"ลบ nsec\",\n      },\n      signing_extension: {\n        title: \"ส่วนขยายการลงนาม\",\n        description: \"ใช้ส่วนขยายการลงนาม NIP-07\",\n        not_found: \"ไม่พบส่วนขยายการลงนาม NIP-07\",\n      },\n    },\n    payment_requests: {\n      title: \"คำขอชำระเงิน\",\n      description:\n        \"คำขอชำระเงินช่วยให้คุณรับการชำระเงินผ่าน nostr ได้ หากเปิดใช้งาน Wallet ของคุณจะสมัครสมาชิก Nostr relays ของคุณ\",\n      enable_toggle: \"เปิดใช้งานคำขอชำระเงิน\",\n      claim_automatically: {\n        toggle: \"รับอัตโนมัติ\",\n        description: \"รับการชำระเงินขาเข้าโดยอัตโนมัติ\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"Nostr Wallet Connect (NWC)\",\n      description: \"ใช้ NWC เพื่อควบคุม Wallet ของคุณจากแอปพลิเคชันอื่นใด\",\n      enable_toggle: \"เปิดใช้งาน NWC\",\n      payments_note:\n        \"คุณสามารถใช้ NWC สำหรับการชำระเงินจากยอดคงเหลือ Bitcoin ของคุณเท่านั้น การชำระเงินจะทำจาก Mint ที่เปิดใช้งานของคุณ\",\n      connection: {\n        copy_tooltip: \"คัดลอกสตริงการเชื่อมต่อ\",\n        qr_tooltip: \"แสดงรหัส QR\",\n        allowance_label: \"ยอดคงเหลือที่เหลือ (sat)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"เลือกการเข้ารหัสสำหรับการเขียนลงในการ์ด NFC\",\n        text: {\n          title: \"ข้อความ\",\n          description: \"เก็บ token ในรูปแบบข้อความธรรมดา\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"เก็บ URL ไปยัง Wallet นี้พร้อม token\",\n        },\n        binary: {\n          title: \"ไบนารี\",\n          description: \"จัดเก็บโทเค็นเป็นข้อมูลไบนารี\",\n        },\n        quick_access: {\n          toggle: \"เข้าถึง NFC ด่วน\",\n          description:\n            \"สแกนการ์ด NFC ได้อย่างรวดเร็วในเมนู รับ Ecash ตัวเลือกนี้จะเพิ่มปุ่ม NFC ในเมนู รับ Ecash\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"สร้างคู่คีย์เพื่อรับ ecash ที่ล็อกด้วย P2PK คำเตือน: คุณสมบัตินี้เป็นการทดลอง ใช้เฉพาะกับจำนวนเล็กน้อยเท่านั้น หากคุณทำคีย์ส่วนตัวของคุณหาย จะไม่มีใครสามารถปลดล็อก ecash ที่ล็อกด้วยคีย์นั้นได้อีกต่อไป\",\n      generate_button: \"สร้างคีย์\",\n      import_button: \"นำเข้า nsec\",\n      quick_access: {\n        toggle: \"เข้าถึงล็อกด่วน\",\n        description:\n          \"ใช้สิ่งนี้เพื่อแสดงคีย์ล็อก P2PK ของคุณอย่างรวดเร็วในเมนูรับ ecash\",\n      },\n      keys_expansion: {\n        label: \"คลิกเพื่อเรียกดู {count} คีย์\",\n        used_badge: \"ใช้แล้ว\",\n      },\n    },\n    privacy: {\n      title: \"ความเป็นส่วนตัว\",\n      description: \"การตั้งค่าเหล่านี้ส่งผลต่อความเป็นส่วนตัวของคุณ\",\n      check_incoming: {\n        toggle: \"ตรวจสอบใบแจ้งหนี้ขาเข้า\",\n        description:\n          \"หากเปิดใช้งาน Wallet จะตรวจสอบใบแจ้งหนี้ล่าสุดในเบื้องหลัง ซึ่งช่วยเพิ่มความสามารถในการตอบสนองของ Wallet ทำให้การสร้างรอยนิ้วมือทำได้ง่ายขึ้น คุณสามารถตรวจสอบใบแจ้งหนี้ที่ยังไม่ได้ชำระด้วยตนเองได้ในแท็บใบแจ้งหนี้\",\n      },\n      check_startup: {\n        toggle: \"ตรวจสอบใบแจ้งหนี้ที่รอดำเนินการเมื่อเริ่มต้น\",\n        description:\n          \"หากเปิดใช้งาน Wallet จะตรวจสอบใบแจ้งหนี้ที่รอดำเนินการในช่วง 24 ชั่วโมงที่ผ่านมาเมื่อเริ่มต้น\",\n      },\n      check_all: {\n        toggle: \"ตรวจสอบใบแจ้งหนี้ทั้งหมด\",\n        description:\n          \"หากเปิดใช้งาน Wallet จะตรวจสอบใบแจ้งหนี้ที่ยังไม่ได้ชำระเป็นระยะๆ ในเบื้องหลังเป็นเวลาสูงสุดสองสัปดาห์ ซึ่งช่วยเพิ่มกิจกรรมออนไลน์ของ Wallet ทำให้การสร้างรอยนิ้วมือทำได้ง่ายขึ้น คุณสามารถตรวจสอบใบแจ้งหนี้ที่ยังไม่ได้ชำระด้วยตนเองได้ในแท็บใบแจ้งหนี้\",\n      },\n      check_sent: {\n        toggle: \"ตรวจสอบ ecash ที่ส่ง\",\n        description:\n          \"หากเปิดใช้งาน Wallet จะใช้การตรวจสอบเบื้องหลังเป็นระยะๆ เพื่อพิจารณาว่าโทเค็นที่ส่งถูกแลกแล้วหรือไม่ ซึ่งเพิ่มกิจกรรมออนไลน์ของ Wallet ทำให้การสร้างรอยนิ้วมือทำได้ง่ายขึ้น\",\n      },\n      websockets: {\n        toggle: \"ใช้ WebSockets\",\n        description:\n          \"หากเปิดใช้งาน Wallet จะใช้การเชื่อมต่อ WebSocket ที่มีอายุยืนยาวเพื่อรับการอัปเดตเกี่ยวกับใบแจ้งหนี้ที่ชำระแล้วและโทเค็นที่ใช้จ่ายจาก Mints ซึ่งเพิ่มความสามารถในการตอบสนองของ Wallet แต่ก็ทำให้การสร้างรอยนิ้วมือทำได้ง่ายขึ้นเช่นกัน\",\n      },\n      bitcoin_price: {\n        toggle: \"รับอัตราแลกเปลี่ยนจาก Coinbase\",\n        description:\n          \"หากเปิดใช้งาน จะดึงอัตราแลกเปลี่ยน Bitcoin ปัจจุบันจาก coinbase.com และแสดงยอดคงเหลือที่แปลงแล้วของคุณ\",\n        currency: {\n          title: \"สกุลเงินเฟียต\",\n          description: \"เลือกสกุลเงินเฟียตสำหรับการแสดงราคา Bitcoin\",\n        },\n      },\n    },\n    experimental: {\n      title: \"ทดลอง\",\n      description: \"คุณสมบัติเหล่านี้เป็นคุณสมบัติทดลอง\",\n      receive_swaps: {\n        toggle: \"รับ swaps\",\n        badge: \"เบต้า\",\n        description:\n          \"ตัวเลือกในการแลกเปลี่ยน Ecash ที่ได้รับไปยัง Mint ที่เปิดใช้งานของคุณในกล่องโต้ตอบ รับ Ecash\",\n      },\n      auto_paste: {\n        toggle: \"วาง Ecash โดยอัตโนมัติ\",\n        description:\n          \"วาง ecash ในคลิปบอร์ดของคุณโดยอัตโนมัติเมื่อคุณกด รับ, จากนั้น Ecash, จากนั้น วาง การวางอัตโนมัติอาจทำให้เกิดความผิดปกติของ UI ใน iOS ให้ปิดหากคุณประสบปัญหา\",\n      },\n      auditor: {\n        toggle: \"เปิดใช้งานผู้ตรวจสอบ\",\n        badge: \"เบต้า\",\n        description:\n          \"หากเปิดใช้งาน Wallet จะแสดงข้อมูลผู้ตรวจสอบในกล่องโต้ตอบรายละเอียด Mint ผู้ตรวจสอบคือบริการบุคคลที่สามที่ตรวจสอบความน่าเชื่อถือของ Mints\",\n        url_label: \"URL ผู้ตรวจสอบ\",\n        api_url_label: \"URL API ผู้ตรวจสอบ\",\n      },\n      multinut: {\n        toggle: \"เปิดใช้งาน Multinut\",\n        description:\n          \"หากเปิดใช้งาน Wallet จะใช้ Multinut เพื่อชำระค่าใบแจ้งหนี้จากหลาย mints พร้อมกัน\",\n      },\n      nostr_mint_backup: {\n        toggle: \"สำรองข้อมูล Mint list บน Nostr\",\n        description:\n          \"หากเปิดใช้งาน รายการ Mint ของคุณจะถูกสำรองข้อมูลไปยัง Nostr relays โดยอัตโนมัติโดยใช้คีย์ Nostr ที่กำหนดค่าไว้ สิ่งนี้ช่วยให้คุณสามารถกู้คืนรายการ Mint ของคุณในอุปกรณ์ต่างๆ ได้\",\n        notifications: {\n          enabled: \"เปิดใช้งานการสำรองข้อมูล Nostr mint แล้ว\",\n          disabled: \"ปิดใช้งานการสำรองข้อมูล Nostr mint แล้ว\",\n          failed: \"ไม่สามารถเปิดใช้งานการสำรองข้อมูล Nostr mint ได้\",\n        },\n      },\n    },\n    appearance: {\n      keyboard: {\n        title: \"แป้นพิมพ์บนหน้าจอ\",\n        description: \"ใช้แป้นพิมพ์ตัวเลขสำหรับการป้อนจำนวนเงิน\",\n        toggle: \"ใช้แป้นพิมพ์ตัวเลข\",\n        toggle_description:\n          \"หากเปิดใช้งาน จะใช้แป้นพิมพ์ตัวเลขสำหรับการป้อนจำนวนเงิน\",\n      },\n      theme: {\n        title: \"รูปลักษณ์\",\n        description: \"เปลี่ยนรูปลักษณ์ของ Wallet ของคุณ\",\n        tooltips: {\n          mono: \"mono\",\n          cyber: \"cyber\",\n          freedom: \"freedom\",\n          nostr: \"nostr\",\n          bitcoin: \"bitcoin\",\n          mint: \"mint\",\n          nut: \"nut\",\n          blu: \"blu\",\n          flamingo: \"flamingo\",\n        },\n      },\n      bip177: {\n        title: \"สัญลักษณ์ Bitcoin\",\n        description: \"ใช้สัญลักษณ์ ₿ แทน sats\",\n        toggle: \"ใช้สัญลักษณ์ ₿\",\n      },\n    },\n    advanced: {\n      title: \"ขั้นสูง\",\n      developer: {\n        title: \"การตั้งค่าสำหรับนักพัฒนา\",\n        description: \"การตั้งค่าต่อไปนี้มีไว้สำหรับการพัฒนาและการดีบั๊ก\",\n        new_seed: {\n          button: \"สร้างวลีสำหรับกู้คืนใหม่\",\n          description:\n            \"สิ่งนี้จะสร้างวลีสำหรับกู้คืนใหม่ คุณต้องส่งยอดเงินทั้งหมดของคุณไปให้ตัวเองเพื่อที่จะกู้คืนด้วยวลีสำหรับกู้คืนใหม่ได้\",\n          confirm_question:\n            \"คุณแน่ใจหรือไม่ว่าต้องการสร้างวลีสำหรับกู้คืนใหม่?\",\n          cancel: \"ยกเลิก\",\n          confirm: \"ยืนยัน\",\n        },\n        remove_spent: {\n          button: \"ลบหลักฐานที่ใช้แล้ว\",\n          description:\n            \"ตรวจสอบว่าโทเค็น ecash จาก mints ที่เปิดใช้งานของคุณถูกใช้ไปแล้วหรือไม่ และลบโทเค็นที่ใช้แล้วออกจาก Wallet ของคุณ ใช้สิ่งนี้เฉพาะเมื่อ Wallet ของคุณติดค้าง\",\n        },\n        debug_console: {\n          button: \"สลับคอนโซลดีบั๊ก\",\n          description:\n            \"เปิดเทอร์มินัลดีบั๊ก Javascript ห้ามวางสิ่งใดๆ ลงในเทอร์มินัลนี้ที่คุณไม่เข้าใจ ขโมยอาจพยายามหลอกให้คุณวางโค้ดที่เป็นอันตรายที่นี่\",\n        },\n        export_proofs: {\n          button: \"ส่งออกหลักฐานที่ใช้งานอยู่\",\n          description:\n            \"คัดลอกยอดคงเหลือทั้งหมดของคุณจาก mint ที่เปิดใช้งานเป็นโทเค็น Cashu ไปยังคลิปบอร์ดของคุณ นี่จะส่งออกเฉพาะโทเค็นจาก mint และหน่วยที่เลือก สำหรับการส่งออกทั้งหมด ให้เลือก mint และหน่วยอื่นแล้วส่งออกอีกครั้ง\",\n        },\n        keyset_counters: {\n          title: \"เพิ่มเคาน์เตอร์ keyset\",\n          description:\n            'คลิกที่ Keyset ID เพื่อเพิ่มเคาน์เตอร์ derivation path สำหรับ keysets ใน Wallet ของคุณ สิ่งนี้มีประโยชน์หากคุณเห็นข้อผิดพลาด \"outputs have already been signed\"',\n          counter: \"ตัวนับ: {count}\",\n        },\n        unset_reserved: {\n          button: \"ยกเลิกการสำรองโทเค็นทั้งหมด\",\n          description:\n            'Wallet นี้จะทำเครื่องหมาย ecash ขาออกที่รอดำเนินการว่าถูกสำรอง (และหักออกจากยอดคงเหลือของคุณ) เพื่อป้องกันความพยายามในการใช้จ่ายซ้ำ ปุ่มนี้จะยกเลิกการสำรองโทเค็นทั้งหมดเพื่อให้สามารถใช้ได้อีกครั้ง หากคุณทำเช่นนี้ Wallet ของคุณอาจมีหลักฐานที่ใช้แล้ว กดปุ่ม \"ลบหลักฐานที่ใช้แล้ว\" เพื่อกำจัดออกไป',\n        },\n        show_onboarding: {\n          button: \"แสดงหน้าแนะนำ\",\n          description: \"แสดงหน้าจอแนะนำอีกครั้ง\",\n        },\n        reset_wallet: {\n          button: \"รีเซ็ตข้อมูล Wallet\",\n          description:\n            \"รีเซ็ตข้อมูล Wallet ของคุณ คำเตือน: สิ่งนี้จะลบทุกอย่าง! ตรวจสอบให้แน่ใจว่าคุณสร้างการสำรองข้อมูลก่อน\",\n          confirm_question: \"คุณแน่ใจหรือไม่ว่าต้องการลบข้อมูล Wallet ของคุณ?\",\n          cancel: \"ยกเลิก\",\n          confirm: \"ลบ Wallet\",\n        },\n        export_wallet: {\n          button: \"ส่งออกข้อมูล Wallet\",\n          description:\n            \"ดาวน์โหลดข้อมูล Wallet ของคุณ คุณสามารถกู้คืน Wallet ของคุณจากไฟล์นี้บนหน้าจอต้อนรับของ Wallet ใหม่ ไฟล์นี้จะไม่ตรงกันหากคุณยังคงใช้ Wallet ของคุณหลังจากส่งออก\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"เข้าร่วม Mint\",\n    subtitle:\n      \"คุณยังไม่ได้เข้าร่วม Cashu mint ใด ๆ เพิ่ม URL ของ mint ในการตั้งค่าหรือรับ ecash จาก mint ใหม่เพื่อเริ่มต้น\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"รับ Ecash\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"ประวัติ\",\n      },\n      invoices: {\n        label: \"ใบแจ้งหนี้\",\n      },\n      mints: {\n        label: \"Mints\",\n      },\n    },\n    install: {\n      text: \"ติดตั้ง\",\n      tooltip: \"ติดตั้ง Cashu\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"ไม่.\",\n    text: \"มีแท็บอื่นกำลังทำงานอยู่แล้ว ปิดแท็บนี้แล้วลองอีกครั้ง\",\n    actions: {\n      retry: {\n        label: \"ลองใหม่\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"อุ๊บส์. ไม่มีอะไรที่นี่…\",\n    actions: {\n      home: {\n        label: \"กลับหน้าหลัก\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Mint\",\n    },\n    mintBalance: {\n      label: \"ยอดเงินคงเหลือ\",\n    },\n    mintError: {\n      label: \"ข้อผิดพลาด Mint\",\n    },\n    pending: {\n      label: \"รอดำเนินการ\",\n      tooltip: \"ตรวจสอบโทเค็นที่รอดำเนินการทั้งหมด\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"ก่อนหน้า\",\n      },\n      next: {\n        label: \"ถัดไป\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"ยินดีต้อนรับสู่ Cashu\",\n    text: \"Cashu.me คือ Wallet Bitcoin ที่ฟรีและเป็นโอเพนซอร์ส ซึ่งใช้ ecash เพื่อรักษาความปลอดภัยและความเป็นส่วนตัวของเงินของคุณ\",\n    actions: {\n      more: {\n        label: \"คลิกเพื่อเรียนรู้เพิ่มเติม\",\n      },\n    },\n    p1: {\n      text: \"Cashu เป็นโปรโตคอล ecash ที่ฟรีและเป็นโอเพนซอร์สสำหรับ Bitcoin คุณสามารถเรียนรู้เพิ่มเติมได้ที่ { link }\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"Wallet นี้ไม่มีส่วนเกี่ยวข้องกับ Mint ใด ๆ ในการใช้ Wallet นี้ คุณต้องเชื่อมต่อกับ Mint Cashu ที่คุณเชื่อถืออย่างน้อยหนึ่งแห่ง\",\n    },\n    p3: {\n      text: \"Wallet นี้จัดเก็บ ecash ที่มีเพียงคุณเท่านั้นที่เข้าถึงได้ หากคุณลบข้อมูลเบราว์เซอร์ของคุณโดยไม่มีการสำรองวลีสำหรับกู้คืน คุณจะสูญเสียโทเค็นของคุณ\",\n    },\n    p4: {\n      text: \"Wallet นี้อยู่ในช่วงเบต้า เราไม่รับผิดชอบต่อบุคคลที่สูญเสียการเข้าถึงเงินทุน ใช้งานด้วยความเสี่ยงของคุณเอง! รหัสนี้เป็นโอเพนซอร์สและได้รับอนุญาตภายใต้ใบอนุญาต MIT\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"ติดตั้ง PWA\",\n    alt: { pwa_example: \"ตัวอย่างการติดตั้ง PWA\" },\n    installing: \"กำลังติดตั้ง…\",\n    instruction: {\n      intro: {\n        text: \"เพื่อประสบการณ์ที่ดีที่สุด ใช้ Wallet นี้กับเว็บเบราว์เซอร์พื้นฐานของอุปกรณ์ของคุณเพื่อติดตั้งเป็น Progressive Web App ทำสิ่งนี้ทันที\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"แตะเมนู (มุมขวาบน)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"กด { buttonText }\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"แตะ แชร์ (ด้านล่าง)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"กด { buttonText }\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"เมื่อคุณติดตั้งแอปนี้บนอุปกรณ์ของคุณแล้ว ให้ปิดหน้าต่างเบราว์เซอร์นี้และใช้แอปจากหน้าจอหลักของคุณ\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"สำเร็จ!\",\n        text: \"คุณกำลังใช้ Cashu เป็น PWA ปิดหน้าต่างเบราว์เซอร์อื่นที่เปิดอยู่และใช้แอปจากหน้าจอหลักของคุณ\",\n        nextSteps: \"ตอนนี้คุณสามารถปิดแท็บนี้และเปิดแอปจากหน้าจอหลักได้\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"แตะ { icon } และ { buttonText }\",\n    buttonText: \"เพิ่มไปยังหน้าจอหลัก\",\n  },\n  AndroidPWAPrompt: {\n    text: \"แตะ { icon } และ { buttonText }\",\n    buttonText: \"เพิ่มไปยังหน้าจอหลัก\",\n  },\n  WelcomeSlide3: {\n    title: \"วลีสำหรับกู้คืนของคุณ\",\n    text: \"จัดเก็บวลีสำหรับกู้คืนของคุณไว้ในตัวจัดการรหัสผ่านหรือบนกระดาษ วลีสำหรับกู้คืนของคุณเป็นวิธีเดียวที่จะกู้คืนเงินทุนของคุณได้หากคุณสูญเสียการเข้าถึงอุปกรณ์นี้\",\n    inputs: {\n      seed_phrase: {\n        label: \"วลีสำหรับกู้คืน\",\n        caption: \"คุณสามารถดูวลีสำหรับกู้คืนของคุณได้ในการตั้งค่า\",\n      },\n      checkbox: {\n        label: \"ฉันได้จดไว้แล้ว\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"เงื่อนไข\",\n    actions: {\n      more: {\n        label: \"อ่านข้อกำหนดในการให้บริการ\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"ฉันได้อ่านและยอมรับข้อกำหนดและเงื่อนไขเหล่านี้\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"ตั้งค่ากระเป๋าเงินของคุณ\",\n    text: \"คุณต้องการกู้คืนจากวลีสำหรับกู้คืนหรือสร้างกระเป๋าเงินใหม่?\",\n    options: {\n      new: {\n        title: \"สร้างกระเป๋าเงินใหม่\",\n        subtitle: \"สร้างวลีสำหรับกู้คืนใหม่และเพิ่ม Mint\",\n      },\n      recover: {\n        title: \"กู้คืนกระเป๋าเงิน\",\n        subtitle: \"ป้อนวลีสำหรับกู้คืนของคุณ กู้คืน Mint และ ecash\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"เพิ่ม Mint\",\n    text: \"Mint คือเซิร์ฟเวอร์ที่ช่วยให้คุณส่งและรับ ecash เลือก Mint ที่ค้นพบหรือเพิ่มด้วยตนเอง คุณสามารถข้ามและเพิ่มในภายหลังได้\",\n    sections: { your_mints: \"Mint ของคุณ\" },\n    restoring: \"กำลังกู้คืน Mint…\",\n    placeholder: { mint_url: \"https://\" },\n  },\n  WelcomeRecoverSeed: {\n    title: \"ป้อนวลีสำหรับกู้คืนของคุณ\",\n    text: \"วางหรือพิมพ์วลี 12 คำเพื่อกู้คืน\",\n    inputs: { word: \"คำ { index }\" },\n    actions: { paste_all: \"วางทั้งหมด\" },\n    disclaimer:\n      \"วลีสำหรับกู้คืนใช้เฉพาะในเครื่องเพื่อสร้างคีย์กระเป๋าเงินของคุณ\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"กู้คืน ecash ของคุณ\",\n    text: \"สแกนหา proof ที่ยังไม่ถูกใช้บน Mint ที่กำหนดค่าไว้และเพิ่มลงในกระเป๋าเงินของคุณ\",\n  },\n  MintRatings: {\n    title: \"รีวิว Mint\",\n    reviews: \"รีวิว\",\n    ratings: \"คะแนน\",\n    no_reviews: \"ไม่พบบทรีวิว\",\n    your_review: \"รีวิวของคุณ\",\n    no_reviews_to_display: \"ไม่มีรีวิวที่จะแสดง\",\n    no_rating: \"ไม่มีคะแนน\",\n    out_of: \"จาก\",\n    rows: \"Reviews\",\n    sort: \"เรียงลำดับ\",\n    sort_options: {\n      newest: \"ใหม่ที่สุด\",\n      oldest: \"เก่าที่สุด\",\n      highest: \"สูงที่สุด\",\n      lowest: \"ต่ำที่สุด\",\n    },\n    actions: { write_review: \"เขียนรีวิว\" },\n    empty_state_subtitle:\n      \"ช่วยเหลือโดยการเขียนรีวิว แบ่งปันประสบการณ์ของคุณกับ Mint นี้และช่วยเหลือผู้อื่นโดยการเขียนรีวิว\",\n  },\n  CreateMintReview: {\n    title: \"รีวิว Mint\",\n    publishing_as: \"เผยแพร่เป็น\",\n    inputs: {\n      rating: { label: \"คะแนน\" },\n      review: { label: \"รีวิว (ไม่บังคับ)\" },\n    },\n    actions: {\n      publish: { label: \"เผยแพร่\", in_progress: \"กำลังเผยแพร่…\" },\n    },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"กู้คืนจากวลีสำหรับกู้คืน\",\n      caption:\n        \"ป้อนวลีสำหรับกู้คืนของคุณเพื่อกู้คืน Wallet ของคุณ ก่อนที่จะกู้คืน ตรวจสอบให้แน่ใจว่าคุณได้เพิ่ม Mint ทั้งหมดที่คุณเคยใช้มาก่อน\",\n      inputs: {\n        seed_phrase: {\n          label: \"วลีสำหรับกู้คืน\",\n          caption: \"คุณสามารถดูวลีสำหรับกู้คืนของคุณได้ในการตั้งค่า\",\n        },\n      },\n    },\n    information: {\n      label: \"ข้อมูล\",\n      caption:\n        \"วิซาร์ดจะกู้คืน ecash จากวลีสำหรับกู้คืนอื่นเท่านั้น คุณจะไม่สามารถใช้วลีสำหรับกู้คืนนี้หรือเปลี่ยนวลีสำหรับกู้คืนของ Wallet ที่คุณกำลังใช้อยู่ได้ ซึ่งหมายความว่า ecash ที่กู้คืนจะไม่ได้รับการป้องกันโดยวลีสำหรับกู้คืนปัจจุบันของคุณ ตราบใดที่คุณยังไม่ได้ส่ง ecash ให้ตัวเองหนึ่งครั้ง\",\n    },\n    restore_mints: {\n      label: \"กู้คืน Mints\",\n      caption:\n        'เลือก Mint ที่จะกู้คืน คุณสามารถเพิ่ม Mint เพิ่มเติมในหน้าจอหลักภายใต้ \"Mints\" และกู้คืนได้ที่นี่',\n    },\n    actions: {\n      paste: {\n        error: \"อ่านเนื้อหาในคลิปบอร์ดไม่สำเร็จ\",\n      },\n      validate: {\n        error: \"Mnemonic ควรมีอย่างน้อย 12 คำ\",\n      },\n      select_all: {\n        label: \"เลือกทั้งหมด\",\n      },\n      deselect_all: {\n        label: \"ไม่เลือกทั้งหมด\",\n      },\n      restore: {\n        label: \"กู้คืน\",\n        in_progress: \"กำลังกู้คืน Mint…\",\n        error: \"ข้อผิดพลาดในการกู้คืน Mint: { error }\",\n      },\n      restore_all_mints: {\n        label: \"กู้คืน Mints ทั้งหมด\",\n        in_progress: \"กำลังกู้คืน Mint { index } จาก { length }…\",\n        success: \"กู้คืนสำเร็จ\",\n        error: \"ข้อผิดพลาดในการกู้คืน Mints: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"กู้คืน Mint ที่เลือก ({count})\",\n        in_progress: \"กำลังกู้คืน Mint {index} จาก {length} ...\",\n        success: \"กู้คืน {count} Mint(s) สำเร็จ\",\n        error: \"ข้อผิดพลาดในการกู้คืน Mint ที่เลือก: {error}\",\n      },\n    },\n    nostr_mints: {\n      label: \"กู้คืน Mint จาก Nostr\",\n      caption:\n        \"ค้นหาการสำรองข้อมูล Mint ที่เก็บไว้ใน Nostr relays โดยใช้วลีสำหรับกู้คืนของคุณ สิ่งนี้จะช่วยให้คุณค้นพบ Mint ที่คุณเคยใช้มาก่อน\",\n      search_button: \"ค้นหาการสำรองข้อมูล Mint\",\n      select_all: \"เลือกทั้งหมด\",\n      deselect_all: \"ไม่เลือกทั้งหมด\",\n      backed_up: \"สำรองแล้ว\",\n      already_added: \"เพิ่มแล้ว\",\n      add_selected: \"เพิ่มที่เลือก ({count})\",\n      no_backups_found: \"ไม่พบการสำรองข้อมูล Mint\",\n      no_backups_hint:\n        \"ตรวจสอบให้แน่ใจว่าได้เปิดใช้งานการสำรองข้อมูล Nostr mint ในการตั้งค่าเพื่อสำรองข้อมูลรายการ Mint ของคุณโดยอัตโนมัติ\",\n      invalid_mnemonic: \"โปรดป้อนวลีสำหรับกู้คืนที่ถูกต้องก่อนค้นหา\",\n      search_error: \"ไม่สามารถค้นหาการสำรองข้อมูล Mint ได้\",\n      add_error: \"ไม่สามารถเพิ่ม Mint ที่เลือกได้\",\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"เพิ่ม Mint\",\n      description:\n        \"ป้อน URL ของ Cashu mint เพื่อเชื่อมต่อ Wallet นี้ไม่มีส่วนเกี่ยวข้องกับ Mint ใดๆ\",\n      inputs: {\n        nickname: {\n          placeholder: \"ชื่อเล่น (เช่น Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"URL ไม่ถูกต้อง\",\n        },\n        scan: {\n          label: \"สแกนรหัส QR\",\n        },\n      },\n    },\n    discover: {\n      title: \"สำรวจ Mints\",\n      overline: \"สำรวจ\",\n      caption: \"สำรวจ Mints ที่ผู้ใช้คนอื่นแนะนำบน nostr\",\n      actions: {\n        discover: {\n          label: \"สำรวจ Mints\",\n          in_progress: \"กำลังโหลด…\",\n          error_no_mints: \"ไม่พบ Mints\",\n          success: \"พบ { length } Mints\",\n        },\n      },\n      recommendations: {\n        overline: \"พบ { length } Mints\",\n        caption:\n          \"Mints เหล่านี้ถูกแนะนำโดยผู้ใช้ Nostr คนอื่น ๆ โปรดใช้ความระมัดระวังและทำการวิจัยของคุณเองก่อนใช้ Mint\",\n        actions: {\n          browse: {\n            label: \"คลิกเพื่อเรียกดู Mints\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"แลกเปลี่ยน\",\n      overline: \"การแลกเปลี่ยนระหว่าง Mints\",\n      caption:\n        \"แลกเปลี่ยนเงินระหว่าง Mints ผ่าน Lightning หมายเหตุ: เผื่อค่าธรรมเนียม Lightning ที่อาจเกิดขึ้น หากการชำระเงินขาเข้าไม่สำเร็จ ให้ตรวจสอบใบแจ้งหนี้ด้วยตนเอง\",\n      inputs: {\n        from: {\n          label: \"จาก\",\n        },\n        to: {\n          label: \"ถึง\",\n        },\n        amount: {\n          label: \"จำนวน ({ ticker }) )\",\n        },\n      },\n      actions: {\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n    },\n    error_badge: \"ข้อผิดพลาด\",\n    reviews_text: \"รีวิว\",\n    no_reviews_yet: \"ยังไม่มีรีวิว\",\n    discover_mints_button: \"สำรวจ Mints\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - สแกนต่อไป\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"รับ Lightning\",\n    create_invoice_title: \"สร้างใบแจ้งหนี้\",\n    inputs: {\n      amount: {\n        label: \"จำนวน ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"สร้างใบแจ้งหนี้\",\n        label_blocked: \"กำลังสร้างใบแจ้งหนี้…\",\n        in_progress: \"กำลังสร้าง\",\n      },\n    },\n    invoice: {\n      caption: \"ใบแจ้งหนี้ Lightning\",\n      status_paid_text: \"ชำระแล้ว!\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"ส่ง\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"ไม่มี Mints ให้เลือก\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"ไม่มี Mints ให้เลือก\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"ส่ง Ecash\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"ออฟไลน์\",\n    inputs: {\n      amount: {\n        label: \"จำนวน ({ ticker }) *\",\n        invalid_too_much_error_text: \"มากเกินไป\",\n      },\n      p2pk_pubkey: {\n        label: \"คีย์สาธารณะของผู้รับ\",\n        label_invalid: \"คีย์สาธารณะของผู้รับ\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"คัดลอก Emoji\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"คัดลอกลิงก์\",\n      },\n      share: {\n        tooltip_text: \"แชร์ ecash\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"ลบออกจากประวัติ\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"แฟลชไปยังการ์ด NFC\",\n          ndef_unsupported_text: \"ไม่รองรับ NDEF\",\n        },\n      },\n    },\n  },\n  ReceiveDialog: {\n    title: \"รับ\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"ไม่มี Mints ให้เลือก\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"คุณต้องเชื่อมต่อกับ Mint เพื่อรับผ่าน Lightning\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"รับ Ecash\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"ขอ\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"กำลังสแกน…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"รับ Ecash\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"วางโทเค็น Cashu\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"โทเค็นไม่ถูกต้อง\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"ไม่สามารถรับได้ คีย์ล็อก P2PK ของโทเค็นนี้ไม่ตรงกับคีย์สาธารณะของคุณ\",\n      },\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"กำลังเพิ่ม Mint…\",\n      },\n      swap: {\n        label: \"@:global.actions.swap.label\",\n        tooltip_text: \"แลกเปลี่ยนไปยัง Mint ที่เชื่อถือได้\",\n        caption: \"แลกเปลี่ยน { value }\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"ยกเลิกการแลกเปลี่ยน\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      later: {\n        label: \"รับภายหลัง\",\n        tooltip_text: \"เพิ่มไปยังประวัติเพื่อรับภายหลัง\",\n        already_in_history_success_text: \"Ecash อยู่ในประวัติแล้ว\",\n        added_to_history_success_text: \"เพิ่ม Ecash ในประวัติแล้ว\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"อ่านจากการ์ด NFC\",\n          ndef_unsupported_text: \"ไม่รองรับ NDEF\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"คีย์ P2PK\",\n      description: \"รับ ecash ที่ล็อกด้วยคีย์นี้\",\n      used_warning_text:\n        \"คำเตือน: คีย์นี้เคยถูกใช้มาก่อน ใช้คีย์ใหม่เพื่อความเป็นส่วนตัวที่ดีขึ้น\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"สร้างคีย์ใหม่\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"คำขอชำระเงิน\",\n      description: \"รับการชำระเงินผ่าน Nostr\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"คำขอใหม่\",\n      },\n      add_amount: {\n        label: \"เพิ่มจำนวนเงิน\",\n      },\n      use_active_mint: {\n        label: \"Mint ใดก็ได้\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"ป้อนจำนวนเงิน\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"ปิดใช้งานแป้นพิมพ์แล้ว คุณสามารถเปิดใช้งานแป้นพิมพ์ได้อีกครั้งในการตั้งค่า\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostr Wallet Connect\",\n      description:\n        \"ควบคุม Wallet ของคุณจากระยะไกลด้วย NWC กดที่รหัส QR เพื่อเชื่อมโยง Wallet ของคุณกับแอปพลิเคชันที่เข้ากันได้\",\n      warning_text:\n        \"คำเตือน: ใครก็ตามที่เข้าถึงสตริงการเชื่อมต่อนี้สามารถเริ่มการชำระเงินจาก Wallet ของคุณได้ ห้ามแบ่งปัน!\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"ข้อความจาก Mint\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"ติดต่อ\",\n    },\n    details: {\n      title: \"รายละเอียด Mint\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"แสดงทั้งหมด\",\n          },\n          hide: {\n            label: \"ซ่อน\",\n          },\n        },\n      },\n      currency: {\n        label: \"สกุลเงิน\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"เวอร์ชัน\",\n      },\n    },\n    actions: {\n      title: \"การดำเนินการ\",\n      copy_mint_url: {\n        label: \"คัดลอก Mint URL\",\n      },\n      delete: {\n        label: \"ลบ Mint\",\n      },\n      edit: {\n        label: \"แก้ไข Mint\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"เลือก Mint\",\n    badge_mint_error_text: \"ข้อผิดพลาด\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"ยังไม่มีประวัติ\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"{ value } ที่ผ่านมา\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"ตรวจสอบสถานะ\",\n      },\n      receive: {\n        tooltip_text: \"รับ\",\n      },\n      filter_pending: {\n        label: \"กรองที่รอดำเนินการ\",\n      },\n      show_all: {\n        label: \"แสดงทั้งหมด\",\n      },\n    },\n    old_token_not_found_error_text: \"ไม่พบโทเค็นเก่า\",\n  },\n  InvoiceTable: {\n    empty_text: \"ยังไม่มีใบแจ้งหนี้\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"คลิกเพื่อคัดลอก\",\n      date_label: \"{ value } ที่ผ่านมา\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"ตรวจสอบสถานะ\",\n      },\n      filter_pending: {\n        label: \"กรองที่รอดำเนินการ\",\n      },\n      show_all: {\n        label: \"แสดงทั้งหมด\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"คุณแน่ใจหรือไม่ว่าต้องการลบ Mint นี้?\",\n    nickname: {\n      label: \"ชื่อเล่น\",\n    },\n    balances: {\n      label: \"ยอดเงินคงเหลือ\",\n    },\n    warning_text:\n      \"หมายเหตุ: เนื่องจาก Wallet นี้มีความระมัดระวังสูง ecash ของคุณจาก Mint นี้จะไม่ถูกลบจริง แต่จะยังคงเก็บไว้ในอุปกรณ์ของคุณ คุณจะเห็นมันปรากฏขึ้นอีกครั้งหากคุณเพิ่ม Mint นี้อีกครั้งในภายหลัง\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"ลบ Mint\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"Cashu token หรือที่อยู่ Lightning\",\n      receive: \"Cashu token\",\n      pay: \"ที่อยู่ Lightning หรือใบแจ้งหนี้\",\n    },\n    qr_scanner: {\n      title: \"สแกนรหัส QR\",\n      description: \"แตะเพื่อสแกนที่อยู่\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"ชำระเงิน Lightning\",\n      inputs: {\n        invoice_data: {\n          label: \"ใบแจ้งหนี้หรือที่อยู่ Lightning\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"{ payee } กำลังขอ { value } { ticker }\",\n      amount_range_label:\n        \"{ payee } กำลังขอ{br}ระหว่าง { min } และ { max } { ticker }\",\n      sending_to_lightning_address: \"กำลังส่งไปยัง { address }\",\n      inputs: {\n        amount: {\n          label: \"จำนวน ({ ticker }) *\",\n        },\n        comment: {\n          label: \"ความคิดเห็น (ไม่บังคับ)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"ชำระเงิน { value }\",\n      paying: \"กำลังชำระเงิน\",\n      paid: \"ชำระแล้ว\",\n      fee: \"ค่าธรรมเนียม\",\n      memo: {\n        label: \"บันทึก\",\n      },\n      processing_info_text: \"กำลังประมวลผล…\",\n      balance_too_low_warning_text: \"ยอดเงินคงเหลือต่ำเกินไป\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"ชำระเงิน\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"ข้อผิดพลาด\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"แก้ไข Mint\",\n    inputs: {\n      nickname: {\n        label: \"ชื่อเล่น\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"คุณเชื่อถือ Mint นี้หรือไม่?\",\n    description:\n      \"ก่อนที่จะใช้ Mint นี้ ตรวจสอบให้แน่ใจว่าคุณเชื่อถือได้ Mints อาจกลายเป็นอันตรายหรือหยุดดำเนินการได้ทุกเมื่อ\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"กำลังเพิ่ม Mint\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"โปรดป้อน mnemonic\",\n    restore_mint_error_text: \"ข้อผิดพลาดในการกู้คืน Mint: { error }\",\n    prepare_info_text: \"กำลังเตรียมกระบวนการกู้คืน…\",\n    restored_proofs_for_keyset_info_text:\n      \"กู้คืน { restoreCounter } proofs สำหรับ keyset { keysetId }\",\n    checking_proofs_for_keyset_info_text:\n      \"กำลังตรวจสอบ proofs { startIndex } ถึง { endIndex } สำหรับ keyset { keysetId }\",\n    no_proofs_info_text: \"ไม่พบ proofs ที่จะกู้คืน\",\n    restored_amount_success_text: \"กู้คืน { amount }\",\n  },\n  swap: {\n    in_progress_warning_text: \"กำลังดำเนินการแลกเปลี่ยน\",\n    invalid_swap_data_error_text: \"ข้อมูลการแลกเปลี่ยนไม่ถูกต้อง\",\n    swap_error_text: \"ข้อผิดพลาดในการแลกเปลี่ยน\",\n  },\n  TokenInformation: {\n    fee: \"ค่าธรรมเนียม\",\n    unit: \"หน่วย\",\n    fiat: \"สกุลเงิน\",\n    p2pk: \"P2PK\",\n    locked: \"ล็อค\",\n    locked_to_you: \"ล็อคให้คุณ\",\n    mint: \"โรงกษาปณ์\",\n    memo: \"บันทึก\",\n    payment_request: \"คำขอชำระเงิน\",\n    nostr: \"Nostr\",\n    token_copied: \"คัดลอกโทเค็นไปยังคลิปบอร์ดแล้ว\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/tr-TR/index.ts",
    "content": "export default {\n  MultinutPicker: {\n    payment: \"Multinut ödeme\",\n    selectMints: \"Ödeme yapmak için bir veya birden fazla mint seçin.\",\n    totalSelectedBalance: \"Seçilen Toplam Bakiye\",\n    multiMintPay: \"Çoklu-Mint Ödeme\",\n    balanceNotEnough: \"Çoklu mint bakiyesi bu faturayı karşılamaya yetmiyor\",\n    failed: \"İşlenemedi: {error}\",\n    paid: \"Lightning ile {amount} ödendi\",\n  },\n  // merged into single Settings block above\n  advanced: {\n    developer: {},\n  },\n\n  global: {\n    copy_to_clipboard: {\n      success: \"Panoya kopyalandı!\",\n    },\n    actions: {\n      add_mint: {\n        label: \"Nane ekle\",\n      },\n      cancel: {\n        label: \"İptal\",\n      },\n      copy: {\n        label: \"Kopyala\",\n      },\n      close: {\n        label: \"Kapat\",\n      },\n      enter: {\n        label: \"Gir\",\n      },\n      lock: {\n        label: \"Kilitle\",\n      },\n      paste: {\n        label: \"Yapıştır\",\n      },\n      receive: {\n        label: \"Al\",\n      },\n      scan: {\n        label: \"Tara\",\n      },\n      send: {\n        label: \"Gönder\",\n      },\n      swap: {\n        label: \"Değiştir\",\n      },\n      update: {\n        label: \"Güncelle\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"Nane URL'si\",\n      },\n    },\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"Bakiye çok düşük\",\n      received: \"{amount} alındı\",\n      fee: \" (ücret: {fee})\",\n      could_not_request_mint: \"Nane isteği yapılamadı\",\n      invoice_still_pending: \"Fatura hala beklemede\",\n      paid_lightning: \"Lightning üzerinden {amount} ödendi\",\n      payment_pending_refresh:\n        \"Ödeme beklemede. Faturayı manuel olarak yenileyin.\",\n      sent: \"{amount} gönderildi\",\n      token_still_pending: \"Token hala beklemede\",\n      received_lightning: \"Lightning üzerinden {amount} alındı\",\n      lightning_payment_failed: \"Lightning ödemesi başarısız oldu\",\n      failed_to_decode_invoice: \"Fatura çözülemedi\",\n\n      invalid_lnurl: \"Geçersiz LNURL\",\n      lnurl_error: \"LNURL hatası\",\n      no_amount: \"Tutar yok\",\n      no_lnurl_data: \"LNURL verisi yok\",\n      no_price_data: \"Fiyat verisi yok.\",\n      please_try_again: \"Lütfen tekrar deneyin.\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"Nane zaten eklenmiş\",\n        added: \"Nane eklendi\",\n        not_found: \"Nane bulunamadı\",\n        activation_failed: \"Nane etkinleştirmesi başarısız oldu\",\n        no_active_mint: \"Aktif nane yok\",\n        unit_activation_failed: \"Birim etkinleştirmesi başarısız oldu\",\n        unit_not_supported: \"Birim nane tarafından desteklenmiyor\",\n        activated: \"Nane etkinleştirildi\",\n        could_not_connect: \"Naneye bağlanılamadı\",\n        could_not_get_info: \"Nane bilgisi alınamadı\",\n        could_not_get_keys: \"Nane anahtarları alınamadı\",\n        could_not_get_keysets: \"Nane anahtar setleri alınamadı\",\n        removed: \"Nane kaldırıldı\",\n        error: \"Nane hatası\",\n        mint_validation_error: \"Mint doğrulama hatası\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"Ayarlar\",\n        settings: {\n          title: \"Ayarlar\",\n          caption: \"Cüzdan yapılandırması\",\n        },\n      },\n      terms: {\n        title: \"Şartlar\",\n        terms: {\n          title: \"Şartlar\",\n          caption: \"Hizmet Şartları\",\n        },\n      },\n      links: {\n        title: \"Bağlantılar\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"Bağış yap\",\n          caption: \"Cashu'yu Destekle\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"Çevrimdışı\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"{ countdown } içinde yeniden yükle\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"Hazırlık aşaması – gerçek fonlarla kullanmayın!\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"Cüzdan\",\n      },\n    },\n  },\n  Settings: {\n    language: {\n      title: \"Dil\",\n      description: \"Lütfen aşağıdaki listeden tercih ettiğiniz dili seçin.\",\n    },\n    sections: {\n      backup_restore: \"YEDEKLE & GERİ YÜKLE\",\n      lightning_address: \"LIGHTNING ADRESİ\",\n      nostr_keys: \"NOSTR ANAHTARLARI\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"Röleleri düzenlemek için tıklayın\",\n          add: {\n            title: \"Röle ekle\",\n            description:\n              \"Cüzdanınız ödeme istekleri, nostr cüzdan bağlantısı ve yedeklemeler gibi nostr işlemleri için bu röleleri kullanır.\",\n          },\n          list: {\n            title: \"Röleler\",\n            description: \"Cüzdanınız bu rölelere bağlanacak.\",\n            copy_tooltip: \"Röleyi kopyala\",\n            remove_tooltip: \"Röleyi kaldır\",\n          },\n        },\n      },\n\n      payment_requests: \"ÖDEME TALEPLERİ\",\n      nostr_wallet_connect: \"NOSTR CÜZDAN BAĞLANTISI\",\n      hardware_features: \"DONANIM ÖZELLİKLERİ\",\n      p2pk_features: \"P2PK ÖZELLİKLERİ\",\n      privacy: \"GİZLİLİK\",\n      experimental: \"DENEYSEL\",\n      appearance: \"GÖRÜNÜM\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"Kurtarma kelimelerini yedekle\",\n        description:\n          \"Kurtarma kelimeleriniz cüzdanınızı geri yükleyebilir. Güvenli ve gizli tutun.\",\n        seed_phrase_label: \"Kurtarma kelimeleri\",\n      },\n      restore_ecash: {\n        title: \"Ecash'i geri yükle\",\n        description:\n          \"Geri yükleme sihirbazı, kayıp ecash'inizi anımsatıcı kurtarma kelimelerinden kurtarmanıza olanak tanır. Mevcut cüzdanınızın kurtarma kelimeleri etkilenmeyecektir, sihirbaz yalnızca başka bir kurtarma kelimesinden ecash'i geri yüklemenizi sağlayacaktır.\",\n        button: \"Geri Yükle\",\n      },\n    },\n    lightning_address: {\n      title: \"Lightning adresi\",\n      description: \"Lightning adresinize ödeme alın.\",\n      enable: {\n        toggle: \"Etkinleştir\",\n        description: \"npub.cash ile Lightning adresi\",\n      },\n      address: {\n        copy_tooltip: \"Lightning adresini kopyala\",\n      },\n      automatic_claim: {\n        toggle: \"Otomatik olarak talep et\",\n        description: \"Gelen ödemeleri otomatik olarak alın.\",\n      },\n      npc_v2: {\n        choose_mint_title: \"npub.cash v2 için mint seçin\",\n        choose_mint_placeholder: \"Bir mint seçin…\",\n      },\n    },\n    nostr_keys: {\n      title: \"Nostr anahtarlarınız\",\n      description: \"Lightning adresiniz için nostr anahtarlarını ayarlayın.\",\n      wallet_seed: {\n        title: \"Cüzdan kurtarma kelimeleri\",\n        description:\n          \"Cüzdan kurtarma kelimelerinden nostr anahtar çifti oluştur\",\n        copy_nsec: \"nsec'i kopyala\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"Bir NIP-46 bunker kullanın\",\n        delete_tooltip: \"Bağlantıyı sil\",\n      },\n      use_nsec: {\n        title: \"nsec'inizi kullanın\",\n        description: \"Bu yöntem tehlikelidir ve önerilmez\",\n        delete_tooltip: \"nsec'i sil\",\n      },\n      signing_extension: {\n        title: \"İmzalama uzantısı\",\n        description: \"Bir NIP-07 imzalama uzantısı kullanın\",\n        not_found: \"NIP-07 imzalama uzantısı bulunamadı\",\n      },\n    },\n\n    payment_requests: {\n      title: \"Ödeme talepleri\",\n      description:\n        \"Ödeme talepleri, nostr aracılığıyla ödeme almanıza olanak tanır. Bunu etkinleştirirseniz, cüzdanınız nostr rölelerinize abone olacaktır.\",\n      enable_toggle: \"Ödeme Taleplerini Etkinleştir\",\n      claim_automatically: {\n        toggle: \"Otomatik olarak talep et\",\n        description: \"Gelen ödemeleri otomatik olarak alın.\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"Nostr Cüzdan Bağlantısı (NWC)\",\n      description:\n        \"NWC'yi kullanarak cüzdanınızı başka herhangi bir uygulamadan kontrol edin.\",\n      enable_toggle: \"NWC'yi Etkinleştir\",\n      payments_note:\n        \"NWC'yi yalnızca Bitcoin bakiyenizden ödemeler için kullanabilirsiniz. Ödemeler etkin nanenizden yapılacaktır.\",\n      connection: {\n        copy_tooltip: \"Bağlantı dizesini kopyala\",\n        qr_tooltip: \"QR kodunu göster\",\n        allowance_label: \"Kalan ödenek (sat)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"NFC kartlarına yazmak için kodlamayı seçin\",\n        text: {\n          title: \"Metin\",\n          description: \"Token'ı düz metin olarak sakla\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"Bu cüzdanın URL'sini token ile sakla\",\n        },\n        binary: {\n          title: \"İkilik\",\n          description: \"Token'ları ikili veri olarak sakla\",\n        },\n        quick_access: {\n          toggle: \"NFC'ye hızlı erişim\",\n          description:\n            \"Ecash Al menüsünde NFC kartlarını hızlıca tarayın. Bu seçenek Ecash Al menüsüne bir NFC düğmesi ekler.\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"P2PK kilitli ecash almak için bir anahtar çifti oluşturun. Uyarı: Bu özellik deneyseldir. Yalnızca küçük miktarlarla kullanın. Özel anahtarlarınızı kaybederseniz, artık kimse ona kilitlenmiş ecash'in kilidini açamaz.\",\n      generate_button: \"Anahtar oluştur\",\n      import_button: \"nsec'i içe aktar\",\n      quick_access: {\n        toggle: \"Kilitlemeye hızlı erişim\",\n        description:\n          \"Bunu, P2PK kilitleme anahtarınızı ecash alma menüsünde hızlıca göstermek için kullanın.\",\n      },\n      keys_expansion: {\n        label: \"{count} anahtara göz atmak için tıklayın\",\n        used_badge: \"kullanıldı\",\n      },\n    },\n    privacy: {\n      title: \"Gizlilik\",\n      description: \"Bu ayarlar gizliliğinizi etkiler.\",\n      check_incoming: {\n        toggle: \"Gelen faturayı kontrol et\",\n        description:\n          \"Etkinleştirilirse, cüzdan arka planda en son faturayı kontrol edecektir. Bu, parmak izi almayı kolaylaştıran cüzdanın tepkiselliğini artırır. Ödenmemiş faturaları Faturalar sekmesinde manuel olarak kontrol edebilirsiniz.\",\n      },\n      check_startup: {\n        toggle: \"Başlangıçta bekleyen faturaları kontrol et\",\n        description:\n          \"Etkinleştirilirse, cüzdan başlangıçta son 24 saat içindeki bekleyen faturaları kontrol edecektir.\",\n      },\n      check_all: {\n        toggle: \"Tüm faturaları kontrol et\",\n        description:\n          \"Etkinleştirilirse, cüzdan iki haftaya kadar ödenmemiş faturaları arka planda periyodik olarak kontrol edecektir. Bu, parmak izi almayı kolaylaştıran cüzdanın çevrimiçi aktivitesini artırır. Ödenmemiş faturaları Faturalar sekmesinde manuel olarak kontrol edebilirsiniz.\",\n      },\n      check_sent: {\n        toggle: \"Gönderilen ecash'i kontrol et\",\n        description:\n          \"Etkinleştirilirse, cüzdan gönderilen token'ların kullanılıp kullanılmadığını belirlemek için periyodik arka plan kontrollerini kullanacaktır. Bu, parmak izi almayı kolaylaştıran cüzdanın çevrimiçi aktivitesini artırır.\",\n      },\n      websockets: {\n        toggle: \"WebSockets kullan\",\n        description:\n          \"Etkinleştirilirse, cüzdan ödenen faturalar ve nane'lerden harcanan token'larla ilgili güncellemeleri almak için uzun ömürlü WebSocket bağlantıları kullanacaktır. Bu, cüzdanın tepkiselliğini artırır ancak parmak izi almayı da kolaylaştırır.\",\n      },\n      bitcoin_price: {\n        toggle: \"Döviz kurunu Coinbase'den al\",\n        description:\n          \"Etkinleştirilirse, güncel Bitcoin döviz kuru coinbase.com'dan alınacak ve dönüştürülmüş bakiyeniz görüntülenecektir.\",\n        currency: {\n          title: \"Fiat Para Birimi\",\n          description: \"Bitcoin fiyat gösterimi için fiat para birimini seçin.\",\n        },\n      },\n    },\n    experimental: {\n      title: \"Deneysel\",\n      description: \"Bu özellikler deneyseldir.\",\n      receive_swaps: {\n        toggle: \"Takasları al\",\n        badge: \"Beta\",\n        description:\n          \"Ecash Al iletişim kutusunda alınan Ecash'i etkin nanenizle takas etme seçeneği.\",\n      },\n      auto_paste: {\n        toggle: \"Ecash'i otomatik yapıştır\",\n        description:\n          \"Al, sonra Ecash, sonra Yapıştır düğmesine bastığınızda panonuzdaki ecash'i otomatik olarak yapıştırın. Otomatik yapıştırma iOS'ta UI hatalarına neden olabilir, sorun yaşıyorsanız kapatın.\",\n      },\n      auditor: {\n        toggle: \"Denetleyiciyi etkinleştir\",\n        badge: \"Beta\",\n        description:\n          \"Etkinleştirilirse, cüzdan nane detayları iletişim kutusunda denetleyici bilgilerini görüntüler. Denetleyici, nane'lerin güvenilirliğini izleyen üçüncü taraf bir hizmettir.\",\n        url_label: \"Denetleyici URL'si\",\n        api_url_label: \"Denetleyici API URL'si\",\n      },\n      multinut: {\n        toggle: \"Multinut'u Etkinleştir\",\n        description:\n          \"Etkinleştirilirse, cüzdan faturaları aynı anda birden fazla nane'den ödemek için Multinut'u kullanacaktır.\",\n      },\n      nostr_mint_backup: {\n        toggle: \"Nostr'da nane listesini yedekle\",\n        description:\n          \"Etkinleştirilirse, nane listeniz yapılandırılmış Nostr anahtarlarınız kullanılarak otomatik olarak Nostr rölelerine yedeklenecektir. Bu, nane listenizi cihazlar arasında geri yüklemenizi sağlar.\",\n        notifications: {\n          enabled: \"Nostr nane yedeği etkinleştirildi\",\n          disabled: \"Nostr nane yedeği devre dışı bırakıldı\",\n          failed: \"Nostr nane yedeği etkinleştirilemedi\",\n        },\n      },\n    },\n    appearance: {\n      bip177: {\n        title: \"Bitcoin sembolü\",\n        description: \"sats yerine ₿ sembolünü kullan.\",\n        toggle: \"₿ sembolünü kullan\",\n      },\n      keyboard: {\n        title: \"Ekran klavyesi\",\n        description: \"Miktarları girmek için sayısal klavyeyi kullanın.\",\n        toggle: \"Sayısal klavye kullan\",\n        toggle_description:\n          \"Etkinleştirilirse, miktarları girmek için sayısal klavye kullanılacaktır.\",\n      },\n      theme: {\n        title: \"Görünüm\",\n        description: \"Cüzdanınızın görünümünü değiştirin.\",\n        tooltips: {\n          mono: \"mono\",\n          cyber: \"siber\",\n          freedom: \"özgürlük\",\n          nostr: \"nostr\",\n          bitcoin: \"bitcoin\",\n          mint: \"nane\",\n          nut: \"ceviz\",\n          blu: \"mavi\",\n          flamingo: \"flamingo\",\n        },\n      },\n    },\n    advanced: {\n      title: \"Gelişmiş\",\n      developer: {\n        title: \"Geliştirici ayarları\",\n        description: \"Aşağıdaki ayarlar geliştirme ve hata ayıklama içindir.\",\n        new_seed: {\n          button: \"Yeni kurtarma kelimeleri oluştur\",\n          description:\n            \"Bu, yeni bir kurtarma kelimesi oluşturacaktır. Yeni bir kurtarma kelimesiyle geri yükleyebilmek için tüm bakiyenizi kendinize göndermelisiniz.\",\n          confirm_question:\n            \"Yeni bir kurtarma kelimesi oluşturmak istediğinizden emin misiniz?\",\n          cancel: \"İptal\",\n          confirm: \"Onayla\",\n        },\n        remove_spent: {\n          button: \"Harcanmış kanıtları kaldır\",\n          description:\n            \"Etkin nane'lerinizden ecash token'larının harcanıp harcanmadığını kontrol edin ve harcananları cüzdanınızdan kaldırın. Bunu yalnızca cüzdanınız takılı kalırsa kullanın.\",\n        },\n        debug_console: {\n          button: \"Hata Ayıklama Konsolunu Aç/Kapat\",\n          description:\n            \"Javascript hata ayıklama terminalini açın. Anlamadığınız hiçbir şeyi bu terminale yapıştırmayın. Bir hırsız sizi buraya kötü amaçlı kod yapıştırmaya kandırmaya çalışabilir.\",\n        },\n        export_proofs: {\n          button: \"Aktif kanıtları dışa aktar\",\n          description:\n            \"Aktif nane'den tüm bakiyenizi bir Cashu token'ı olarak panonuza kopyalayın. Bu yalnızca seçilen nane ve birimin token'larını dışa aktaracaktır. Tam bir dışa aktarma için farklı bir nane ve birim seçin ve tekrar dışa aktarın.\",\n        },\n        keyset_counters: {\n          title: \"Anahtar kümesi sayaçlarını artır\",\n          description:\n            'Cüzdanınızdaki anahtar kümeleri için türetme yolu sayaçlarını artırmak için anahtar kümesi kimliğine tıklayın. Bu, \"çıktılar zaten imzalandı\" hatasını görüyorsanız yararlıdır.',\n          counter: \"sayaç: {count}\",\n        },\n\n        unset_reserved: {\n          button: \"Tüm ayrılmış token'ları kaldır\",\n          description:\n            \"Bu cüzdan, çifte harcama girişimlerini önlemek için bekleyen giden ecash'i ayrılmış olarak işaretler (ve bakiyenizden düşer). Bu düğme tüm ayrılmış token'ları kaldıracaktır, böylece tekrar kullanılabilirler. Bunu yaparsanız, cüzdanınız harcanmış kanıtlar içerebilir. Onlardan kurtulmak için Harcanmış kanıtları kaldır düğmesine basın.\",\n        },\n        show_onboarding: {\n          button: \"Başlangıç ekranını göster\",\n          description: \"Başlangıç ekranını tekrar gösterin.\",\n        },\n        reset_wallet: {\n          button: \"Cüzdan verilerini sıfırla\",\n          description:\n            \"Cüzdan verilerinizi sıfırlayın. Uyarı: Bu her şeyi siler! Önce bir yedek oluşturduğunuzdan emin olun.\",\n          confirm_question:\n            \"Cüzdan verilerinizi silmek istediğinizden emin misiniz?\",\n          cancel: \"İptal\",\n          confirm: \"Cüzdanı sil\",\n        },\n        export_wallet: {\n          button: \"Cüzdan verilerini dışa aktar\",\n          description:\n            \"Cüzdanınızın bir dökümünü indirin. Yeni bir cüzdanın karşılama ekranından bu dosyadan cüzdanınızı geri yükleyebilirsiniz. Bu dosya, dışa aktardıktan sonra cüzdanınızı kullanmaya devam ederseniz senkronize olmayacaktır.\",\n        },\n      },\n    },\n\n    web_of_trust: {\n      title: \"Güven ağı\",\n      known_pubkeys: \"Bilinen pubkeyler: {wotCount}\",\n\n      continue_crawl: \"Taramaya devam et\",\n      crawl_odell: \"ODELL'İN WEB OF TRUST'unu tara\",\n      crawl_wot: \"Web of trust tara\",\n      pause: \"Duraklat\",\n      reset: \"Sıfırla\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"npubx.cash kullan\",\n      copy_lightning_address: \"Lightning adresini kopyala\",\n      v2_mint: \"npub.cash v2 mint\",\n    },\n    multinut: {\n      use_multinut: \"Multinut kullan\",\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"Bir nane'ye katılın\",\n    subtitle:\n      \"Henüz bir Cashu nane'sine katılmadınız. Başlamak için ayarlardan bir nane URL'si ekleyin veya yeni bir nane'den ecash alın.\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"Ecash Al\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"Geçmiş\",\n      },\n      invoices: {\n        label: \"Faturalar\",\n      },\n      mints: {\n        label: \"Naneler\",\n      },\n    },\n    install: {\n      text: \"Yükle\",\n      tooltip: \"Cashu'yu Yükle\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"Hayır.\",\n    text: \"Başka bir sekme zaten çalışıyor. Bu sekmeyi kapatın ve tekrar deneyin.\",\n    actions: {\n      retry: {\n        label: \"Tekrar dene\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"Oops. Burada bir şey yok…\",\n    actions: {\n      home: {\n        label: \"Ana sayfaya geri dön\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Nane\",\n    },\n    mintBalance: {\n      label: \"Bakiye\",\n    },\n    mintError: {\n      label: \"Nane hatası\",\n    },\n    pending: {\n      label: \"Beklemede\",\n      tooltip: \"Tüm bekleyen token'ları kontrol et\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"Önceki\",\n      },\n      next: {\n        label: \"Sonraki\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"Cashu'ya hoş geldiniz\",\n    text: \"Cashu.me, fonlarınızı güvenli ve gizli tutmak için ecash kullanan ücretsiz ve açık kaynaklı bir Bitcoin cüzdanıdır.\",\n    actions: {\n      more: {\n        label: \"Daha fazla bilgi edinmek için tıklayın\",\n      },\n    },\n    p1: {\n      text: \"Cashu, Bitcoin için ücretsiz ve açık kaynaklı bir ecash protokolüdür. { link } adresinden daha fazla bilgi edinebilirsiniz.\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"Bu cüzdan herhangi bir nane'ye bağlı değildir. Bu cüzdanı kullanmak için güvendiğiniz bir veya daha fazla Cashu nane'sine bağlanmanız gerekir.\",\n    },\n    p3: {\n      text: \"Bu cüzdan, yalnızca sizin erişiminiz olan ecash'i saklar. Kurtarma kelimeleri yedeklemesi olmadan tarayıcı verilerinizi silerseniz, token'larınızı kaybedersiniz.\",\n    },\n    p4: {\n      text: \"Bu cüzdan beta aşamasındadır. Fonlara erişimini kaybeden kişilerden sorumlu değiliz. Kendi sorumluluğunuzda kullanın! Bu kod açık kaynaklıdır ve MIT lisansı altında lisanslanmıştır.\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"PWA Yükle\",\n    alt: { pwa_example: \"PWA kurulum örneği\" },\n    installing: \"Yükleniyor…\",\n    instruction: {\n      intro: {\n        text: \"En iyi deneyim için, cihazınızın yerel web tarayıcısını kullanarak bu cüzdanı Aşamalı Web Uygulaması olarak yükleyin. Bunu hemen yapın.\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Menüye dokunun (sağ üst)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"{ buttonText }'e basın\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"Paylaş'a dokunun (alt)\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"{ buttonText }'e basın\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"Bu uygulamayı cihazınıza yükledikten sonra bu tarayıcı penceresini kapatın ve uygulamayı ana ekranınızdan kullanın.\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"Başarılı!\",\n        text: \"Cashu'yu PWA olarak kullanıyorsunuz. Diğer açık tarayıcı pencerelerini kapatın ve uygulamayı ana ekranınızdan kullanın.\",\n        nextSteps:\n          \"Artık bu sekmeyi kapatıp uygulamayı ana ekranınızdan açabilirsiniz.\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"{ icon } ve { buttonText }'e dokunun\",\n    buttonText: \"Ana Ekrana Ekle\",\n  },\n  AndroidPWAPrompt: {\n    text: \"{ icon } ve { buttonText }'e dokunun\",\n    buttonText: \"Ana Ekrana Ekle\",\n  },\n  WelcomeSlide3: {\n    title: \"Kurtarma Kelimeleriniz\",\n    text: \"Kurtarma kelimelerinizi bir parola yöneticisinde veya kağıt üzerinde saklayın. Cihaza erişiminizi kaybederseniz fonlarınızı kurtarmanın tek yolu kurtarma kelimelerinizdir.\",\n    inputs: {\n      seed_phrase: {\n        label: \"Kurtarma Kelimeleri\",\n        caption: \"Kurtarma kelimelerinizi ayarlarda görebilirsiniz.\",\n      },\n      checkbox: {\n        label: \"Yazdım\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"Şartlar\",\n    actions: {\n      more: {\n        label: \"Hizmet Şartlarını Oku\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"Bu şartları ve koşulları okudum ve kabul ediyorum\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"Cüzdanınızı ayarlayın\",\n    text: \"Bir seed ifadesinden mi kurtarmak istersiniz yoksa yeni bir cüzdan mı oluşturmak istersiniz?\",\n    options: {\n      new: {\n        title: \"Yeni cüzdan oluştur\",\n        subtitle: \"Yeni bir seed oluşturun ve mint ekleyin.\",\n      },\n      recover: {\n        title: \"Cüzdanı kurtar\",\n        subtitle: \"Seed ifadenizi girin, mintleri ve ecash'i geri yükleyin.\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"Mint ekle\",\n    text: \"Mintler ecash göndermenize ve almanıza yardımcı olan sunuculardır. Keşfedilen bir minti seçin veya manuel ekleyin. Daha sonra da ekleyebilirsiniz.\",\n    sections: { your_mints: \"Mintleriniz\" },\n    restoring: \"Mintler geri yükleniyor…\",\n    placeholder: { mint_url: \"https://\" },\n  },\n  WelcomeRecoverSeed: {\n    title: \"Seed ifadenizi girin\",\n    text: \"Kurtarmak için 12 kelimelik seed ifadenizi yapıştırın veya yazın.\",\n    inputs: { word: \"Kelime { index }\" },\n    actions: { paste_all: \"Tümünü yapıştır\" },\n    disclaimer:\n      \"Seed ifadeniz yalnızca yerelde cüzdan anahtarlarını türetmek için kullanılır.\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"Ecash'inizi geri yükleyin\",\n    text: \"Yapılandırılmış mintlerinizde harcanmamış kanıtları tarayın ve cüzdanınıza ekleyin.\",\n  },\n  MintRatings: {\n    title: \"Mint yorumları\",\n    reviews: \"yorum\",\n    ratings: \"Değerlendirmeler\",\n    no_reviews: \"Hiç yorum bulunamadı\",\n    your_review: \"Yorumunuz\",\n    no_reviews_to_display: \"Gösterilecek yorum yok.\",\n    no_rating: \"Puan yok\",\n    out_of: \"üzerinden\",\n    rows: \"Reviews\",\n    sort: \"Sırala\",\n    sort_options: {\n      newest: \"En yeni\",\n      oldest: \"En eski\",\n      highest: \"En yüksek\",\n      lowest: \"En düşük\",\n    },\n    actions: { write_review: \"Yorum yaz\" },\n    empty_state_subtitle:\n      \"Bir yorum bırakarak yardımcı olun. Bu mint ile ilgili deneyiminizi paylaşın ve bir yorum bırakarak başkalarına yardımcı olun.\",\n  },\n  CreateMintReview: {\n    title: \"Mint yorumu\",\n    publishing_as: \"Şu kişi olarak yayımlanıyor\",\n    inputs: {\n      rating: { label: \"Puan\" },\n      review: { label: \"Yorum (isteğe bağlı)\" },\n    },\n    actions: {\n      publish: { label: \"Yayımla\", in_progress: \"Yayımlanıyor…\" },\n    },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"Kurtarma Kelimelerinden Geri Yükle\",\n      caption:\n        \"Cüzdanınızı geri yüklemek için kurtarma kelimelerinizi girin. Geri yüklemeden önce, daha önce kullandığınız tüm nane'leri eklediğinizden emin olun.\",\n      inputs: {\n        seed_phrase: {\n          label: \"Kurtarma kelimeleri\",\n          caption: \"Kurtarma kelimelerinizi ayarlarda görebilirsiniz.\",\n        },\n      },\n    },\n    information: {\n      label: \"Bilgi\",\n      caption:\n        \"Sihirbaz yalnızca başka bir kurtarma kelimesinden ecash'i geri yükleyecektir, şu anda kullandığınız cüzdanın kurtarma kelimesini kullanamayacak veya değiştiremeyeceksiniz. Bu, geri yüklenen ecash'in bir kez kendinize göndermediğiniz sürece mevcut kurtarma kelimeniz tarafından korunmayacağı anlamına gelir.\",\n    },\n    restore_mints: {\n      label: \"Nane'leri Geri Yükle\",\n      caption:\n        \"Geri yüklenecek nane'yi seçin. Ana ekranda 'Naneler' altında daha fazla nane ekleyebilir ve buradan geri yükleyebilirsiniz.\",\n    },\n    nostr_mints: {\n      label: \"Nostr'dan Naneleri Geri Yükle\",\n      caption:\n        \"Seed ifadenizi kullanarak Nostr rölelerinde depolanan nane yedeklerini arayın. Bu, daha önce kullandığınız naneleri keşfetmenize yardımcı olacaktır.\",\n      search_button: \"Nane Yedeklerini Ara\",\n      select_all: \"Tümünü Seç\",\n      deselect_all: \"Tüm Seçimi Kaldır\",\n      backed_up: \"Yedeklendi\",\n      already_added: \"Zaten Eklendi\",\n      add_selected: \"Seçileni Ekle ({count})\",\n      no_backups_found: \"Nane yedeği bulunamadı\",\n      no_backups_hint:\n        \"Nane listenizi otomatik olarak yedeklemek için ayarlarda Nostr nane yedeğinin etkinleştirildiğinden emin olun.\",\n      invalid_mnemonic: \"Lütfen aramadan önce geçerli bir seed ifadesi girin.\",\n      search_error: \"Nane yedekleri aranırken hata oluştu.\",\n      add_error: \"Seçilen naneler eklenirken hata oluştu.\",\n    },\n    actions: {\n      paste: {\n        error: \"Pano içeriği okunamadı.\",\n      },\n      validate: {\n        error: \"Anımsatıcı en az 12 kelime olmalıdır.\",\n      },\n      select_all: {\n        label: \"Tümünü Seç\",\n      },\n      deselect_all: {\n        label: \"Tüm Seçimi Kaldır\",\n      },\n      restore: {\n        label: \"Geri Yükle\",\n        in_progress: \"Nane geri yükleniyor…\",\n        error: \"Nane geri yükleme hatası: { error }\",\n      },\n      restore_all_mints: {\n        label: \"Tüm Nane'leri Geri Yükle\",\n        in_progress: \"{ length } nane'den { index } geri yükleniyor…\",\n        success: \"Geri yükleme başarıyla tamamlandı\",\n        error: \"Nane'leri geri yükleme hatası: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"Seçili Naneleri Geri Yükle ({count})\",\n        in_progress: \"{ length } nane'den { index } geri yükleniyor…\",\n        success: \"{count} nane başarıyla geri yüklendi\",\n        error: \"Seçili naneleri geri yükleme hatası: { error }\",\n      },\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"Nane ekle\",\n      description:\n        \"Bağlanmak için bir Cashu nane'sinin URL'sini girin. Bu cüzdan herhangi bir nane'ye bağlı değildir.\",\n      inputs: {\n        nickname: {\n          placeholder: \"Takma ad (örneğin Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"Geçersiz URL\",\n        },\n        scan: {\n          label: \"QR Kodu Tara\",\n        },\n      },\n    },\n    discover: {\n      title: \"Nane'leri keşfet\",\n      overline: \"Keşfet\",\n      caption: \"Diğer kullanıcıların nostr'da önerdiği nane'leri keşfet.\",\n      actions: {\n        discover: {\n          label: \"Nane'leri keşfet\",\n          in_progress: \"Yükleniyor…\",\n          error_no_mints: \"Nane bulunamadı\",\n          success: \"{ length } nane bulundu\",\n        },\n      },\n      recommendations: {\n        overline: \"{ length } nane bulundu\",\n        caption:\n          \"Bu nane'ler diğer Nostr kullanıcıları tarafından önerildi. Dikkatli olun ve bir nane kullanmadan önce kendi araştırmanızı yapın.\",\n        actions: {\n          browse: {\n            label: \"Nane'lere göz atmak için tıklayın\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"Değiştir\",\n      overline: \"Çoklu Nane Takasları\",\n      caption:\n        \"Fonları Lightning aracılığıyla nane'ler arasında değiştirin. Not: Potansiyel Lightning ücretleri için yer bırakın. Gelen ödeme başarılı olmazsa, faturayı manuel olarak kontrol edin.\",\n      inputs: {\n        from: {\n          label: \"Kimden\",\n        },\n        to: {\n          label: \"Kime\",\n        },\n        amount: {\n          label: \"Miktar ({ ticker })\",\n        },\n      },\n      actions: {\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n    },\n    error_badge: \"Hata\",\n    reviews_text: \"yorumlar\",\n    no_reviews_yet: \"Henüz yorum yok\",\n    discover_mints_button: \"Naneleri keşfet\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"%{ percentage }\",\n      keep_scanning_text: \" - Taramaya devam et\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"Lightning Al\",\n    create_invoice_title: \"Fatura Oluştur\",\n    inputs: {\n      amount: {\n        label: \"Miktar ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"Fatura Oluştur\",\n        label_blocked: \"Fatura oluşturuluyor…\",\n        in_progress: \"Oluşturuluyor\",\n      },\n    },\n    invoice: {\n      caption: \"Lightning faturası\",\n      status_paid_text: \"Ödendi!\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"Gönder\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Nane yok\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"Nane yok\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"Ecash Gönder\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"Çevrimdışı\",\n    inputs: {\n      amount: {\n        label: \"Miktar ({ ticker }) *\",\n        invalid_too_much_error_text: \"Çok fazla\",\n      },\n      p2pk_pubkey: {\n        label: \"Alıcının genel anahtarı\",\n        label_invalid: \"Alıcının genel anahtarı\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"Emoji kopyala\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"Bağlantıyı kopyala\",\n      },\n      share: {\n        tooltip_text: \"Ecash'ını paylaş\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"Geçmişten sil\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"NFC kartına flaşla\",\n          ndef_unsupported_text: \"NDEF desteklenmiyor\",\n        },\n      },\n    },\n  },\n  ReceiveDialog: {\n    title: \"Al\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"Nane yok\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints:\n          \"Lightning aracılığıyla almak için bir nane'ye bağlanmanız gerekir\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"Ecash Al\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"Talep et\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"Taranıyor…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"Ecash Al\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"Cashu token'ını yapıştır\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"Geçersiz token\",\n      },\n      p2pk_lock_mismatch: {\n        label:\n          \"Alınamıyor. Bu token'ın P2PK kilidi genel anahtarınızla eşleşmiyor.\",\n      },\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"Nane ekleniyor…\",\n      },\n      swap: {\n        label: \"@:global.actions.swap.label\",\n        tooltip_text: \"Güvenilen bir nane'ye takas et\",\n        caption: \"{ value } takas et\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"Takası iptal et\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      later: {\n        label: \"Daha sonra al\",\n        tooltip_text: \"Daha sonra almak için geçmişe ekle\",\n        already_in_history_success_text: \"Ecash zaten Geçmişte\",\n        added_to_history_success_text: \"Ecash Geçmişe eklendi\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"NFC kartından oku\",\n          ndef_unsupported_text: \"NDEF desteklenmiyor\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"P2PK Anahtarı\",\n      description: \"Bu anahtara kilitlenmiş ecash al\",\n      used_warning_text:\n        \"Uyarı: Bu anahtar daha önce kullanıldı. Daha iyi gizlilik için yeni bir anahtar kullanın.\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"Yeni anahtar oluştur\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"Ödeme Talebi\",\n      description: \"Nostr aracılığıyla ödeme al\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"Yeni talep\",\n      },\n      add_amount: {\n        label: \"Miktar ekle\",\n      },\n      use_active_mint: {\n        label: \"Herhangi bir nane\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"Miktar girin\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text:\n          \"Klavye devre dışı bırakıldı. Klavyeyi ayarlardan yeniden etkinleştirebilirsiniz.\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostr Cüzdan Bağlantısı\",\n      description:\n        \"NWC ile cüzdanınızı uzaktan kontrol edin. Cüzdanınızı uyumlu bir uygulamayla bağlamak için QR koduna basın.\",\n      warning_text:\n        \"Uyarı: Bu bağlantı dizesine erişimi olan herkes cüzdanınızdan ödeme başlatabilir. Paylaşmayın!\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"Nane Mesajı\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"İletişim\",\n    },\n    details: {\n      title: \"Nane detayları\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Kurallar\",\n        actions: {\n          show: {\n            label: \"Tümünü görüntüle\",\n          },\n          hide: {\n            label: \"Gizle\",\n          },\n        },\n      },\n      currency: {\n        label: \"Para Birimi\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"Sürüm\",\n      },\n    },\n    actions: {\n      title: \"Eylemler\",\n      copy_mint_url: {\n        label: \"Nane URL'sini kopyala\",\n      },\n      delete: {\n        label: \"Nane'yi sil\",\n      },\n      edit: {\n        label: \"Nane'yi düzenle\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"Bir nane seçin\",\n    badge_mint_error_text: \"Hata\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"Henüz geçmiş yok\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"{ value } önce\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Durumu kontrol et\",\n      },\n      receive: {\n        tooltip_text: \"Al\",\n      },\n      filter_pending: {\n        label: \"Bekleyenleri filtrele\",\n      },\n      show_all: {\n        label: \"Tümünü göster\",\n      },\n    },\n    old_token_not_found_error_text: \"Eski token bulunamadı\",\n  },\n  InvoiceTable: {\n    empty_text: \"Henüz fatura yok\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"Kopyalamak için tıklayın\",\n      date_label: \"{ value } önce\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"Durumu kontrol et\",\n      },\n      filter_pending: {\n        label: \"Bekleyenleri filtrele\",\n      },\n      show_all: {\n        label: \"Tümünü göster\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"Bu nane'yi silmek istediğinizden emin misiniz?\",\n    nickname: {\n      label: \"Takma ad\",\n    },\n    balances: {\n      label: \"Bakiyeler\",\n    },\n    warning_text:\n      \"Not: Bu cüzdan paranoyak olduğu için, bu nane'den ecash'iniz aslında silinmeyecek, ancak cihazınızda saklanmaya devam edecektir. Bu nane'yi daha sonra tekrar eklerseniz yeniden göründüğünü göreceksiniz.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"Nane'yi kaldır\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"Cashu token veya Lightning adresi\",\n      receive: \"Cashu token\",\n      pay: \"Lightning adresi veya faturası\",\n    },\n    qr_scanner: {\n      title: \"QR Kodu Tara\",\n      description: \"Bir adresi taramak için dokunun\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"Lightning öde\",\n      inputs: {\n        invoice_data: {\n          label: \"Lightning faturası veya adresi\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"{ payee }, { value } { ticker } talep ediyor\",\n      amount_range_label:\n        \"{ payee }{br} { min } ile { max } { ticker } arasında talep ediyor\",\n      sending_to_lightning_address: \"{ address } adresine gönderiliyor\",\n      inputs: {\n        amount: {\n          label: \"Miktar ({ ticker }) *\",\n        },\n        comment: {\n          label: \"Yorum (isteğe bağlı)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"{ value } öde\",\n      paying: \"Ödeniyor\",\n      paid: \"Ödendi\",\n      fee: \"Ücret\",\n      memo: {\n        label: \"Memo\",\n      },\n      processing_info_text: \"İşleniyor…\",\n      balance_too_low_warning_text: \"Bakiye yetersiz\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"Öde\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"Hata\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"Nane'yi düzenle\",\n    inputs: {\n      nickname: {\n        label: \"Takma ad\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"Bu nane'ye güveniyor musunuz?\",\n    description:\n      \"Bu nane'yi kullanmadan önce güvendiğinizden emin olun. Nane'ler herhangi bir zamanda kötü niyetli hale gelebilir veya faaliyetlerini durdurabilir.\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"Nane ekleniyor\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"Lütfen bir anımsatıcı girin\",\n    restore_mint_error_text: \"Nane geri yükleme hatası: { error }\",\n    prepare_info_text: \"Geri yükleme süreci hazırlanıyor…\",\n    restored_proofs_for_keyset_info_text:\n      \"{ keysetId } anahtar kümesi için { restoreCounter } kanıt geri yüklendi\",\n    checking_proofs_for_keyset_info_text:\n      \"{ keysetId } anahtar kümesi için { startIndex } ila { endIndex } kanıtları kontrol ediliyor\",\n    no_proofs_info_text: \"Geri yüklenecek kanıt bulunamadı\",\n    restored_amount_success_text: \"{ amount } geri yüklendi\",\n  },\n  swap: {\n    in_progress_warning_text: \"Takas devam ediyor\",\n    invalid_swap_data_error_text: \"Geçersiz takas verisi\",\n    swap_error_text: \"Takas hatası\",\n  },\n  TokenInformation: {\n    fee: \"Ücret\",\n    unit: \"Birim\",\n    fiat: \"Fiat\",\n    p2pk: \"P2PK\",\n    locked: \"Kilitli\",\n    locked_to_you: \"Size kilitli\",\n    mint: \"Darphane\",\n    memo: \"Not\",\n    payment_request: \"Ödeme talebi\",\n    nostr: \"Nostr\",\n    token_copied: \"Token panoya kopyalandı\",\n  },\n};\n"
  },
  {
    "path": "src/i18n/zh-CN/index.ts",
    "content": "export default {\n  MultinutPicker: {\n    payment: \"多坚果支付\",\n    selectMints: \"选择一个或多个铸币厂来发起支付。\",\n    totalSelectedBalance: \"所选总余额\",\n    multiMintPay: \"多铸币支付\",\n    balanceNotEnough: \"多铸币余额不足以支付此发票\",\n    failed: \"处理失败：{error}\",\n    paid: \"通过闪电网络支付了 {amount}\",\n  },\n\n  global: {\n    copy_to_clipboard: {\n      success: \"已复制到剪贴板！\",\n    },\n    actions: {\n      add_mint: {\n        label: \"添加 Mint\",\n      },\n      cancel: {\n        label: \"取消\",\n      },\n      copy: {\n        label: \"复制\",\n      },\n      close: {\n        label: \"关闭\",\n      },\n      enter: {\n        label: \"输入\",\n      },\n      lock: {\n        label: \"锁定\",\n      },\n      paste: {\n        label: \"粘贴\",\n      },\n      receive: {\n        label: \"接收\",\n      },\n      scan: {\n        label: \"扫描\",\n      },\n      send: {\n        label: \"发送\",\n      },\n      swap: {\n        label: \"兑换\",\n      },\n      update: {\n        label: \"更新\",\n      },\n    },\n    inputs: {\n      mint_url: {\n        label: \"Mint URL\",\n      },\n    },\n  },\n  wallet: {\n    notifications: {\n      balance_too_low: \"余额不足\",\n      received: \"已收到 {amount}\",\n      fee: \" (手续费: {fee})\",\n      could_not_request_mint: \"无法请求铸造\",\n      invoice_still_pending: \"发票仍在处理中\",\n      paid_lightning: \"通过闪电网络支付了 {amount}\",\n      payment_pending_refresh: \"付款正在处理。请手动刷新发票。\",\n      sent: \"已发送 {amount}\",\n      token_still_pending: \"代币仍在处理中\",\n      received_lightning: \"通过闪电网络收到 {amount}\",\n      lightning_payment_failed: \"闪电网络支付失败\",\n      failed_to_decode_invoice: \"无法解码发票\",\n\n      invalid_lnurl: \"无效的LNURL\",\n      lnurl_error: \"LNURL错误\",\n      no_amount: \"没有金额\",\n      no_lnurl_data: \"没有LNURL数据\",\n      no_price_data: \"没有价格数据。\",\n      please_try_again: \"请重试。\",\n    },\n    mint: {\n      notifications: {\n        already_added: \"Mint 已添加\",\n        added: \"Mint 已添加\",\n        not_found: \"未找到 Mint\",\n        activation_failed: \"Mint 激活失败\",\n        no_active_mint: \"没有激活的 Mint\",\n        unit_activation_failed: \"单位激活失败\",\n        unit_not_supported: \"Mint 不支持该单位\",\n        activated: \"Mint 已激活\",\n        could_not_connect: \"无法连接到 Mint\",\n        could_not_get_info: \"无法获取 Mint 信息\",\n        could_not_get_keys: \"无法获取 Mint 密钥\",\n        could_not_get_keysets: \"无法获取 Mint 密钥集\",\n        removed: \"Mint 已移除\",\n        error: \"Mint 错误\",\n        mint_validation_error: \"铸币厂验证错误\",\n      },\n    },\n  },\n  MainHeader: {\n    menu: {\n      settings: {\n        title: \"设置\",\n        settings: {\n          title: \"设置\",\n          caption: \"钱包配置\",\n        },\n      },\n      terms: {\n        title: \"条款\",\n        terms: {\n          title: \"条款\",\n          caption: \"服务条款\",\n        },\n      },\n      links: {\n        title: \"链接\",\n        cashuSpace: {\n          title: \"Cashu.space\",\n          caption: \"cashu.space\",\n        },\n        github: {\n          title: \"Github\",\n          caption: \"github.com/cashubtc\",\n        },\n        telegram: {\n          title: \"Telegram\",\n          caption: \"t.me/CashuMe\",\n        },\n        twitter: {\n          title: \"Twitter\",\n          caption: \"{'@'}CashuBTC\",\n        },\n        donate: {\n          title: \"捐赠\",\n          caption: \"支持 Cashu\",\n        },\n      },\n    },\n    offline: {\n      warning: {\n        text: \"离线\",\n      },\n    },\n    reload: {\n      warning: {\n        text: \"在 { countdown } 后重新加载\",\n      },\n    },\n    staging: {\n      warning: {\n        text: \"测试环境 – 请勿使用真实资金！\",\n      },\n    },\n  },\n  FullscreenHeader: {\n    actions: {\n      back: {\n        label: \"钱包\",\n      },\n    },\n  },\n  Settings: {\n    web_of_trust: {\n      title: \"信任网络\",\n      known_pubkeys: \"已知公钥：{wotCount}\",\n\n      continue_crawl: \"继续抓取\",\n      crawl_odell: \"抓取 ODELL 的信任网络\",\n      crawl_wot: \"抓取信任网络\",\n      pause: \"暂停\",\n      reset: \"重置\",\n      progress: \"{crawlProcessed} / {crawlTotal}\",\n    },\n    npub_cash: {\n      use_npubx: \"使用 npubx.cash\",\n      copy_lightning_address: \"复制闪电地址\",\n      v2_mint: \"npub.cash v2 铸币厂\",\n    },\n    multinut: {\n      use_multinut: \"使用 Multinut\",\n    },\n    language: {\n      title: \"语言\",\n      description: \"请从下方列表中选择您的首选语言。\",\n    },\n    sections: {\n      backup_restore: \"备份与恢复\",\n      lightning_address: \"LIGHTNING 地址\",\n      nostr_keys: \"NOSTR 密钥\",\n      nostr: {\n        title: \"NOSTR\",\n        relays: {\n          expand_label: \"点击编辑中继\",\n          add: {\n            title: \"添加中继\",\n            description:\n              \"您的钱包使用这些中继进行nostr操作，例如付款请求、nostr钱包连接和备份。\",\n          },\n          list: {\n            title: \"中继\",\n            description: \"您的钱包将连接到这些中继。\",\n            copy_tooltip: \"复制中继\",\n            remove_tooltip: \"删除中继\",\n          },\n        },\n      },\n\n      payment_requests: \"支付请求\",\n      nostr_wallet_connect: \"NOSTR 钱包连接\",\n      hardware_features: \"硬件功能\",\n      p2pk_features: \"P2PK 功能\",\n      privacy: \"隐私\",\n      experimental: \"实验性\",\n      appearance: \"外观\",\n    },\n    backup_restore: {\n      backup_seed: {\n        title: \"备份种子短语\",\n        description: \"您的种子短语可以恢复您的钱包。请务必妥善保管并保密。\",\n        seed_phrase_label: \"种子短语\",\n      },\n      restore_ecash: {\n        title: \"恢复 ecash\",\n        description:\n          \"恢复向导允许您从助记符种子短语中恢复丢失的 ecash。您当前钱包的种子短语不会受到影响，该向导仅允许您从另一个种子短语中 恢复 ecash。\",\n        button: \"恢复\",\n      },\n    },\n    lightning_address: {\n      title: \"Lightning 地址\",\n      description: \"接收支付到您的 Lightning 地址。\",\n      enable: {\n        toggle: \"启用\",\n        description: \"带有 npub.cash 的 Lightning 地址\",\n      },\n      address: {\n        copy_tooltip: \"复制 Lightning 地址\",\n      },\n      automatic_claim: {\n        toggle: \"自动认领\",\n        description: \"自动接收收到的支付。\",\n      },\n      npc_v2: {\n        choose_mint_title: \"为 npub.cash v2 选择铸币厂\",\n        choose_mint_placeholder: \"选择一个铸币厂…\",\n      },\n    },\n    nostr_keys: {\n      title: \"您的 Nostr 密钥\",\n      description: \"为您的 Lightning 地址设置 Nostr 密钥。\",\n      wallet_seed: {\n        title: \"钱包种子短语\",\n        description: \"从钱包种子生成 Nostr 密钥对\",\n        copy_nsec: \"复制 nsec\",\n      },\n      nsec_bunker: {\n        title: \"Nsec Bunker\",\n        description: \"使用 NIP-46 bunker\",\n        delete_tooltip: \"删除连接\",\n      },\n      use_nsec: {\n        title: \"使用您的 nsec\",\n        description: \"这种方法很危险，不建议使用\",\n        delete_tooltip: \"删除 nsec\",\n      },\n      signing_extension: {\n        title: \"签名扩展\",\n        description: \"使用 NIP-07 签名扩展\",\n        not_found: \"未找到 NIP-07 签名扩展\",\n      },\n    },\n\n    payment_requests: {\n      title: \"支付请求\",\n      description:\n        \"支付请求允许您通过 Nostr 接收支付。如果您启用此功能，您的钱包将订阅您的 Nostr 中继。\",\n      enable_toggle: \"启用支付请求\",\n      claim_automatically: {\n        toggle: \"自动认领\",\n        description: \"自动接收收到的支付。\",\n      },\n    },\n    nostr_wallet_connect: {\n      title: \"Nostr 钱包连接 (NWC)\",\n      description: \"使用 NWC 从任何其他应用程序控制您的钱包。\",\n      enable_toggle: \"启用 NWC\",\n      payments_note:\n        \"您只能使用 NWC 从您的比特币余额支付。支付将从您激活的 Mint 进行。\",\n      connection: {\n        copy_tooltip: \"复制连接字符串\",\n        qr_tooltip: \"显示二维码\",\n        allowance_label: \"剩余额度 (sat)\",\n      },\n    },\n    hardware_features: {\n      webnfc: {\n        title: \"WebNFC\",\n        description: \"选择写入 NFC 卡的编码\",\n        text: {\n          title: \"文本\",\n          description: \"以纯文本格式存储 token\",\n        },\n        weburl: {\n          title: \"URL\",\n          description: \"存储此钱包的 URL 和 token\",\n        },\n        binary: {\n          title: \"二进制\",\n          description: \"将令牌存储为二进制数据\",\n        },\n        quick_access: {\n          toggle: \"NFC 快速访问\",\n          description:\n            \"在 '接收 Ecash' 菜单中快速扫描 NFC 卡。此选项会在 '接收 Ecash' 菜单中添加一个 NFC 按钮。\",\n        },\n      },\n    },\n    p2pk_features: {\n      title: \"P2PK\",\n      description:\n        \"生成密钥对以接收 P2PK 锁定的 ecash。警告：此功能是实验性的。仅用于小额。如果您丢失了您的私钥，将没有人能够再解锁锁定到它的 ecash。\",\n      generate_button: \"生成密钥\",\n      import_button: \"导入 nsec\",\n      quick_access: {\n        toggle: \"快速访问锁定\",\n        description:\n          \"使用此功能在 '接收 Ecash' 菜单中快速显示您的 P2PK 锁定密钥。\",\n      },\n      keys_expansion: {\n        label: \"点击浏览 {count} 个密钥\",\n        used_badge: \"已使用\",\n      },\n    },\n    privacy: {\n      title: \"隐私\",\n      description: \"这些设置会影响您的隐私。\",\n      check_incoming: {\n        toggle: \"检查收到的发票\",\n        description:\n          \"如果启用，钱包会在后台检查最新的发票。这增加了钱包的响应速度，但也使指纹识别更容易。您可以在发票标签中手动检查未付款的发票。\",\n      },\n      check_startup: {\n        toggle: \"启动时检查待处理发票\",\n        description: \"如果启用，钱包会在启动时检查过去 24 小时内待处理的发票。\",\n      },\n      check_all: {\n        toggle: \"检查所有发票\",\n        description:\n          \"如果启用，钱包会在后台定期检查未付款的发票，最长可达两周。这增加了钱包的在线活动，从而使指纹识别更容易。您可以在发票标签中手动检查未付款的发票。\",\n      },\n      check_sent: {\n        toggle: \"检查已发送的 ecash\",\n        description:\n          \"如果启用，钱包会使用周期性后台检查来确定已发送的 token 是否已被兑换。这增加了钱包的在线活动，从而使指纹识别更容易。\",\n      },\n      websockets: {\n        toggle: \"使用 WebSockets\",\n        description:\n          \"如果启用，钱包将使用长连接的 WebSocket 来接收有关已付款发票和已花费 token 的更新信息。这增加了钱包的响应速度，但也使指纹识别更容易。\",\n      },\n      bitcoin_price: {\n        toggle: \"从 Coinbase 获取汇率\",\n        description:\n          \"如果启用，将从 coinbase.com 获取当前比特币汇率并显示您的转换后余额。\",\n        currency: {\n          title: \"法定货币\",\n          description: \"选择用于比特币价格显示的法定货币。\",\n        },\n      },\n    },\n    experimental: {\n      title: \"实验性\",\n      description: \"这些功能是实验性的。\",\n      receive_swaps: {\n        toggle: \"接收 swaps\",\n        badge: \"测试版\",\n        description:\n          \"在接收 Ecash 对话框中，选择将接收到的 Ecash 兑换为您的激活 Mint。\",\n      },\n      auto_paste: {\n        toggle: \"自动粘贴 Ecash\",\n        description:\n          \"当您按接收，然后Ecash，然后粘贴时，自动粘贴剪贴板中的 ecash。自动粘贴可能会导致 iOS 中的 UI 故障，如果您遇到问题，请关闭此功能。\",\n      },\n      auditor: {\n        toggle: \"启用审计器\",\n        badge: \"测试版\",\n        description:\n          \"如果启用，钱包将在 Mint 详细信息对话框中显示审计器信息。审计器是第三方服务，用于监控 Mint 的可靠性。\",\n        url_label: \"审计器 URL\",\n        api_url_label: \"审计器 API URL\",\n      },\n      multinut: {\n        toggle: \"启用 Multinut\",\n        description: \"如果启用，钱包将使用 Multinut 同时从多个 Mint 支付发票。\",\n      },\n      nostr_mint_backup: {\n        toggle: \"在 Nostr 上备份 Mint 列表\",\n        description:\n          \"如果启用，您的 Mint 列表将使用您配置的 Nostr 密钥自动备份到 Nostr 中继。这允许您在不同设备上恢复您的 Mint 列表。\",\n        notifications: {\n          enabled: \"Nostr Mint 备份已启用\",\n          disabled: \"Nostr Mint 备份已禁用\",\n          failed: \"无法启用 Nostr Mint 备份\",\n        },\n      },\n    },\n    appearance: {\n      keyboard: {\n        title: \"屏幕键盘\",\n        description: \"使用数字键盘输入金额。\",\n        toggle: \"使用数字键盘\",\n        toggle_description: \"如果启用，将使用数字键盘输入金额。\",\n      },\n      theme: {\n        title: \"外观\",\n        description: \"更改您钱包的外观。\",\n        tooltips: {\n          mono: \"单色\",\n          cyber: \"赛博\",\n          freedom: \"自由\",\n          nostr: \"nostr\",\n          bitcoin: \"比特币\",\n          mint: \"薄荷\",\n          nut: \"坚果\",\n          blu: \"蓝色\",\n          flamingo: \"火烈鸟\",\n        },\n      },\n      bip177: {\n        title: \"比特币符号\",\n        description: \"使用 ₿ 符号代替 sats。\",\n        toggle: \"使用 ₿ 符号\",\n      },\n    },\n    advanced: {\n      title: \"高级\",\n      developer: {\n        title: \"开发者设置\",\n        description: \"以下设置为开发和调试用途。\",\n        new_seed: {\n          button: \"生成新的种子短语\",\n          description:\n            \"这将生成一个新的种子短语。您必须将您的全部余额发送给自己，以便能够使用新的种子恢复。\",\n          confirm_question: \"您确定要生成新的种子短语吗？\",\n          cancel: \"取消\",\n          confirm: \"确认\",\n        },\n        remove_spent: {\n          button: \"删除已花费的证明\",\n          description:\n            \"检查您的活动 Mint 中的 ecash token 是否已花费，并从您的钱包中删除已花费的 token。仅当您的钱包卡住时使用此功能。\",\n        },\n        debug_console: {\n          button: \"切换调试控制台\",\n          description:\n            \"打开 Javascript 调试终端。切勿向此终端粘贴您不理解的任何内容。小偷可能会试图欺骗您在此处粘贴恶意代码。\",\n        },\n        export_proofs: {\n          button: \"导出活动证明\",\n          description:\n            \"将活动 Mint 中的全部余额作为 Cashu token 复制到剪贴板。这只会导出所选 Mint 和单位的 token。要进行完全导出，请选择不同的 Mint 和单位并再次导出。\",\n        },\n        keyset_counters: {\n          title: \"增加 keyset 计数器\",\n          description:\n            \"点击 keyset ID 以增加您钱包中 keysets 的 derivation path 计数器。如果您看到输出已被签名错误，这将很有用。\",\n          counter: \"计数器: {count}\",\n        },\n        unset_reserved: {\n          button: \"取消所有保留的 token\",\n          description:\n            \"此钱包会将待处理的传出 ecash 标记为已保留（并从您的余额中扣除），以防止双重支付尝试。此按钮将取消所有保留的 token，以便可以再次使用它们。如果您执行此操作，您的钱包可能会包含已花费的证明。按删除已花费的证明按钮以清除它们。\",\n        },\n        show_onboarding: {\n          button: \"显示入门指南\",\n          description: \"再次显示入门指南屏幕。\",\n        },\n        reset_wallet: {\n          button: \"重置钱包数据\",\n          description:\n            \"重置您的钱包数据。警告：这将删除所有内容！请务必先创建备份。\",\n          confirm_question: \"您确定要删除您的钱包数据吗？\",\n          cancel: \"取消\",\n          confirm: \"删除钱包\",\n        },\n        export_wallet: {\n          button: \"导出钱包数据\",\n          description:\n            \"下载您的钱包数据。您可以在新钱包的欢迎屏幕上从该文件中恢复您的钱包。如果您在导出后继续使用您的钱包，该文件将不同步。\",\n        },\n      },\n    },\n  },\n  NoMintWarnBanner: {\n    title: \"加入 Mint\",\n    subtitle:\n      \"您还没有加入任何 Cashu Mint。请在设置中添加 Mint URL 或接收来自新 Mint 的 ecash 以开始。\",\n    actions: {\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n      },\n      receive: {\n        label: \"接收 Ecash\",\n      },\n    },\n  },\n  WalletPage: {\n    actions: {\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n      },\n    },\n    tabs: {\n      history: {\n        label: \"历史记录\",\n      },\n      invoices: {\n        label: \"发票\",\n      },\n      mints: {\n        label: \"Mints\",\n      },\n    },\n    install: {\n      text: \"安装\",\n      tooltip: \"安装 Cashu\",\n    },\n  },\n  AlreadyRunning: {\n    title: \"不行。\",\n    text: \"另一个标签页正在运行。请关闭此标签页并重试。\",\n    actions: {\n      retry: {\n        label: \"重试\",\n      },\n    },\n  },\n  ErrorNotFound: {\n    title: \"404\",\n    text: \"哎呀。这里什么都没有…\",\n    actions: {\n      home: {\n        label: \"返回主页\",\n      },\n    },\n  },\n  BalanceView: {\n    mintUrl: {\n      label: \"Mint\",\n    },\n    mintBalance: {\n      label: \"余额\",\n    },\n    mintError: {\n      label: \"Mint 错误\",\n    },\n    pending: {\n      label: \"待处理\",\n      tooltip: \"检查所有待处理的 token\",\n    },\n  },\n  WelcomePage: {\n    actions: {\n      previous: {\n        label: \"上一步\",\n      },\n      next: {\n        label: \"下一步\",\n      },\n    },\n  },\n  WelcomeSlide1: {\n    title: \"欢迎使用 Cashu\",\n    text: \"Cashu.me 是一款免费且开源的比特币钱包，使用 ecash 确保您的资金安全和隐私。\",\n    actions: {\n      more: {\n        label: \"点击了解更多\",\n      },\n    },\n    p1: {\n      text: \"Cashu 是一个免费且开源的比特币 ecash 协议。您可以在 { link } 了解更多。\",\n      link: {\n        text: \"cashu.space\",\n      },\n    },\n    p2: {\n      text: \"此钱包不隶属于任何 Mint。要使用此钱包，您需要连接到一个或多个您信任的 Cashu Mint。\",\n    },\n    p3: {\n      text: \"此钱包存储只有您才能访问的 ecash。如果您在没有种子短语备份的情况下删除您的浏览器数据，您将丢失您的 token。\",\n    },\n    p4: {\n      text: \"此钱包处于测试阶段。我们对用户丢失资金概不负责。使用风险自负！此代码是开源的，并在 MIT 许可证下获得许可。\",\n    },\n  },\n  WelcomeSlide2: {\n    title: \"安装 PWA\",\n    alt: { pwa_example: \"PWA 安装示例\" },\n    installing: \"正在安装…\",\n    instruction: {\n      intro: {\n        text: \"为了获得最佳体验，请使用您设备的本地网络浏览器将此钱包安装为渐进式 Web 应用程序。请立即执行此操作。\",\n      },\n      android: {\n        title: \"Android (Chrome)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"点击菜单（右上角）\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"按 { buttonText }\",\n          buttonText: \"@:AndroidPWAPrompt.buttonText\",\n        },\n      },\n      ios: {\n        title: \"iOS (Safari)\",\n        step1: {\n          item: \"1. { icon } { text }\",\n          text: \"点击分享（底部）\",\n        },\n        step2: {\n          item: \"2. { icon } { text }\",\n          text: \"按 { buttonText }\",\n          buttonText: \"@:iOSPWAPrompt.buttonText\",\n        },\n      },\n      outro: {\n        text: \"在您的设备上安装此应用后，关闭此浏览器窗口并从主屏幕使用该应用。\",\n      },\n    },\n    pwa: {\n      success: {\n        title: \"成功！\",\n        text: \"您正在使用 Cashu 作为 PWA。关闭所有其他打开的浏览器窗口，并从主屏幕使用该应用。\",\n        nextSteps: \"您现在可以关闭此标签页，并从主屏幕打开应用。\",\n      },\n    },\n  },\n  iOSPWAPrompt: {\n    text: \"点击 { icon } 和 { buttonText }\",\n    buttonText: \"添加到主屏幕\",\n  },\n  AndroidPWAPrompt: {\n    text: \"点击 { icon } 和 { buttonText }\",\n    buttonText: \"添加到主屏幕\",\n  },\n  WelcomeSlide3: {\n    title: \"您的种子短语\",\n    text: \"将您的种子短语存储在密码管理器或纸上。如果您的设备丢失，您的种子短语是恢复资金的唯一途径。\",\n    inputs: {\n      seed_phrase: {\n        label: \"种子短语\",\n        caption: \"您可以在设置中查看您的种子短语。\",\n      },\n      checkbox: {\n        label: \"我已经写下来了\",\n      },\n    },\n  },\n  WelcomeSlide4: {\n    title: \"条款\",\n    actions: {\n      more: {\n        label: \"阅读服务条款\",\n      },\n    },\n    inputs: {\n      checkbox: {\n        label: \"我已阅读并接受这些条款和条件\",\n      },\n    },\n  },\n  WelcomeSlideChoice: {\n    title: \"设置您的钱包\",\n    text: \"您想从种子短语恢复，还是创建一个新钱包？\",\n    options: {\n      new: {\n        title: \"创建新钱包\",\n        subtitle: \"生成新的种子并添加 Mint。\",\n      },\n      recover: {\n        title: \"恢复钱包\",\n        subtitle: \"输入您的种子短语，恢复 Mints 和 ecash。\",\n      },\n    },\n  },\n  WelcomeMintSetup: {\n    title: \"添加 Mints\",\n    text: \"Mint 是帮助您发送和接收 ecash 的服务器。选择一个已发现的 Mint 或手动添加。您也可以稍后添加。\",\n    sections: { your_mints: \"您的 Mints\" },\n    restoring: \"正在恢复 Mints…\",\n    placeholder: { mint_url: \"https://\" },\n  },\n  WelcomeRecoverSeed: {\n    title: \"输入您的种子短语\",\n    text: \"粘贴或输入您的 12 个词种子短语以进行恢复。\",\n    inputs: { word: \"第 { index } 个词\" },\n    actions: { paste_all: \"全部粘贴\" },\n    disclaimer: \"您的种子短语仅在本地使用，用于派生您的钱包密钥。\",\n  },\n  WelcomeRestoreEcash: {\n    title: \"恢复您的 ecash\",\n    text: \"扫描您配置的 Mints 上未花费的证明，并将其添加到您的钱包。\",\n  },\n  MintRatings: {\n    title: \"Mint 评价\",\n    reviews: \"条评价\",\n    ratings: \"评分\",\n    no_reviews: \"未找到评价\",\n    your_review: \"您的评价\",\n    no_reviews_to_display: \"暂无可显示的评价。\",\n    no_rating: \"暂无评分\",\n    out_of: \"共\",\n    rows: \"Reviews\",\n    sort: \"排序\",\n    sort_options: {\n      newest: \"最新\",\n      oldest: \"最旧\",\n      highest: \"最高\",\n      lowest: \"最低\",\n    },\n    actions: { write_review: \"撰写评价\" },\n    empty_state_subtitle:\n      \"通过留下评价来帮助他人。分享您对此 Mint 的体验，通过留下评价来帮助他人。\",\n  },\n  CreateMintReview: {\n    title: \"评价 Mint\",\n    publishing_as: \"发布身份\",\n    inputs: {\n      rating: { label: \"评分\" },\n      review: { label: \"评价（可选）\" },\n    },\n    actions: {\n      publish: { label: \"发布\", in_progress: \"发布中…\" },\n    },\n  },\n  RestoreView: {\n    seed_phrase: {\n      label: \"从种子短语恢复\",\n      caption:\n        \"输入您的种子短语以恢复您的钱包。在恢复之前，请确保您已添加所有您之前使用过的 Mint。\",\n      inputs: {\n        seed_phrase: {\n          label: \"种子短语\",\n          caption: \"您可以在设置中查看您的种子短语。\",\n        },\n      },\n    },\n    information: {\n      label: \"信息\",\n      caption:\n        \"该向导仅从另一个种子短语恢复 ecash，您将无法使用此种子短语或更改您当前使用的钱包的种子短语。这意味着，除非您将 ecash 发送给自己一次，否则恢复的 ecash 将不会受到您当前种子短语的保护。\",\n    },\n    restore_mints: {\n      label: \"恢复 Mints\",\n      caption:\n        \"选择要恢复的 Mint。您可以在主屏幕的Mints下添加更多 Mint 并在此处恢复它们。\",\n    },\n    nostr_mints: {\n      label: \"从 Nostr 恢复 Mints\",\n      caption:\n        \"使用您的种子短语在 Nostr 中继上搜索存储的 Mint 备份。这将帮助您发现以前使用过的 Mint。\",\n      search_button: \"搜索 Mint 备份\",\n      select_all: \"全选\",\n      deselect_all: \"取消全选\",\n      backed_up: \"已备份\",\n      already_added: \"已添加\",\n      add_selected: \"添加所选 ({count})\",\n      no_backups_found: \"未找到 Mint 备份\",\n      no_backups_hint:\n        \"请确保在设置中启用 Nostr Mint 备份以自动备份您的 Mint 列表。\",\n      invalid_mnemonic: \"请在搜索前输入有效的种子短语。\",\n      search_error: \"搜索 Mint 备份失败。\",\n      add_error: \"添加所选 Mint 失败。\",\n    },\n    actions: {\n      paste: {\n        error: \"读取剪贴板内容失败。\",\n      },\n      validate: {\n        error: \"助记符应至少包含 12 个词。\",\n      },\n      select_all: {\n        label: \"全选\",\n      },\n      deselect_all: {\n        label: \"取消全选\",\n      },\n      restore: {\n        label: \"恢复\",\n        in_progress: \"正在恢复 Mint…\",\n        error: \"恢复 Mint 错误: { error }\",\n      },\n      restore_all_mints: {\n        label: \"恢复所有 Mints\",\n        in_progress: \"正在恢复第 { index } 个 Mint，共 { length } 个…\",\n        success: \"恢复成功\",\n        error: \"恢复 Mints 错误: { error }\",\n      },\n      restore_selected_mints: {\n        label: \"恢复所选 Mints ({count})\",\n        in_progress: \"正在恢复第 { index } 个 Mint，共 { length } 个…\",\n        success: \"成功恢复 {count} 个 Mint\",\n        error: \"恢复所选 Mints 错误: { error }\",\n      },\n    },\n  },\n  MintSettings: {\n    add: {\n      title: \"添加 Mint\",\n      description: \"输入 Cashu Mint 的 URL 以连接。此钱包不隶属于任何 Mint。\",\n      inputs: {\n        nickname: {\n          placeholder: \"昵称 (例如 Testnet)\",\n        },\n      },\n      actions: {\n        add_mint: {\n          label: \"@:global.actions.add_mint.label\",\n          error_invalid_url: \"无效的 URL\",\n        },\n        scan: {\n          label: \"扫描二维码\",\n        },\n      },\n    },\n    discover: {\n      title: \"发现 Mints\",\n      overline: \"发现\",\n      caption: \"发现其他用户在 Nostr 上推荐的 Mints。\",\n      actions: {\n        discover: {\n          label: \"发现 Mints\",\n          in_progress: \"正在加载…\",\n          error_no_mints: \"未找到 Mints\",\n          success: \"找到 { length } 个 Mints\",\n        },\n      },\n      recommendations: {\n        overline: \"找到 { length } 个 Mints\",\n        caption:\n          \"这些 Mints 是由其他 Nostr 用户推荐的。请小心谨慎，并在使用 Mint 之前自行研究。\",\n        actions: {\n          browse: {\n            label: \"点击浏览 Mints\",\n          },\n        },\n      },\n    },\n    swap: {\n      title: \"兑换\",\n      overline: \"多 Mint 兑换\",\n      caption:\n        \"通过 Lightning 在 Mints 之间兑换资金。注意：留出潜在的 Lightning 费用。如果收到的支付不成功，请手动检查发票。\",\n      inputs: {\n        from: {\n          label: \"从\",\n        },\n        to: {\n          label: \"到\",\n        },\n        amount: {\n          label: \"金额 ({ ticker })\",\n        },\n      },\n      actions: {\n        swap: {\n          label: \"@:global.actions.swap.label\",\n          in_progress: \"@:MintSettings.swap.actions.swap.label\",\n        },\n      },\n    },\n    error_badge: \"错误\",\n    reviews_text: \"评论\",\n    no_reviews_yet: \"暂无评论\",\n    discover_mints_button: \"发现 Mints\",\n  },\n  QrcodeReader: {\n    progress: {\n      text: \"{ percentage }{ addon }\",\n      percentage: \"{ percentage }%\",\n      keep_scanning_text: \" - 继续扫描\",\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  InvoiceDetailDialog: {\n    title: \"接收 Lightning\",\n    create_invoice_title: \"创建发票\",\n    inputs: {\n      amount: {\n        label: \"金额 ({ ticker }) *\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      create: {\n        label: \"创建发票\",\n        label_blocked: \"正在创建发票…\",\n        in_progress: \"正在创建\",\n      },\n    },\n    invoice: {\n      caption: \"Lightning 发票\",\n      status_paid_text: \"已付款！\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        copy: {\n          label: \"@:global.actions.copy.label\",\n        },\n      },\n    },\n  },\n  SendDialog: {\n    title: \"发送\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"没有可用的 Mints\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"没有可用的 Mints\",\n      },\n    },\n  },\n  SendTokenDialog: {\n    title: \"发送 Ecash\",\n    title_ecash_text: \"Ecash\",\n    badge_offline_text: \"离线\",\n    inputs: {\n      amount: {\n        label: \"金额 ({ ticker }) *\",\n        invalid_too_much_error_text: \"太多了\",\n      },\n      p2pk_pubkey: {\n        label: \"接收者公钥\",\n        label_invalid: \"接收者公钥\",\n      },\n    },\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      close_card_scanner: {\n        label: \"@:global.actions.close.label\",\n      },\n      copy_emoji: {\n        label: \"🥜\",\n        tooltip_text: \"复制 Emoji\",\n      },\n      copy_tokens: {\n        label: \"@:global.actions.copy.label\",\n      },\n      copy_link: {\n        tooltip_text: \"复制链接\",\n      },\n      share: {\n        tooltip_text: \"分享ecash代币\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      paste_p2pk_pubkey: {\n        tooltip_text: \"@:global.actions.paste.label\",\n      },\n      send: {\n        label: \"@:global.actions.send.label\",\n      },\n      delete: {\n        tooltip_text: \"从历史记录中删除\",\n      },\n      write_tokens_to_card: {\n        tooltips: {\n          ndef_supported_text: \"写入 NFC 卡\",\n          ndef_unsupported_text: \"不支持 NDEF\",\n        },\n      },\n    },\n  },\n  ReceiveDialog: {\n    title: \"接收\",\n    actions: {\n      ecash: {\n        label: \"Ecash\",\n        error_no_mints: \"没有可用的 Mints\",\n      },\n      lightning: {\n        label: \"Lightning\",\n        error_no_mints: \"您需要连接到 Mint 才能通过 Lightning 接收\",\n      },\n    },\n  },\n  ReceiveEcashDrawer: {\n    title: \"接收 Ecash\",\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      request: {\n        label: \"请求\",\n      },\n      lock: {\n        label: \"@:global.actions.lock.label\",\n      },\n      nfc: {\n        label: \"NFC\",\n        scanning_text: \"正在扫描…\",\n      },\n    },\n  },\n  ReceiveTokenDialog: {\n    title: \"接收 Ecash\",\n    title_ecash_text: \"Ecash\",\n    inputs: {\n      tokens_base64: {\n        label: \"粘贴 Cashu token\",\n      },\n    },\n    errors: {\n      invalid_token: {\n        label: \"无效的 token\",\n      },\n      p2pk_lock_mismatch: {\n        label: \"无法接收。此令牌的P2PK锁定与您的公钥不匹配。\",\n      },\n    },\n    actions: {\n      paste: {\n        label: \"@:global.actions.paste.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      scan: {\n        label: \"@:global.actions.scan.label\",\n      },\n      receive: {\n        label: \"@:global.actions.receive.label\",\n        label_known_mint: \"@:ReceiveTokenDialog.actions.receive.label\",\n        label_adding_mint: \"正在添加 Mint…\",\n      },\n      swap: {\n        label: \"@:global.actions.swap.label\",\n        tooltip_text: \"兑换到信任的 Mint\",\n        caption: \"兑换 { value }\",\n      },\n      cancel_swap: {\n        label: \"@:global.actions.cancel.label\",\n        tooltip_text: \"取消兑换\",\n      },\n      confirm_swap: {\n        label: \"@:ReceiveTokenDialog.actions.swap.label\",\n        tooltip_text: \"@:ReceiveTokenDialog.actions.swap.tooltip_text\",\n        in_progress: \"@:ReceiveTokenDialog.actions.confirm_swap.label\",\n      },\n      later: {\n        label: \"稍后接收\",\n        tooltip_text: \"添加到历史记录，稍后接收\",\n        already_in_history_success_text: \"Ecash 已在历史记录中\",\n        added_to_history_success_text: \"Ecash 已添加到历史记录\",\n      },\n      nfc: {\n        label: \"NFC\",\n        tooltips: {\n          ndef_supported_text: \"从 NFC 卡读取\",\n          ndef_unsupported_text: \"不支持 NDEF\",\n        },\n      },\n    },\n  },\n  P2PKDialog: {\n    p2pk: {\n      caption: \"P2PK 密钥\",\n      description: \"接收此密钥锁定的 ecash\",\n      used_warning_text:\n        \"警告：此密钥已被使用过。请使用新密钥以获得更好的隐私。\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_key: {\n        label: \"生成新密钥\",\n      },\n    },\n  },\n  PaymentRequestDialog: {\n    payment_request: {\n      caption: \"支付请求\",\n      description: \"通过 Nostr 接收支付\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n      new_request: {\n        label: \"新请求\",\n      },\n      add_amount: {\n        label: \"添加金额\",\n      },\n      use_active_mint: {\n        label: \"任何 Mint\",\n      },\n    },\n    inputs: {\n      amount: {\n        placeholder: \"输入金额\",\n      },\n    },\n  },\n  NumericKeyboard: {\n    actions: {\n      close: {\n        label: \"@:global.actions.close.label\",\n        closed_info_text: \"键盘已禁用。您可以在设置中重新启用键盘。\",\n      },\n      enter: {\n        label: \"@:global.actions.enter.label\",\n      },\n    },\n  },\n  NWCDialog: {\n    nwc: {\n      caption: \"Nostr 钱包连接\",\n      description:\n        \"使用 NWC 远程控制您的钱包。按下二维码将您的钱包与兼容的应用程序链接。\",\n      warning_text:\n        \"警告：任何有权访问此连接字符串的人都可以从您的钱包发起支付。请勿分享！\",\n    },\n    actions: {\n      copy: {\n        label: \"@:global.actions.copy.label\",\n      },\n      close: {\n        label: \"@:global.actions.close.label\",\n      },\n    },\n  },\n  MintMotdMessage: {\n    title: \"Mint 消息\",\n  },\n  MintDetailsDialog: {\n    contact: {\n      title: \"联系\",\n    },\n    details: {\n      title: \"Mint 详情\",\n      url: {\n        label: \"URL\",\n      },\n      nuts: {\n        label: \"Nuts\",\n        actions: {\n          show: {\n            label: \"显示全部\",\n          },\n          hide: {\n            label: \"隐藏\",\n          },\n        },\n      },\n      currency: {\n        label: \"货币\",\n      },\n      currencies: {\n        label: \"@:MintDetailsDialog.details.currency.label\",\n      },\n      version: {\n        label: \"版本\",\n      },\n    },\n    actions: {\n      title: \"操作\",\n      copy_mint_url: {\n        label: \"复制 Mint URL\",\n      },\n      delete: {\n        label: \"删除 Mint\",\n      },\n      edit: {\n        label: \"编辑 Mint\",\n      },\n    },\n  },\n  ChooseMint: {\n    title: \"选择 Mint\",\n    badge_mint_error_text: \"错误\",\n    badge_option_mint_error_text: \"@:ChooseMint.badge_mint_error_text\",\n  },\n  HistoryTable: {\n    empty_text: \"暂无历史记录\",\n    row: {\n      type_label: \"Ecash\",\n      date_label: \"{ value } 前\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"检查状态\",\n      },\n      receive: {\n        tooltip_text: \"接收\",\n      },\n      filter_pending: {\n        label: \"过滤待处理\",\n      },\n      show_all: {\n        label: \"显示全部\",\n      },\n    },\n    old_token_not_found_error_text: \"未找到旧 token\",\n  },\n  InvoiceTable: {\n    empty_text: \"暂无发票\",\n    row: {\n      type_label: \"Lightning\",\n      type_tooltip_text: \"点击复制\",\n      date_label: \"{ value } 前\",\n    },\n    actions: {\n      check_status: {\n        tooltip_text: \"检查状态\",\n      },\n      filter_pending: {\n        label: \"过滤待处理\",\n      },\n      show_all: {\n        label: \"显示全部\",\n      },\n    },\n  },\n  RemoveMintDialog: {\n    title: \"您确定要删除此 Mint 吗？\",\n    nickname: {\n      label: \"昵称\",\n    },\n    balances: {\n      label: \"余额\",\n    },\n    warning_text:\n      \"注意：由于此钱包是偏执的，您的此 Mint 中的 ecash 不会真正删除，但会保留在您的设备上。如果您稍后再次添加此 Mint，您会再次看到它。\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      confirm: {\n        label: \"删除 Mint\",\n      },\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n    },\n  },\n  ParseInputComponent: {\n    placeholder: {\n      default: \"Cashu token 或闪电地址\",\n      receive: \"Cashu token\",\n      pay: \"闪电地址或发票\",\n    },\n    qr_scanner: {\n      title: \"扫描二维码\",\n      description: \"点击扫描地址\",\n    },\n    paste_button: {\n      label: \"@:global.actions.paste.label\",\n    },\n  },\n  PayInvoiceDialog: {\n    input_data: {\n      title: \"支付 Lightning\",\n      inputs: {\n        invoice_data: {\n          label: \"Lightning 发票或地址\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        enter: {\n          label: \"@:global.actions.enter.label\",\n        },\n        paste: {\n          label: \"@:global.actions.paste.label\",\n        },\n        scan: {\n          label: \"@:global.actions.scan.label\",\n        },\n      },\n    },\n    lnurlpay: {\n      amount_exact_label: \"{ payee } 请求 { value } { ticker }\",\n      amount_range_label:\n        \"{ payee } 请求{br}介于 { min } 和 { max } { ticker } 之间\",\n      sending_to_lightning_address: \"发送至 { address }\",\n      inputs: {\n        amount: {\n          label: \"金额 ({ ticker }) *\",\n        },\n        comment: {\n          label: \"评论 (可选)\",\n        },\n      },\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        send: {\n          label: \"@:global.actions.send.label\",\n        },\n      },\n    },\n    invoice: {\n      title: \"支付 { value }\",\n      paying: \"支付中\",\n      paid: \"已支付\",\n      fee: \"费用\",\n      memo: {\n        label: \"备忘录\",\n      },\n      processing_info_text: \"正在处理…\",\n      balance_too_low_warning_text: \"余额不足\",\n      actions: {\n        close: {\n          label: \"@:global.actions.close.label\",\n        },\n        pay: {\n          label: \"支付\",\n          in_progress: \"@:PayInvoiceDialog.invoice.processing_info_text\",\n          error: \"错误\",\n        },\n      },\n    },\n  },\n  EditMintDialog: {\n    title: \"编辑 Mint\",\n    inputs: {\n      nickname: {\n        label: \"昵称\",\n      },\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      update: {\n        label: \"@:global.actions.update.label\",\n      },\n    },\n  },\n  AddMintDialog: {\n    title: \"您信任此 Mint 吗？\",\n    description:\n      \"在使用此 Mint 之前，请确保您信任它。Mints 随时可能变得恶意或停止运营。\",\n    inputs: {\n      mint_url: {\n        label: \"@:global.inputs.mint_url.label\",\n      },\n    },\n    actions: {\n      cancel: {\n        label: \"@:global.actions.cancel.label\",\n      },\n      add_mint: {\n        label: \"@:global.actions.add_mint.label\",\n        in_progress: \"正在添加 Mint\",\n      },\n    },\n  },\n  restore: {\n    mnemonic_error_text: \"请输入助记符\",\n    restore_mint_error_text: \"恢复 Mint 错误: { error }\",\n    prepare_info_text: \"正在准备恢复流程…\",\n    restored_proofs_for_keyset_info_text:\n      \"已为 keyset { keysetId } 恢复 { restoreCounter } 个证明\",\n    checking_proofs_for_keyset_info_text:\n      \"正在检查 keyset { keysetId } 的证明 { startIndex } 到 { endIndex }\",\n    no_proofs_info_text: \"未找到要恢复的证明\",\n    restored_amount_success_text: \"已恢复 { amount }\",\n  },\n  swap: {\n    in_progress_warning_text: \"兑换进行中\",\n    invalid_swap_data_error_text: \"无效的兑换数据\",\n    swap_error_text: \"兑换错误\",\n  },\n  TokenInformation: {\n    fee: \"费用\",\n    unit: \"单位\",\n    fiat: \"法币\",\n    p2pk: \"P2PK\",\n    locked: \"已锁定\",\n    locked_to_you: \"已锁定给您\",\n    mint: \"铸币厂\",\n    memo: \"备注\",\n    payment_request: \"支付请求\",\n    nostr: \"Nostr\",\n    token_copied: \"令牌已复制到剪贴板\",\n  },\n};\n"
  },
  {
    "path": "src/icons.js",
    "content": "import { createApp } from \"vue\";\nimport {\n  LucideX,\n  LucideQrCode,\n  LucideWallet,\n  LucideZap,\n  // Add other icons you need here\n} from \"lucide-vue-next\";\n\nexport default function (app) {\n  app.component(\"IconClose\", LucideX);\n  app.component(\"IconQrCode\", LucideQrCode);\n  app.component(\"IconWallet\", LucideWallet);\n  app.component(\"IconZap\", LucideZap);\n  // Register other icons here\n}\n"
  },
  {
    "path": "src/js/__tests__/legacy-qr.test.js",
    "content": "import { describe, expect, it } from \"vitest\";\nimport {\n  isLegacyRetailQR,\n  translateLegacyQRToLightningAddress,\n} from \"../legacy-qr\";\n\ndescribe(\"legacy-qr\", () => {\n  describe(\"isLegacyRetailQR\", () => {\n    it(\"should return false for non-string values\", () => {\n      expect(isLegacyRetailQR(null)).toBe(false);\n      expect(isLegacyRetailQR(undefined)).toBe(false);\n      expect(isLegacyRetailQR(123)).toBe(false);\n      expect(isLegacyRetailQR({})).toBe(false);\n      expect(isLegacyRetailQR([])).toBe(false);\n    });\n\n    it(\"should return true for EMV QR codes (starting with 000201)\", () => {\n      expect(isLegacyRetailQR(\"000201\")).toBe(true);\n      expect(\n        isLegacyRetailQR(\n          \"00020126260008za.co.mp0110248723666427530023za.co.electrum.picknpay0122ydgKJviKSomaVw0297RaZw5303710540571.406304CE9C\"\n        )\n      ).toBe(true);\n    });\n\n    it(\"should return false for non-EMV codes\", () => {\n      expect(isLegacyRetailQR(\"00020\")).toBe(false);\n      expect(isLegacyRetailQR(\"lnbc1234567890\")).toBe(false);\n      expect(isLegacyRetailQR(\"cashuA123456\")).toBe(false);\n    });\n\n    it(\"should handle whitespace\", () => {\n      expect(isLegacyRetailQR(\"  000201  \")).toBe(true);\n    });\n  });\n\n  describe(\"translateLegacyQRToLightningAddress\", () => {\n    it(\"should convert PicknPay EMV QR code\", () => {\n      const qr =\n        \"00020126260008za.co.mp0110248723666427530023za.co.electrum.picknpay0122ydgKJviKSomaVw0297RaZw5303710540571.406304CE9C\";\n      expect(translateLegacyQRToLightningAddress(qr)).toBe(\n        `${qr}@cryptoqr.net`\n      );\n    });\n\n    it(\"should convert Ecentric EMV QR code\", () => {\n      const qr =\n        \"00020129530019za.co.ecentric.payment0122RD2HAK3KTI53EC/confirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2\";\n      // slash is URL-encoded as %2F\n      expect(translateLegacyQRToLightningAddress(qr)).toBe(\n        \"00020129530019za.co.ecentric.payment0122RD2HAK3KTI53EC%2Fconfirm520458125303710540115802ZA5916cryptoqrtestscan6002CT63049BE2@cryptoqr.net\"\n      );\n    });\n\n    it(\"should return null for non-EMV codes\", () => {\n      expect(translateLegacyQRToLightningAddress(\"lnbc123\")).toBeNull();\n    });\n\n    it(\"should return null for unsupported merchants\", () => {\n      expect(\n        translateLegacyQRToLightningAddress(\n          \"00020129530023other.merchant.code0122test\"\n        )\n      ).toBeNull();\n    });\n  });\n});\n"
  },
  {
    "path": "src/js/__tests__/token.test.js",
    "content": "import token from \"../token\";\nimport { describe, expect, it } from \"vitest\";\n\nconst VALID_V4_TOKEN =\n  \"cashuBo2FteCJodHRwczovL21pbnQubWluaWJpdHMuY2FzaC9CaXRjb2luYXVjc2F0YXSBomFpSABQBVDwSUFGYXCBpGFhAWFzeEBiMjBjYjNkZmZjMzY0ZWQ2ZDhiNWIzZjAzNDEzODQ4ZDU2MTZhMmZiMGMxZGEyMDNlN2ExYTNlNzM4NzBhNWJjYWNYIQIBBWMjM-FXqkXqHeiQMsK24hFzqeittTtTRBv9o6-LO2Fko2FlWCC49Cmyit61XiLZeotP_058iw6Av1Du2r3HY4oNoU1ws2FzWCDb9bMQubjVh9BzuQPP2JyEE0pExaEk6Vk8-h_c3UofuGFyWCCwhsZx88PRY9DMO5h6uiT140WF9zgBKNUrvcY3ssEHmA\";\nconst VALID_V3_TOKEN =\n  \"cashuAeyJ0b2tlbiI6W3sicHJvb2ZzIjpbeyJpZCI6IkkyeU4raVJZZmt6VCIsImFtb3VudCI6MSwiQyI6IjAyZTRkYmJmMGZmNDI4YTU4ZDZjNjZjMTljNjI0YWRlY2MxNzg0YzdlNTU5ODZhNGVmNDQ4NDM5MzZhM2M4ZjM1OSIsInNlY3JldCI6ImZHWVpzSlVjME1mU1orVlhGandEZXNsNkJScW5wNmRSblZpUGQ2L00yQ0k9In1dLCJtaW50IjoiaHR0cHM6Ly84MzMzLnNwYWNlOjMzMzgifV19\";\nconst VALID_V2_TOKEN =\n  \"eyJwcm9vZnMiOlt7ImlkIjoiSTJ5TitpUllma3pUIiwiYW1vdW50IjoxLCJDIjoiMDNjMzAwYzMzMzAzNTMzNDA3MjYwMzU3MzA3NzViNGM2YjRlMDRlYmVjOGY2OGVmYzVmYjY2ZDE3OTI0ZDRkMmQyIiwic2VjcmV0IjoicjE5S3I1anlwQXNaWm1tOUg3cUtFQWJsc1c1ZmsxaWsycFQwUWs2TFUxWT0ifV0sIm1pbnRzIjpbeyJ1cmwiOiJodHRwczovLzgzMzMuc3BhY2U6MzMzOCIsImlkcyI6WyJMM3p4eFJCL0k4dUUiLCJJMnlOK2lSWWZrelQiXX1dfQ==\";\n\ndescribe(\"token\", () => {\n  describe(\"decode\", () => {\n    it(\"should properly decode a V4 token\", () => {\n      const decoded = token.decode(VALID_V4_TOKEN);\n      expect(decoded.proofs.length).toEqual(1);\n      expect(decoded.mint).toEqual(\"https://mint.minibits.cash/Bitcoin\");\n      expect(decoded.proofs.length).toEqual(1);\n    });\n\n    it(\"should properly decode a V3 token\", () => {\n      const decoded = token.decode(VALID_V3_TOKEN);\n      expect(decoded.proofs.length).toEqual(1);\n      expect(decoded.mint).toEqual(\"https://8333.space:3338\");\n      expect(decoded.proofs.length).toEqual(1);\n    });\n\n    it(\"should throw unsupported token error for a V2 token\", () => {\n      expect(() => token.decode(VALID_V2_TOKEN)).toThrow(\n        \"Token version is not supported\"\n      );\n    });\n  });\n\n  it(\"should throw if the token is invalid or V2\", () => {\n    expect(() => token.decode(\"invalid\")).toThrow();\n  });\n});\n"
  },
  {
    "path": "src/js/base64.js",
    "content": "function unescapeBase64Url(str) {\n  return (str + \"===\".slice((str.length + 3) % 4))\n    .replace(/-/g, \"+\")\n    .replace(/_/g, \"/\");\n}\n\nfunction escapeBase64Url(str) {\n  return str.replace(/\\+/g, \"-\").replace(/\\//g, \"_\").replace(/=/g, \"\");\n}\n\nconst uint8ToBase64 = (function (exports) {\n  \"use strict\";\n\n  const encode = function encode(uint8array) {\n    const output = [];\n\n    for (let i = 0; i < uint8array.length; i++) {\n      output.push(String.fromCharCode(uint8array[i]));\n    }\n\n    return btoa(output.join(\"\"));\n  };\n\n  const asCharCode = function asCharCode(c) {\n    return c.charCodeAt(0);\n  };\n\n  const decode = function decode(chars) {\n    return Uint8Array.from(atob(chars), asCharCode);\n  };\n\n  exports.decode = decode;\n  exports.encode = encode;\n\n  return exports;\n})({});\n\nexport { uint8ToBase64 };\n"
  },
  {
    "path": "src/js/dhke.js",
    "content": "import { bytesToNumber } from \"./utils\";\nimport * as nobleSecp256k1 from \"@noble/secp256k1\";\n\nasync function hashToCurve(secretMessage) {\n  let point;\n  while (!point) {\n    const hash = await nobleSecp256k1.utils.sha256(secretMessage);\n    const hashHex = nobleSecp256k1.utils.bytesToHex(hash);\n    const pointX = \"02\" + hashHex;\n    try {\n      point = nobleSecp256k1.Point.fromHex(pointX);\n    } catch (error) {\n      secretMessage = await nobleSecp256k1.utils.sha256(secretMessage);\n    }\n  }\n  return point;\n}\n\nasync function step1Alice(secretMessage) {\n  secretMessage = nobleSecp256k1.utils.bytesToHex(secretMessage);\n  secretMessage = new TextEncoder().encode(secretMessage);\n  const Y = await hashToCurve(secretMessage);\n  const r_bytes = nobleSecp256k1.utils.randomPrivateKey();\n  const r = bytesToNumber(r_bytes);\n  const P = nobleSecp256k1.Point.fromPrivateKey(r);\n  const B_ = Y.add(P);\n  return { B_: B_.toHex(true), r: nobleSecp256k1.utils.bytesToHex(r_bytes) };\n}\n\nfunction step3Alice(C_, r, A) {\n  const rInt = bytesToNumber(r);\n  const C = C_.subtract(A.multiply(rInt));\n  return C;\n}\n\nexport { step1Alice, step3Alice, hashToCurve };\n"
  },
  {
    "path": "src/js/eventBus.js",
    "content": "import { reactive } from \"vue\";\n\nexport const EventBus = reactive({\n  events: {},\n\n  on(event, callback) {\n    if (!this.events[event]) {\n      this.events[event] = [];\n    }\n    this.events[event].push(callback);\n  },\n\n  off(event, callback) {\n    if (!this.events[event]) return;\n    this.events[event] = this.events[event].filter((cb) => cb !== callback);\n  },\n\n  emit(event, payload) {\n    console.log(\"eventBus emit\", event, payload);\n    if (!this.events[event]) return;\n    this.events[event].forEach((callback) => callback(payload));\n  },\n});\n"
  },
  {
    "path": "src/js/legacy-qr.js",
    "content": "// Converts South African retail EMV QR codes to Lightning Address format via cryptoqr.net\n\nconst MERCHANT_PATTERNS = [\n  /(?<identifier>.*za\\.co\\.electrum\\.picknpay.*)/iu,\n  /(?<identifier>.*za\\.co\\.ecentric.*)/iu,\n];\n\nexport function isLegacyRetailQR(code) {\n  return typeof code === \"string\" && code.trim().startsWith(\"000201\");\n}\n\nexport function translateLegacyQRToLightningAddress(qrCode) {\n  if (!isLegacyRetailQR(qrCode)) return null;\n\n  const trimmed = qrCode.trim();\n  for (const pattern of MERCHANT_PATTERNS) {\n    const match = trimmed.match(pattern);\n    if (match?.groups?.identifier) {\n      return `${encodeURIComponent(match.groups.identifier)}@cryptoqr.net`;\n    }\n  }\n  return null;\n}\n"
  },
  {
    "path": "src/js/notify.ts",
    "content": "import { Notify, QNotifyCreateOptions } from \"quasar\";\n\ntype StatusMap = { [x: number]: \"warning\" | \"negative\" };\nconst errorTypes = {\n  400: \"warning\",\n  401: \"warning\",\n  500: \"negative\",\n} as StatusMap;\n\nasync function notifyApiError(\n  error: Error,\n  caption: string = \"\",\n  position = \"top\" as QNotifyCreateOptions[\"position\"]\n) {\n  try {\n    Notify.create({\n      timeout: 5000,\n      type: \"warning\",\n      position,\n      message: error.message,\n      caption: caption ?? null,\n      progress: true,\n      actions: [\n        {\n          icon: \"close\",\n          color: \"white\",\n          handler: () => {},\n        },\n      ],\n    });\n  } catch (e) {\n    // skip\n  }\n}\n\nasync function notifySuccess(\n  message: string,\n  position = \"top\" as QNotifyCreateOptions[\"position\"]\n) {\n  Notify.create({\n    timeout: 5000,\n    type: \"positive\",\n    message: message,\n    position,\n    progress: true,\n    actions: [\n      {\n        icon: \"close\",\n        color: \"white\",\n        handler: () => {},\n      },\n    ],\n  });\n}\n\nasync function notifyError(message: string, caption?: string) {\n  Notify.create({\n    color: \"red\",\n    message: message,\n    caption,\n    position: \"top\",\n    progress: true,\n    actions: [\n      {\n        icon: \"close\",\n        color: \"white\",\n        handler: () => {},\n      },\n    ],\n  });\n}\n\nasync function notifyWarning(\n  message: string,\n  caption?: string,\n  timeout = 5000\n) {\n  Notify.create({\n    timeout: timeout,\n    type: \"warning\",\n    message: message,\n    caption: caption,\n    position: \"top\",\n    progress: true,\n    actions: [\n      {\n        icon: \"close\",\n        color: \"black\",\n        handler: () => {},\n      },\n    ],\n  });\n}\n\nasync function notify(\n  message: string,\n  position = \"top\" as QNotifyCreateOptions[\"position\"]\n) {\n  // failure\n  Notify.create({\n    timeout: 5000,\n    type: \"null\",\n    color: \"grey\",\n    message: message,\n    position: position,\n    actions: [\n      {\n        icon: \"close\",\n        color: \"white\",\n        handler: () => {},\n      },\n    ],\n  });\n}\n\nexport { notifyApiError, notifySuccess, notifyError, notifyWarning, notify };\n"
  },
  {
    "path": "src/js/string-utils.js",
    "content": "function shortenString(s, length = 20, lastchars = 5) {\n  if (s.length > length + lastchars) {\n    return (\n      s.substring(0, length) +\n      \"...\" +\n      s.substring(s.length - lastchars, s.length)\n    );\n  }\n}\n\nexport { shortenString };\n"
  },
  {
    "path": "src/js/token.ts",
    "content": "import {\n  type Token,\n  getDecodedToken,\n  getTokenMetadata,\n  Mint,\n  TokenMetadata,\n} from \"@cashu/cashu-ts\";\nimport { useMintsStore, WalletProof } from \"src/stores/mints\";\nimport { useProofsStore } from \"src/stores/proofs\";\nexport default { decode, decodeFull, getProofs, getMint, getUnit, getMemo };\n\n/**\n * Decodes an encoded cashu token metadata\n */\nfunction decode(encoded_token: string): TokenMetadata {\n  if (!encoded_token || encoded_token === \"\") return;\n  const metadata = getTokenMetadata(encoded_token);\n  metadata.proofs = metadata.incompleteProofs;\n  return metadata;\n}\n\n/**\n * Decodes an encoded cashu token with full proofs\n */\nasync function decodeFull(encoded_token: string): Promise<Token> {\n  if (!encoded_token || encoded_token === \"\") return;\n  try {\n    return getDecodedToken(encoded_token, useMintsStore().allMintKeysets);\n  } catch (error) {\n    const tokenMint = getTokenMetadata(encoded_token).mint;\n    const fetchKeysets = await new Mint(tokenMint).getKeySets();\n    return getDecodedToken(encoded_token, fetchKeysets.keysets);\n  }\n}\n\n/**\n * Returns a list of proofs from a decoded token\n */\nfunction getProofs(decoded_token: Token): WalletProof[] {\n  if (!(decoded_token.proofs.length > 0)) {\n    throw new Error(\"Token format wrong\");\n  }\n  const proofs = decoded_token.proofs.flat();\n  return useProofsStore().proofsToWalletProofs(proofs);\n}\n\nfunction getMint(decoded_token: Token) {\n  /*\n      Returns first mint of a token (very rough way).\n      */\n  if (decoded_token.proofs.length > 0) {\n    return decoded_token.mint;\n  } else {\n    return \"\";\n  }\n}\n\nfunction getUnit(decoded_token: Token) {\n  if (decoded_token.unit != null) {\n    return decoded_token.unit;\n  } else {\n    // search for unit in mints[...].keysets[...].unit\n    const mintStore = useMintsStore();\n    const mint = getMint(decoded_token);\n    const keysets = mintStore.mints\n      .filter((m) => m.url === mint)\n      .flatMap((m) => m.keysets);\n    if (keysets.length > 0) {\n      return keysets[0].unit;\n    }\n    return \"\";\n  }\n}\n\nfunction getMemo(decoded_token: Token) {\n  if (decoded_token.memo != null) {\n    return decoded_token.memo;\n  } else {\n    return \"\";\n  }\n}\n"
  },
  {
    "path": "src/js/utils.js",
    "content": "import { date } from \"quasar\";\nimport * as nobleSecp256k1 from \"@noble/secp256k1\";\n\nfunction splitAmount(value) {\n  const chunks = [];\n  for (let i = 0; i < 32; i++) {\n    const mask = 1 << i;\n    if ((value & mask) !== 0) chunks.push(Math.pow(2, i));\n  }\n  return chunks;\n}\n\nfunction bytesToNumber(bytes) {\n  return hexToNumber(nobleSecp256k1.etc.bytesToHex(bytes));\n}\n\nfunction bigIntStringify(key, value) {\n  return typeof value === \"bigint\" ? value.toString() : value;\n}\n\nfunction hexToNumber(hex) {\n  if (typeof hex !== \"string\") {\n    throw new TypeError(\"hexToNumber: expected string, got \" + typeof hex);\n  }\n  return BigInt(`0x${hex}`);\n}\n\nfunction currentDateStr() {\n  return date.formatDate(new Date(), \"YYYY-MM-DD HH:mm:ss\");\n}\n\nexport { splitAmount, bytesToNumber, bigIntStringify, currentDateStr };\n"
  },
  {
    "path": "src/js/wallet-helpers.js",
    "content": "function getShortUrl(url) {\n  url = url.replace(\"https://\", \"\");\n  url = url.replace(\"http://\", \"\");\n  const cut_param = 26;\n  if (url.length > cut_param && url.indexOf(\"/\") != -1) {\n    url =\n      url.substring(0, url.indexOf(\"/\") + 1) +\n      \"...\" +\n      url.substring(url.length - cut_param / 2, url.length);\n  }\n  // cut the url if it is too long, keep the first cut_param/2 characters and the last cut_param/2 characters\n  if (url.length > cut_param) {\n    url =\n      url.substring(0, cut_param / 2) +\n      \"...\" +\n      url.substring(url.length - cut_param / 2, url.length);\n  }\n\n  return url;\n}\n\nexport { getShortUrl };\n"
  },
  {
    "path": "src/layouts/BlankLayout.vue",
    "content": "<template>\n  <q-layout view=\"lHh Lpr lFf\">\n    <q-page-container>\n      <router-view />\n    </q-page-container>\n  </q-layout>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, ref } from \"vue\";\n\nexport default defineComponent({\n  name: \"BlankLayout\",\n  mixins: [windowMixin],\n  components: {},\n});\n</script>\n"
  },
  {
    "path": "src/layouts/FullscreenLayout.vue",
    "content": "<template>\n  <q-layout view=\"lHh Lpr lFf\">\n    <FullscreenHeader />\n    <q-page-container>\n      <router-view />\n    </q-page-container>\n  </q-layout>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, ref } from \"vue\";\nimport FullscreenHeader from \"components/FullscreenHeader.vue\";\n\nexport default defineComponent({\n  name: \"FullscreenLayout\",\n  mixins: [windowMixin],\n  components: {\n    FullscreenHeader,\n  },\n});\n</script>\n"
  },
  {
    "path": "src/layouts/MainLayout.vue",
    "content": "<template>\n  <q-layout view=\"lHh Lpr lFf\">\n    <MainHeader />\n    <q-page-container>\n      <router-view />\n    </q-page-container>\n  </q-layout>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, ref } from \"vue\";\nimport MainHeader from \"components/MainHeader.vue\";\n\nexport default defineComponent({\n  name: \"MainLayout\",\n  mixins: [windowMixin],\n  components: {\n    MainHeader,\n  },\n});\n</script>\n"
  },
  {
    "path": "src/main.js",
    "content": "import { createApp } from \"vue\";\nimport App from \"./App.vue\";\nimport registerIcons from \"./icons\";\n\nconst app = createApp(App);\nregisterIcons(app);\n"
  },
  {
    "path": "src/pages/AlreadyRunning.vue",
    "content": "<template>\n  <div\n    class=\"fullscreen bg-dark text-white text-center q-pa-md flex flex-center\"\n  >\n    <div>\n      <div class=\"text-h3\">{{ $t(\"AlreadyRunning.title\") }}</div>\n      <div class=\"text-h5 q-ma-lg text-grey\">\n        {{ $t(\"AlreadyRunning.text\") }}\n      </div>\n      <q-btn\n        rounded\n        class=\"q-mt-md\"\n        color=\"white\"\n        text-color=\"black\"\n        unelevated\n        to=\"/\"\n        :label=\"$t('AlreadyRunning.actions.retry.label')\"\n      />\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\n\nexport default defineComponent({\n  name: \"AlreadyRunning\",\n});\n</script>\n"
  },
  {
    "path": "src/pages/CreateMintReviewPage.vue",
    "content": "<template>\n  <div class=\"bg-dark text-white q-pa-md flex flex-center\">\n    <div class=\"review-page-content\">\n      <div class=\"review-content-container\">\n        <!-- Mint Header - Matching MintDetailsPage and MintRatingsPage style -->\n        <div class=\"mint-header-container q-mb-lg\">\n          <div class=\"mint-header q-pa-md\">\n            <q-avatar size=\"56px\" class=\"q-mb-sm\">\n              <img\n                v-if=\"mintInfo?.icon_url\"\n                :src=\"mintInfo.icon_url\"\n                alt=\"Mint Profile\"\n              />\n              <q-icon\n                v-else\n                name=\"account_balance\"\n                size=\"36px\"\n                color=\"grey-7\"\n              />\n            </q-avatar>\n            <div class=\"mint-name q-mb-xs\">\n              {{ mintInfo?.name || \"Mint\" }}\n            </div>\n            <div class=\"mint-url text-grey-6\">\n              {{ mintUrl }}\n            </div>\n          </div>\n        </div>\n\n        <!-- Rating Section -->\n        <div class=\"rating-section q-mb-lg\">\n          <div class=\"section-card q-pa-lg\">\n            <div class=\"text-body1 text-grey-5 q-mb-md text-center\">\n              {{ $t(\"CreateMintReview.inputs.rating.label\") }}\n            </div>\n            <div class=\"row justify-center\">\n              <q-rating\n                v-model=\"rating\"\n                max=\"5\"\n                size=\"42px\"\n                color=\"amber\"\n                icon-half=\"star_half\"\n                icon=\"star\"\n                icon-selected=\"star\"\n                :disable=\"prefilling || submitting\"\n              />\n            </div>\n          </div>\n        </div>\n\n        <!-- Review Text Section -->\n        <div class=\"review-text-section q-mb-lg\">\n          <div class=\"section-card q-pa-lg\">\n            <div class=\"text-body1 text-weight-medium q-mb-md\">\n              Write a public review (optional)\n            </div>\n            <q-input\n              v-model=\"review\"\n              type=\"textarea\"\n              autogrow\n              outlined\n              placeholder=\"Share your experience with this mint...\"\n              :disable=\"prefilling || submitting\"\n              class=\"review-input\"\n              :input-style=\"{ minHeight: '120px' }\"\n            />\n          </div>\n        </div>\n\n        <!-- Publishing Info -->\n        <div class=\"publisher-info q-mb-lg text-center\">\n          <div class=\"row items-center justify-center text-caption text-grey-6\">\n            <q-avatar v-if=\"publisherPicture\" size=\"20px\" class=\"q-mr-xs\">\n              <q-img\n                :src=\"publisherPicture\"\n                spinner-color=\"white\"\n                spinner-size=\"xs\"\n              />\n            </q-avatar>\n            <span>{{ $t(\"CreateMintReview.publishing_as\") }}:</span>\n            <span v-if=\"publisherName\" class=\"text-white q-ml-xs\">{{\n              publisherName\n            }}</span>\n            <span class=\"monospace q-ml-xs\">{{ shortDisplayNpub }}</span>\n            <q-icon\n              v-if=\"displayNpub\"\n              name=\"content_copy\"\n              size=\"14px\"\n              class=\"q-ml-xs cursor-pointer\"\n              @click=\"copyText(displayNpub)\"\n            />\n          </div>\n        </div>\n\n        <!-- Submit Button -->\n        <div class=\"submit-section\">\n          <q-btn\n            color=\"primary\"\n            size=\"lg\"\n            rounded\n            unelevated\n            :disable=\"!canPublish || prefilling\"\n            :loading=\"submitting\"\n            @click=\"publishReview\"\n            class=\"full-width submit-btn\"\n            style=\"min-height: 56px; font-size: 1rem; font-weight: 600\"\n          >\n            <span>{{ $t(\"CreateMintReview.actions.publish.label\") }}</span>\n            <template v-slot:loading>\n              <q-spinner class=\"on-left\" />\n              <span>{{\n                $t(\"CreateMintReview.actions.publish.in_progress\")\n              }}</span>\n            </template>\n          </q-btn>\n        </div>\n      </div>\n\n      <q-inner-loading :showing=\"prefilling\">\n        <q-spinner size=\"32px\" color=\"primary\" />\n      </q-inner-loading>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent, computed, ref, onMounted } from \"vue\";\nimport { useRouter } from \"vue-router\";\nimport { useNostrStore } from \"src/stores/nostr\";\nimport NDK, { NDKEvent, NDKKind } from \"@nostr-dev-kit/ndk\";\nimport { notifyError, notifySuccess } from \"src/js/notify\";\nimport { nip19 } from \"nostr-tools\";\nimport { useMintRecommendationsStore } from \"src/stores/mintRecommendations\";\nimport { useMintsStore } from \"src/stores/mints\";\n\nexport default defineComponent({\n  name: \"CreateMintReviewPage\",\n  setup() {\n    const router = useRouter();\n    const nostr = useNostrStore();\n    const rating = ref<number>(0);\n    const review = ref<string>(\"\");\n    const submitting = ref(false);\n    const prefilling = ref(false);\n    const publisherName = ref<string>(\"\");\n    const publisherPicture = ref<string>(\"\");\n    const mintUrl = ref<string>(\"\");\n    const mintInfo = ref<any>(null);\n\n    const displayPubkey = computed(() => {\n      const pk = nostr.pubkey || nostr.seedSignerPublicKey || \"\";\n      return pk ? `${pk.slice(0, 8)}…${pk.slice(-4)}` : \"\";\n    });\n\n    const displayNpub = computed(() => {\n      try {\n        if (!nostr.pubkey) return \"\";\n        return nip19.npubEncode(nostr.pubkey);\n      } catch {\n        return \"\";\n      }\n    });\n\n    const shortDisplayNpub = computed(() => {\n      const v = displayNpub.value;\n      if (!v) return \"\";\n      return `${v.slice(0, 12)}…${v.slice(-6)}`;\n    });\n\n    const canPublish = computed(\n      () => rating.value >= 1 && rating.value <= 5 && !!mintUrl.value\n    );\n\n    const ensureNdk = async () => {\n      if (!nostr.connected || !nostr.ndk) {\n        nostr.initNdkReadOnly();\n      }\n    };\n\n    const buildContent = () => {\n      let content = \"\";\n      if (rating.value) content += `[${rating.value}/5]`;\n      if (review.value && review.value.trim().length > 0)\n        content += ` ${review.value.trim()}`;\n      return content;\n    };\n\n    const publishReview = async () => {\n      if (!canPublish.value) return;\n      submitting.value = true;\n      try {\n        await ensureNdk();\n        if (!nostr.signer) {\n          notifyError(\n            \"Please connect a Nostr account in Settings to publish a review.\"\n          );\n          return;\n        }\n        const ndk: NDK = nostr.ndk as any;\n        const event = new NDKEvent(ndk);\n        event.kind = 38000 as NDKKind;\n        event.content = buildContent();\n        event.tags = [];\n        event.tags.push([\"k\", \"38172\"]);\n        event.tags.push([\"u\", mintUrl.value, \"cashu\"]);\n        const dIdentifier =\n          (mintInfo.value as any)?.pubkey ||\n          (mintInfo.value as any)?.mintPubkey ||\n          \"\";\n        if (dIdentifier) event.tags.push([\"d\", dIdentifier]);\n        await event.sign(nostr.signer as any);\n        if ((ndk as any).pool?.size === 0) ndk.connect();\n        await event.publish();\n        // Immediately update recommendations store so UI reflects our review\n        try {\n          const recs = useMintRecommendationsStore();\n          // @ts-ignore NDKEvent\n          recs.handleReviewEvent(event as any);\n        } catch {}\n        notifySuccess(\"Review published\");\n        router.back();\n      } catch (e) {\n        console.error(e);\n        notifyError(\"Failed to publish review\");\n      } finally {\n        submitting.value = false;\n      }\n    };\n\n    const prefillExisting = async () => {\n      try {\n        prefilling.value = true;\n        await ensureNdk();\n        const ndk: NDK = nostr.ndk as any;\n        if (!nostr.pubkey) return;\n        const filter: any = {\n          kinds: [38000 as NDKKind],\n          authors: [nostr.pubkey],\n          \"#u\": [mintUrl.value],\n          \"#k\": [\"38172\"],\n          limit: 1,\n        };\n        const events = await ndk.fetchEvents(filter);\n        let latest: any = null;\n        for (const ev of events) {\n          if (!latest || (ev.created_at || 0) > (latest.created_at || 0))\n            latest = ev;\n        }\n        if (latest) {\n          const m = (latest.content || \"\").match(\n            /\\s*\\[(\\d)\\s*\\/\\s*5\\]\\s*(.*)$/s\n          );\n          if (m) {\n            const r = parseInt(m[1], 10);\n            if (!isNaN(r)) rating.value = r;\n            review.value = (m[2] || \"\").trim();\n          } else {\n            review.value = latest.content || \"\";\n          }\n        }\n      } catch {\n      } finally {\n        prefilling.value = false;\n      }\n    };\n\n    const loadPublisherProfile = async () => {\n      try {\n        await ensureNdk();\n        const ndk: NDK = nostr.ndk as any;\n        if (!nostr.pubkey) return;\n        const user = ndk.getUser({ pubkey: nostr.pubkey });\n        await user.fetchProfile();\n        publisherName.value =\n          (user.profile as any)?.name ||\n          (user.profile as any)?.display_name ||\n          \"\";\n        publisherPicture.value =\n          (user.profile as any)?.image || (user.profile as any)?.picture || \"\";\n      } catch {}\n    };\n\n    const loadMintInfo = () => {\n      // Try to get mint info from stores\n      const recsStore = useMintRecommendationsStore();\n      let info = recsStore.getHttpInfoForUrl(mintUrl.value);\n\n      if (!info) {\n        const mintsStore = useMintsStore();\n        const mint = mintsStore.mints.find((m) => m.url === mintUrl.value);\n        if (mint && mint.info) {\n          info = mint.info;\n        }\n      }\n\n      if (info) {\n        mintInfo.value = info;\n      }\n    };\n\n    onMounted(async () => {\n      // Get mint URL from route query params\n      const route = router.currentRoute.value;\n      if (route.query.mintUrl) {\n        mintUrl.value = route.query.mintUrl as string;\n        loadMintInfo();\n        await loadPublisherProfile();\n        await prefillExisting();\n      } else {\n        // No mint URL, go back\n        router.push(\"/\");\n      }\n    });\n\n    return {\n      rating,\n      review,\n      submitting,\n      prefilling,\n      canPublish,\n      publishReview,\n      displayPubkey,\n      displayNpub,\n      shortDisplayNpub,\n      publisherName,\n      publisherPicture,\n      mintUrl,\n      mintInfo,\n      copyText: (t: string) => {\n        try {\n          navigator.clipboard.writeText(t);\n          notifySuccess(\"Copied to clipboard\");\n        } catch {}\n      },\n    };\n  },\n});\n</script>\n\n<style scoped>\n.review-page-content {\n  max-width: 640px;\n  margin: 0 auto;\n  width: 100%;\n  height: 100%;\n  overflow-y: auto;\n  position: absolute;\n  top: 0;\n}\n\n.review-content-container {\n  max-width: 640px;\n  margin: 0 auto;\n  padding: 0 16px;\n  position: relative;\n}\n\n/* Mint Header - Matching MintDetailsPage and MintRatingsPage exactly */\n.mint-header-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  width: 100%;\n  margin-top: 50px;\n}\n\n.mint-header {\n  width: 100%;\n  border-radius: 12px;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  background-color: rgba(255, 255, 255, 0.05);\n}\n\n.mint-name {\n  font-size: 24px;\n  font-weight: 600;\n  text-align: center;\n  color: white;\n}\n\n.mint-url {\n  font-size: 14px;\n  font-weight: 500;\n  text-align: center;\n  word-break: break-all;\n}\n\n/* Section Cards */\n.section-card {\n  background-color: rgba(255, 255, 255, 0.03);\n  border-radius: 12px;\n  border: 1px solid rgba(255, 255, 255, 0.08);\n}\n\n.rating-section,\n.review-text-section {\n  width: 100%;\n}\n\n/* Review Input - Less rounded, more elegant */\n.review-input :deep(.q-field__control) {\n  background-color: rgba(0, 0, 0, 0.2);\n  border-radius: 8px;\n}\n\n.review-input :deep(.q-field__control-container) {\n  border-radius: 8px;\n}\n\n.review-input :deep(.q-field__native) {\n  color: white;\n}\n\n/* Publisher Info */\n.publisher-info {\n  opacity: 0.8;\n}\n\n.monospace {\n  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,\n    \"Liberation Mono\", \"Courier New\", monospace;\n}\n\n/* Submit Button */\n.submit-section {\n  width: 100%;\n  padding-bottom: 32px;\n}\n\n.submit-btn {\n  text-transform: none;\n}\n</style>\n"
  },
  {
    "path": "src/pages/ErrorNotFound.vue",
    "content": "<template>\n  <div\n    class=\"fullscreen bg-dark text-white text-center q-pa-md flex flex-center\"\n  >\n    <div>\n      <div style=\"font-size: 30vh\">{{ $t(\"ErrorNotFound.title\") }}</div>\n      <div class=\"text-h3 q-pb-lg\" style=\"opacity: 0.8\">\n        {{ $t(\"ErrorNotFound.text\") }}\n      </div>\n      <q-btn\n        rounded\n        size=\"lg\"\n        class=\"q-mt-xl\"\n        color=\"white\"\n        text-color=\"black\"\n        unelevated\n        to=\"/\"\n        :label=\"$t('ErrorNotFound.actions.home.label')\"\n      />\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\n\nexport default defineComponent({\n  name: \"ErrorNotFound\",\n});\n</script>\n"
  },
  {
    "path": "src/pages/MintDetailsPage.vue",
    "content": "<template>\n  <div class=\"bg-dark text-white q-pa-md flex flex-center\">\n    <div class=\"mint-details-page-content\">\n      <EditMintDialog\n        :mint=\"mintToEdit\"\n        :showEditMintDialog=\"showEditMintDialog\"\n        @update:showEditMintDialog=\"showEditMintDialog = $event\"\n      />\n      <RemoveMintDialog\n        :mintToRemove=\"mintToRemove\"\n        :showRemoveMintDialog=\"showRemoveMintDialog\"\n        @update:showRemoveMintDialog=\"showRemoveMintDialog = $event\"\n        @remove=\"removeMint\"\n      />\n\n      <div class=\"mint-content-container q-px-md\">\n        <!-- Mint Header Profile Name Section -->\n        <div class=\"mint-header-container q-mb-lg\">\n          <div class=\"mint-header q-pa-md\">\n            <!-- Mint Profile Name Section -->\n            <q-avatar size=\"56px\" class=\"mint-profile-icon q-mb-sm\">\n              <img\n                v-if=\"mintData.info?.icon_url\"\n                :src=\"mintData.info.icon_url\"\n                alt=\"Mint Profile\"\n              />\n              <building-icon v-else size=\"36\" />\n            </q-avatar>\n            <div class=\"mint-name q-mb-xs\">\n              {{ mintData.info?.name || \"Mint\" }}\n            </div>\n\n            <!-- QR Code Icon -->\n            <div class=\"top-icons\">\n              <qr-code-icon\n                size=\"24\"\n                class=\"qr-icon cursor-pointer text-white\"\n                @click=\"showQrCode = !showQrCode\"\n              />\n            </div>\n\n            <!-- QR Code Section (toggleable) -->\n            <div class=\"qr-code-container\">\n              <transition appear name=\"smooth-slide\">\n                <div\n                  v-if=\"showQrCode\"\n                  class=\"qr-code-section q-my-md\"\n                  key=\"qr-code\"\n                >\n                  <vue-qrcode\n                    :value=\"mintData.url\"\n                    :options=\"{ width: 300 }\"\n                    class=\"rounded-borders\"\n                  />\n                </div>\n              </transition>\n            </div>\n          </div>\n\n          <div class=\"mint-descriptions q-mt-lg\">\n            <!-- MOTD Component -->\n            <transition\n              appear\n              enter-active-class=\"animated pulse\"\n              name=\"smooth-slide\"\n            >\n              <mint-motd-message\n                v-if=\"mintData.info?.motd && !mintData.motdDismissed\"\n                :message=\"mintData.info.motd\"\n                :mint-url=\"mintData.url\"\n                :dismissed=\"mintData.motdDismissed\"\n                @dismiss=\"motdDismissed = true\"\n              />\n            </transition>\n\n            <div class=\"mint-description\" v-if=\"mintData.info?.description\">\n              {{ mintData.info.description }}\n            </div>\n            <div\n              class=\"mint-description-long q-mt-md\"\n              v-if=\"mintData.info?.description_long\"\n            >\n              {{ mintData.info.description_long }}\n            </div>\n          </div>\n          <transition name=\"smooth-slide\">\n            <MintMotdMessage\n              v-if=\"mintData.info?.motd && mintData.motdDismissed\"\n              :message=\"mintData.info.motd\"\n              :mintUrl=\"mintData.url\"\n              :dismissed=\"mintData.motdDismissed\"\n              @dismiss=\"dismissMotd\"\n            />\n          </transition>\n        </div>\n\n        <!-- Section Divider -->\n        <div\n          class=\"section-divider q-mb-md\"\n          v-if=\"mintData.info?.contact?.length > 0\"\n        >\n          <div class=\"divider-line\"></div>\n          <div class=\"divider-text\">\n            {{ $t(\"MintDetailsDialog.contact.title\") }}\n          </div>\n          <div class=\"divider-line\"></div>\n        </div>\n\n        <!-- Contact Info Section -->\n        <div class=\"contact-section q-mb-lg\">\n          <div\n            v-for=\"contactInfo in mintData.info?.contact\"\n            :key=\"contactInfo.method\"\n            class=\"contact-item q-mb-md\"\n          >\n            <div class=\"contact-icon-container\">\n              <mail-icon\n                v-if=\"contactInfo.method === 'email'\"\n                size=\"20\"\n                color=\"#9E9E9E\"\n                class=\"contact-icon\"\n              />\n              <img\n                v-else-if=\"contactInfo.method === 'nostr'\"\n                src=\"nostr-icon.svg\"\n                class=\"contact-icon\"\n                alt=\"\"\n              />\n              <img\n                v-else-if=\"contactInfo.method === 'twitter'\"\n                src=\"/x-logo.svg\"\n                class=\"contact-icon\"\n                alt=\"\"\n              />\n              <img\n                v-else-if=\"contactInfo.method === 'telegram'\"\n                src=\"/telegram-icon.svg\"\n                class=\"contact-icon\"\n                alt=\"\"\n              />\n              <div v-else class=\"contact-text q-ml-xs\">\n                {{ contactInfo.method }}\n              </div>\n            </div>\n            <div class=\"contact-text\">{{ contactInfo.info }}</div>\n            <copy-icon\n              @click=\"copyText(contactInfo.info)\"\n              size=\"20\"\n              color=\"#9E9E9E\"\n              class=\"copy-icon cursor-pointer\"\n            />\n          </div>\n        </div>\n\n        <!-- Section Divider -->\n        <div class=\"section-divider q-mb-md\">\n          <div class=\"divider-line\"></div>\n          <div class=\"divider-text\">\n            {{ $t(\"MintDetailsDialog.details.title\") }}\n          </div>\n          <div class=\"divider-line\"></div>\n        </div>\n\n        <!-- Mint Details Section -->\n        <div class=\"mint-details-section q-mb-lg\">\n          <!-- URL -->\n          <div class=\"detail-item q-mb-md\">\n            <div class=\"detail-label\">\n              <link-icon size=\"20\" color=\"#9E9E9E\" class=\"detail-icon\" />\n              <div class=\"detail-name\">\n                {{ $t(\"MintDetailsDialog.details.url.label\") }}\n              </div>\n            </div>\n            <div\n              class=\"detail-value items-center\"\n              @click=\"copyText(mintData.url)\"\n            >\n              {{ mintData.url }}\n            </div>\n          </div>\n\n          <!-- Nuts -->\n          <div class=\"detail-item q-mb-md\" v-if=\"mintData.info?.nuts\">\n            <div class=\"detail-label\">\n              <nut-icon size=\"20\" color=\"#9E9E9E\" class=\"detail-icon\" />\n              <div class=\"detail-name\">\n                {{ $t(\"MintDetailsDialog.details.nuts.label\") }}\n              </div>\n            </div>\n            <div\n              class=\"detail-value\"\n              v-if=\"!showAllNuts\"\n              @click=\"showAllNuts = true\"\n            >\n              {{ $t(\"MintDetailsDialog.details.nuts.actions.show.label\") }}\n            </div>\n            <div class=\"detail-value\" v-else @click=\"showAllNuts = false\">\n              {{ $t(\"MintDetailsDialog.details.nuts.actions.hide.label\") }}\n            </div>\n          </div>\n\n          <!-- Expanded Nuts Section (when showAllNuts is true) -->\n          <div\n            class=\"nuts-expanded-section\"\n            v-if=\"showAllNuts && mintData.info?.nuts\"\n          >\n            <div class=\"nuts-grid\">\n              <div\n                v-for=\"(nutName, nutNumber) in visibleNuts\"\n                :key=\"nutNumber\"\n                class=\"nut-pill\"\n              >\n                <div class=\"nut-content\">\n                  <span class=\"nut-number\">{{ nutNumber }}:</span> {{ nutName }}\n                </div>\n              </div>\n            </div>\n          </div>\n\n          <!-- Currency (if available) -->\n          <div class=\"detail-item q-mb-md\" v-if=\"mintData.info?.currencies\">\n            <div class=\"detail-label\">\n              <currency-icon size=\"20\" color=\"#9E9E9E\" class=\"detail-icon\" />\n              <div class=\"detail-name\">\n                {{ $t(\"MintDetailsDialog.details.currency.label\") }}\n              </div>\n            </div>\n            <div class=\"detail-value\">{{ mintData.info.currencies }}</div>\n          </div>\n\n          <!-- Currency Units (if available) -->\n          <div\n            class=\"detail-item q-mb-md\"\n            v-if=\"mintUnits && mintUnits.length > 0\"\n          >\n            <div class=\"detail-label\">\n              <banknote-icon size=\"20\" color=\"#9E9E9E\" class=\"detail-icon\" />\n              <div class=\"detail-name\">\n                {{ $t(\"MintDetailsDialog.details.currencies.label\") }}\n              </div>\n            </div>\n            <div class=\"detail-value\">\n              {{ mintUnits.map((unit) => unit.toUpperCase()).join(\", \") }}\n            </div>\n          </div>\n\n          <!-- Version -->\n          <div class=\"detail-item\" v-if=\"mintData.info?.version\">\n            <div class=\"detail-label\">\n              <info-icon size=\"20\" color=\"#9E9E9E\" class=\"detail-icon\" />\n              <div class=\"detail-name\">\n                {{ $t(\"MintDetailsDialog.details.version.label\") }}\n              </div>\n            </div>\n            <div class=\"detail-value\">{{ mintData.info.version }}</div>\n          </div>\n        </div>\n\n        <!-- Section Divider for Audit Info -->\n        <div v-if=\"settings.auditorEnabled\" class=\"section-divider q-mb-md\">\n          <div class=\"divider-line\"></div>\n          <div class=\"divider-text\">AUDIT INFO</div>\n          <div class=\"divider-line\"></div>\n        </div>\n\n        <!-- Mint Audit Info Section -->\n        <MintAuditInfo\n          v-if=\"settings.auditorEnabled && mintData.url\"\n          :mintUrl=\"mintData.url\"\n          @close=\"() => {}\"\n        />\n\n        <!-- Section Divider -->\n        <div class=\"section-divider q-mb-md\">\n          <div class=\"divider-line\"></div>\n          <div class=\"divider-text\">\n            {{ $t(\"MintDetailsDialog.actions.title\") }}\n          </div>\n          <div class=\"divider-line\"></div>\n        </div>\n\n        <!-- Action Buttons -->\n        <div class=\"action-buttons-section\">\n          <div class=\"action-buttons-container\">\n            <div\n              class=\"action-button cursor-pointer\"\n              @click=\"openEditMintDialog\"\n            >\n              <pencil-icon size=\"20\" color=\"#9E9E9E\" class=\"action-icon\" />\n              <div class=\"action-label\">\n                {{ $t(\"MintDetailsDialog.actions.edit.label\") }}\n              </div>\n            </div>\n\n            <div\n              class=\"action-button cursor-pointer\"\n              @click=\"copyText(mintData.url)\"\n            >\n              <copy-icon size=\"20\" color=\"#9E9E9E\" class=\"action-icon\" />\n              <div class=\"action-label\">\n                {{ $t(\"MintDetailsDialog.actions.copy_mint_url.label\") }}\n              </div>\n            </div>\n\n            <div\n              class=\"action-button cursor-pointer\"\n              @click=\"openCreateReviewDialog\"\n            >\n              <q-icon name=\"rate_review\" size=\"20px\" class=\"action-icon\" />\n              <div class=\"action-label\">Review Mint</div>\n            </div>\n\n            <div\n              class=\"action-button delete-button cursor-pointer\"\n              @click=\"openRemoveMintDialog\"\n            >\n              <trash-icon size=\"20\" color=\"#FF453A\" class=\"action-icon\" />\n              <div class=\"action-label\">\n                {{ $t(\"MintDetailsDialog.actions.delete.label\") }}\n              </div>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport VueQrcode from \"@chenfengyuan/vue-qrcode\";\nimport { useMintsStore, MintClass } from \"src/stores/mints\";\nimport { useSettingsStore } from \"src/stores/settings\";\nimport EditMintDialog from \"src/components/EditMintDialog.vue\";\nimport RemoveMintDialog from \"src/components/RemoveMintDialog.vue\";\nimport MintMotdMessage from \"src/components/MintMotdMessage.vue\";\nimport MintAuditInfo from \"src/components/MintAuditInfo.vue\";\nimport {\n  QrCode as QrCodeIcon,\n  Link as LinkIcon,\n  Nut as NutIcon,\n  DollarSign as CurrencyIcon,\n  Info as InfoIcon,\n  Mail as MailIcon,\n  Copy as CopyIcon,\n  Pencil as PencilIcon,\n  Trash as TrashIcon,\n  Building as BuildingIcon,\n  Banknote as BanknoteIcon,\n} from \"lucide-vue-next\";\n\nexport default defineComponent({\n  name: \"MintDetailsPage\",\n  mixins: [windowMixin],\n  components: {\n    VueQrcode,\n    QrCodeIcon,\n    LinkIcon,\n    NutIcon,\n    CurrencyIcon,\n    InfoIcon,\n    MailIcon,\n    CopyIcon,\n    PencilIcon,\n    TrashIcon,\n    BuildingIcon,\n    BanknoteIcon,\n    EditMintDialog,\n    RemoveMintDialog,\n    MintMotdMessage,\n    MintAuditInfo,\n  },\n  data: function () {\n    return {\n      contactIcons: {\n        email: \"mail\",\n      },\n      contactMethods: {\n        twitter: \"X\",\n        nostr: \"Nostr\",\n      },\n      showQrCode: false,\n      showAllNuts: false,\n      nutNames: {\n        7: \"Token state check\",\n        8: \"Overpaid Lightning fees\",\n        9: \"Signature restore\",\n        10: \"Spending conditions\",\n        11: \"Pay-To-Pubkey (P2PK)\",\n        12: \"DLEQ proofs\",\n        13: \"Deterministic secrets\",\n        14: \"Hashed Timelock Contracts\",\n        15: \"Partial multi-path payments\",\n        16: \"Animated QR codes\",\n        17: \"WebSocket subscriptions\",\n        18: \"Payment requests\",\n        19: \"Cached Responses\",\n        20: \"Signature on Mint Quote\",\n        21: \"Clear authentication\",\n        22: \"Blind authentication\",\n      },\n      motdDismissed: false,\n      settings: useSettingsStore(),\n      mintData: {},\n      mintToEdit: {},\n      mintToRemove: {},\n    };\n  },\n  computed: {\n    ...mapWritableState(useMintsStore, [\n      \"showEditMintDialog\",\n      \"showRemoveMintDialog\",\n    ]),\n    filteredNutNames() {\n      // Only include nuts 7 and above\n      const filteredNuts = {};\n      Object.keys(this.nutNames).forEach((nutNumber) => {\n        if (parseInt(nutNumber) >= 7) {\n          filteredNuts[nutNumber] = this.nutNames[nutNumber];\n        }\n      });\n      return filteredNuts;\n    },\n    visibleNuts() {\n      // Return only the nuts that are both in our filtered list and supported by the mint\n      const result = {};\n      if (this.mintData && this.mintData.info && this.mintData.info.nuts) {\n        Object.keys(this.filteredNutNames).forEach((nutNumber) => {\n          if (this.mintData.info.nuts[nutNumber]) {\n            result[nutNumber] = this.filteredNutNames[nutNumber];\n          }\n        });\n      }\n      return result;\n    },\n    mintUnits() {\n      if (this.mintData) {\n        const mintClassInstance = new MintClass(this.mintData);\n        return mintClassInstance.units;\n      }\n      return [];\n    },\n  },\n  methods: {\n    ...mapActions(useMintsStore, [\n      \"removeMint\",\n      \"fetchMintInfo\",\n      \"triggerMintInfoMotdChanged\",\n    ]),\n    shortenText: function (text, maxLength) {\n      if (text.length > maxLength) {\n        return text.substring(0, maxLength) + \"...\";\n      }\n      return text;\n    },\n    copyText(text) {\n      navigator.clipboard.writeText(text);\n      this.$q.notify({\n        message: this.$i18n.t(\"global.copy_to_clipboard.success\"),\n        color: \"positive\",\n        position: \"top\",\n        timeout: 1000,\n      });\n    },\n    openEditMintDialog() {\n      this.mintToEdit = Object.assign({}, this.mintData);\n      this.showEditMintDialog = true;\n    },\n    openRemoveMintDialog() {\n      this.mintToRemove = Object.assign({}, this.mintData);\n      this.showRemoveMintDialog = true;\n    },\n    openCreateReviewDialog() {\n      // Navigate to create review page\n      this.$router.push({\n        path: \"/createreview\",\n        query: {\n          mintUrl: this.mintData.url,\n        },\n      });\n    },\n    dismissMotd() {\n      // Handle MOTD dismissal\n      this.motdDismissed = true;\n    },\n    async refreshMintInfo() {\n      try {\n        console.log(\"Refreshing mint info for:\", this.mintData.url);\n        const newMintInfo = await this.fetchMintInfo(this.mintData);\n        this.triggerMintInfoMotdChanged(newMintInfo, this.mintData, false);\n        const mintsStore = useMintsStore();\n        const target = mintsStore.mints.find(\n          (m) => m.url === this.mintData.url\n        );\n        if (target) {\n          target.info = newMintInfo;\n        }\n        if (this.mintData) {\n          this.mintData.info = newMintInfo;\n        }\n      } catch (error) {\n        console.log(\"Failed to fetch mint info:\", error);\n      }\n    },\n  },\n  created() {\n    // Get mint data from query params or store\n    if (this.$route.query.mintUrl) {\n      const mintsStore = useMintsStore();\n      const mint = mintsStore.mints.find(\n        (m) => m.url === this.$route.query.mintUrl\n      );\n      if (mint) {\n        this.mintData = mint;\n        this.refreshMintInfo();\n      } else {\n        // Mint not found, redirect back\n        this.$router.push(\"/\");\n      }\n    } else {\n      // No mint URL provided, redirect back\n      this.$router.push(\"/\");\n    }\n  },\n});\n</script>\n\n<style scoped>\n.mint-details-page-content {\n  max-width: 600px;\n  margin: 0 auto;\n  color: white;\n  height: 100%;\n  overflow-y: auto;\n  position: absolute;\n  top: 0;\n  width: 100%;\n}\n\n.mint-content-container {\n  max-width: 600px;\n  margin: 0 auto;\n  color: white;\n  height: 100%;\n  overflow-y: auto;\n  position: relative;\n}\n\n/* Top Icons */\n.top-icons {\n  padding-top: 10px;\n  position: relative;\n  width: 100%;\n  display: flex;\n  justify-content: center;\n}\n\n/* Mint Header */\n.mint-header-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  width: 100%;\n  margin-top: 50px;\n}\n\n.mint-header {\n  width: 100%;\n  border-radius: 12px;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  background-color: rgba(255, 255, 255, 0.05);\n}\n\n.mint-name {\n  font-size: 24px;\n  font-weight: 600;\n  text-align: center;\n}\n\n.mint-balance {\n  font-size: 20px;\n  font-weight: 600;\n  text-align: center;\n}\n\n.mint-descriptions {\n  width: 100%;\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n}\n\n.mint-description {\n  font-size: 16px;\n  font-weight: 600;\n  line-height: 24px;\n  width: 100%;\n}\n\n.mint-description-long {\n  align-self: stretch;\n  position: relative;\n  font-size: 14px;\n  line-height: 20px;\n  color: #9e9e9e;\n  width: 100%;\n  font-weight: 500;\n}\n\n/* Section Divider */\n.section-divider {\n  display: flex;\n  align-items: center;\n  width: 100%;\n}\n\n.divider-line {\n  flex: 1;\n  height: 1px;\n  background-color: #333;\n}\n\n.divider-text {\n  padding: 0 10px;\n  font-size: 14px;\n  font-weight: 600;\n  color: #ffffff;\n  text-transform: uppercase;\n}\n\n/* Contact Section */\n.contact-section {\n  width: 100%;\n}\n\n.contact-item {\n  display: flex;\n  align-items: center;\n  width: 100%;\n}\n\n.contact-icon-container {\n  width: 24px;\n  display: flex;\n  justify-content: center;\n  margin-right: 10px;\n}\n\n.contact-icon {\n  width: 20px;\n  height: 20px;\n  color: #636366;\n}\n\n.contact-text {\n  flex: 1;\n  font-size: 16px;\n  font-weight: 600;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n.copy-icon {\n  color: #636366;\n  margin-left: 10px;\n}\n\n/* Mint Details Section */\n.mint-details-section {\n  width: 100%;\n}\n\n.detail-item {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  width: 100%;\n}\n\n.detail-label {\n  display: flex;\n  align-items: center;\n}\n\n.detail-icon {\n  margin-right: 10px;\n}\n\n.detail-name {\n  font-size: 16px;\n  font-weight: 600;\n  color: #9e9e9e;\n}\n\n.detail-value {\n  font-size: 16px;\n  font-weight: 600;\n  color: white;\n  text-align: right;\n  max-width: 60%;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n}\n\n/* Action Buttons */\n.action-buttons-section {\n  width: 100%;\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: flex-start;\n  margin-top: 16px;\n  margin-bottom: 32px;\n}\n\n.action-buttons-container {\n  align-self: stretch;\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  justify-content: center;\n  width: 100%;\n}\n\n.action-button {\n  display: flex;\n  flex-direction: row;\n  align-items: center;\n  justify-content: flex-start;\n  gap: 16px;\n  padding: 8px;\n  border-radius: 8px;\n  transition: background-color 0.3s;\n  width: 100%;\n  margin-bottom: 16px;\n}\n\n.action-button:last-child {\n  margin-bottom: 0;\n}\n\n.action-button:hover {\n  background-color: rgba(255, 255, 255, 0.1);\n}\n\n.action-icon {\n  min-width: 20px;\n}\n\n.action-label {\n  position: relative;\n  line-height: 24px;\n  font-weight: 500;\n  font-size: 16px;\n}\n\n.delete-button {\n  color: #ff453a;\n}\n\n/* QR Code Container and Animation */\n.qr-code-container {\n  min-height: 0;\n  display: flex;\n  justify-content: center;\n  width: 100%;\n  overflow: hidden;\n}\n\n.qr-code-section {\n  width: 100%;\n  display: flex;\n  justify-content: center;\n}\n\n.smooth-slide-enter-active {\n  transition: all 0.5s cubic-bezier(0.34, 1.56, 0.64, 1);\n  max-height: 350px;\n  margin-bottom: 16px;\n  opacity: 1;\n  pointer-events: auto;\n}\n\n.smooth-slide-leave-active {\n  transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);\n  max-height: 350px;\n  margin-bottom: 16px;\n  opacity: 1;\n}\n\n.smooth-slide-enter-from,\n.smooth-slide-leave-to {\n  max-height: 0;\n  margin-bottom: 0;\n  opacity: 0;\n  transform: translateY(-10px);\n  pointer-events: none;\n}\n\n.nuts-expanded-section {\n  width: 100%;\n  margin-bottom: 16px;\n}\n\n.nuts-grid {\n  display: flex;\n  flex-direction: column;\n  gap: 8px;\n  width: 100%;\n}\n\n.nut-pill {\n  border-radius: 4px;\n  padding: 8px;\n  width: 100%;\n}\n\n.nut-content {\n  font-size: 14px;\n  font-weight: 600;\n  line-height: 24px;\n  color: white;\n}\n\n.nut-number {\n  color: #9e9e9e;\n}\n\n/* Make \"View all\" and \"Hide\" text clickable */\n.detail-value[v-if=\"!showAllNuts\"],\n.detail-value[v-else] {\n  cursor: pointer;\n  color: white;\n  font-weight: 600;\n}\n\n/* Currency Units */\n.currency-units-container {\n  display: flex;\n  flex-wrap: wrap;\n  gap: 8px;\n  justify-content: flex-end;\n}\n\n.currency-unit-pill {\n  border-radius: 4px;\n  background-color: #1d1d1d;\n  padding: 4px 8px;\n  font-size: 14px;\n  font-weight: 600;\n  color: white;\n  display: inline-block;\n}\n</style>\n"
  },
  {
    "path": "src/pages/MintDiscoveryPage.vue",
    "content": "<template>\n  <div class=\"bg-dark text-white q-pa-md flex flex-center\">\n    <div class=\"page-content q-px-md\">\n      <div class=\"section-header q-mb-md\">\n        <div class=\"divider-line\"></div>\n        <div class=\"divider-text\">{{ $t(\"MintSettings.discover.title\") }}</div>\n        <div class=\"divider-line\"></div>\n      </div>\n      <MintDiscovery :autoDiscover=\"true\" :allowCreateReview=\"false\" />\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport MintDiscovery from \"src/components/MintDiscovery.vue\";\n\nexport default defineComponent({\n  name: \"MintDiscoveryPage\",\n  components: { MintDiscovery },\n});\n</script>\n\n<style scoped>\n.page-content {\n  width: 100%;\n  max-width: 800px;\n  margin: 0 auto;\n}\n\n.section-header {\n  display: flex;\n  align-items: center;\n  width: 100%;\n}\n\n.divider-line {\n  flex: 1;\n  height: 1px;\n  background-color: #48484a;\n}\n\n.divider-text {\n  padding: 0 10px;\n  font-size: 14px;\n  font-weight: 600;\n  color: #ffffff;\n  text-transform: uppercase;\n}\n</style>\n"
  },
  {
    "path": "src/pages/MintRatingsPage.vue",
    "content": "<template>\n  <div class=\"bg-dark text-white q-pa-md flex flex-center\">\n    <div class=\"mint-ratings-page-content\">\n      <div class=\"mint-content-container\">\n        <MintRatingsComponent\n          v-if=\"mintUrl\"\n          :url=\"mintUrl\"\n          :reviews=\"reviews\"\n          :allowCreateReview=\"allowCreateReview\"\n          :mintInfo=\"mintInfo\"\n          @close=\"goBack\"\n        />\n        <div v-else class=\"text-center q-pa-lg\">\n          <div class=\"text-h6 text-grey-5\">No mint URL provided</div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport MintRatingsComponent from \"src/components/MintRatingsComponent.vue\";\nimport { useMintRecommendationsStore } from \"src/stores/mintRecommendations\";\nimport { useMintsStore } from \"src/stores/mints\";\n\nexport default defineComponent({\n  name: \"MintRatingsPage\",\n  components: {\n    MintRatingsComponent,\n  },\n  data() {\n    return {\n      mintUrl: \"\",\n      reviews: [],\n      allowCreateReview: true,\n      mintInfo: null,\n    };\n  },\n  methods: {\n    goBack() {\n      // Navigate back to the previous page\n      this.$router.back();\n    },\n    loadMintInfo() {\n      // Try to get mint info from the recommendations store first\n      const recsStore = useMintRecommendationsStore();\n      let info = recsStore.getHttpInfoForUrl(this.mintUrl);\n\n      // If not found in recommendations, try to get it from mints store\n      if (!info) {\n        const mintsStore = useMintsStore();\n        const mint = mintsStore.mints.find((m) => m.url === this.mintUrl);\n        if (mint && mint.info) {\n          info = mint.info;\n        }\n      }\n\n      if (info) {\n        this.mintInfo = info;\n      }\n    },\n  },\n  created() {\n    // Get data from route query params\n    if (this.$route.query.mintUrl) {\n      this.mintUrl = this.$route.query.mintUrl as string;\n      this.allowCreateReview = this.$route.query.allowCreateReview !== \"false\";\n\n      // Load mint info from store\n      this.loadMintInfo();\n    } else {\n      // No mint URL provided, redirect back\n      this.$router.push(\"/\");\n    }\n  },\n});\n</script>\n\n<style scoped>\n.mint-ratings-page-content {\n  max-width: 820px;\n  margin: 0 auto;\n  color: white;\n  height: 100%;\n  overflow-y: auto;\n  position: absolute;\n  top: 0;\n  width: 100%;\n}\n\n.mint-content-container {\n  max-width: 820px;\n  margin: 0 auto;\n  color: white;\n  height: 100%;\n  overflow-y: auto;\n  position: relative;\n}\n</style>\n"
  },
  {
    "path": "src/pages/Restore.vue",
    "content": "<template>\n  <div class=\"bg-dark text-white text-center q-pa-md flex flex-center\">\n    <RestoreView />\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport RestoreView from \"components/RestoreView.vue\";\n\nexport default defineComponent({\n  name: \"ErrorNotFound\",\n  components: {\n    RestoreView,\n  },\n});\n</script>\n"
  },
  {
    "path": "src/pages/Settings.vue",
    "content": "<template>\n  <div class=\"bg-dark text-white text-center q-pa-md flex flex-center\">\n    <SettingsView />\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { defineComponent } from \"vue\";\nimport SettingsView from \"components/SettingsView.vue\";\n\nexport default defineComponent({\n  name: \"ErrorNotFound\",\n  components: {\n    SettingsView,\n  },\n});\n</script>\n"
  },
  {
    "path": "src/pages/TermsPage.vue",
    "content": "<!-- src/components/WelcomePage.vue -->\n<template>\n  <q-card class=\"bg-dark q-pa-none\" style=\"height: 100%\">\n    <WelcomeSlide4 />\n  </q-card>\n  <q-dialog persistent transition-show=\"slide-up\" transition-hide=\"fadeOut\">\n  </q-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { onMounted, ref } from \"vue\";\nimport WelcomeSlide4 from \"./welcome/WelcomeSlide4.vue\";\n\nexport default {\n  name: \"TermsPage\",\n  components: {\n    WelcomeSlide4,\n  },\n  setup() {\n    onMounted(() => {});\n\n    return {};\n  },\n};\n</script>\n\n<style scoped>\n.q-dialog__inner {\n  height: 100%;\n  width: 100%;\n  margin: 0; /* Align dialog to cover the entire viewport */\n}\n\n.q-card {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.q-carousel {\n  flex: 1;\n}\n\n.custom-navigation {\n  display: flex;\n  justify-content: space-between;\n  padding: 16px;\n}\n</style>\n"
  },
  {
    "path": "src/pages/WalletPage.vue",
    "content": "<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-8 text-center q-gutter-y-md\">\n      <ActivityOrb />\n      <NoMintWarnBanner v-if=\"mints.length == 0\" />\n      <BalanceView v-else :set-tab=\"setTab\" />\n      <div\n        class=\"row items-center justify-center no-wrap q-mb-none q-mx-none q-px-none q-pt-lg q-pb-md position-relative\"\n      >\n        <div class=\"col-6 q-mb-md flex justify-center items-center\">\n          <q-btn\n            rounded\n            dense\n            class=\"q-px-md q-mr-md wallet-action-btn\"\n            color=\"primary\"\n            @click=\"showReceiveDialog = true\"\n          >\n            <div class=\"button-content\">\n              <span>{{ $t(\"WalletPage.actions.receive.label\") }}</span>\n            </div>\n          </q-btn>\n        </div>\n\n        <transition appear enter-active-class=\"animated pulse\">\n          <div class=\"scan-button-container\">\n            <q-btn size=\"lg\" outline color=\"primary\" flat @click=\"showCamera\">\n              <ScanIcon size=\"2em\" />\n            </q-btn>\n          </div>\n        </transition>\n\n        <!-- button to showSendDialog -->\n        <div class=\"col-6 q-mb-md flex justify-center items-center\">\n          <q-btn\n            rounded\n            dense\n            class=\"q-px-md q-ml-md wallet-action-btn\"\n            color=\"primary\"\n            @click=\"showSendDialog = true\"\n          >\n            <div class=\"button-content\">\n              <span>{{ $t(\"WalletPage.actions.send.label\") }}</span>\n            </div>\n          </q-btn>\n        </div>\n        <ReceiveDialog v-model=\"showReceiveDialog\" />\n        <SendDialog v-model=\"showSendDialog\" />\n      </div>\n      <!-- ///////////////////////////////////////////\n      ////////////////// TABLES /////////////////\n      /////////////////////////////////////////// -->\n      <q-expansion-item expand-icon-class=\"hidden\" v-model=\"expandHistory\">\n        <template v-slot:header=\"{ expanded }\">\n          <q-item-section class=\"item-center text-center\">\n            <span\n              ><q-icon\n                color=\"primary\"\n                :name=\"expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down'\"\n            /></span>\n          </q-item-section>\n        </template>\n        <q-tabs\n          v-model=\"tab\"\n          no-caps\n          :class=\"$q.dark.isActive ? 'bg-dark' : 'bg-white'\"\n        >\n          <q-tab\n            name=\"history\"\n            class=\"text-secondary\"\n            :label=\"$t('WalletPage.tabs.history.label')\"\n          ></q-tab>\n          <!-- <q-tab name=\"tokens\" label=\"Tokens\"></q-tab> -->\n          <q-tab\n            name=\"mints\"\n            class=\"text-secondary\"\n            :label=\"$t('WalletPage.tabs.mints.label')\"\n          ></q-tab>\n        </q-tabs>\n\n        <q-tab-panels\n          :class=\"$q.dark.isActive ? 'bg-dark' : 'bg-white'\"\n          v-model=\"tab\"\n          animated\n        >\n          <!-- ////////////////// UNIFIED HISTORY LIST ///////////////// -->\n\n          <q-tab-panel name=\"history\">\n            <HistoryTable />\n          </q-tab-panel>\n\n          <!-- ////////////////////// SETTINGS ////////////////// -->\n\n          <q-tab-panel name=\"mints\" class=\"q-px-sm\">\n            <MintSettings />\n          </q-tab-panel>\n        </q-tab-panels>\n      </q-expansion-item>\n\n      <div style=\"margin-bottom: 0rem\">\n        <div class=\"row q-pt-sm\">\n          <div class=\"col-12 q-pt-xs\">\n            <q-btn\n              class=\"q-mx-xs q-px-sm q-my-sm\"\n              outline\n              size=\"0.6rem\"\n              v-if=\"\n                getPwaDisplayMode() == 'browser' &&\n                deferredPWAInstallPrompt != null\n              \"\n              color=\"primary\"\n              @click=\"triggerPwaInstall()\"\n              ><b>{{ $t(\"WalletPage.install.text\") }}</b\n              ><q-tooltip>{{\n                $t(\"WalletPage.install.tooltip\")\n              }}</q-tooltip></q-btn\n            >\n          </div>\n        </div>\n      </div>\n\n      <iOSPWAPrompt />\n      <AndroidPWAPrompt />\n    </div>\n\n    <!-- BOTTOM LIGHTNING BUTTONS -->\n\n    <!-- DIALOGS  -->\n\n    <!-- INPUT PARSER  -->\n    <PayInvoiceDialog v-model=\"payInvoiceData.show\" />\n\n    <!-- QR CODE SCANNER  -->\n    <q-dialog v-model=\"camera.show\" backdrop-filter=\"blur(2px) brightness(60%)\">\n      <QrcodeReader @decode=\"decodeQR\" />\n    </q-dialog>\n\n    <!-- WELCOME DIALOG  -->\n    <WelcomeDialog\n      :welcome-dialog=\"welcomeDialog\"\n      :trigger-pwa-install=\"triggerPwaInstall\"\n      :set-tab=\"setTab\"\n      :get-pwa-display-mode=\"getPwaDisplayMode\"\n      :set-welcome-dialog-seen=\"setWelcomeDialogSeen\"\n    />\n\n    <!-- INVOICE DETAILS  -->\n    <CreateInvoiceDialog v-model=\"showCreateInvoiceDialog\" />\n    <InvoiceDetailDialog v-model=\"showInvoiceDetails\" />\n\n    <!-- SEND TOKENS DIALOG  -->\n    <SendTokenDialog v-model=\"showSendTokens\" />\n\n    <!-- RECEIVE TOKENS DIALOG  -->\n    <ReceiveTokenDialog v-model=\"showReceiveTokens\" />\n  </div>\n</template>\n<style>\n* {\n  touch-action: manipulation;\n}\n\n.keypad {\n  display: grid;\n  grid-gap: 8px;\n  grid-template-columns: repeat(4, 1fr);\n  grid-template-rows: repeat(4, 1fr);\n}\n\n.keypad .btn {\n  height: 100%;\n}\n\n.keypad .btn-confirm {\n  grid-area: 1 / 4 / 5 / 4;\n}\n\n.wallet-action-btn {\n  min-width: 140px;\n  width: auto;\n  white-space: nowrap;\n  font-size: 1.2rem;\n}\n\n.button-content {\n  display: flex;\n  align-items: center;\n  white-space: nowrap;\n}\n\n/* Apply equal widths to wallet action buttons after render */\n.equal-width-buttons {\n  display: flex;\n  justify-content: space-between;\n}\n\n.scan-button-container {\n  position: absolute;\n  z-index: 1;\n  padding-bottom: 15px;\n}\n</style>\n<script lang=\"ts\">\nimport { date } from \"quasar\";\nimport * as _ from \"underscore\";\nimport { shortenString } from \"src/js/string-utils\";\nimport token from \"src/js/token\";\n\n// Vue components\nimport BalanceView from \"components/BalanceView.vue\";\nimport MintSettings from \"components/MintSettings.vue\";\nimport HistoryTable from \"components/HistoryTable.vue\";\nimport NoMintWarnBanner from \"components/NoMintWarnBanner.vue\";\nimport WelcomeDialog from \"components/WelcomeDialog.vue\";\nimport SendTokenDialog from \"components/SendTokenDialog.vue\";\nimport PayInvoiceDialog from \"components/PayInvoiceDialog.vue\";\nimport InvoiceDetailDialog from \"components/InvoiceDetailDialog.vue\";\nimport CreateInvoiceDialog from \"components/CreateInvoiceDialog.vue\";\nimport SendDialog from \"components/SendDialog.vue\";\nimport ReceiveDialog from \"components/ReceiveDialog.vue\";\nimport QrcodeReader from \"components/QrcodeReader.vue\";\nimport iOSPWAPrompt from \"components/iOSPWAPrompt.vue\";\nimport AndroidPWAPrompt from \"components/AndroidPWAPrompt.vue\";\nimport ActivityOrb from \"components/ActivityOrb.vue\";\n\n// pinia stores\nimport { mapActions, mapState, mapWritableState } from \"pinia\";\nimport { useMintsStore } from \"src/stores/mints\";\nimport { useSendTokensStore } from \"src/stores/sendTokensStore\";\nimport { useReceiveTokensStore } from \"src/stores/receiveTokensStore\";\nimport { useWorkersStore } from \"src/stores/workers\";\nimport { useTokensStore } from \"src/stores/tokens\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { useProofsStore } from \"src/stores/proofs\";\nimport { useCameraStore } from \"src/stores/camera\";\nimport { useP2PKStore } from \"src/stores/p2pk\";\nimport { useNWCStore } from \"src/stores/nwc\";\nimport { useNPCStore } from \"src/stores/npubcash\";\nimport { useNPCV2Store } from \"src/stores/npcv2\";\nimport { useNostrStore } from \"src/stores/nostr\";\nimport { usePRStore } from \"src/stores/payment-request\";\nimport { useDexieStore } from \"src/stores/dexie\";\n\nimport { useStorageStore } from \"src/stores/storage\";\nimport ReceiveTokenDialog from \"src/components/ReceiveTokenDialog.vue\";\nimport { useWelcomeStore } from \"../stores/welcome\";\nimport { useInvoicesWorkerStore } from \"src/stores/invoicesWorker\";\nimport { notifyError, notify } from \"../js/notify\";\n\nimport {\n  X as XIcon,\n  Banknote as BanknoteIcon,\n  Zap as ZapIcon,\n  Scan as ScanIcon,\n} from \"lucide-vue-next\";\n\nimport { useMigrationsStore } from \"src/stores/migrations\";\n\nexport default {\n  mixins: [windowMixin],\n  components: {\n    BalanceView,\n    MintSettings,\n    HistoryTable,\n    NoMintWarnBanner,\n    WelcomeDialog,\n    SendTokenDialog,\n    ReceiveTokenDialog,\n    PayInvoiceDialog,\n    InvoiceDetailDialog,\n    CreateInvoiceDialog,\n    QrcodeReader,\n    SendDialog,\n    ReceiveDialog,\n    iOSPWAPrompt,\n    AndroidPWAPrompt,\n    ScanIcon,\n    ActivityOrb,\n  },\n  data: function () {\n    return {\n      name: \"\",\n      mintId: \"\",\n      mintName: \"\",\n      deferredPWAInstallPrompt: null,\n      action: \"main\",\n      parse: {\n        show: false,\n        invoice: null,\n        lnurlpay: null,\n        lnurlauth: null,\n        data: {\n          request: \"\",\n          amount: 0,\n          comment: \"\",\n        },\n        camera: {\n          show: false,\n          camera: \"auto\",\n        },\n      },\n      payments: [],\n      paymentsChart: {\n        show: false,\n      },\n      welcomeDialog: {\n        show: false,\n      },\n      baseHost: location.protocol + \"//\" + location.host,\n      baseURL: location.protocol + \"//\" + location.host + location.pathname,\n      credit: 0,\n      newName: \"\",\n    };\n  },\n  computed: {\n    ...mapState(useUiStore, [\"tickerShort\"]),\n    ...mapWritableState(useUiStore, [\n      \"showInvoiceDetails\",\n      \"showCreateInvoiceDialog\",\n      \"tab\",\n      \"showSendDialog\",\n      \"showReceiveDialog\",\n    ]),\n    ...mapWritableState(useUiStore, [\"expandHistory\"]),\n    ...mapWritableState(useReceiveTokensStore, [\n      \"showReceiveTokens\",\n      \"receiveData\",\n    ]),\n    ...mapWritableState(useSendTokensStore, [\"showSendTokens\", \"sendData\"]),\n    ...mapState(useMintsStore, [\n      \"activeMintUrl\",\n      \"activeProofs\",\n      \"keys\",\n      \"mints\",\n      \"activeMint\",\n    ]),\n    ...mapWritableState(useWalletStore, [\n      \"invoiceHistory\",\n      \"invoiceData\",\n      \"payInvoiceData\",\n    ]),\n    ...mapWritableState(useMintsStore, [\"addMintData\", \"showAddMintDialog\"]),\n    ...mapWritableState(useWorkersStore, [\n      \"invoiceCheckListener\",\n      \"tokensCheckSpendableListener\",\n    ]),\n    ...mapState(useTokensStore, [\"historyTokens\"]),\n    ...mapState(usePRStore, [\"enablePaymentRequest\"]),\n    ...mapWritableState(useCameraStore, [\"camera\", \"hasCamera\"]),\n    ...mapWritableState(useP2PKStore, [\"showP2PKDialog\"]),\n    ...mapWritableState(useNWCStore, [\"showNWCDialog\", \"nwcEnabled\"]),\n    pendingPaymentsExist: function () {\n      return this.payments.findIndex((payment) => payment.pending) !== -1;\n    },\n\n    balance: function () {\n      return this.activeProofs\n        .map((t) => t)\n        .flat()\n        .reduce((sum, el) => (sum += el.amount), 0);\n    },\n  },\n  filters: {},\n  methods: {\n    ...mapActions(useProofsStore, [\n      \"serializeProofs\",\n      \"getProofsMint\",\n      \"serializeProofsV2\",\n      \"sumProofs\",\n      \"deleteProofs\",\n    ]),\n    ...mapActions(useMintsStore, [\n      \"activateMintUrl\",\n      \"addMint\",\n      \"assertMintError\",\n      \"getBalance\",\n      \"setActiveProofs\",\n      \"setProofs\",\n      \"getKeysForKeyset\",\n    ]),\n    ...mapActions(useWorkersStore, [\"clearAllWorkers\", \"invoiceCheckWorker\"]),\n    ...mapActions(useTokensStore, [\"setTokenPaid\"]),\n    ...mapActions(useWalletStore, [\n      \"setInvoicePaid\",\n      \"mint\",\n      \"checkPendingTokens\",\n      \"decodeRequest\",\n      \"initializeMnemonic\",\n    ]),\n    ...mapActions(useCameraStore, [\"closeCamera\", \"showCamera\"]),\n    ...mapActions(useNWCStore, [\"listenToNWCCommands\"]),\n    ...mapActions(useNPCStore, [\"generateNPCConnection\", \"claimAllTokens\"]),\n    ...mapActions(useNPCV2Store, [\n      \"generateNPCV2Connection\",\n      \"getLatestQuotes\",\n    ]),\n    ...mapActions(useNostrStore, [\n      \"sendNip04DirectMessage\",\n      \"sendNip17DirectMessage\",\n      \"subscribeToNip04DirectMessages\",\n      \"subscribeToNip17DirectMessages\",\n      \"sendNip17DirectMessageToNprofile\",\n      \"initSigner\",\n    ]),\n    ...mapActions(useDexieStore, [\"migrateToDexie\"]),\n    ...mapActions(useStorageStore, [\"checkLocalStorage\"]),\n    ...mapActions(usePRStore, [\"createPaymentRequest\"]),\n    ...mapActions(useInvoicesWorkerStore, [\n      \"startInvoiceCheckerWorker\",\n      \"checkPendingInvoices\",\n    ]),\n    // TOKEN METHODS\n    getProofs: function (decoded_token) {\n      return token.getProofs(decoded_token);\n    },\n    getMint: function (decoded_token) {\n      return token.getMint(decoded_token);\n    },\n    //\n    shortenString: function (s) {\n      return shortenString(s, 20, 10);\n    },\n    getTokenList: function () {\n      const amounts = this.activeProofs.map((t) => t.amount);\n      const counts = {};\n\n      for (const num of amounts) {\n        counts[num] = counts[num] ? counts[num] + 1 : 1;\n      }\n      return Object.keys(counts).map((k) => ({\n        value: parseInt(k),\n        count: parseInt(counts[k]),\n        sum: k * counts[k],\n      }));\n    },\n\n    paymentTableRowKey: function (row) {\n      return row.payment_hash + row.amount;\n    },\n    showChart: function () {\n      this.paymentsChart.show = true;\n      this.$nextTick(() => {\n        generateChart(this.$refs.canvas, this.payments);\n      });\n    },\n    focusInput(el) {\n      // TODO: fix this\n      // this.$nextTick(() => this.$refs[el].focus());\n    },\n    showParseDialog: function () {\n      this.payInvoiceData.show = true;\n      this.payInvoiceData.invoice = null;\n      this.payInvoiceData.lnurlpay = null;\n      this.payInvoiceData.domain = \"\";\n      this.payInvoiceData.lnurlauth = null;\n      this.payInvoiceData.input.request = \"\";\n      this.payInvoiceData.input.comment = \"\";\n      this.camera.show = false;\n      this.focusInput(\"parseDialogInput\");\n    },\n    showWelcomePage: function () {\n      if (!useWelcomeStore().termsAccepted) {\n        useWelcomeStore().showWelcome = true;\n      }\n      if (useWelcomeStore().showWelcome) {\n        const currentQuery = window.location.search;\n        const currentHash = window.location.hash;\n        this.$router.push(\"/welcome\" + currentQuery + currentHash);\n      }\n    },\n    setTab: function (to) {\n      this.tab = to;\n    },\n    decodeQR: function (res) {\n      this.camera.data = res;\n      this.camera.show = false;\n      this.decodeRequest(res);\n    },\n    /////////////////////////////////// WALLET ///////////////////////////////////\n    showInvoiceCreateDialog: async function () {\n      console.log(\"##### showInvoiceCreateDialog\");\n      this.invoiceData.amount = \"\";\n      this.invoiceData.bolt11 = \"\";\n      this.invoiceData.hash = \"\";\n      this.invoiceData.memo = \"\";\n      this.showCreateInvoiceDialog = true;\n    },\n    showSendTokensDialog: function () {\n      console.log(\"##### showSendTokensDialog\");\n      this.sendData.tokens = \"\";\n      this.sendData.tokensBase64 = \"\";\n      this.sendData.amount = null;\n      this.sendData.memo = \"\";\n      this.showSendTokens = true;\n    },\n    hideSendTokensDialog: function () {\n      this.showSendTokens = false;\n    },\n    showReceiveTokensDialog: function () {\n      this.receiveData.tokensBase64 = \"\";\n      this.showReceiveTokens = true;\n    },\n\n    //////////////////////// MINT //////////////////////////////////////////\n\n    // ////////////// UI HELPERS //////////////\n\n    ////////////// WORKERS //////////////\n\n    ////////////// UI HELPERS /////////////\n    registerPWAEventHook: function () {\n      // register event listener for PWA install prompt\n      window.addEventListener(\"beforeinstallprompt\", (e) => {\n        // Prevent the mini-infobar from appearing on mobile\n        // e.preventDefault()\n        // Stash the event so it can be triggered later.\n        this.deferredPWAInstallPrompt = e;\n        console.log(\n          `'beforeinstallprompt' event was fired.`,\n          this.getPwaDisplayMode()\n        );\n      });\n    },\n    getPwaDisplayMode: function () {\n      const isStandalone = window.matchMedia(\n        \"(display-mode: standalone)\"\n      ).matches;\n      if (document.referrer.startsWith(\"android-app://\")) {\n        return \"twa\";\n      } else if (navigator.standalone || isStandalone) {\n        return \"standalone\";\n      }\n      return \"browser\";\n    },\n    triggerPwaInstall: function () {\n      // Show the install prompt\n      // Note: this doesn't work with IOS, we do it with iOSPWAPrompt\n      this.deferredPWAInstallPrompt.prompt();\n      // Wait for the user to respond to the prompt\n      this.deferredPWAInstallPrompt.userChoice.then((choiceResult) => {\n        if (choiceResult.outcome === \"accepted\") {\n          console.log(\"User accepted the install prompt\");\n          this.setWelcomeDialogSeen();\n        } else {\n          console.log(\"User dismissed the install prompt\");\n        }\n      });\n    },\n    registerBroadcastChannel: async function () {\n      // uses session storage to identify the tab so we can ignore incoming messages from the same tab\n      if (!sessionStorage.getItem(\"tabId\")) {\n        sessionStorage.setItem(\n          \"tabId\",\n          Math.random().toString(36).substring(2) +\n            new Date().getTime().toString(36)\n        );\n      }\n      const tabId = sessionStorage.getItem(\"tabId\");\n      const channel = new BroadcastChannel(\"app_channel\");\n      channel.postMessage({ type: \"new_tab_opened\", senderId: tabId });\n      channel.onmessage = async (event) => {\n        // console.log(\"Received message in tab \" + tabId, event.data);\n        if (event.data.senderId === tabId) {\n          return; // Ignore the message if it comes from the same tab\n        }\n        if (event.data.type == \"new_tab_opened\") {\n          channel.postMessage({ type: \"already_running\", senderId: tabId });\n        } else if (event.data.type == \"already_running\") {\n          this.$router.push(\"/already-running\");\n        }\n      };\n    },\n    equalizeButtonWidths: function () {\n      this.$nextTick(() => {\n        const actionBtns = document.querySelectorAll(\".wallet-action-btn\");\n        if (actionBtns.length >= 2) {\n          // Reset widths first\n          actionBtns.forEach((btn) => {\n            btn.style.width = \"auto\";\n          });\n\n          // Get the maximum width\n          let maxWidth = 0;\n          actionBtns.forEach((btn) => {\n            maxWidth = Math.max(maxWidth, btn.offsetWidth);\n          });\n\n          // Apply the maximum width to all buttons\n          actionBtns.forEach((btn) => {\n            btn.style.width = `${maxWidth}px`;\n          });\n        }\n      });\n    },\n  },\n  watch: {},\n\n  mounted: function () {\n    // generate NPC connection\n    this.generateNPCConnection();\n    this.claimAllTokens();\n    this.generateNPCV2Connection();\n    this.getLatestQuotes();\n    // Ensure wallet action buttons have equal width\n    this.$nextTick(this.equalizeButtonWidths);\n    // Add window resize listener to handle responsive layouts\n    window.addEventListener(\"resize\", this.equalizeButtonWidths);\n  },\n\n  beforeUnmount: function () {\n    // Remove event listener when component is destroyed\n    window.removeEventListener(\"resize\", this.equalizeButtonWidths);\n  },\n\n  created: async function () {\n    console.log(`Git commit: ${GIT_COMMIT}`);\n\n    // Initialize and run migrations\n    const migrationsStore = useMigrationsStore();\n    migrationsStore.initMigrations();\n    await migrationsStore.runMigrations();\n\n    // check if another tab is open\n    this.registerBroadcastChannel();\n\n    const params = new URL(document.location).searchParams;\n    const hash = new URL(document.location).hash;\n\n    // mint url\n    if (params.get(\"mint\")) {\n      const addMintUrl = params.get(\"mint\");\n      await this.setTab(\"mints\");\n      this.showAddMintDialog = true;\n      this.addMintData = { url: addMintUrl };\n    }\n    if (!localStorage.getItem(\"cashu.activeMintUrl\")) {\n      this.setTab(\"mints\");\n    }\n\n    console.log(\"Mint URL \" + this.activeMintUrl);\n    console.log(\"Wallet URL \" + this.baseURL);\n\n    // get token to receive tokens from a link\n    if (params.get(\"token\") || hash.includes(\"token\")) {\n      const tokenBase64 = params.get(\"token\") || hash.split(\"token=\")[1];\n      // make sure to react only to tokens not in the users history\n      let seen = false;\n      for (let i = 0; i < this.historyTokens.length; i++) {\n        const thisToken = this.historyTokens[i].token;\n        if (thisToken == tokenBase64 && this.historyTokens[i].amount > 0) {\n          seen = true;\n        }\n      }\n      if (!seen) {\n        // show receive token dialog\n        this.receiveData.tokensBase64 = tokenBase64;\n        this.showReceiveTokens = true;\n      }\n    }\n\n    // get lightning invoice from a link\n    if (params.get(\"lightning\")) {\n      this.showParseDialog();\n      this.payInvoiceData.input.request = params.get(\"lightning\");\n    }\n\n    // Clear all parameters from URL without refreshing the page\n    /*\n    window.history.pushState(\n      {},\n      document.title,\n      window.location.href.split(\"?\")[0].split(\"#\")[0]\n    );\n    */\n    console.log(`location.hash: ${window.location.hash}`);\n\n    // startup tasks\n\n    // debug console\n    useUiStore().enableDebugConsole();\n\n    // migrate to dexie\n    await this.migrateToDexie();\n\n    // check local storage\n    this.checkLocalStorage();\n\n    // PWA install hook\n    this.registerPWAEventHook();\n\n    // generate mnemonic only if onboarding is finished or path is 'new'\n    try {\n      const welcome = useWelcomeStore();\n      if (!welcome.showWelcome || welcome.onboardingPath === \"new\") {\n        this.initializeMnemonic();\n      }\n    } catch (e) {\n      // fallback safe\n      this.initializeMnemonic();\n    }\n\n    this.initSigner();\n\n    // show welcome dialog\n    this.showWelcomePage();\n\n    // listen to NWC commands if enabled\n    if (this.nwcEnabled) {\n      this.listenToNWCCommands();\n    }\n\n    if (this.enablePaymentRequest) {\n      this.subscribeToNip17DirectMessages();\n    }\n\n    // start invoice checker worker\n    this.startInvoiceCheckerWorker();\n\n    // reconnect all websockets\n    this.checkPendingInvoices();\n  },\n};\n</script>\n"
  },
  {
    "path": "src/pages/WelcomePage.vue",
    "content": "<!-- src/components/WelcomePage.vue -->\n<template>\n  <q-dialog\n    v-model=\"welcomeStore.showWelcome\"\n    persistent\n    transition-show=\"slide-up\"\n    transition-hide=\"fadeOut\"\n    full-screen\n    @drop.prevent=\"dragFile\"\n    @dragover.prevent\n  >\n    <q-card class=\"q-pa-none\" style=\"height: 100%\">\n      <q-carousel\n        v-model=\"welcomeStore.currentSlide\"\n        animated\n        control-color=\"primary\"\n        class=\"flex-1\"\n        style=\"height: 100%\"\n      >\n        <q-carousel-slide :name=\"0\">\n          <WelcomeSlide1 />\n        </q-carousel-slide>\n        <q-carousel-slide :name=\"1\">\n          <WelcomeSlide2 />\n        </q-carousel-slide>\n        <q-carousel-slide :name=\"2\">\n          <WelcomeSlideChoice />\n        </q-carousel-slide>\n        <!-- New wallet flow: seed display at slide 3 -->\n        <q-carousel-slide\n          :name=\"3\"\n          v-if=\"welcomeStore.onboardingPath === 'new'\"\n        >\n          <WelcomeSlide3 />\n        </q-carousel-slide>\n        <!-- Recover flow: seed input at slide 3 -->\n        <q-carousel-slide\n          :name=\"3\"\n          v-else-if=\"welcomeStore.onboardingPath === 'recover'\"\n        >\n          <WelcomeRecoverSeed />\n        </q-carousel-slide>\n        <!-- Mints setup at slide 4 (both paths) -->\n        <q-carousel-slide :name=\"4\" v-if=\"welcomeStore.onboardingPath\">\n          <WelcomeMintSetup />\n        </q-carousel-slide>\n        <!-- Recover flow: restore ecash at slide 5 -->\n        <q-carousel-slide\n          :name=\"5\"\n          v-if=\"welcomeStore.onboardingPath === 'recover'\"\n        >\n          <WelcomeRestoreEcash />\n        </q-carousel-slide>\n      </q-carousel>\n\n      <div\n        class=\"q-pa-md flex justify-between\"\n        v-if=\"welcomeStore.currentSlide > 0\"\n      >\n        <q-btn\n          flat\n          icon=\"arrow_left\"\n          :label=\"$t('WelcomePage.actions.previous.label')\"\n          v-if=\"welcomeStore.canGoPrev\"\n          @click=\"welcomeStore.goToPrevSlide\"\n        />\n        <!-- language selector (hidden on first slide since it's now in the slide itself) -->\n        <div\n          class=\"q-ml-md\"\n          v-if=\"!welcomeStore.canGoPrev && welcomeStore.currentSlide > 0\"\n          style=\"position: relative; top: -5px\"\n        >\n          <q-select\n            v-model=\"selectedLanguage\"\n            :options=\"languageOptions\"\n            emit-value\n            dense\n            map-options\n            @update:model-value=\"changeLanguage\"\n            style=\"max-width: 200px; max-height: 20px\"\n          />\n        </div>\n        <q-space />\n        <q-btn\n          flat\n          icon=\"arrow_right\"\n          :label=\"$t('WelcomePage.actions.next.label')\"\n          :disable=\"!welcomeStore.canProceed\"\n          @click=\"welcomeStore.goToNextSlide\"\n          v-if=\"\n            welcomeStore.currentSlide > 0 && welcomeStore.currentSlide !== 2\n          \"\n        />\n      </div>\n    </q-card>\n  </q-dialog>\n</template>\n\n<script lang=\"ts\">\nimport { onMounted, ref } from \"vue\";\nimport { useWelcomeStore } from \"src/stores/welcome\";\nimport { useStorageStore } from \"src/stores/storage\";\nimport WelcomeSlide1 from \"./welcome/WelcomeSlide1.vue\";\nimport WelcomeSlide2 from \"./welcome/WelcomeSlide2.vue\";\nimport WelcomeSlide3 from \"./welcome/WelcomeSlide3.vue\";\nimport WelcomeSlideChoice from \"./welcome/WelcomeSlideChoice.vue\";\nimport WelcomeRecoverSeed from \"./welcome/WelcomeRecoverSeed.vue\";\nimport WelcomeMintSetup from \"./welcome/WelcomeMintSetup.vue\";\nimport WelcomeRestoreEcash from \"./welcome/WelcomeRestoreEcash.vue\";\n\nexport default {\n  name: \"WelcomePage\",\n  components: {\n    WelcomeSlide1,\n    WelcomeSlide2,\n    WelcomeSlide3,\n    WelcomeSlideChoice,\n    WelcomeRecoverSeed,\n    WelcomeMintSetup,\n    WelcomeRestoreEcash,\n  },\n  data() {\n    return {\n      selectedLanguage: \"\",\n      languageOptions: [\n        { label: \"English\", value: \"en-US\" },\n        { label: \"Español\", value: \"es-ES\" },\n        { label: \"Italiano\", value: \"it-IT\" },\n        { label: \"Deutsch\", value: \"de-DE\" },\n        { label: \"Français\", value: \"fr-FR\" },\n        { label: \"Čeština\", value: \"cs-CZ\" },\n        { label: \"Português (Brasil)\", value: \"pt-BR\" },\n        { label: \"Svenska\", value: \"sv-SE\" },\n        { label: \"Ελληνικά\", value: \"el-GR\" },\n        { label: \"Türkçe\", value: \"tr-TR\" },\n        { label: \"ไทย\", value: \"th-TH\" },\n        { label: \"العربية\", value: \"ar-SA\" },\n        { label: \"中文\", value: \"zh-CN\" },\n        { label: \"日本語\", value: \"ja-JP\" },\n      ],\n    };\n  },\n  methods: {\n    changeLanguage(locale) {\n      // Set the i18n locale\n      this.$i18n.locale = locale;\n\n      // Store the selected language in localStorage\n      localStorage.setItem(\"cashu.language\", locale);\n    },\n  },\n  created() {\n    // Set the initial selected language based on the current locale or from storage\n    this.selectedLanguage =\n      this.languageOptions.find(\n        (option) => option.value === this.$i18n.locale || navigator.language\n      )?.label || \"Language\";\n  },\n  setup() {\n    const welcomeStore = useWelcomeStore();\n    const storageStore = useStorageStore();\n    const fileUpload = ref(null);\n\n    const onChangeFileUpload = () => {\n      const file = fileUpload.value.files[0];\n      if (file) readFile(file);\n    };\n\n    const readFile = (file) => {\n      const reader = new FileReader();\n      reader.onload = (f) => {\n        const backup = JSON.parse(f.target.result);\n        storageStore.restoreFromBackup(backup);\n      };\n      reader.readAsText(file);\n    };\n\n    const dragFile = (ev) => {\n      const file = ev.dataTransfer.files[0];\n      if (file) readFile(file);\n    };\n\n    onMounted(() => {\n      welcomeStore.initializeWelcome();\n    });\n\n    return {\n      welcomeStore,\n      fileUpload,\n      onChangeFileUpload,\n      dragFile,\n    };\n  },\n};\n</script>\n\n<style scoped>\n.q-dialog__inner {\n  height: 100%;\n  width: 100%;\n  margin: 0; /* Align dialog to cover the entire viewport */\n}\n\n.q-card {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n}\n\n.q-carousel {\n  flex: 1;\n}\n\n.custom-navigation {\n  display: flex;\n  justify-content: space-between;\n  padding: 16px;\n}\n</style>\n"
  },
  {
    "path": "src/pages/welcome/WelcomeMintSetup.vue",
    "content": "<template>\n  <div class=\"mint-setup-slide\">\n    <!-- Main content area -->\n    <div class=\"content\">\n      <!-- Header Icon -->\n      <div class=\"header-icon\">\n        <div class=\"icon-circle\">\n          <q-icon name=\"account_tree\" size=\"2.5em\" color=\"white\" />\n        </div>\n      </div>\n\n      <!-- Title -->\n      <h1 class=\"title\">{{ $t(\"WelcomeMintSetup.title\") }}</h1>\n\n      <!-- Description -->\n      <p class=\"description\">\n        {{ $t(\"WelcomeMintSetup.text\") }}\n      </p>\n\n      <!-- Your added mints -->\n      <div class=\"mints-section\">\n        <div class=\"mints-section-header\">\n          <h3 v-if=\"mints.mints.length > 0\" class=\"section-title\">\n            {{ $t(\"WelcomeMintSetup.sections.your_mints\") }}\n          </h3>\n          <!-- Restoring indicator during recover + Nostr search -->\n          <div\n            v-if=\"welcome.onboardingPath === 'recover' && restoringMints\"\n            class=\"row items-center q-mb-md q-px-lg\"\n          >\n            <q-spinner-dots size=\"24px\" color=\"grey-5\" class=\"q-mr-sm\" />\n            <div class=\"text-grey-6\">\n              {{ $t(\"WelcomeMintSetup.restoring\") }}\n            </div>\n          </div>\n        </div>\n        <div v-for=\"mint in mints.mints\" :key=\"mint.url\" class=\"mint-item\">\n          <div class=\"mint-content\">\n            <q-avatar\n              v-if=\"getMintIconUrlExisting(mint)\"\n              size=\"34px\"\n              class=\"mint-icon\"\n            >\n              <q-img\n                spinner-color=\"white\"\n                spinner-size=\"xs\"\n                :src=\"getMintIconUrlExisting(mint)\"\n                alt=\"Mint Icon\"\n                style=\"height: 34px; max-width: 34px; font-size: 12px\"\n              />\n            </q-avatar>\n            <div class=\"mint-info\">\n              <div class=\"mint-name\">\n                {{ mint.nickname || mint.info?.name }}\n              </div>\n              <div class=\"mint-url\">{{ mint.url }}</div>\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- Manual add mint -->\n      <div class=\"add-mint-section\">\n        <h3 class=\"section-title\">{{ $t(\"MintSettings.add.title\") }}</h3>\n        <div class=\"add-mint-inputs\">\n          <q-input\n            rounded\n            outlined\n            v-model=\"addMintData.url\"\n            :placeholder=\"$t('WelcomeMintSetup.placeholder.mint_url')\"\n            @keydown.enter.prevent=\"sanitizeMintUrlAndShowAddDialog\"\n            ref=\"mintInput\"\n            class=\"q-mb-md mint-input url-input\"\n          />\n        </div>\n        <div class=\"add-mint-button\">\n          <q-btn\n            flat\n            rounded\n            :disable=\"addMintData.url.length === 0\"\n            @click=\"\n              addMintData.url.length > 0\n                ? sanitizeMintUrlAndShowAddDialog()\n                : null\n            \"\n            class=\"add-mint-btn\"\n          >\n            <q-icon name=\"add\" size=\"18px\" class=\"q-mr-sm\" />\n            <span>{{ $t(\"MintSettings.add.actions.add_mint.label\") }}</span>\n          </q-btn>\n        </div>\n      </div>\n\n      <!-- Recovery-specific: search user mints on nostr using seed -->\n      <div v-if=\"welcome.onboardingPath === 'recover'\">\n        <NostrMintRestore\n          :mnemonic=\"restore.mnemonicToRestore\"\n          :is-mnemonic-valid=\"isSeedValid\"\n          :auto-add=\"true\"\n        />\n      </div>\n\n      <!-- Discover mints on nostr (global) -->\n      <div>\n        <MintDiscovery :autoDiscover=\"true\" :allowCreateReview=\"false\" />\n      </div>\n    </div>\n\n    <AddMintDialog\n      :addMintData=\"addMintData\"\n      :showAddMintDialog=\"showAddMintDialog\"\n      @update:showAddMintDialog=\"showAddMintDialog = $event\"\n      :addMintBlocking=\"addMintBlocking\"\n      @add=\"addMintInternal\"\n    />\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { computed, ref, watch, onMounted } from \"vue\";\nimport { useWelcomeStore } from \"src/stores/welcome\";\nimport { useRestoreStore } from \"src/stores/restore\";\nimport { useMintsStore, MintClass } from \"src/stores/mints\";\nimport { useNostrStore } from \"src/stores/nostr\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { notifyError, notifySuccess } from \"src/js/notify\";\nimport AddMintDialog from \"src/components/AddMintDialog.vue\";\nimport NostrMintRestore from \"src/components/NostrMintRestore.vue\";\nimport { useNostrMintBackupStore } from \"src/stores/nostrMintBackup\";\nimport MintDiscovery from \"../../components/MintDiscovery.vue\";\n\nexport default {\n  name: \"WelcomeMintSetup\",\n  components: { AddMintDialog, NostrMintRestore, MintDiscovery },\n  setup() {\n    const welcome = useWelcomeStore();\n    const restore = useRestoreStore();\n    const mints = useMintsStore();\n    const nostr = useNostrStore();\n    const nostrMintBackup = useNostrMintBackupStore();\n    const ui = useUiStore();\n\n    const isSeedValid = computed(() => {\n      const s = restore.mnemonicToRestore?.trim() || \"\";\n      return s.split(/\\s+/).length >= 12;\n    });\n\n    // Show spinner while the Nostr mint search is in progress in recover flow\n    const restoringMints = computed(\n      () => isSeedValid.value && nostrMintBackup.searchInProgress\n    );\n\n    const addMintData = mints.addMintData; // reactive from store\n    const showAddMintDialog = computed({\n      get: () => mints.showAddMintDialog,\n      set: (v: boolean) => (mints.showAddMintDialog = v),\n    });\n    const addMintBlocking = computed(() => mints.addMintBlocking);\n\n    const sanitizeMintUrlAndShowAddDialog = () => {\n      mints.showAddMintDialog = true;\n    };\n    const addMintInternal = async (data: {\n      url: string;\n      nickname?: string;\n    }) => {\n      await mints.addMint(data, true);\n      mints.addMintData = { url: \"\", nickname: \"\" } as any;\n    };\n\n    const mintClass = (mint: any) => new MintClass(mint);\n    const formatCurrency = (amount: number, unit: string) =>\n      ui.formatCurrency(amount, unit);\n    const getMintIconUrlExisting = (mint: any) =>\n      mint?.info?.icon_url || undefined;\n\n    const markDone = () => {\n      welcome.mintSetupCompleted = true;\n    };\n\n    // Mark mint setup as completed on mount since it's optional\n    // Users can proceed with or without adding mints\n    onMounted(() => {\n      welcome.mintSetupCompleted = true;\n    });\n\n    return {\n      welcome,\n      restore,\n      mints,\n      isSeedValid,\n      addMintData,\n      showAddMintDialog,\n      addMintBlocking,\n      sanitizeMintUrlAndShowAddDialog,\n      addMintInternal,\n      mintClass,\n      formatCurrency,\n      getMintIconUrlExisting,\n      markDone,\n      restoringMints,\n    };\n  },\n};\n</script>\n\n<style scoped>\n.mint-setup-slide {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  height: 100%;\n  background: var(--q-dark);\n  color: white;\n  padding: 40px 20px 20px 20px;\n  box-sizing: border-box;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  text-align: left;\n  flex: 1;\n  width: 100%;\n  max-width: 500px;\n}\n\n.header-icon {\n  margin-bottom: 20px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.icon-circle {\n  width: 80px;\n  height: 80px;\n  border-radius: 50%;\n  background: rgba(255, 255, 255, 0.1);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.title {\n  font-size: 1.8rem;\n  font-weight: 700;\n  margin: 0 0 16px 0;\n  color: white;\n  line-height: 1.2;\n  text-align: left;\n  width: 100%;\n}\n\n.description {\n  font-size: 0.95rem;\n  line-height: 1.5;\n  color: rgba(255, 255, 255, 0.8);\n  margin: 0 0 32px 0;\n  text-align: left;\n  width: 100%;\n}\n\n.mints-section,\n.add-mint-section {\n  width: 100%;\n  margin-bottom: 24px;\n}\n\n.section-title {\n  font-size: 15.2px;\n  font-family: Inter, -apple-system, \"system-ui\", \"Segoe UI\", Roboto,\n    \"Helvetica Neue\", Arial, sans-serif;\n  font-weight: 600;\n  color: #ffffff;\n  margin: 0 0 16px 0;\n}\n\n.mint-item {\n  padding: 12px 12px 12px 0px;\n  border-radius: 12px;\n  transition: all 0.2s ease;\n  margin-bottom: 8px;\n}\n\n.mint-content {\n  display: flex;\n  align-items: flex-start;\n  gap: 12px;\n}\n\n.mint-icon {\n  flex-shrink: 0;\n  margin-top: 2px;\n}\n\n.mint-info {\n  flex: 1;\n}\n\n.mint-name {\n  font-size: 15.2px;\n  font-family: Inter, -apple-system, \"system-ui\", \"Segoe UI\", Roboto,\n    \"Helvetica Neue\", Arial, sans-serif;\n  font-weight: 600;\n  color: #ffffff;\n  margin: 0 0 4px 0;\n}\n\n.mint-url {\n  font-size: 0.9rem;\n  color: rgba(255, 255, 255, 0.8);\n  margin: 0;\n  line-height: 1.4;\n}\n\n.add-mint-inputs {\n  margin-bottom: 16px;\n}\n\n.add-mint-button {\n  margin-top: 12px;\n}\n\n.add-mint-btn {\n  width: auto;\n  min-width: 100px;\n  height: 36px;\n  font-weight: 500;\n  text-transform: none;\n  font-size: 0.9rem;\n  border-radius: 18px;\n  color: var(--q-primary);\n  background: rgba(var(--q-primary-rgb), 0.1);\n  transition: all 0.2s ease;\n}\n\n.add-mint-btn:hover {\n  background: rgba(var(--q-primary-rgb), 0.2);\n  transform: translateY(-1px);\n}\n\n.add-mint-btn:disabled {\n  color: rgba(255, 255, 255, 0.3);\n  background: transparent;\n  transform: none;\n}\n\n.mint-input .q-field__control {\n  height: 44px;\n  border-radius: 8px;\n  background: rgba(255, 255, 255, 0.05);\n  border: 1px solid rgba(255, 255, 255, 0.1);\n  transition: all 0.2s ease;\n}\n\n.mint-input .q-field__control:hover {\n  background: rgba(255, 255, 255, 0.08);\n  border-color: rgba(255, 255, 255, 0.2);\n}\n\n.mint-input .q-field--focused .q-field__control {\n  background: rgba(255, 255, 255, 0.08);\n  border-color: rgba(var(--q-primary-rgb), 0.5);\n  box-shadow: 0 0 0 2px rgba(var(--q-primary-rgb), 0.1);\n}\n\n/* Mobile adjustments */\n@media (max-width: 600px) {\n  .mint-setup-slide {\n    padding: 30px 15px 15px 15px;\n  }\n\n  .title {\n    font-size: 1.6rem;\n  }\n\n  .description {\n    font-size: 0.9rem;\n    margin-bottom: 28px;\n  }\n\n  .mints-section,\n  .add-mint-section {\n    width: 100%;\n  }\n\n  .section-title {\n    font-size: 14px;\n  }\n\n  .mint-name {\n    font-size: 14px;\n  }\n\n  .mint-url {\n    font-size: 0.85rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/pages/welcome/WelcomeRecoverSeed.vue",
    "content": "<template>\n  <div class=\"recover-slide\">\n    <!-- Main content area -->\n    <div class=\"content\">\n      <!-- Header Icon -->\n      <div class=\"header-icon\">\n        <div class=\"icon-circle\">\n          <q-icon name=\"vpn_key\" size=\"2.5em\" color=\"white\" />\n        </div>\n      </div>\n\n      <!-- Title -->\n      <h1 class=\"title\">{{ $t(\"WelcomeRecoverSeed.title\") }}</h1>\n\n      <!-- Description -->\n      <p class=\"description\">\n        {{ $t(\"WelcomeRecoverSeed.text\") }}\n      </p>\n\n      <!-- Seed phrase input grid -->\n      <div class=\"input-section\">\n        <div class=\"words-grid\">\n          <div v-for=\"index in 12\" :key=\"index\" class=\"word-input-wrapper\">\n            <span class=\"word-number\">{{ index }}</span>\n            <q-input\n              :model-value=\"words[index - 1]\"\n              @update:model-value=\"updateWord($event, index - 1)\"\n              outlined\n              dense\n              :ref=\"(el) => setInputRef(el, index - 1)\"\n              @paste=\"handlePaste($event, index - 1)\"\n              class=\"word-input\"\n              :placeholder=\"$t('WelcomeRecoverSeed.inputs.word', { index })\"\n              :input-attrs=\"{\n                autocapitalize: 'none',\n                autocorrect: 'off',\n                autocomplete: 'off',\n                spellcheck: 'false',\n              }\"\n            />\n          </div>\n        </div>\n\n        <!-- Paste button below inputs -->\n        <q-btn\n          flat\n          :label=\"$t('WelcomeRecoverSeed.actions.paste_all')\"\n          @click=\"paste\"\n          class=\"paste-btn\"\n        />\n\n        <!-- Error message -->\n        <p v-if=\"errorMsg\" class=\"error-msg\">{{ errorMsg }}</p>\n      </div>\n\n      <!-- Disclaimer -->\n      <p class=\"disclaimer\">\n        {{ $t(\"WelcomeRecoverSeed.disclaimer\") }}\n      </p>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { ref, computed, watch } from \"vue\";\nimport { useWelcomeStore } from \"src/stores/welcome\";\nimport { useRestoreStore } from \"src/stores/restore\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { validateMnemonic as validateBip39Mnemonic } from \"@scure/bip39\";\nimport { wordlist } from \"@scure/bip39/wordlists/english\";\nimport { i18n } from \"../../boot/i18n\";\n\nexport default {\n  name: \"WelcomeRecoverSeed\",\n  setup() {\n    const welcome = useWelcomeStore();\n    const restore = useRestoreStore();\n    const wallet = useWalletStore();\n    const ui = useUiStore();\n\n    const words = ref<string[]>(Array(12).fill(\"\"));\n    const inputRefs = ref<any[]>([]);\n\n    const setInputRef = (el: any, index: number) => {\n      if (el) {\n        inputRefs.value[index] = el;\n      }\n    };\n\n    type QInputModel = string | number | null;\n\n    const updateWord = (val: QInputModel, index: number) => {\n      const lower = String(val ?? \"\").toLowerCase();\n\n      // Multi word (paste or user typed spaces)\n      if (/\\s/.test(lower.trim()) && lower.trim().split(/\\s+/).length > 1) {\n        const splitWords = lower.trim().split(/\\s+/);\n        splitWords.forEach((w, i) => {\n          if (index + i < 12) {\n            words.value[index + i] = w;\n          }\n        });\n        const nextIndex = Math.min(index + splitWords.length, 11);\n        setTimeout(() => {\n          inputRefs.value[nextIndex]?.focus();\n        }, 50);\n\n        return;\n      }\n\n      // Space to advance, store trimmed\n      if (lower.endsWith(\" \") && index < 11) {\n        words.value[index] = lower.trim();\n        setTimeout(() => {\n          inputRefs.value[index + 1]?.focus();\n        }, 50);\n        return;\n      }\n\n      // Normal typing\n      words.value[index] = lower;\n    };\n\n    // Combine individual words into mnemonic\n    const mnemonic = computed(() => {\n      return words.value\n        .filter((w) => w.trim())\n        .join(\" \")\n        .trim()\n        .toLowerCase();\n    });\n\n    // Count of words filled\n    const filledWords = computed(\n      () => words.value.filter((w) => w.trim()).length\n    );\n\n    // Message to show\n    const errorMsg = computed(() => {\n      if (filledWords.value === 0) return \"\";\n      if (filledWords.value < 12)\n        return `${filledWords.value}/12 words entered`;\n      if (!validateBip39Mnemonic(mnemonic.value, wordlist)) {\n        return i18n.global.t(\"RestoreView.actions.validate.error\");\n      }\n      return \"\";\n    });\n\n    // Watch mnemonic and update store\n    watch(mnemonic, (val) => {\n      restore.mnemonicToRestore = val;\n      const valid = validateBip39Mnemonic(val, wordlist);\n      welcome.seedEnteredValid = valid;\n      if (valid) {\n        wallet.setMnemonicFromUser(val);\n      }\n    });\n\n    // Handle paste on individual input\n    const handlePaste = (event: ClipboardEvent, index: number) => {\n      event.preventDefault();\n      const pastedText = event.clipboardData?.getData(\"text\") || \"\";\n      updateWord(pastedText, index);\n    };\n\n    // Paste all from clipboard\n    const paste = async () => {\n      try {\n        const text = await ui.pasteFromClipboard();\n        updateWord(text, 0);\n      } catch {}\n    };\n\n    return {\n      welcome,\n      restore,\n      wallet,\n      words,\n      errorMsg,\n      paste,\n      handlePaste,\n      updateWord,\n      setInputRef,\n    };\n  },\n};\n</script>\n\n<style scoped>\n.recover-slide {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  height: 100%;\n  background: var(--q-dark);\n  color: white;\n  padding: 40px 20px 20px 20px;\n  box-sizing: border-box;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  text-align: left;\n  flex: 1;\n}\n\n.header-icon {\n  margin-bottom: 20px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.icon-circle {\n  width: 80px;\n  height: 80px;\n  border-radius: 50%;\n  background: rgba(255, 255, 255, 0.1);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.title {\n  font-size: 1.8rem;\n  font-weight: 700;\n  margin: 0 0 16px 0;\n  color: white;\n  line-height: 1.2;\n  text-align: left;\n  width: 100%;\n  max-width: 500px; /* unified */\n}\n\n.description {\n  font-size: 0.95rem;\n  line-height: 1.5;\n  color: rgba(255, 255, 255, 0.8);\n  margin: 0 0 32px 0;\n  text-align: left;\n  max-width: 500px; /* unified */\n  width: 100%;\n}\n\n.input-section {\n  width: 100%;\n  max-width: 500px; /* unified */\n  margin-bottom: 16px;\n}\n\n.words-grid {\n  display: grid;\n  grid-template-columns: repeat(3, 1fr);\n  gap: 14px;\n  margin-bottom: 20px;\n}\n\n.word-input-wrapper {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n}\n\n.word-number {\n  font-size: 0.75rem;\n  color: rgba(255, 255, 255, 0.4);\n  min-width: 22px;\n  text-align: right;\n  font-weight: 600;\n}\n\n.word-input {\n  flex: 1;\n  font-family: monospace;\n  font-size: 0.9rem;\n}\n\n.word-input :deep(.q-field__control) {\n  min-height: 44px;\n  border-radius: 8px;\n  background: rgba(255, 255, 255, 0.05);\n  border: 1px solid rgba(255, 255, 255, 0.1);\n  transition: all 0.2s ease;\n}\n\n.word-input :deep(.q-field__control):hover {\n  background: rgba(255, 255, 255, 0.08);\n  border-color: rgba(255, 255, 255, 0.2);\n}\n\n.word-input :deep(.q-field__native) {\n  padding: 0 12px;\n}\n\n.word-input :deep(.q-field--focused .q-field__control) {\n  background: rgba(255, 255, 255, 0.08);\n  border-color: rgba(var(--q-primary-rgb), 0.5);\n  box-shadow: 0 0 0 2px rgba(var(--q-primary-rgb), 0.1);\n}\n\n.word-input :deep(input) {\n  font-family: monospace;\n  font-size: 0.9rem;\n  color: white;\n}\n\n.word-input :deep(input::placeholder) {\n  color: rgba(255, 255, 255, 0.3);\n  font-size: 0.85rem;\n}\n\n.error-msg {\n  font-size: 0.85rem;\n  color: rgba(255, 200, 87, 0.9);\n  margin: 8px 0 0 0;\n  line-height: 1.4;\n}\n\n.paste-btn {\n  width: auto;\n  height: auto;\n  font-weight: 500;\n  text-transform: none;\n  font-size: 0.9rem;\n  color: var(--q-primary);\n  background: transparent;\n  padding: 8px 0;\n  transition: color 0.2s ease;\n}\n\n.paste-btn:hover {\n  color: rgba(var(--q-primary-rgb), 0.8);\n  background: transparent;\n}\n\n.disclaimer {\n  font-size: 0.85rem;\n  color: rgba(255, 255, 255, 0.7);\n  margin: 0;\n  line-height: 1.4;\n  text-align: left;\n}\n\n/* Mobile adjustments */\n@media (max-width: 600px) {\n  .recover-slide {\n    padding: 30px 15px 15px 15px;\n  }\n\n  .title {\n    font-size: 1.6rem;\n  }\n\n  .description {\n    font-size: 0.9rem;\n    margin-bottom: 28px;\n  }\n\n  .input-section {\n    max-width: 100%;\n  }\n\n  .words-grid {\n    grid-template-columns: repeat(2, 1fr);\n    gap: 10px;\n  }\n\n  .word-number {\n    font-size: 0.7rem;\n    min-width: 18px;\n  }\n\n  .word-input {\n    font-size: 0.85rem;\n  }\n\n  .word-input :deep(input) {\n    font-size: 0.85rem;\n  }\n\n  .paste-btn {\n    font-size: 0.85rem;\n    padding: 6px 0;\n  }\n\n  .disclaimer {\n    font-size: 0.8rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/pages/welcome/WelcomeRestoreEcash.vue",
    "content": "<template>\n  <div class=\"restore-ecash-slide\">\n    <!-- Main content area -->\n    <div class=\"content\">\n      <!-- Header Icon -->\n      <div class=\"header-icon\">\n        <div class=\"icon-circle\">\n          <q-icon name=\"restore\" size=\"2.5em\" color=\"white\" />\n        </div>\n      </div>\n\n      <!-- Title -->\n      <h1 class=\"title\">{{ $t(\"WelcomeRestoreEcash.title\") }}</h1>\n\n      <!-- Description -->\n      <p class=\"description\">\n        {{ $t(\"WelcomeRestoreEcash.text\") }}\n      </p>\n\n      <!-- Restore View Component -->\n      <div class=\"restore-content\">\n        <RestoreView :onboarding=\"true\" />\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport RestoreView from \"src/components/RestoreView.vue\";\nimport { useWelcomeStore } from \"src/stores/welcome\";\n\nexport default {\n  name: \"WelcomeRestoreEcash\",\n  components: { RestoreView },\n  setup() {\n    const welcome = useWelcomeStore();\n    return { welcome };\n  },\n};\n</script>\n\n<style scoped>\n.restore-ecash-slide {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  height: 100%;\n  background: var(--q-dark);\n  color: white;\n  padding: 40px 20px 20px 20px;\n  box-sizing: border-box;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start; /* Left-aligned */\n  text-align: left;\n  flex: 1;\n}\n\n.header-icon {\n  margin-bottom: 20px;\n  display: flex;\n  justify-content: flex-start; /* Left-aligned */\n  align-items: center;\n}\n\n.icon-circle {\n  width: 80px;\n  height: 80px;\n  border-radius: 50%;\n  background: rgba(255, 255, 255, 0.1);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.title {\n  font-size: 1.8rem;\n  font-weight: 700;\n  margin: 0 0 16px 0;\n  color: white;\n  line-height: 1.2;\n  text-align: left;\n  width: 100%;\n  max-width: 500px;\n}\n\n.description {\n  font-size: 0.95rem;\n  line-height: 1.5;\n  color: rgba(255, 255, 255, 0.8);\n  margin: 0 0 32px 0;\n  text-align: left;\n  width: 100%;\n  max-width: 500px;\n}\n\n.restore-content {\n  width: 100%;\n  max-width: 500px;\n}\n\n/* Override RestoreView component padding to align with title */\n.restore-content :deep(.q-px-xs) {\n  padding-left: 0 !important;\n  padding-right: 0 !important;\n}\n\n.restore-content :deep(.q-list) {\n  padding: 0 !important;\n}\n\n.restore-content :deep(.q-item) {\n  padding: 0 !important;\n}\n\n.restore-content :deep(.q-item__section) {\n  padding: 0 !important;\n}\n\n/* Mobile adjustments */\n@media (max-width: 600px) {\n  .restore-ecash-slide {\n    padding: 30px 15px 15px 15px;\n  }\n\n  .title {\n    font-size: 1.6rem;\n  }\n\n  .description {\n    font-size: 0.9rem;\n    margin-bottom: 28px;\n  }\n\n  .restore-content {\n    width: 100%;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/pages/welcome/WelcomeSlide1.vue",
    "content": "<!-- src/components/WelcomeSlide1.vue -->\n<template>\n  <div class=\"welcome-slide\">\n    <!-- Logo -->\n    <div class=\"logo\">\n      <transition appear enter-active-class=\"animated bounce\">\n        <img src=\"/clean.png\" alt=\"Cashu Logo\" class=\"logo-image\" />\n      </transition>\n    </div>\n\n    <!-- Title -->\n    <h1 class=\"title\">{{ $t(\"WelcomeSlide1.title\") }}</h1>\n\n    <!-- Description -->\n    <p class=\"description\">{{ $t(\"WelcomeSlide1.text\") }}</p>\n\n    <!-- Welcome actions -->\n    <div class=\"welcome-actions\">\n      <!-- Next button -->\n      <q-btn\n        color=\"primary\"\n        rounded\n        :label=\"$t('WelcomePage.actions.next.label')\"\n        @click=\"goToNext\"\n        class=\"welcome-next-btn\"\n      />\n\n      <!-- Terms agreement -->\n      <div class=\"terms-agreement\">\n        <p class=\"terms-text\">\n          By continuing, you agree to the\n          <span class=\"terms-link\" @click=\"showTermsSheet\"\n            >Terms of Service</span\n          >\n        </p>\n      </div>\n    </div>\n\n    <!-- Language selector at bottom -->\n    <div class=\"language-section\">\n      <div class=\"language-trigger\" @click=\"toggleLanguageMenu\">\n        <span class=\"language-text\">{{ selectedLanguage }}</span>\n        <q-icon name=\"keyboard_arrow_up\" class=\"language-icon\" />\n      </div>\n    </div>\n\n    <!-- Custom full-width language menu -->\n    <div\n      v-if=\"showLanguageMenu\"\n      class=\"language-menu-overlay\"\n      @click=\"closeLanguageMenu\"\n    >\n      <div class=\"language-menu\" @click.stop>\n        <div class=\"language-menu-header\">\n          <h3>Select Language</h3>\n          <q-btn\n            flat\n            round\n            icon=\"close\"\n            @click=\"closeLanguageMenu\"\n            class=\"close-btn\"\n          />\n        </div>\n        <div class=\"language-options\">\n          <div\n            v-for=\"option in languageOptions\"\n            :key=\"option.value\"\n            class=\"language-option\"\n            :class=\"{ active: selectedLanguage === option.label }\"\n            @click=\"selectLanguage(option)\"\n          >\n            {{ option.label }}\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <!-- Terms of Service bottom sheet -->\n    <div v-if=\"showTerms\" class=\"terms-menu-overlay\" @click=\"closeTermsSheet\">\n      <div class=\"terms-menu\" @click.stop>\n        <div class=\"terms-menu-header\">\n          <h3>Terms of Service</h3>\n          <q-btn\n            flat\n            round\n            icon=\"close\"\n            @click=\"closeTermsSheet\"\n            class=\"close-btn\"\n          />\n        </div>\n        <div class=\"terms-content\">\n          <div class=\"terms-text-content\">\n            <p><strong>Last Updated: 20.12.2024</strong></p>\n            <p>\n              <strong>\n                IMPORTANT NOTICE: THESE TERMS OF SERVICE INCLUDE A\n                MEDIATION-FIRST CLAUSE REQUIRING MEDIATION BEFORE ARBITRATION OR\n                LITIGATION. PLEASE READ THESE TERMS CAREFULLY. IF YOU DO NOT\n                AGREE, DO NOT USE CASHU.ME.\n              </strong>\n            </p>\n            <p>\n              <strong>\n                ALL REFERENCES TO LAW, REGULATION, AND JURISDICTION IN THESE\n                TERMS REFER TO THE LAWS AND REGULATIONS OF THE USER'S\n                JURISDICTION. USERS ARE RESPONSIBLE FOR DETERMINING THE LEGALITY\n                AND REGULATORY COMPLIANCE OF THEIR ACTIVITIES.\n              </strong>\n            </p>\n            <p>\n              <strong\n                >CASHU.ME DOES NOT HOLD ECASH, DOES NOT EXECUTE AND CANNOT\n                MONITOR TRANSACTIONS, AND DOES NOT OPERATE OR VERIFY ANY\n                MINTS.</strong\n              >\n            </p>\n            <p>\n              These Terms of Service (these \"Terms\") constitute the entire\n              agreement and understanding between you (\"you\" or \"your\") and\n              Cashu.me (\"Cashu.me,\" \"we,\" \"us,\" or \"our\") regarding your use of\n              the Cashu.me website and any related applications, software, code,\n              or services (collectively, the \"Site\" or \"Services\"). By accessing\n              or using the Site or Services, you acknowledge that you have read,\n              understand, and agree to be bound by these Terms. If you do not\n              agree, do not access or use the Site or Services.\n            </p>\n            <p><strong>1. Nature of the Services</strong></p>\n            <p>\n              1.1 <strong>Non-Custodial Web Application:</strong> Cashu.me\n              provides a non-custodial web application (\"wallet\") that is\n              executed entirely on your device. Our Site merely makes available\n              client-side code implementing the open-source Cashu protocol. We\n              do not run a server that holds your ecash or executes transactions\n              on your behalf.\n            </p>\n            <p>\n              1.2 <strong>No Control Over Mints:</strong> Cashu.me does not\n              issue ecash and does not operate or control any Mint. The choice\n              of any Mint and any transaction or relationship you establish with\n              that Mint is solely between you and that Mint. Cashu.me has no\n              involvement, responsibility, or liability in any such interaction.\n            </p>\n            <p>\n              1.3 <strong>No Funds Access:</strong> At no time does Cashu.me\n              have custody, possession, or control of your ecash. Transactions\n              occur solely by your actions and through your chosen Mint. We do\n              not monitor, verify, or facilitate transfers between you and any\n              Mint or other parties.\n            </p>\n            <p>\n              1.4 <strong>Web Server Only:</strong> Cashu.me does not operate\n              any servers except for the web server that delivers the\n              application to your device. The application is executed entirely\n              on your device. Once the code is served, all logic executes\n              locally and Cashu.me has no control over the application.\n            </p>\n            <p>\n              1.5 <strong>Open Source Code:</strong> The application code is\n              open source, meaning it can be self-hosted and run by third\n              parties using different domains. Cashu.me has no control over, and\n              does not endorse or assume responsibility for, any instances of\n              the code running outside of the Cashu.me service. Your use of any\n              such third-party instances is at your own risk.\n            </p>\n            <p>\n              1.6 <strong>Code Disclaimer:</strong> The open-source code is\n              provided without any guarantee of security, error-free operation,\n              or technical support. Users must independently verify the\n              authenticity, security, and integrity of the open-source code\n              prior to use.\n            </p>\n            <p><strong>2. User Responsibilities & Disclaimers</strong></p>\n            <p>\n              2.1 <strong>User's Sole Responsibility:</strong> You understand\n              and agree that you use the Site and Services at your own risk and\n              for your own account. You alone are fully responsible for\n              selecting Mints, conducting transactions, and safeguarding your\n              ecash and secret values. Cashu.me is not a party to and disclaims\n              any responsibility for any agreements, terms, or disputes between\n              you and any Mint.\n            </p>\n            <p>\n              2.2 <strong>No Partnership with Mints:</strong> Cashu.me is not\n              affiliated with, endorsed by, or responsible for any Mint. We make\n              no representations, warranties, or guarantees about any Mint's\n              integrity, legality, liquidity, or functionality. Your\n              relationship with any Mint, including the issuance, redemption, or\n              valuation of ecash, is solely a matter between you and that Mint.\n              Cashu.me is not a party to any transaction between you and any\n              Mint or third party. No agency, partnership, or joint venture\n              relationship is formed by your use of the Site.\n            </p>\n            <p>\n              2.3 <strong>Risk of Ecash:</strong> Ecash is an experimental,\n              bearer-like digital asset that may not be recognized as money,\n              currency, or a store of value. Anyone with the secret value has\n              control over the ecash. You agree to review and understand all\n              risks disclosed in our Risk Disclosure Statement before using\n              ecash.\n            </p>\n            <p><strong>3. Modifications to Terms</strong></p>\n            <p>\n              We may amend or update these Terms at any time without notice. You\n              are advised to review these Terms periodically. Your continued use\n              of the Site or Services after any modifications constitutes\n              acceptance of the updated Terms. If you do not agree, discontinue\n              your use.\n            </p>\n            <p><strong>4. Compliance with Laws</strong></p>\n            <p>\n              4.1 <strong>Legal Compliance:</strong> Your use of the Site and\n              any Services is void where prohibited by law. You must determine\n              whether your use of ecash and related activities are lawful. You\n              are solely responsible for compliance with all applicable laws,\n              taxes, and regulations.\n            </p>\n            <p>\n              4.2 <strong>Not Financial Services:</strong> The Services are not\n              intended to constitute regulated financial, banking, e-money, or\n              payment services. You are solely responsible for determining\n              whether your use of ecash or related activities requires any form\n              of license, registration, or compliance with financial regulations\n              in your jurisdiction.\n            </p>\n            <p><strong>5. License to Use the Site</strong></p>\n            <p>\n              Subject to your compliance with these Terms, we grant you a\n              limited, personal, non-exclusive, non-transferable, revocable\n              license to use the Site. We may suspend or terminate your access\n              at our sole discretion.\n            </p>\n            <p><strong>6. Risks and Limitation of Liability</strong></p>\n            <p>\n              6.1 <strong>No Liability for Interactions with Mints:</strong>\n              Cashu.me is not liable for any transactions, disputes, or issues\n              arising from your dealings with Mints.\n            </p>\n            <p>\n              6.2 <strong>Assumption of Risk:</strong> You acknowledge\n              ecash-related activities involve significant risks, including\n              market volatility, theft, and regulatory uncertainty.\n            </p>\n            <p>\n              6.3 <strong>Waiver of Accountability:</strong> By using the Site,\n              you waive any right to hold Cashu.me accountable for any damages,\n              losses, or disputes arising from your use of the Site or Services.\n            </p>\n            <p>\n              6.4 <strong>No Warranties:</strong> THE SITE AND SERVICES ARE\n              PROVIDED \"AS IS\" WITHOUT ANY WARRANTIES. WE DISCLAIM ALL\n              WARRANTIES TO THE MAXIMUM EXTENT PERMITTED BY LAW.\n            </p>\n            <p>\n              6.5 <strong>Limitation of Liability:</strong> TO THE FULLEST\n              EXTENT PERMITTED BY LAW, CASHU.ME IS NOT LIABLE FOR INDIRECT,\n              INCIDENTAL, SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING\n              OUT OF OR RELATED TO THESE TERMS, THE SITE, OR THE SERVICES.\n            </p>\n            <p>\n              6.6 <strong>Maximum Liability:</strong> To the maximum extent\n              permitted by applicable law, the Site and Services are provided\n              'as is' without warranties of any kind. This does not affect any\n              statutory warranties or rights which cannot be excluded under your\n              jurisdiction's law.\n            </p>\n            <p><strong>7. Indemnification and Release</strong></p>\n            <p>\n              You agree to indemnify and hold harmless Cashu.me and its\n              affiliates from claims arising out of your use of the Site or\n              Services. If you have a dispute with any Mint or third party, you\n              release Cashu.me from all related claims.\n            </p>\n            <p><strong>8. Mediation and Dispute Resolution</strong></p>\n            <p>\n              8.1 <strong>Mediation Requirement:</strong> If a dispute arises\n              out of or relates to these Terms, the Site, or the Services, the\n              parties agree to first attempt to resolve the dispute through\n              good-faith mediation administered by a reputable mediation\n              provider. Each party shall bear its own costs for the mediation,\n              and the costs of the mediator shall be shared equally.\n            </p>\n            <p>\n              8.2 <strong>Arbitration Option:</strong> If mediation does not\n              resolve the dispute within 30 days (or a mutually agreed period),\n              either party may initiate final and binding arbitration\n              administered by a reputable arbitration provider within the user's\n              jurisdiction. Arbitration shall be conducted on an individual\n              basis, and class actions or collective proceedings are not\n              permitted.\n            </p>\n            <p>\n              8.3 <strong>Waiver of Jury Trial:</strong> If arbitration is not\n              invoked and the dispute proceeds to court, you waive your right to\n              a trial by jury to the fullest extent permitted by the governing\n              law of your jurisdiction.\n            </p>\n            <p>\n              8.4 <strong>EU Consumer Rights:</strong> If you are residing in\n              the EU, any mandatory statutory rights regarding dispute\n              resolution procedures remain unaffected by this clause. Nothing in\n              this Section 8 shall limit or affect any mandatory rights you may\n              have under EU consumer protection or other applicable statutory\n              laws.\n            </p>\n            <p><strong>9. Prohibited Uses</strong></p>\n            <p>\n              You may not use the Site or Services for unlawful activities, to\n              violate applicable laws, or to engage in market manipulation. We\n              may suspend or terminate access for prohibited uses. You agree not\n              to use the Site or Services to engage in any activity that\n              violates applicable anti-money laundering (AML), counter-terrorism\n              financing (CTF), or other financial crime regulations. Any use of\n              the Site or Services for unlawful or fraudulent purposes is\n              strictly prohibited.\n            </p>\n            <p><strong>10. Privacy and Data Protection</strong></p>\n            <p>\n              10.1 <strong>GDPR Compliance:</strong> Cashu.me does not collect\n              or store any personal data, including IP addresses. No data is\n              shared with third parties. As such, Cashu.me does not engage in\n              data processing activities that would subject it to GDPR or\n              similar regulations. Users remain responsible for ensuring their\n              own device's security and verifying the authenticity of the code\n              they run.\n            </p>\n            <p>\n              10.2 <strong>Security:</strong> Because the code executes entirely\n              on your device and no personal data is collected, Cashu.me does\n              not perform any data processing activities that would fall under\n              the GDPR or similar data protection laws.\n            </p>\n            <p>\n              10.3 <strong>Local Data:</strong> Any data or information stored\n              locally on your device, including browser storage, cookies, or\n              application state, is controlled by you and not transmitted to or\n              accessible by Cashu.me.\n            </p>\n            <p><strong>11. Your Representations and Warranties</strong></p>\n            <p>\n              You represent and warrant you have the right and authority to\n              enter into these Terms and that your use of the Site will be\n              lawful.\n            </p>\n            <p><strong>12. No Investment Advice</strong></p>\n            <p>\n              Cashu.me does not provide investment, legal, or tax advice. No\n              fiduciary, advisory, or trust relationship is formed between you\n              and Cashu.me by using the Site or Services.\n            </p>\n            <p><strong>13. No Waiver</strong></p>\n            <p>\n              No failure or delay to exercise any right by Cashu.me shall\n              constitute a waiver of that right.\n            </p>\n            <p><strong>14. Force Majeure</strong></p>\n            <p>\n              Cashu.me is not liable for delays or failures due to events beyond\n              our reasonable control.\n            </p>\n            <p><strong>15. Assignment</strong></p>\n            <p>\n              You may not assign your rights without our consent. We may assign\n              our rights freely.\n            </p>\n            <p><strong>16. Severability</strong></p>\n            <p>\n              If any provision is deemed invalid, remaining provisions remain in\n              effect.\n            </p>\n            <p><strong>17. Electronic Communications; Language</strong></p>\n            <p>\n              By using the Site or Services, you consent to receive\n              communications electronically. We will communicate in English.\n            </p>\n            <p><strong>18. Governing Law</strong></p>\n            <p>\n              18.1 <strong>Choice of Law:</strong>\n              These Terms and any dispute arising from or related to these\n              Terms, the Site, or the Services shall be governed by the\n              applicable laws of your jurisdiction, without regard to conflict\n              of law principles.\n            </p>\n            <p>\n              18.2 <strong>Consumer Rights:</strong>\n              Nothing in these Terms shall exclude or limit any rights you may\n              have under applicable mandatory consumer protection laws or\n              regulations in your jurisdiction, including any rights under EU\n              law that cannot be lawfully limited or disclaimed.\n            </p>\n            <p><strong>19. E-Sign Consent Policy</strong></p>\n            <p>\n              By using the Site, you consent to receive all communications\n              electronically.\n            </p>\n            <p><strong>20. Risk Disclosure Statement</strong></p>\n            <p>\n              Using ecash involves significant risks including legal, market,\n              liquidity, counterparty, and operational risks. You acknowledge\n              and accept these risks.\n            </p>\n            <p><strong>21. Entire Agreement</strong></p>\n            <p>\n              These Terms represent the entire agreement between you and\n              Cashu.me.\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { useWelcomeStore } from \"src/stores/welcome\";\n\nexport default {\n  name: \"WelcomeSlide1\",\n  data() {\n    return {\n      selectedLanguage: \"\",\n      showLanguageMenu: false,\n      showTerms: false,\n      termsAccepted: true,\n      languageOptions: [\n        { label: \"English\", value: \"en-US\" },\n        { label: \"Español\", value: \"es-ES\" },\n        { label: \"Italiano\", value: \"it-IT\" },\n        { label: \"Deutsch\", value: \"de-DE\" },\n        { label: \"Français\", value: \"fr-FR\" },\n        { label: \"Čeština\", value: \"cs-CZ\" },\n        { label: \"Português (Brasil)\", value: \"pt-BR\" },\n        { label: \"Svenska\", value: \"sv-SE\" },\n        { label: \"Ελληνικά\", value: \"el-GR\" },\n        { label: \"Türkçe\", value: \"tr-TR\" },\n        { label: \"ไทย\", value: \"th-TH\" },\n        { label: \"العربية\", value: \"ar-SA\" },\n        { label: \"中文\", value: \"zh-CN\" },\n        { label: \"日本語\", value: \"ja-JP\" },\n      ],\n    };\n  },\n  methods: {\n    changeLanguage(locale: string) {\n      this.$i18n.locale = locale;\n      localStorage.setItem(\"cashu.language\", locale);\n    },\n    goToNext() {\n      const welcomeStore = useWelcomeStore();\n      // Mark terms as accepted since user is proceeding\n      welcomeStore.acceptTerms();\n      welcomeStore.goToNextSlide();\n    },\n    toggleLanguageMenu() {\n      this.showLanguageMenu = !this.showLanguageMenu;\n    },\n    closeLanguageMenu() {\n      this.showLanguageMenu = false;\n    },\n    selectLanguage(option: any) {\n      this.selectedLanguage = option.label;\n      this.changeLanguage(option.value);\n      this.closeLanguageMenu();\n    },\n    showTermsSheet() {\n      this.showTerms = true;\n    },\n    closeTermsSheet() {\n      this.showTerms = false;\n      this.termsAccepted = true;\n    },\n  },\n  created() {\n    this.selectedLanguage =\n      this.languageOptions.find(\n        (option) => option.value === this.$i18n.locale || navigator.language\n      )?.label || \"English\";\n  },\n};\n</script>\n\n<style scoped>\n.welcome-slide {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  align-items: center;\n  height: 100%;\n  background: var(--q-dark);\n  color: white;\n  padding: 40px 20px 20px 20px;\n  box-sizing: border-box;\n  text-align: center;\n}\n\n.logo {\n  margin-bottom: 30px;\n  width: 120px;\n  height: 120px;\n  border-radius: 50%;\n  background: rgba(255, 255, 255, 0.1);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.logo-image {\n  width: 80px;\n  height: 80px;\n  filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3));\n}\n\n.title {\n  font-size: 2.2rem;\n  font-weight: 700;\n  margin: 0 0 20px 0;\n  color: white;\n  line-height: 1.2;\n  letter-spacing: -0.02em;\n}\n\n.description {\n  font-size: 1.1rem;\n  line-height: 1.5;\n  color: rgba(255, 255, 255, 0.8);\n  margin: 0 0 50px 0;\n  max-width: 500px;\n  text-align: left;\n}\n\n/* Unified content width */\n.title,\n.welcome-actions,\n.language-section {\n  width: 100%;\n  max-width: 500px;\n}\n\n.welcome-actions {\n  display: flex;\n  flex-direction: column;\n  align-items: stretch;\n  flex: 1;\n  justify-content: center;\n  width: 100%;\n}\n\n.welcome-next-btn {\n  width: 100%;\n  height: 44px;\n  font-weight: 600;\n  text-transform: none;\n  font-size: 1rem;\n  border-radius: 22px;\n  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n  transition: all 0.3s ease;\n}\n\n.welcome-next-btn:hover {\n  transform: translateY(-2px);\n  box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4);\n}\n\n.terms-agreement {\n  margin-top: 16px;\n  text-align: center;\n}\n\n.terms-text {\n  font-size: 0.85rem;\n  color: rgba(255, 255, 255, 0.7);\n  margin: 0;\n  line-height: 1.4;\n}\n\n.terms-link {\n  color: var(--q-primary);\n  cursor: pointer;\n  text-decoration: underline;\n  transition: color 0.2s ease;\n}\n\n.terms-link:hover {\n  color: rgba(var(--q-primary-rgb), 0.8);\n}\n\n/* Language trigger button */\n.language-trigger {\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 8px;\n  color: rgba(255, 255, 255, 0.7);\n  cursor: pointer;\n  transition: color 0.2s ease;\n  padding: 8px 16px;\n  border-radius: 8px;\n}\n\n.language-trigger:hover {\n  color: rgba(255, 255, 255, 0.9);\n}\n\n.language-text {\n  font-size: 0.9rem;\n  font-weight: 500;\n}\n\n.language-icon {\n  font-size: 1rem;\n  transition: transform 0.2s ease;\n}\n\n.language-trigger:hover .language-icon {\n  transform: scale(1.1);\n}\n\n/* Full-width language menu overlay */\n.language-menu-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background: rgba(0, 0, 0, 0.5);\n  backdrop-filter: blur(4px);\n  z-index: 9999;\n  display: flex;\n  align-items: flex-end;\n  animation: fadeIn 0.3s ease;\n}\n\n.language-menu {\n  width: 100%;\n  background: rgba(20, 20, 20, 0.98);\n  backdrop-filter: blur(20px);\n  border-top: 1px solid rgba(255, 255, 255, 0.1);\n  border-radius: 20px 20px 0 0;\n  height: 85vh;\n  overflow: hidden;\n  animation: slideUp 0.3s ease;\n  display: flex;\n  flex-direction: column;\n}\n\n.language-menu-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 20px 24px 16px 24px;\n  border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n}\n\n.language-menu-header h3 {\n  color: white;\n  font-size: 1.1rem;\n  font-weight: 600;\n  margin: 0;\n}\n\n.close-btn {\n  color: rgba(255, 255, 255, 0.7) !important;\n}\n\n.language-options {\n  flex: 1;\n  overflow-y: auto;\n  padding-bottom: env(safe-area-inset-bottom);\n}\n\n.language-option {\n  padding: 16px 24px;\n  color: rgba(255, 255, 255, 0.9);\n  font-size: 1rem;\n  font-weight: 500;\n  cursor: pointer;\n  transition: all 0.2s ease;\n  border-bottom: 1px solid rgba(255, 255, 255, 0.05);\n  text-align: center;\n}\n\n.language-option:hover {\n  background: rgba(255, 255, 255, 0.08);\n  color: white;\n}\n\n.language-option.active {\n  background: rgba(var(--q-primary-rgb), 0.2);\n  color: var(--q-primary);\n}\n\n.language-option:last-child {\n  border-bottom: none;\n}\n\n/* Animations */\n@keyframes fadeIn {\n  from {\n    opacity: 0;\n  }\n  to {\n    opacity: 1;\n  }\n}\n\n@keyframes slideUp {\n  from {\n    transform: translateY(100%);\n  }\n  to {\n    transform: translateY(0);\n  }\n}\n\n/* Terms of Service bottom sheet */\n.terms-menu-overlay {\n  position: fixed;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  background: rgba(0, 0, 0, 0.5);\n  backdrop-filter: blur(4px);\n  z-index: 9999;\n  display: flex;\n  align-items: flex-end;\n  animation: fadeIn 0.3s ease;\n}\n\n.terms-menu {\n  width: 100%;\n  background: rgba(20, 20, 20, 0.98);\n  backdrop-filter: blur(20px);\n  border-top: 1px solid rgba(255, 255, 255, 0.1);\n  border-radius: 20px 20px 0 0;\n  height: 85vh;\n  overflow: hidden;\n  animation: slideUp 0.3s ease;\n  display: flex;\n  flex-direction: column;\n}\n\n.terms-menu-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  padding: 20px 24px 16px 24px;\n  border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n  flex-shrink: 0;\n}\n\n.terms-menu-header h3 {\n  color: white;\n  font-size: 1.1rem;\n  font-weight: 600;\n  margin: 0;\n}\n\n.terms-content {\n  flex: 1;\n  overflow-y: auto;\n  padding: 20px 24px;\n  padding-bottom: env(safe-area-inset-bottom);\n}\n\n.terms-text-content {\n  color: rgba(255, 255, 255, 0.9);\n  line-height: 1.5;\n}\n\n.terms-text-content p {\n  margin: 0 0 16px 0;\n  font-size: 0.9rem;\n}\n\n.terms-text-content strong {\n  color: white;\n  font-weight: 600;\n}\n\n.language-section {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  padding: 10px 0;\n  flex-shrink: 0;\n}\n\n/* Animation */\n.animated.bounce {\n  animation-duration: 0.8s;\n  animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);\n}\n</style>\n"
  },
  {
    "path": "src/pages/welcome/WelcomeSlide2.vue",
    "content": "<!-- src/components/WelcomeSlide2.vue -->\n<template>\n  <div class=\"pwa-slide\">\n    <!-- Main content area -->\n    <div class=\"content\">\n      <!-- Header Image -->\n      <div\n        class=\"header-image\"\n        :class=\"{ centered: isInstalling || wasInstalled }\"\n      >\n        <transition\n          v-if=\"isPWA()\"\n          appear\n          enter-active-class=\"animated bounceIn\"\n        >\n          <q-icon name=\"check_circle\" size=\"3em\" color=\"positive\" />\n        </transition>\n        <div v-else class=\"pwa-image-wrapper\">\n          <img\n            src=\"/pwa-example.jpg\"\n            :alt=\"$t('WelcomeSlide2.alt.pwa_example')\"\n            class=\"pwa-image\"\n          />\n          <div v-if=\"isInstalling\" class=\"installing-overlay\">\n            <q-spinner color=\"primary\" size=\"48px\" />\n            <div class=\"installing-text\">\n              {{ $t(\"WelcomeSlide2.installing\") }}\n            </div>\n          </div>\n        </div>\n      </div>\n\n      <!-- Title -->\n      <h1 class=\"title\">{{ $t(\"WelcomeSlide2.title\") }}</h1>\n\n      <!-- Content based on PWA status -->\n      <div\n        v-if=\"!isPWA()\"\n        class=\"instructions\"\n        :class=\"{ 'fade-out': isInstalling }\"\n      >\n        <div class=\"q-mb-md\" v-if=\"canPromptInstall\">\n          <q-btn\n            color=\"primary\"\n            icon=\"add_box\"\n            :label=\"$t('WalletPage.install.text')\"\n            rounded\n            @click=\"promptInstall\"\n          />\n        </div>\n        <p v-if=\"!wasInstalled\" class=\"intro-text\">\n          {{ $t(\"WelcomeSlide2.instruction.intro.text\") }}\n        </p>\n\n        <!-- Android Instructions -->\n        <div v-if=\"!wasInstalled && !isIos\" class=\"platform-section\">\n          <h3 class=\"platform-title\">\n            {{ $t(\"WelcomeSlide2.instruction.android.title\") }}\n          </h3>\n          <div class=\"instruction-steps\">\n            <div class=\"step\">\n              <q-icon name=\"more_vert\" size=\"1.2em\" class=\"step-icon\" />\n              <span>{{\n                $t(\"WelcomeSlide2.instruction.android.step1.text\")\n              }}</span>\n            </div>\n            <div class=\"step\">\n              <q-icon name=\"mobile_friendly\" size=\"1.2em\" class=\"step-icon\" />\n              <i18n-t keypath=\"WelcomeSlide2.instruction.android.step2.text\">\n                <template v-slot:buttonText>\n                  <strong>{{\n                    $t(\"WelcomeSlide2.instruction.android.step2.buttonText\")\n                  }}</strong>\n                </template>\n              </i18n-t>\n            </div>\n          </div>\n        </div>\n\n        <!-- iOS Instructions -->\n        <div v-if=\"!wasInstalled && !isAndroid\" class=\"platform-section\">\n          <h3 class=\"platform-title\">\n            {{ $t(\"WelcomeSlide2.instruction.ios.title\") }}\n          </h3>\n          <div class=\"instruction-steps\">\n            <div class=\"step\">\n              <q-icon name=\"ios_share\" size=\"1.2em\" class=\"step-icon\" />\n              <span>{{ $t(\"WelcomeSlide2.instruction.ios.step1.text\") }}</span>\n            </div>\n            <div class=\"step\">\n              <q-icon name=\"add_box_outline\" size=\"1.2em\" class=\"step-icon\" />\n              <i18n-t keypath=\"WelcomeSlide2.instruction.ios.step2.text\">\n                <template v-slot:buttonText>\n                  <strong>{{\n                    $t(\"WelcomeSlide2.instruction.ios.step2.buttonText\")\n                  }}</strong>\n                </template>\n              </i18n-t>\n            </div>\n          </div>\n        </div>\n\n        <p v-if=\"!wasInstalled\" class=\"outro-text\">\n          {{ $t(\"WelcomeSlide2.instruction.outro.text\") }}\n        </p>\n      </div>\n\n      <!-- Success message when PWA is installed or installation confirmed -->\n      <div v-if=\"isPWA() || wasInstalled\" class=\"success-message\">\n        <transition appear enter-active-class=\"animated tada\">\n          <h3 class=\"success-title\">\n            {{ $t(\"WelcomeSlide2.pwa.success.title\") }}\n          </h3>\n        </transition>\n        <p v-if=\"isPWA()\" class=\"success-text\">\n          {{ $t(\"WelcomeSlide2.pwa.success.text\") }}\n        </p>\n        <p v-else class=\"success-text\">\n          {{ $t(\"WelcomeSlide2.pwa.success.nextSteps\") }}\n        </p>\n      </div>\n    </div>\n\n    <!-- Spacer to match step 1's controls height -->\n    <div class=\"spacer\"></div>\n\n    <!-- PWA Prompts -->\n    <iOSPWAPrompt v-if=\"!isPWA() && !wasInstalled && !isInstalling && isIos\" />\n    <AndroidPWAPrompt\n      v-if=\"\n        !isPWA() &&\n        !wasInstalled &&\n        !isInstalling &&\n        !canPromptInstall &&\n        !isIos\n      \"\n    />\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { ref, onMounted, onBeforeUnmount, computed } from \"vue\";\nimport iOSPWAPrompt from \"components/iOSPWAPrompt.vue\";\nimport AndroidPWAPrompt from \"components/AndroidPWAPrompt.vue\";\n\nexport default {\n  name: \"WelcomeSlide2\",\n  components: {\n    iOSPWAPrompt,\n    AndroidPWAPrompt,\n  },\n  setup() {\n    const isPWA = () => {\n      return (\n        window.matchMedia(\"(display-mode: standalone)\").matches ||\n        (navigator as any).standalone === true\n      );\n    };\n\n    const deferredPrompt = ref<any | null>(null);\n    const canPromptInstall = ref(false);\n    const isInstalling = ref(false);\n    const wasInstalled = ref(false);\n    const installStartTs = ref<number | null>(null);\n    const minInstallDurationMs = 4000;\n    const isIos = computed(() => {\n      const ua = window.navigator.userAgent.toLowerCase();\n      return /iphone|ipad|ipod/.test(ua);\n    });\n    const isAndroid = computed(() => {\n      const ua = window.navigator.userAgent.toLowerCase();\n      return /android/.test(ua);\n    });\n\n    const handleBeforeInstallPrompt = (e: any) => {\n      // Prevent the mini-infobar on mobile\n      e.preventDefault();\n      deferredPrompt.value = e;\n      canPromptInstall.value = true;\n    };\n\n    const handleAppInstalled = () => {\n      deferredPrompt.value = null;\n      canPromptInstall.value = false;\n      const now = Date.now();\n      const elapsed = installStartTs.value ? now - installStartTs.value : 0;\n      const remaining = Math.max(0, minInstallDurationMs - elapsed);\n      window.setTimeout(() => {\n        wasInstalled.value = true;\n        isInstalling.value = false;\n        installStartTs.value = null;\n      }, remaining);\n    };\n\n    const promptInstall = async () => {\n      if (!deferredPrompt.value) return;\n      deferredPrompt.value.prompt();\n      try {\n        const { outcome } = await deferredPrompt.value.userChoice;\n        if (outcome === \"accepted\") {\n          // user accepted: show installing state until appinstalled or timeout\n          isInstalling.value = true;\n          installStartTs.value = Date.now();\n          // Fallback timeout to clear installing state if appinstalled doesn't fire\n          window.setTimeout(() => {\n            if (!isPWA()) {\n              isInstalling.value = false;\n              installStartTs.value = null;\n            }\n          }, 15000);\n        }\n      } finally {\n        // Chrome requires nulling after one use\n        deferredPrompt.value = null;\n        canPromptInstall.value = false;\n      }\n    };\n\n    onMounted(() => {\n      window.addEventListener(\"beforeinstallprompt\", handleBeforeInstallPrompt);\n      window.addEventListener(\"appinstalled\", handleAppInstalled);\n      // Use globally captured event if it fired before this component mounted\n      const anyWindow = window as any;\n      if (anyWindow.__deferredBeforeInstallPrompt) {\n        deferredPrompt.value = anyWindow.__deferredBeforeInstallPrompt;\n        canPromptInstall.value = true;\n      }\n      const onBipAvailable = () => {\n        const w: any = window;\n        if (w.__deferredBeforeInstallPrompt) {\n          deferredPrompt.value = w.__deferredBeforeInstallPrompt;\n          canPromptInstall.value = true;\n        }\n      };\n      window.addEventListener(\"bip-available\", onBipAvailable);\n      // Keep a reference for cleanup\n      (onMounted as any)._onBipAvailable = onBipAvailable;\n    });\n\n    onBeforeUnmount(() => {\n      window.removeEventListener(\n        \"beforeinstallprompt\",\n        handleBeforeInstallPrompt\n      );\n      window.removeEventListener(\"appinstalled\", handleAppInstalled);\n      const refHandler = (onMounted as any)._onBipAvailable;\n      if (refHandler) {\n        window.removeEventListener(\"bip-available\", refHandler);\n      }\n    });\n\n    return {\n      isPWA,\n      canPromptInstall,\n      promptInstall,\n      isIos,\n      isAndroid,\n      isInstalling,\n      wasInstalled,\n    };\n  },\n};\n</script>\n\n<style scoped>\n.pwa-slide {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  height: 100%;\n  background: var(--q-dark);\n  color: white;\n  padding: 40px 20px 20px 20px;\n  box-sizing: border-box;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  text-align: center;\n  flex: 1;\n}\n\n.spacer {\n  height: 76px; /* Height of controls (36px) + padding (20px) + gap (20px) */\n  flex-shrink: 0;\n}\n\n.header-image {\n  margin-bottom: 20px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.header-image {\n  transition: margin-top 0.45s ease-in-out;\n}\n\n.header-image.centered {\n  margin-top: 15vh;\n}\n\n.pwa-image {\n  max-width: 100%;\n  max-height: 200px;\n  width: auto;\n  height: auto;\n  object-fit: contain;\n}\n\n.pwa-image-wrapper {\n  position: relative;\n}\n\n.installing-overlay {\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -65%);\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 8px;\n}\n\n.installing-text {\n  font-size: 0.9rem;\n  color: rgba(255, 255, 255, 0.9);\n}\n\n.title {\n  font-size: 1.8rem;\n  font-weight: 700;\n  margin: 0 0 16px 0;\n  color: white;\n  line-height: 1.2;\n}\n\n.instructions {\n  max-width: 500px;\n  width: 100%;\n}\n\n.instructions.fade-out {\n  opacity: 0;\n  height: 0;\n  margin: 0;\n  overflow: hidden;\n  transition: opacity 0.3s ease, height 0.3s ease, margin 0.3s ease;\n}\n\n.intro-text {\n  font-size: 0.95rem;\n  line-height: 1.5;\n  color: rgba(255, 255, 255, 0.8);\n  margin: 0 0 20px 0;\n  text-align: left;\n}\n\n.platform-section {\n  margin-bottom: 16px;\n  text-align: left;\n}\n\n.platform-title {\n  font-size: 1rem;\n  font-weight: 600;\n  color: white;\n  margin: 0 0 8px 0;\n}\n\n.instruction-steps {\n  display: flex;\n  flex-direction: column;\n  gap: 6px;\n}\n\n.step {\n  display: flex;\n  align-items: flex-start;\n  gap: 10px;\n  font-size: 0.9rem;\n  line-height: 1.4;\n  color: rgba(255, 255, 255, 0.9);\n}\n\n.step-icon {\n  color: var(--q-primary);\n  margin-top: 1px;\n  flex-shrink: 0;\n  font-size: 1.05em;\n}\n\n.outro-text {\n  font-size: 0.9rem;\n  line-height: 1.4;\n  color: rgba(255, 255, 255, 0.8);\n  margin: 16px 0 0 0;\n  text-align: left;\n}\n\n.success-message {\n  max-width: 500px;\n}\n\n/* Unified content width */\n.title,\n.instructions,\n.success-message {\n  width: 100%;\n  max-width: 500px;\n}\n\n.success-title {\n  font-size: 1.4rem;\n  font-weight: 600;\n  color: white;\n  margin: 0 0 15px 0;\n}\n\n.success-text {\n  font-size: 0.95rem;\n  line-height: 1.4;\n  color: rgba(255, 255, 255, 0.8);\n  margin: 0;\n}\n\n/* Mobile adjustments */\n@media (max-width: 600px) {\n  .pwa-slide {\n    padding: 30px 15px 15px 15px;\n  }\n\n  .title {\n    font-size: 1.6rem;\n  }\n\n  .instructions {\n    max-width: 100%;\n  }\n\n  .intro-text,\n  .outro-text {\n    font-size: 0.9rem;\n  }\n\n  .step {\n    font-size: 0.85rem;\n  }\n\n  .platform-title {\n    font-size: 0.95rem;\n  }\n}\n\n/* Animations */\n.animated.bounceIn {\n  animation-duration: 0.8s;\n}\n\n.animated.tada {\n  animation-duration: 1s;\n}\n</style>\n"
  },
  {
    "path": "src/pages/welcome/WelcomeSlide3.vue",
    "content": "<!-- src/components/WelcomeSlide3.vue -->\n<template>\n  <div class=\"seed-phrase-slide\">\n    <!-- Main content area -->\n    <div class=\"content\">\n      <!-- Header Icon -->\n      <div class=\"header-icon\">\n        <div class=\"icon-circle\">\n          <q-icon name=\"vpn_key\" size=\"2.5em\" color=\"white\" />\n        </div>\n      </div>\n\n      <!-- Title -->\n      <h1 class=\"title\">{{ $t(\"WelcomeSlide3.title\") }}</h1>\n\n      <!-- Description -->\n      <p class=\"description\">{{ $t(\"WelcomeSlide3.text\") }}</p>\n\n      <!-- Seed Phrase Section -->\n      <div class=\"seed-section\">\n        <div class=\"seed-label\">\n          {{ $t(\"WelcomeSlide3.inputs.seed_phrase.label\") }}\n        </div>\n\n        <div class=\"seed-box\">\n          <div class=\"seed-text\">{{ hiddenMnemonic }}</div>\n          <div class=\"seed-actions\">\n            <q-btn\n              flat\n              round\n              dense\n              :icon=\"hideMnemonic ? 'visibility' : 'visibility_off'\"\n              @click=\"toggleMnemonicVisibility\"\n              class=\"action-btn\"\n              size=\"sm\"\n            >\n              <q-tooltip>{{ hideMnemonic ? \"Show\" : \"Hide\" }}</q-tooltip>\n            </q-btn>\n            <q-btn\n              flat\n              round\n              dense\n              icon=\"content_copy\"\n              @click=\"copyText(walletStore.mnemonic)\"\n              class=\"action-btn\"\n              size=\"sm\"\n            >\n              <q-tooltip>Copy</q-tooltip>\n            </q-btn>\n          </div>\n        </div>\n\n        <p class=\"seed-caption\">\n          {{ $t(\"WelcomeSlide3.inputs.seed_phrase.caption\") }}\n        </p>\n      </div>\n\n      <!-- Checkbox -->\n      <div class=\"checkbox-section\">\n        <q-checkbox\n          v-model=\"welcomeStore.seedPhraseValidated\"\n          :label=\"$t('WelcomeSlide3.inputs.checkbox.label')\"\n          color=\"primary\"\n          class=\"validation-checkbox\"\n        />\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\ndeclare const windowMixin: any;\nimport { useWelcomeStore } from \"src/stores/welcome\";\nimport { useWalletStore } from \"src/stores/wallet\";\nimport { ref, computed, onMounted } from \"vue\";\nimport { useQuasar, copyToClipboard } from \"quasar\";\n\nexport default {\n  name: \"WelcomeSlide3\",\n  mixins: [windowMixin],\n  setup() {\n    const welcomeStore = useWelcomeStore();\n    const walletStore = useWalletStore();\n    const $q = useQuasar();\n    const hideMnemonic = ref(true);\n\n    onMounted(() => {\n      // Ensure mnemonic is generated for new-wallet flow only\n      if (welcomeStore.onboardingPath === \"new\" && !walletStore.mnemonic) {\n        walletStore.initializeMnemonic();\n      }\n    });\n\n    const hiddenMnemonic = computed(() => {\n      if (hideMnemonic.value) {\n        return walletStore.mnemonic\n          .split(\" \")\n          .map((w) => \"*\".repeat(6))\n          .join(\" \");\n      }\n      return walletStore.mnemonic;\n    });\n\n    const toggleMnemonicVisibility = () => {\n      hideMnemonic.value = !hideMnemonic.value;\n    };\n\n    const copyText = async (text: string) => {\n      try {\n        await copyToClipboard(text);\n        $q.notify({ message: \"Copied to clipboard!\", position: \"bottom\" });\n      } catch {}\n    };\n\n    const proceed = () => {\n      welcomeStore.seedPhraseValidated = true;\n    };\n\n    return {\n      welcomeStore,\n      walletStore,\n      proceed,\n      toggleMnemonicVisibility,\n      hiddenMnemonic,\n      hideMnemonic,\n      copyText,\n    };\n  },\n};\n</script>\n\n<style scoped>\n.seed-phrase-slide {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  height: 100%;\n  background: var(--q-dark);\n  color: white;\n  padding: 40px 20px 20px 20px;\n  box-sizing: border-box;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  align-items: flex-start;\n  text-align: left;\n  flex: 1;\n}\n\n.header-icon {\n  margin-bottom: 20px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  width: 100%;\n}\n\n.icon-circle {\n  width: 80px;\n  height: 80px;\n  border-radius: 50%;\n  background: rgba(255, 255, 255, 0.1);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.title {\n  font-size: 1.8rem;\n  font-weight: 700;\n  margin: 0 0 16px 0;\n  color: white;\n  line-height: 1.2;\n  text-align: left;\n  width: 100%;\n  max-width: 500px;\n}\n\n.description {\n  font-size: 0.95rem;\n  line-height: 1.5;\n  color: rgba(255, 255, 255, 0.8);\n  margin: 0 0 32px 0;\n  text-align: left;\n  width: 100%;\n  max-width: 500px;\n}\n\n.seed-section {\n  width: 100%;\n  margin-bottom: 24px;\n}\n\n/* Unified content width */\n.seed-section,\n.checkbox-section {\n  max-width: 500px;\n}\n\n.seed-label {\n  font-size: 15.2px;\n  font-family: Inter, -apple-system, \"system-ui\", \"Segoe UI\", Roboto,\n    \"Helvetica Neue\", Arial, sans-serif;\n  font-weight: 600;\n  color: #ffffff;\n  margin-bottom: 12px;\n}\n\n.seed-box {\n  position: relative;\n  background: rgba(255, 255, 255, 0.05);\n  border: 1px solid rgba(255, 255, 255, 0.1);\n  border-radius: 12px;\n  padding: 20px;\n  margin-bottom: 12px;\n  transition: all 0.2s ease;\n}\n\n.seed-box:hover {\n  background: rgba(255, 255, 255, 0.08);\n  border-color: rgba(255, 255, 255, 0.2);\n}\n\n.seed-text {\n  font-family: monospace;\n  font-size: 0.9rem;\n  line-height: 1.8;\n  color: white;\n  word-wrap: break-word;\n  padding-right: 80px; /* Space for action buttons */\n}\n\n.seed-actions {\n  position: absolute;\n  top: 16px;\n  right: 12px;\n  display: flex;\n  gap: 4px;\n}\n\n.action-btn {\n  width: 32px;\n  height: 32px;\n  color: rgba(255, 255, 255, 0.7);\n  transition: all 0.2s ease;\n}\n\n.action-btn:hover {\n  color: white;\n  background: rgba(255, 255, 255, 0.1);\n}\n\n.seed-caption {\n  font-size: 0.85rem;\n  color: rgba(255, 255, 255, 0.6);\n  margin: 0;\n  line-height: 1.4;\n}\n\n.checkbox-section {\n  justify-content: center;\n  display: flex;\n  width: 100%;\n  margin-bottom: 20px;\n}\n\n.validation-checkbox {\n  font-size: 0.95rem;\n  color: rgba(255, 255, 255, 0.9);\n}\n\n.validation-checkbox :deep(.q-checkbox__label) {\n  color: rgba(255, 255, 255, 0.9);\n  font-size: 0.95rem;\n  line-height: 1.4;\n}\n\n/* Mobile adjustments */\n@media (max-width: 600px) {\n  .seed-phrase-slide {\n    padding: 30px 15px 15px 15px;\n  }\n\n  .title {\n    font-size: 1.6rem;\n  }\n\n  .description {\n    font-size: 0.9rem;\n    margin-bottom: 28px;\n  }\n\n  .seed-text {\n    font-size: 0.85rem;\n    padding-right: 50px;\n  }\n\n  .seed-actions {\n    top: 12px;\n    right: 8px;\n  }\n\n  .action-btn {\n    width: 28px;\n    height: 28px;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/pages/welcome/WelcomeSlide4.vue",
    "content": "<template>\n  <div class=\"q-pa-md flex flex-center\">\n    <div class=\"text-center\">\n      <q-icon name=\"gavel\" size=\"4em\" color=\"primary\" />\n      <h2 class=\"text-center q-mt-xl\">{{ $t(\"WelcomeSlide4.title\") }}</h2>\n      <q-expansion-item\n        :label=\"$t('WelcomeSlide4.actions.more.label')\"\n        class=\"q-mt-sm\"\n      >\n        <div\n          class=\"text-left q-mt-sm\"\n          style=\"\n            max-height: 350px;\n            max-width: 90%;\n            overflow-y: auto;\n            margin: 0 auto;\n          \"\n        >\n          <p><strong>Last Updated: 20.12.2024</strong></p>\n          <p>\n            <strong>\n              IMPORTANT NOTICE: THESE TERMS OF SERVICE INCLUDE A MEDIATION-FIRST\n              CLAUSE REQUIRING MEDIATION BEFORE ARBITRATION OR LITIGATION.\n              PLEASE READ THESE TERMS CAREFULLY. IF YOU DO NOT AGREE, DO NOT USE\n              CASHU.ME.\n            </strong>\n          </p>\n          <p>\n            <strong>\n              ALL REFERENCES TO LAW, REGULATION, AND JURISDICTION IN THESE TERMS\n              REFER TO THE LAWS AND REGULATIONS OF THE USER’S JURISDICTION.\n              USERS ARE RESPONSIBLE FOR DETERMINING THE LEGALITY AND REGULATORY\n              COMPLIANCE OF THEIR ACTIVITIES.\n            </strong>\n          </p>\n          <p>\n            <strong\n              >CASHU.ME DOES NOT HOLD ECASH, DOES NOT EXECUTE AND CANNOT MONITOR\n              TRANSACTIONS, AND DOES NOT OPERATE OR VERIFY ANY MINTS.</strong\n            >\n          </p>\n          <p>\n            These Terms of Service (these “Terms”) constitute the entire\n            agreement and understanding between you (“you” or “your”) and\n            Cashu.me (“Cashu.me,” “we,” “us,” or “our”) regarding your use of\n            the Cashu.me website and any related applications, software, code,\n            or services (collectively, the “Site” or “Services”). By accessing\n            or using the Site or Services, you acknowledge that you have read,\n            understand, and agree to be bound by these Terms. If you do not\n            agree, do not access or use the Site or Services.\n          </p>\n          <p><strong>1. Nature of the Services</strong></p>\n          <p>\n            1.1 <strong>Non-Custodial Web Application:</strong> Cashu.me\n            provides a non-custodial web application (“wallet”) that is executed\n            entirely on your device. Our Site merely makes available client-side\n            code implementing the open-source Cashu protocol. We do not run a\n            server that holds your ecash or executes transactions on your\n            behalf.\n          </p>\n          <p>\n            1.2 <strong>No Control Over Mints:</strong> Cashu.me does not issue\n            ecash and does not operate or control any Mint. The choice of any\n            Mint and any transaction or relationship you establish with that\n            Mint is solely between you and that Mint. Cashu.me has no\n            involvement, responsibility, or liability in any such interaction.\n          </p>\n          <p>\n            1.3 <strong>No Funds Access:</strong> At no time does Cashu.me have\n            custody, possession, or control of your ecash. Transactions occur\n            solely by your actions and through your chosen Mint. We do not\n            monitor, verify, or facilitate transfers between you and any Mint or\n            other parties.\n          </p>\n          <p>\n            1.4 <strong>Web Server Only:</strong> Cashu.me does not operate any\n            servers except for the web server that delivers the application to\n            your device. The application is executed entirely on your device.\n            Once the code is served, all logic executes locally and Cashu.me has\n            no control over the application.\n          </p>\n          <p>\n            1.5 <strong>Open Source Code:</strong> The application code is open\n            source, meaning it can be self-hosted and run by third parties using\n            different domains. Cashu.me has no control over, and does not\n            endorse or assume responsibility for, any instances of the code\n            running outside of the Cashu.me service. Your use of any such\n            third-party instances is at your own risk.\n          </p>\n          <p>\n            1.6 <strong>Code Disclaimer:</strong> The open-source code is\n            provided without any guarantee of security, error-free operation, or\n            technical support. Users must independently verify the authenticity,\n            security, and integrity of the open-source code prior to use.\n          </p>\n          <p><strong>2. User Responsibilities & Disclaimers</strong></p>\n          <p>\n            2.1 <strong>User’s Sole Responsibility:</strong> You understand and\n            agree that you use the Site and Services at your own risk and for\n            your own account. You alone are fully responsible for selecting\n            Mints, conducting transactions, and safeguarding your ecash and\n            secret values. Cashu.me is not a party to and disclaims any\n            responsibility for any agreements, terms, or disputes between you\n            and any Mint.\n          </p>\n          <p>\n            2.2 <strong>No Partnership with Mints:</strong> Cashu.me is not\n            affiliated with, endorsed by, or responsible for any Mint. We make\n            no representations, warranties, or guarantees about any Mint’s\n            integrity, legality, liquidity, or functionality. Your relationship\n            with any Mint, including the issuance, redemption, or valuation of\n            ecash, is solely a matter between you and that Mint. Cashu.me is not\n            a party to any transaction between you and any Mint or third party.\n            No agency, partnership, or joint venture relationship is formed by\n            your use of the Site.\n          </p>\n          <p>\n            2.3 <strong>Risk of Ecash:</strong> Ecash is an experimental,\n            bearer-like digital asset that may not be recognized as money,\n            currency, or a store of value. Anyone with the secret value has\n            control over the ecash. You agree to review and understand all risks\n            disclosed in our Risk Disclosure Statement before using ecash.\n          </p>\n          <p><strong>3. Modifications to Terms</strong></p>\n          <p>\n            We may amend or update these Terms at any time without notice. You\n            are advised to review these Terms periodically. Your continued use\n            of the Site or Services after any modifications constitutes\n            acceptance of the updated Terms. If you do not agree, discontinue\n            your use.\n          </p>\n          <p><strong>4. Compliance with Laws</strong></p>\n          <p>\n            4.1 <strong>Legal Compliance:</strong> Your use of the Site and any\n            Services is void where prohibited by law. You must determine whether\n            your use of ecash and related activities are lawful. You are solely\n            responsible for compliance with all applicable laws, taxes, and\n            regulations.\n          </p>\n          <p>\n            4.2 <strong>Not Financial Services:</strong> The Services are not\n            intended to constitute regulated financial, banking, e-money, or\n            payment services. You are solely responsible for determining whether\n            your use of ecash or related activities requires any form of\n            license, registration, or compliance with financial regulations in\n            your jurisdiction.\n          </p>\n          <p><strong>5. License to Use the Site</strong></p>\n          <p>\n            Subject to your compliance with these Terms, we grant you a limited,\n            personal, non-exclusive, non-transferable, revocable license to use\n            the Site. We may suspend or terminate your access at our sole\n            discretion.\n          </p>\n          <p><strong>6. Risks and Limitation of Liability</strong></p>\n          <p>\n            6.1 <strong>No Liability for Interactions with Mints:</strong>\n            Cashu.me is not liable for any transactions, disputes, or issues\n            arising from your dealings with Mints.\n          </p>\n          <p>\n            6.2 <strong>Assumption of Risk:</strong> You acknowledge\n            ecash-related activities involve significant risks, including market\n            volatility, theft, and regulatory uncertainty.\n          </p>\n          <p>\n            6.3 <strong>Waiver of Accountability:</strong> By using the Site,\n            you waive any right to hold Cashu.me accountable for any damages,\n            losses, or disputes arising from your use of the Site or Services.\n          </p>\n          <p>\n            6.4 <strong>No Warranties:</strong> THE SITE AND SERVICES ARE\n            PROVIDED “AS IS” WITHOUT ANY WARRANTIES. WE DISCLAIM ALL WARRANTIES\n            TO THE MAXIMUM EXTENT PERMITTED BY LAW.\n          </p>\n          <p>\n            6.5 <strong>Limitation of Liability:</strong> TO THE FULLEST EXTENT\n            PERMITTED BY LAW, CASHU.ME IS NOT LIABLE FOR INDIRECT, INCIDENTAL,\n            SPECIAL, CONSEQUENTIAL, OR PUNITIVE DAMAGES ARISING OUT OF OR\n            RELATED TO THESE TERMS, THE SITE, OR THE SERVICES.\n          </p>\n          <p>\n            6.6 <strong>Maximum Liability:</strong> To the maximum extent\n            permitted by applicable law, the Site and Services are provided ‘as\n            is’ without warranties of any kind. This does not affect any\n            statutory warranties or rights which cannot be excluded under your\n            jurisdiction’s law.\n          </p>\n          <p><strong>7. Indemnification and Release</strong></p>\n          <p>\n            You agree to indemnify and hold harmless Cashu.me and its affiliates\n            from claims arising out of your use of the Site or Services. If you\n            have a dispute with any Mint or third party, you release Cashu.me\n            from all related claims.\n          </p>\n          <p><strong>8. Mediation and Dispute Resolution</strong></p>\n          <p>\n            8.1 <strong>Mediation Requirement:</strong> If a dispute arises out\n            of or relates to these Terms, the Site, or the Services, the parties\n            agree to first attempt to resolve the dispute through good-faith\n            mediation administered by a reputable mediation provider. Each party\n            shall bear its own costs for the mediation, and the costs of the\n            mediator shall be shared equally.\n          </p>\n          <p>\n            8.2 <strong>Arbitration Option:</strong> If mediation does not\n            resolve the dispute within 30 days (or a mutually agreed period),\n            either party may initiate final and binding arbitration administered\n            by a reputable arbitration provider within the user’s jurisdiction.\n            Arbitration shall be conducted on an individual basis, and class\n            actions or collective proceedings are not permitted.\n          </p>\n          <p>\n            8.3 <strong>Waiver of Jury Trial:</strong> If arbitration is not\n            invoked and the dispute proceeds to court, you waive your right to a\n            trial by jury to the fullest extent permitted by the governing law\n            of your jurisdiction.\n          </p>\n          <p>\n            8.4 <strong>EU Consumer Rights:</strong> If you are residing in the\n            EU, any mandatory statutory rights regarding dispute resolution\n            procedures remain unaffected by this clause. Nothing in this Section\n            8 shall limit or affect any mandatory rights you may have under EU\n            consumer protection or other applicable statutory laws.\n          </p>\n          <p><strong>9. Prohibited Uses</strong></p>\n          <p>\n            You may not use the Site or Services for unlawful activities, to\n            violate applicable laws, or to engage in market manipulation. We may\n            suspend or terminate access for prohibited uses. You agree not to\n            use the Site or Services to engage in any activity that violates\n            applicable anti-money laundering (AML), counter-terrorism financing\n            (CTF), or other financial crime regulations. Any use of the Site or\n            Services for unlawful or fraudulent purposes is strictly prohibited.\n          </p>\n          <p><strong>10. Privacy and Data Protection</strong></p>\n          <p>\n            10.1 <strong>GDPR Compliance:</strong> Cashu.me does not collect or\n            store any personal data, including IP addresses. No data is shared\n            with third parties. As such, Cashu.me does not engage in data\n            processing activities that would subject it to GDPR or similar\n            regulations. Users remain responsible for ensuring their own\n            device’s security and verifying the authenticity of the code they\n            run.\n          </p>\n          <p>\n            10.2 <strong>Security:</strong> Because the code executes entirely\n            on your device and no personal data is collected, Cashu.me does not\n            perform any data processing activities that would fall under the\n            GDPR or similar data protection laws.\n          </p>\n          <p>\n            10.3 <strong>Local Data:</strong> Any data or information stored\n            locally on your device, including browser storage, cookies, or\n            application state, is controlled by you and not transmitted to or\n            accessible by Cashu.me.\n          </p>\n          <p><strong>11. Your Representations and Warranties</strong></p>\n          <p>\n            You represent and warrant you have the right and authority to enter\n            into these Terms and that your use of the Site will be lawful.\n          </p>\n          <p><strong>12. No Investment Advice</strong></p>\n          <p>\n            Cashu.me does not provide investment, legal, or tax advice. No\n            fiduciary, advisory, or trust relationship is formed between you and\n            Cashu.me by using the Site or Services.\n          </p>\n          <p><strong>13. No Waiver</strong></p>\n          <p>\n            No failure or delay to exercise any right by Cashu.me shall\n            constitute a waiver of that right.\n          </p>\n          <p><strong>14. Force Majeure</strong></p>\n          <p>\n            Cashu.me is not liable for delays or failures due to events beyond\n            our reasonable control.\n          </p>\n          <p><strong>15. Assignment</strong></p>\n          <p>\n            You may not assign your rights without our consent. We may assign\n            our rights freely.\n          </p>\n          <p><strong>16. Severability</strong></p>\n          <p>\n            If any provision is deemed invalid, remaining provisions remain in\n            effect.\n          </p>\n          <p><strong>17. Electronic Communications; Language</strong></p>\n          <p>\n            By using the Site or Services, you consent to receive communications\n            electronically. We will communicate in English.\n          </p>\n          <p><strong>18. Governing Law</strong></p>\n          <p>\n            18.1 <strong>Choice of Law:</strong>\n            These Terms and any dispute arising from or related to these Terms,\n            the Site, or the Services shall be governed by the applicable laws\n            of your jurisdiction, without regard to conflict of law principles.\n          </p>\n          <p>\n            18.2 <strong>Consumer Rights:</strong>\n            Nothing in these Terms shall exclude or limit any rights you may\n            have under applicable mandatory consumer protection laws or\n            regulations in your jurisdiction, including any rights under EU law\n            that cannot be lawfully limited or disclaimed.\n          </p>\n          <p><strong>19. E-Sign Consent Policy</strong></p>\n          <p>\n            By using the Site, you consent to receive all communications\n            electronically.\n          </p>\n          <p><strong>20. Risk Disclosure Statement</strong></p>\n          <p>\n            Using ecash involves significant risks including legal, market,\n            liquidity, counterparty, and operational risks. You acknowledge and\n            accept these risks.\n          </p>\n          <p><strong>21. Entire Agreement</strong></p>\n          <p>\n            These Terms represent the entire agreement between you and Cashu.me.\n          </p>\n        </div>\n      </q-expansion-item>\n      <q-checkbox\n        v-model=\"welcomeStore.termsAccepted\"\n        :label=\"$t('WelcomeSlide4.inputs.checkbox.label')\"\n        class=\"q-mt-md\"\n      />\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { useWelcomeStore } from \"src/stores/welcome\";\nimport { useQuasar } from \"quasar\";\n\nexport default {\n  name: \"WelcomeSlide4\",\n  setup() {\n    const welcomeStore = useWelcomeStore();\n    const $q = useQuasar();\n\n    const acceptTerms = () => {\n      welcomeStore.acceptTerms();\n      welcomeStore.closeWelcome();\n    };\n\n    return {\n      welcomeStore,\n      acceptTerms,\n    };\n  },\n};\n</script>\n\n<style scoped>\nh2 {\n  font-weight: bold;\n}\np {\n  font-size: 0.82rem;\n  color: #c6c6c6;\n}\n\n/* Unified content width for consistency with other slides */\n.q-pa-md .text-center,\n.q-pa-md .q-expansion-item,\n.q-pa-md .q-checkbox {\n  max-width: 500px;\n  width: 100%;\n  margin-left: auto;\n  margin-right: auto;\n}\n</style>\n"
  },
  {
    "path": "src/pages/welcome/WelcomeSlideChoice.vue",
    "content": "<template>\n  <div class=\"choice-slide\">\n    <!-- Main content area -->\n    <div class=\"content\">\n      <!-- Header Icon -->\n      <div class=\"header-icon\">\n        <div class=\"icon-circle\">\n          <q-icon name=\"account_balance_wallet\" size=\"2.5em\" color=\"white\" />\n        </div>\n      </div>\n\n      <!-- Title -->\n      <h1 class=\"title\">{{ $t(\"WelcomeSlideChoice.title\") }}</h1>\n\n      <!-- Description -->\n      <p class=\"description\">\n        {{ $t(\"WelcomeSlideChoice.text\") }}\n      </p>\n\n      <!-- Options -->\n      <div class=\"options\">\n        <!-- Create New Option -->\n        <div class=\"option\" @click=\"choose('new')\">\n          <q-icon name=\"auto_awesome\" size=\"2em\" color=\"primary\" class=\"icon\" />\n          <div class=\"text\">\n            <h3 class=\"title\">\n              {{ $t(\"WelcomeSlideChoice.options.new.title\") }}\n            </h3>\n            <p class=\"subtitle\">\n              {{ $t(\"WelcomeSlideChoice.options.new.subtitle\") }}\n            </p>\n          </div>\n        </div>\n\n        <!-- Recover Option -->\n        <div class=\"option\" @click=\"choose('recover')\">\n          <q-icon name=\"history\" size=\"2em\" color=\"primary\" class=\"icon\" />\n          <div class=\"text\">\n            <h3 class=\"title\">\n              {{ $t(\"WelcomeSlideChoice.options.recover.title\") }}\n            </h3>\n            <p class=\"subtitle\">\n              {{ $t(\"WelcomeSlideChoice.options.recover.subtitle\") }}\n            </p>\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</template>\n\n<script lang=\"ts\">\nimport { useWelcomeStore } from \"src/stores/welcome\";\nimport { useWalletStore } from \"src/stores/wallet\";\nexport default {\n  name: \"WelcomeSlideChoice\",\n  setup() {\n    const welcomeStore = useWelcomeStore();\n    const walletStore = useWalletStore();\n    const choose = (path: \"new\" | \"recover\") => {\n      welcomeStore.setPath(path);\n      // advance to next stage immediately for snappier UX\n      welcomeStore.setCurrentSlide(3);\n    };\n    return { welcomeStore, walletStore, choose };\n  },\n};\n</script>\n\n<style scoped>\n.choice-slide {\n  display: flex;\n  flex-direction: column;\n  justify-content: space-between;\n  height: 100%;\n  background: var(--q-dark);\n  color: white;\n  padding: 40px 20px 20px 20px;\n  box-sizing: border-box;\n}\n\n.content {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  text-align: left;\n  flex: 1;\n}\n\n.header-icon {\n  margin-bottom: 20px;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.icon-circle {\n  width: 80px;\n  height: 80px;\n  border-radius: 50%;\n  background: rgba(255, 255, 255, 0.1);\n  display: flex;\n  justify-content: center;\n  align-items: center;\n}\n\n.title {\n  font-size: 1.8rem;\n  font-weight: 700;\n  margin: 0 0 16px 0;\n  color: white;\n  line-height: 1.2;\n  text-align: left;\n  width: 100%;\n  max-width: 500px;\n}\n\n.description {\n  font-size: 0.95rem;\n  line-height: 1.5;\n  color: rgba(255, 255, 255, 0.8);\n  margin: 0 0 32px 0;\n  text-align: left;\n  max-width: 500px;\n  width: 100%;\n}\n\n.options {\n  display: flex;\n  flex-direction: column;\n  gap: 32px;\n  width: 100%;\n  max-width: 500px;\n  padding: 16px 0px;\n}\n\n.option {\n  display: flex;\n  align-items: flex-start;\n  gap: 16px;\n  cursor: pointer;\n  padding: 16px;\n  border-radius: 12px;\n  transition: all 0.2s ease;\n}\n\n.option:hover {\n  background: rgba(255, 255, 255, 0.05);\n  transform: translateY(-2px);\n}\n\n.icon {\n  flex-shrink: 0;\n  margin-top: 2px;\n}\n\n.text {\n  flex: 1;\n}\n\n.text .title {\n  font-size: 15.2px; /* Exact match to platform titles in WelcomeSlide2 */\n  font-family: Inter, -apple-system, \"system-ui\", \"Segoe UI\", Roboto,\n    \"Helvetica Neue\", Arial, sans-serif;\n  font-weight: 600;\n  color: #ffffff;\n  margin: 0 0 8px 0;\n}\n\n.subtitle {\n  font-size: 0.9rem;\n  color: rgba(255, 255, 255, 0.8);\n  margin: 0 0 8px 0;\n  line-height: 1.4;\n}\n\n/* Mobile adjustments */\n@media (max-width: 600px) {\n  .choice-slide {\n    padding: 30px 15px 15px 15px;\n  }\n\n  .title {\n    font-size: 1.6rem;\n  }\n\n  .description {\n    font-size: 0.9rem;\n    margin-bottom: 28px;\n  }\n\n  .option {\n    padding: 12px;\n  }\n\n  .text .title {\n    font-size: 14px;\n  }\n\n  .subtitle {\n    font-size: 0.85rem;\n  }\n}\n</style>\n"
  },
  {
    "path": "src/router/index.js",
    "content": "import { route } from \"quasar/wrappers\";\nimport {\n  createRouter,\n  createMemoryHistory,\n  createWebHistory,\n  createWebHashHistory,\n} from \"vue-router\";\nimport routes from \"./routes\";\n\n/*\n * If not building with SSR mode, you can\n * directly export the Router instantiation;\n *\n * The function below can be async too; either use\n * async/await or return a Promise which resolves\n * with the Router instance.\n */\n\nexport default route(function (/* { store, ssrContext } */) {\n  const createHistory = process.env.SERVER\n    ? createMemoryHistory\n    : process.env.VUE_ROUTER_MODE === \"history\"\n    ? createWebHistory\n    : createWebHashHistory;\n\n  const Router = createRouter({\n    scrollBehavior: () => ({ left: 0, top: 0 }),\n    routes,\n\n    // Leave this as is and make changes in quasar.conf.js instead!\n    // quasar.conf.js -> build -> vueRouterMode\n    // quasar.conf.js -> build -> publicPath\n    history: createHistory(process.env.VUE_ROUTER_BASE),\n  });\n\n  return Router;\n});\n"
  },
  {
    "path": "src/router/routes.js",
    "content": "const routes = [\n  {\n    path: \"/\",\n    component: () => import(\"layouts/MainLayout.vue\"),\n    children: [\n      { path: \"\", component: () => import(\"src/pages/WalletPage.vue\") },\n    ],\n  },\n  {\n    path: \"/settings\",\n    component: () => import(\"layouts/FullscreenLayout.vue\"),\n    children: [{ path: \"\", component: () => import(\"src/pages/Settings.vue\") }],\n  },\n  {\n    path: \"/mintdetails\",\n    component: () => import(\"layouts/FullscreenLayout.vue\"),\n    children: [\n      { path: \"\", component: () => import(\"src/pages/MintDetailsPage.vue\") },\n    ],\n  },\n  {\n    path: \"/discoverMints\",\n    component: () => import(\"layouts/FullscreenLayout.vue\"),\n    children: [\n      { path: \"\", component: () => import(\"src/pages/MintDiscoveryPage.vue\") },\n    ],\n  },\n  {\n    path: \"/mintratings\",\n    component: () => import(\"layouts/FullscreenLayout.vue\"),\n    children: [\n      { path: \"\", component: () => import(\"src/pages/MintRatingsPage.vue\") },\n    ],\n  },\n  {\n    path: \"/createreview\",\n    component: () => import(\"layouts/FullscreenLayout.vue\"),\n    children: [\n      {\n        path: \"\",\n        component: () => import(\"src/pages/CreateMintReviewPage.vue\"),\n      },\n    ],\n  },\n  {\n    path: \"/restore\",\n    component: () => import(\"layouts/FullscreenLayout.vue\"),\n    children: [{ path: \"\", component: () => import(\"src/pages/Restore.vue\") }],\n  },\n  {\n    path: \"/already-running\",\n    component: () => import(\"layouts/BlankLayout.vue\"),\n    children: [\n      { path: \"\", component: () => import(\"src/pages/AlreadyRunning.vue\") },\n    ],\n  },\n  {\n    path: \"/welcome\",\n    component: () => import(\"layouts/BlankLayout.vue\"),\n    children: [\n      { path: \"\", component: () => import(\"src/pages/WelcomePage.vue\") },\n    ],\n  },\n  {\n    path: \"/terms\",\n    component: () => import(\"layouts/FullscreenLayout.vue\"),\n    children: [\n      { path: \"\", component: () => import(\"src/pages/TermsPage.vue\") },\n    ],\n  },\n\n  // Always leave this as last one,\n  // but you can also remove it\n  {\n    path: \"/:pathMatch(.*)*\",\n    component: () => import(\"src/pages/ErrorNotFound.vue\"),\n  },\n];\n\nexport default routes;\n"
  },
  {
    "path": "src/stores/__tests__/wallet.test.js",
    "content": "import { beforeEach, describe, expect, it, vi } from \"vitest\";\n\nconst h = vi.hoisted(() => {\n  const notify = vi.fn();\n  const notifyApiError = vi.fn();\n  const notifyError = vi.fn();\n  const notifySuccess = vi.fn();\n  const notifyWarning = vi.fn();\n\n  const receiveTokensStore = {\n    showReceiveTokens: false,\n    receiveData: { tokensBase64: \"\", p2pkPrivateKey: \"\" },\n  };\n  const sendTokensStore = {\n    showSendTokens: false,\n    sendData: { p2pkPubkey: \"\" },\n  };\n  const tokensStore = {\n    historyTokens: [],\n    addPaidToken: vi.fn(),\n    setTokenPaid: vi.fn(),\n  };\n  const proofsStore = {\n    proofs: [],\n    getUnreservedProofs: vi.fn((proofs) => proofs.filter((p) => !p.reserved)),\n    sumProofs: vi.fn((proofs) => proofs.reduce((sum, p) => sum + p.amount, 0)),\n    removeProofs: vi.fn(),\n    addProofs: vi.fn(),\n    setReserved: vi.fn(),\n  };\n  const uiStore = {\n    lockMutex: vi.fn(async () => {}),\n    unlockMutex: vi.fn(),\n    closeDialogs: vi.fn(),\n    formatCurrency: vi.fn((amount, unit) => `${amount} ${unit}`),\n    vibrate: vi.fn(),\n  };\n  const p2pkStore = {\n    isValidPubkey: vi.fn(() => false),\n    setPrivateKeyUsed: vi.fn(),\n  };\n  const prStore = {\n    decodePaymentRequest: vi.fn(async () => {}),\n  };\n  const mintsStore = {\n    activeMintUrl: \"https://mint-a.example\",\n    activeUnit: \"sat\",\n    addMintData: { url: \"\", nickname: \"\" },\n    mints: [],\n    mintUnitKeysets: vi.fn((mint, unit) =>\n      mint.keysets.filter((k) => k.unit === unit)\n    ),\n    mintUnitProofs: vi.fn(() => []),\n    updateMintInfoAndKeys: vi.fn(async () => {}),\n  };\n\n  const walletLoadMintFromCache = vi.fn();\n  const walletGetFeesForProofs = vi.fn(() => 7);\n  const keychainMintToCacheDTO = vi.fn(() => ({ cache: \"dto\" }));\n\n  class WalletMock {\n    constructor(url, options) {\n      this.mint = { mintUrl: url };\n      this.unit = options.unit;\n      this.options = options;\n      this.loadMintFromCache = walletLoadMintFromCache;\n      this.getFeesForProofs = walletGetFeesForProofs;\n    }\n\n    selectProofsToSend(proofs, amount) {\n      let running = 0;\n      const send = [];\n      for (const proof of proofs) {\n        if (running >= amount) break;\n        send.push(proof);\n        running += proof.amount;\n      }\n      const sendSecrets = new Set(send.map((p) => p.secret));\n      return {\n        send,\n        keep: proofs.filter((p) => !sendSecrets.has(p.secret)),\n      };\n    }\n  }\n\n  return {\n    notify,\n    notifyApiError,\n    notifyError,\n    notifySuccess,\n    notifyWarning,\n    receiveTokensStore,\n    sendTokensStore,\n    tokensStore,\n    proofsStore,\n    uiStore,\n    p2pkStore,\n    prStore,\n    mintsStore,\n    walletLoadMintFromCache,\n    walletGetFeesForProofs,\n    keychainMintToCacheDTO,\n    WalletMock,\n  };\n});\n\nvi.mock(\"src/js/utils\", () => ({\n  currentDateStr: () => \"2026-03-10T12:00:00.000Z\",\n}));\n\nvi.mock(\"src/js/notify\", () => ({\n  notify: (...args) => h.notify(...args),\n  notifyApiError: (...args) => h.notifyApiError(...args),\n  notifyError: (...args) => h.notifyError(...args),\n  notifySuccess: (...args) => h.notifySuccess(...args),\n  notifyWarning: (...args) => h.notifyWarning(...args),\n}));\n\nvi.mock(\"@vueuse/core\", () => ({\n  useLocalStorage: (_key, value) => value,\n}));\n\nvi.mock(\"vue-i18n\", () => ({\n  useI18n: () => ({ t: (key) => key }),\n  createI18n: () => ({\n    global: {\n      t: (key) => key,\n    },\n  }),\n}));\n\nvi.mock(\"light-bolt11-decoder\", () => ({\n  decode: vi.fn(() => ({ paymentRequest: \"lnbc123\", sections: [] })),\n}));\n\nvi.mock(\"@cashu/cashu-ts\", () => ({\n  Wallet: h.WalletMock,\n  KeyChain: {\n    mintToCacheDTO: (...args) => h.keychainMintToCacheDTO(...args),\n  },\n  CheckStateEnum: { SPENT: \"SPENT\" },\n  MeltQuoteState: { PAID: \"PAID\", PENDING: \"PENDING\" },\n  MintQuoteState: { PAID: \"PAID\", ISSUED: \"ISSUED\", PENDING: \"PENDING\" },\n}));\n\nvi.mock(\"src/stores/receiveTokensStore\", () => ({\n  useReceiveTokensStore: () => h.receiveTokensStore,\n}));\n\nvi.mock(\"src/stores/sendTokensStore\", () => ({\n  useSendTokensStore: () => h.sendTokensStore,\n}));\n\nvi.mock(\"src/stores/p2pk\", () => ({\n  useP2PKStore: () => h.p2pkStore,\n}));\n\nvi.mock(\"src/stores/payment-request\", () => ({\n  usePRStore: () => h.prStore,\n}));\n\nvi.mock(\"src/stores/workers\", () => ({\n  useWorkersStore: () => ({ checkTokenSpendableWorker: vi.fn() }),\n}));\n\nvi.mock(\"src/stores/invoicesWorker\", () => ({\n  useInvoicesWorkerStore: () => ({\n    addInvoice: vi.fn(),\n    removeInvoice: vi.fn(),\n  }),\n}));\n\nvi.mock(\"src/stores/ui\", () => ({\n  useUiStore: () => h.uiStore,\n}));\n\nvi.mock(\"src/stores/proofs\", () => ({\n  useProofsStore: () => h.proofsStore,\n}));\n\nvi.mock(\"src/stores/tokens\", () => ({\n  useTokensStore: () => h.tokensStore,\n}));\n\nvi.mock(\"src/stores/mints\", async () => {\n  const actual = await vi.importActual(\"src/stores/mints\");\n  return {\n    ...actual,\n    useMintsStore: () => h.mintsStore,\n  };\n});\n\nimport { useWalletStore } from \"src/stores/wallet\";\n\ndescribe(\"wallet store\", () => {\n  beforeEach(() => {\n    vi.clearAllMocks();\n\n    h.receiveTokensStore.showReceiveTokens = false;\n    h.receiveTokensStore.receiveData.tokensBase64 = \"\";\n    h.sendTokensStore.sendData.p2pkPubkey = \"\";\n    h.sendTokensStore.showSendTokens = false;\n\n    h.mintsStore.activeMintUrl = \"https://mint-a.example\";\n    h.mintsStore.activeUnit = \"sat\";\n    h.mintsStore.addMintData = { url: \"\", nickname: \"\" };\n    h.mintsStore.mints = [\n      {\n        url: \"https://mint-a.example\",\n        keys: [{ id: \"00aa\" }],\n        keysets: [\n          { id: \"base64-a\", unit: \"sat\", active: true },\n          { id: \"00aa\", unit: \"sat\", active: true },\n        ],\n        info: { name: \"mint-a\" },\n      },\n    ];\n\n    h.proofsStore.getUnreservedProofs.mockImplementation((proofs) =>\n      proofs.filter((p) => !p.reserved)\n    );\n    h.proofsStore.sumProofs.mockImplementation((proofs) =>\n      proofs.reduce((sum, p) => sum + p.amount, 0)\n    );\n    h.p2pkStore.isValidPubkey.mockReturnValue(false);\n  });\n\n  it(\"manages keyset counters\", () => {\n    const wallet = useWalletStore();\n    expect(wallet.keysetCounter(\"k1\")).toBe(1);\n    wallet.increaseKeysetCounter(\"k1\", 4);\n    expect(wallet.keysetCounter(\"k1\")).toBe(5);\n    wallet.increaseKeysetCounter(\"k2\", 3);\n    expect(wallet.keysetCounter(\"k2\")).toBe(3);\n  });\n\n  it(\"creates a new mnemonic and archives previous counters\", () => {\n    const wallet = useWalletStore();\n    wallet.mnemonic = \"old mnemonic\";\n    wallet.keysetCounters = [{ id: \"00aa\", counter: 11 }];\n\n    wallet.newMnemonic();\n\n    expect(wallet.oldMnemonicCounters[0]).toEqual({\n      mnemonic: \"old mnemonic\",\n      keysetCounters: [{ id: \"00aa\", counter: 11 }],\n    });\n    expect(wallet.keysetCounters).toEqual([]);\n    expect(wallet.mnemonic).not.toBe(\"old mnemonic\");\n  });\n\n  it(\"splits amounts into binary chunks\", () => {\n    const wallet = useWalletStore();\n    expect(wallet.splitAmount(0)).toEqual([]);\n    expect(wallet.splitAmount(13)).toEqual([1, 4, 8]);\n  });\n\n  it(\"selects base64 proofs by descending amount\", () => {\n    const wallet = useWalletStore();\n    const proofs = [\n      { id: \"base64-1\", amount: 2, reserved: false },\n      { id: \"00aa\", amount: 32, reserved: false },\n      { id: \"base64-2\", amount: 8, reserved: false },\n      { id: \"base64-3\", amount: 4, reserved: false },\n    ];\n    expect(wallet.coinSelectSpendBase64(proofs, 10)).toEqual([\n      { id: \"base64-2\", amount: 8, reserved: false },\n      { id: \"base64-3\", amount: 4, reserved: false },\n    ]);\n    expect(wallet.coinSelectSpendBase64(proofs, 20)).toEqual([]);\n  });\n\n  it(\"returns spendable proofs and throws on insufficient amount\", () => {\n    const wallet = useWalletStore();\n    const proofs = [\n      { id: \"00aa\", amount: 5, reserved: false },\n      { id: \"00aa\", amount: 4, reserved: true },\n      { id: \"00aa\", amount: 6, reserved: false },\n    ];\n\n    expect(wallet.spendableProofs(proofs, 10)).toHaveLength(2);\n    expect(() => wallet.spendableProofs(proofs, 20)).toThrow(\n      \"wallet.notifications.balance_too_low\"\n    );\n  });\n\n  it(\"chooses active hex keyset first\", () => {\n    const wallet = useWalletStore();\n    expect(wallet.getKeyset(\"https://mint-a.example\", \"sat\")).toBe(\"00aa\");\n  });\n\n  it(\"updates invoice status to paid\", () => {\n    const wallet = useWalletStore();\n    wallet.invoiceHistory = [\n      {\n        quote: \"q-1\",\n        amount: 1,\n        bolt11: \"lnbc\",\n        memo: \"memo\",\n        date: \"old\",\n        status: \"pending\",\n        mint: \"https://mint-a.example\",\n        unit: \"sat\",\n      },\n    ];\n\n    wallet.setInvoicePaid(\"q-1\");\n    expect(wallet.invoiceHistory[0].status).toBe(\"paid\");\n    expect(wallet.invoiceHistory[0].paidDate).toBe(\"2026-03-10T12:00:00.000Z\");\n  });\n\n  it(\"adds, updates and removes outgoing invoices\", async () => {\n    const wallet = useWalletStore();\n    wallet.payInvoiceData.input.request = \"lnbc123\";\n    const quote = { quote: \"melt-q-1\", amount: 101, fee_reserve: 4 };\n\n    await wallet.addOutgoingPendingInvoiceToHistory(\n      quote,\n      \"https://mint-a.example\",\n      \"sat\"\n    );\n    wallet.updateOutgoingInvoiceInHistory(quote, {\n      status: \"paid\",\n      amount: -99,\n    });\n    wallet.removeOutgoingInvoiceFromHistory(\"melt-q-1\");\n\n    expect(wallet.invoiceHistory).toEqual([]);\n  });\n\n  it(\"creates active wallet and loads cache\", async () => {\n    const wallet = useWalletStore();\n    wallet.mnemonic = \"\";\n    wallet.keysetCounters = [{ id: \"00aa\", counter: 13 }];\n\n    const activeWallet = await wallet.activeWallet();\n\n    expect(activeWallet.mint.mintUrl).toBe(\"https://mint-a.example\");\n    expect(h.keychainMintToCacheDTO).toHaveBeenCalled();\n    expect(h.walletLoadMintFromCache).toHaveBeenCalledWith(\n      { name: \"mint-a\" },\n      { cache: \"dto\" }\n    );\n  });\n\n  it(\"refreshes stale keysets when requested\", async () => {\n    const wallet = useWalletStore();\n    h.mintsStore.mints = [\n      {\n        url: \"https://mint-a.example\",\n        keys: [{ id: \"00aa\" }],\n        keysets: [{ id: \"00aa\", unit: \"sat\", active: true }],\n        info: { name: \"mint-a\" },\n        lastKeysetsUpdated: \"1970-01-01T00:00:00.000Z\",\n      },\n    ];\n\n    await wallet.mintWallet(\"https://mint-a.example\", \"sat\", true);\n    expect(h.mintsStore.updateMintInfoAndKeys).toHaveBeenCalledTimes(1);\n  });\n\n  it(\"accounts for signed-output errors\", () => {\n    const wallet = useWalletStore();\n    wallet.keysetCounters = [{ id: \"00aa\", counter: 1 }];\n\n    const handled = wallet.handleOutputsHaveAlreadyBeenSignedError(\"00aa\", {\n      message: \"outputs have already been signed\",\n    });\n\n    expect(handled).toBe(true);\n    expect(wallet.keysetCounter(\"00aa\")).toBe(11);\n    expect(h.notify).toHaveBeenCalledWith(\n      \"wallet.notifications.please_try_again\"\n    );\n  });\n\n  it(\"routes decodeRequest branches\", async () => {\n    const wallet = useWalletStore();\n    vi.spyOn(wallet, \"handleBolt11Invoice\").mockResolvedValue(undefined);\n    vi.spyOn(wallet, \"lnurlPayFirst\").mockResolvedValue(undefined);\n    vi.spyOn(wallet, \"handlePaymentRequest\").mockResolvedValue(undefined);\n\n    await wallet.decodeRequest(\" lightning:lnbcabc \");\n    await wallet.decodeRequest(\n      \"bitcoin:bc1qxyz?lightning=lnbcfrombitcoin&amount=1\"\n    );\n    await wallet.decodeRequest(\n      \"bitcoin:bc1qxyz?creq=creqb1cashurequest&lightning=lnbcfrombitcoin&amount=1\"\n    );\n    await wallet.decodeRequest(\"lnurl:lnurl1example\");\n    await wallet.decodeRequest(\"cashuAabcdef\");\n\n    h.p2pkStore.isValidPubkey.mockReturnValueOnce(true);\n    await wallet.decodeRequest(\"02abcdef\");\n    await wallet.decodeRequest(\"https://mint-b.example\");\n    await wallet.decodeRequest(\"creqA123\");\n    await wallet.decodeRequest(\"creqb1xyz\");\n\n    expect(h.receiveTokensStore.receiveData.tokensBase64).toBe(\"cashuAabcdef\");\n    expect(h.sendTokensStore.sendData.p2pkPubkey).toBe(\"02abcdef\");\n    expect(h.mintsStore.addMintData.url).toBe(\"https://mint-b.example\");\n    expect(wallet.handlePaymentRequest).toHaveBeenCalledWith(\"creqA123\");\n    expect(wallet.handlePaymentRequest).toHaveBeenCalledWith(\"creqb1xyz\");\n    expect(wallet.handlePaymentRequest).toHaveBeenCalledWith(\n      \"creqb1cashurequest\"\n    );\n    expect(h.uiStore.closeDialogs).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "src/stores/camera.ts",
    "content": "import { defineStore } from \"pinia\";\n\nexport const useCameraStore = defineStore(\"camera\", {\n  state: () => ({\n    camera: {\n      data: null,\n      show: false,\n      camera: \"auto\",\n    },\n    hasCamera: function () {\n      navigator.permissions.query({ name: \"camera\" }).then((res) => {\n        return res.state == \"granted\";\n      });\n    },\n  }),\n  actions: {\n    closeCamera: function () {\n      this.camera.show = false;\n    },\n    showCamera: function () {\n      this.camera.show = true;\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/dexie.ts",
    "content": "import { defineStore } from \"pinia\";\nimport Dexie, { Table } from \"dexie\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { WalletProof } from \"./mints\";\nimport { useStorageStore } from \"./storage\";\nimport { useProofsStore } from \"./proofs\";\nimport { notifyError, notifySuccess } from \"../js/notify\";\n\n// export interface Proof {\n//   id: string\n//   C: string\n//   amount: number\n//   reserved: boolean\n//   secret: string\n//   quote?: string\n// }\n\nexport class CashuDexie extends Dexie {\n  proofs!: Table<WalletProof>;\n\n  constructor() {\n    super(\"db\");\n    this.version(1).stores({\n      proofs: \"secret, id, C, amount, reserved, quote\",\n    });\n  }\n}\n\nexport const cashuDb = new CashuDexie();\n\nexport const useDexieStore = defineStore(\"dexie\", {\n  state: () => ({\n    migratedToDexie: useLocalStorage<boolean>(\"cashu.dexie.migrated\", false),\n  }),\n  getters: {},\n  actions: {\n    migrateToDexie: async function () {\n      const proofsStore = useProofsStore();\n      if (this.migratedToDexie) {\n        return;\n      }\n      console.log(\"Migrating to Dexie\");\n      const proofs = localStorage.getItem(\"cashu.proofs\");\n      let parsedProofs: WalletProof[] = [];\n      if (!proofs) {\n        console.log(\"No cashu.proofs in localStorage to migrate\");\n        this.migratedToDexie = true;\n        return;\n      }\n      parsedProofs = JSON.parse(proofs) as WalletProof[];\n      if (!parsedProofs.length) {\n        console.log(\"No proofs to migrate\");\n        this.migratedToDexie = true;\n        return;\n      }\n      // start migration\n      await useStorageStore().exportWalletState();\n      parsedProofs.forEach((proof) => {\n        cashuDb.proofs.add(proof);\n      });\n      console.log(\n        `Migrated ${cashuDb.proofs.count()} proofs. Before: ${\n          parsedProofs.length\n        } proofs, After: ${(await proofsStore.getProofs()).length} proofs`\n      );\n      console.log(\n        `Proofs sum before: ${proofsStore.sumProofs(\n          parsedProofs\n        )}, after: ${proofsStore.sumProofs(await proofsStore.getProofs())}`\n      );\n      this.migratedToDexie = true;\n      // remove proofs from localstorage\n      localStorage.removeItem(\"cashu.proofs\");\n    },\n    deleteAllTables: function () {\n      cashuDb.proofs.clear();\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/index.js",
    "content": "import { store } from \"quasar/wrappers\";\nimport { createPinia } from \"pinia\";\n\n/*\n * If not building with SSR mode, you can\n * directly export the Store instantiation;\n *\n * The function below can be async too; either use\n * async/await or return a Promise which resolves\n * with the Store instance.\n */\n\nexport default store((/* { ssrContext } */) => {\n  const pinia = createPinia();\n\n  // You can add Pinia plugins here\n  // pinia.use(SomePiniaPlugin)\n\n  return pinia;\n});\n"
  },
  {
    "path": "src/stores/invoicesWorker.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { useWalletStore } from \"./wallet\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { useSettingsStore } from \"./settings\";\ninterface InvoiceQuote {\n  quote: string;\n  addedAt: number;\n  lastChecked: number;\n  checkCount: number;\n}\n\nexport const useInvoicesWorkerStore = defineStore(\"invoicesWorker\", {\n  state: () => {\n    return {\n      checkInterval: 5000,\n      maxLength: 50,\n      // Two weeks\n      maxAge: 1000 * 60 * 60 * 24 * 14,\n      oneDay: 1000 * 60 * 60 * 24,\n      oneHour: 1000 * 60 * 60,\n      // Once per day\n      maxInterval: 1000 * 60 * 60 * 24,\n      keepIntervalConstantForNChecks: 5,\n      invoiceCheckListener: null as NodeJS.Timeout | null,\n      invoiceWorkerRunning: false,\n      quotes: useLocalStorage<InvoiceQuote[]>(\n        \"cashu.worker.invoices.quotesQueue\",\n        []\n      ),\n      lastInvoiceCheckTime: 0,\n      maxQuotesToCheckOnStartup: 10,\n      lastPendingInvoiceCheck: useLocalStorage<number>(\n        \"cashu.worker.invoices.lastPendingInvoiceCheck\",\n        0\n      ),\n      checkPendingInvoicesInterval: 1000 * 10, // delay between bulk invoice checks\n    };\n  },\n  actions: {\n    startInvoiceCheckerWorker() {\n      if (!useSettingsStore().periodicallyCheckIncomingInvoices) return;\n      if (this.invoiceCheckListener) return;\n      this.invoiceWorkerRunning = true;\n      this.invoiceCheckListener = setInterval(() => {\n        this.processQuotes();\n      }, this.checkInterval);\n    },\n    stopInvoiceCheckerWorker() {\n      if (this.invoiceCheckListener) {\n        clearInterval(this.invoiceCheckListener);\n        this.invoiceCheckListener = null;\n        this.invoiceWorkerRunning = false;\n      }\n    },\n    addInvoiceToChecker(quote: string) {\n      const existingIndex = this.quotes.findIndex((q) => q.quote === quote);\n      if (existingIndex !== -1) {\n        this.quotes.splice(existingIndex, 1);\n      }\n\n      if (this.quotes.length >= this.maxLength) {\n        this.quotes.shift();\n      }\n\n      this.quotes.push({\n        quote,\n        addedAt: Date.now(),\n        lastChecked: 0,\n        checkCount: 0,\n      });\n      this.startInvoiceCheckerWorker();\n    },\n    removeInvoiceFromChecker(quote: string) {\n      const index = this.quotes.findIndex((q) => q.quote === quote);\n      if (index !== -1) {\n        this.quotes.splice(index, 1);\n      }\n    },\n    dueTime(q: InvoiceQuote) {\n      if (q.checkCount > this.keepIntervalConstantForNChecks) {\n        return (\n          q.lastChecked +\n          Math.min(\n            this.checkInterval *\n              Math.pow(2, q.checkCount - this.keepIntervalConstantForNChecks),\n            this.maxInterval\n          )\n        );\n      } else {\n        return q.lastChecked + this.checkInterval;\n      }\n    },\n    async processQuotes() {\n      const now = Date.now();\n      this.quotes = this.quotes.filter((q) => now - q.addedAt < this.maxAge);\n\n      if (this.quotes.length === 0) return;\n\n      // Global rate limit\n      if (now - this.lastInvoiceCheckTime < this.checkInterval) {\n        return;\n      }\n\n      for (let i = this.quotes.length - 1; i >= 0; i--) {\n        const q = this.quotes[i];\n        const dueTime = this.dueTime(q);\n        if (now > dueTime) {\n          const walletStore = useWalletStore();\n          try {\n            await walletStore.checkInvoice(q.quote, false);\n            this.quotes.splice(i, 1);\n          } catch (error) {\n            q.lastChecked = now;\n            q.checkCount += 1;\n          }\n          this.lastInvoiceCheckTime = now;\n          break;\n        }\n      }\n    },\n    async checkPendingInvoices() {\n      if (!useSettingsStore().checkInvoicesOnStartup) return;\n      if (\n        Date.now() <\n        this.lastPendingInvoiceCheck + this.checkPendingInvoicesInterval\n      )\n        return;\n      const walletStore = useWalletStore();\n      const quotesToCheck = walletStore.invoiceHistory.filter(\n        (q) =>\n          q.status === \"pending\" &&\n          q.amount > 0 &&\n          Date.now() - Date.parse(q.date) < this.oneDay\n      );\n      if (quotesToCheck.length > this.maxQuotesToCheckOnStartup) {\n        quotesToCheck.splice(this.maxQuotesToCheckOnStartup);\n      }\n      this.lastPendingInvoiceCheck = Date.now();\n      console.log(`Checking ${quotesToCheck.length} quotes`);\n      for (const q of quotesToCheck) {\n        try {\n          console.log(`Checking quote ${q.quote}`);\n          walletStore.mintOnPaid(q.quote, false, false);\n        } catch (error) {\n          console.error(error);\n        }\n      }\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/migrations.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { useMintsStore } from \"./mints\";\nimport { notifySuccess } from \"../js/notify\";\nimport { useUiStore } from \"./ui\";\nimport { useSettingsStore } from \"./settings\";\nimport { useNostrMintBackupStore } from \"./nostrMintBackup\";\n\n// Define the migration version type\nexport type Migration = {\n  version: number;\n  name: string;\n  description: string;\n  execute: () => Promise<void>;\n};\n\nexport const useMigrationsStore = defineStore(\"migrations\", {\n  state: () => ({\n    currentVersion: useLocalStorage<number>(\"cashu.migrations.version\", 0),\n    migrations: [] as Migration[],\n  }),\n  actions: {\n    registerMigration(migration: Migration) {\n      // Add migration if it doesn't already exist\n      if (!this.migrations.some((m) => m.version === migration.version)) {\n        this.migrations.push(migration);\n        // Sort migrations by version\n        this.migrations.sort((a, b) => a.version - b.version);\n      }\n    },\n\n    async runMigrations() {\n      // Get migrations that need to be run (newer than current version)\n      const pendingMigrations = this.migrations.filter(\n        (m) => m.version > this.currentVersion\n      );\n\n      if (pendingMigrations.length === 0) {\n        console.log(\"No migrations to run\");\n        return;\n      }\n\n      console.log(`Running ${pendingMigrations.length} migrations...`);\n\n      // Run each migration in order\n      const uIStore = useUiStore();\n      await uIStore.lockMutex();\n      try {\n        for (const migration of pendingMigrations) {\n          console.log(\n            `Running migration ${migration.version}: ${migration.name}`\n          );\n          try {\n            await migration.execute();\n            // Update the current version after successful migration\n            this.currentVersion = migration.version;\n            console.log(\n              `Migration ${migration.version} completed successfully`\n            );\n          } catch (error) {\n            console.error(`Migration ${migration.version} failed:`, error);\n            // Stop running migrations if one fails\n            break;\n          }\n        }\n      } finally {\n        await uIStore.unlockMutex();\n      }\n    },\n\n    // First migration: Update mint URL from stablenuts.cash to umint.cash\n    async migrateStablenutsToCash() {\n      const mintStore = useMintsStore();\n      let updated = false;\n\n      for (let i = 0; i < mintStore.mints.length; i++) {\n        if (mintStore.mints[i].url === \"https://stablenut.umint.cash\") {\n          console.log(\"Updating mint URL from stablenuts.cash to umint.cash\");\n          mintStore.mints[i].url = \"https://stablenut.cashu.network\";\n\n          // If this was the active mint, update the active mint URL as well\n          if (mintStore.activeMintUrl === \"https://stablenut.umint.cash\") {\n            mintStore.activeMintUrl = \"https://stablenut.cashu.network\";\n          }\n\n          updated = true;\n        }\n      }\n\n      if (updated) {\n        console.log(\"Successfully updated mint URL\");\n      } else {\n        console.log(\"No stablenuts.cash mint found to update\");\n      }\n    },\n\n    // Migration v2: add \"wss://relay.primal.net \" relay, enable nostrMintBackup, clear mint recs cache\n    async migrateAddPrimalRelayAndEnableBackupAndClearMintRecs() {\n      const settings = useSettingsStore();\n\n      // 1) Add relay string with leading '@' and trailing space if not present\n      const relayToAdd = \"wss://relay.primal.net \";\n      try {\n        const relays = Array.isArray(settings.defaultNostrRelays)\n          ? settings.defaultNostrRelays\n          : [];\n        if (!relays.includes(relayToAdd)) {\n          relays.push(relayToAdd);\n          settings.defaultNostrRelays = relays;\n          console.log(`Added relay to defaultNostrRelays: ${relayToAdd}`);\n        } else {\n          console.log(\n            `Relay already present in defaultNostrRelays: ${relayToAdd}`\n          );\n        }\n      } catch (e) {\n        console.error(\n          \"Failed to update defaultNostrRelays during migration v2\",\n          e\n        );\n      }\n\n      // 2) Ensure nostrMintBackupEnabled is true\n      try {\n        if (!settings.nostrMintBackupEnabled) {\n          settings.nostrMintBackupEnabled = true;\n          console.log(\"Enabled nostrMintBackupEnabled setting\");\n          // kick off a backup\n          useNostrMintBackupStore().forceBackup();\n        } else {\n          console.log(\"nostrMintBackupEnabled already true\");\n        }\n      } catch (e) {\n        console.error(\n          \"Failed to enable nostrMintBackupEnabled during migration v2\",\n          e\n        );\n      }\n\n      // 3) Clear cached mint recommendations\n      try {\n        localStorage.removeItem(\"cashu.ndk.mintRecommendations\");\n        console.log(\"Cleared localStorage key: cashu.ndk.mintRecommendations\");\n      } catch (e) {\n        console.error(\n          \"Failed to clear cashu.ndk.mintRecommendations during migration v2\",\n          e\n        );\n      }\n    },\n\n    // Initialize migrations\n    initMigrations() {\n      // Register the first migration\n      this.registerMigration({\n        version: 1,\n        name: \"Migrate stablenuts.cash to umint.cash\",\n        description:\n          \"Updates mint URL from https://stablenut.umint.cash to https://stablenut.cashu.network\",\n        execute: async () => await this.migrateStablenutsToCash(),\n      });\n\n      // Register migration v2\n      this.registerMigration({\n        version: 2,\n        name: \"Add wss://relay.primal.net relay; enable mint backup; clear recs\",\n        description:\n          \"Adds 'wss://relay.primal.net ' to defaultNostrRelays, enables nostrMintBackupEnabled, clears cashu.ndk.mintRecommendations\",\n        execute: async () =>\n          await this.migrateAddPrimalRelayAndEnableBackupAndClearMintRecs(),\n      });\n\n      // Add more migrations here in the future\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/mintRecommendations.ts",
    "content": "import { defineStore } from \"pinia\";\nimport NDK, { NDKEvent, NDKFilter, NDKKind } from \"@nostr-dev-kit/ndk\";\nimport { useSettingsStore } from \"./settings\";\nimport { useNostrStore } from \"./nostr\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport Dexie from \"dexie\";\n\nexport type MintReview = {\n  eventId: string;\n  pubkey: string;\n  created_at: number;\n  rating: number | null;\n  comment: string;\n  raw: any;\n};\n\nexport type MintRecommendation = {\n  url: string;\n  reviewsCount: number;\n  averageRating: number | null;\n  info?: any;\n  error?: boolean;\n  lastHttpInfoFetchAt?: number; // unix seconds\n};\n\nfunction parseRatingAndComment(content: string): {\n  rating: number | null;\n  comment: string;\n} {\n  const m = content.match(/\\s*\\[(\\d)\\s*\\/\\s*5\\]\\s*(.*)$/s);\n  if (!m) return { rating: null, comment: content || \"\" };\n  const rating = parseInt(m[1], 10);\n  const comment = (m[2] || \"\").trim();\n  if (isNaN(rating) || rating < 1 || rating > 5)\n    return { rating: null, comment };\n  return { rating, comment };\n}\n\nexport const useMintRecommendationsStore = defineStore(\"mintRecommendations\", {\n  state: () => ({\n    ndk: {} as NDK,\n    connected: false,\n    // Dexie DB handle\n    dbInitialized: false,\n    dbHydrated: false,\n    db: null as MintReviewsDB | null,\n    // Minimal in-memory caches for UI\n    urlReviews: new Map() as Map<string, MintReview[]>,\n    httpInfoByUrl: new Map() as Map<string, any>,\n    infoTimers: new Map() as Map<string, any>,\n    inflightInfo: new Set() as Set<string>,\n    infoTimeoutMs: 10000,\n    httpInfoFetchIntervalSeconds: 60 * 60, // 1 hour\n    // Aggregated list by URL (persisted)\n    recommendations: useLocalStorage<MintRecommendation[]>(\n      \"cashu.ndk.mintRecommendations\",\n      []\n    ),\n    subsActive: false,\n  }),\n  actions: {\n    initDb: async function () {\n      if (this.dbInitialized && this.db) return;\n      this.db = new MintReviewsDB();\n      await this.db.open();\n      this.dbInitialized = true;\n    },\n    ensureDbInitialized: async function () {\n      if (!this.dbInitialized || !this.db) await this.initDb();\n    },\n    init: function () {\n      if (this.connected) return;\n      const settings = useSettingsStore();\n      const nostr = useNostrStore();\n      if (!nostr.ndk || !(nostr.ndk as any).pool) nostr.initNdkReadOnly();\n      this.ndk =\n        nostr.ndk ||\n        new NDK({ explicitRelayUrls: settings.defaultNostrRelays });\n      this.ndk.connect();\n      this.connected = true;\n      this.ensureDbInitialized();\n      this.hydrateFromDb();\n    },\n    // Load all reviews from DB into a lightweight cache for instant UI and build aggregates\n    hydrateFromDb: async function () {\n      try {\n        if (this.dbHydrated) return;\n        await this.ensureDbInitialized();\n        const rows = await (this.db as MintReviewsDB).reviews.toArray();\n        const map = new Map<string, MintReview[]>();\n        for (const r of rows) {\n          if (!r || !r.url || !r.eventId) continue;\n          const list = map.get(r.url) || [];\n          list.push({\n            eventId: r.eventId,\n            pubkey: r.pubkey,\n            created_at: r.created_at,\n            rating: r.rating,\n            comment: r.comment,\n            raw: r.raw,\n          });\n          map.set(r.url, list);\n        }\n        for (const [url, list] of map.entries()) {\n          list.sort((a, b) => (a.created_at || 0) - (b.created_at || 0));\n          this.urlReviews.set(url, list);\n        }\n        this.dbHydrated = true;\n        await this.rebuildAggregates();\n        // After hydration, opportunistically refetch stale HTTP info within interval\n        //void this.refetchStaleHttpInfoForKnownMints();\n      } catch {}\n    },\n    fetchMintInfos: async function () {\n      this.init();\n      await this.ensureDbInitialized();\n      const filter: NDKFilter = { kinds: [38172 as NDKKind], limit: 5000 };\n      const events = await this.ndk.fetchEvents(filter);\n      for (const ev of events) await this.handleMintInfoEvent(ev);\n      await this.rebuildAggregates();\n    },\n    fetchReviews: async function () {\n      this.init();\n      await this.ensureDbInitialized();\n      const filter: NDKFilter = {\n        kinds: [38000 as NDKKind],\n        [\"#k\"]: [\"38172\"],\n        limit: 5000,\n      } as any;\n      const events = await this.ndk.fetchEvents(filter);\n      for (const ev of events) await this.handleReviewEvent(ev);\n      await this.rebuildAggregates();\n    },\n    fetchReviewsForUrl: async function (url: string) {\n      try {\n        this.init();\n        if (!url || typeof url !== \"string\" || !url.startsWith(\"http\")) return;\n        const filter: NDKFilter = {\n          kinds: [38000 as NDKKind],\n          [\"#k\"]: [\"38172\"],\n          [\"#u\"]: [url],\n          limit: 5000,\n        } as any;\n        const events = await this.ndk.fetchEvents(filter);\n        for (const ev of events) await this.handleReviewEvent(ev);\n        await this.rebuildAggregates();\n      } catch {}\n    },\n    fetchMintInfoForUrl: async function (url: string) {\n      try {\n        this.init();\n        if (!url || typeof url !== \"string\" || !url.startsWith(\"http\")) return;\n        const filter: NDKFilter = {\n          kinds: [38172 as NDKKind],\n          [\"#u\"]: [url],\n          limit: 1000,\n        } as any;\n        const events = await this.ndk.fetchEvents(filter);\n        for (const ev of events) await this.handleMintInfoEvent(ev);\n        await this.rebuildAggregates();\n      } catch {}\n    },\n    clearRecommendations: function () {\n      this.recommendations.splice(0, this.recommendations.length);\n    },\n    clearDiscoveryCaches: async function () {\n      try {\n        await this.ensureDbInitialized();\n        // Do NOT clear HTTP info; preserve last known info across reloads\n      } catch {}\n      this.inflightInfo.clear();\n      this.infoTimers.forEach((t) => clearTimeout(t));\n      this.infoTimers.clear();\n      await this.rebuildAggregates();\n    },\n    setInfoTimeoutMs: function (ms: number) {\n      this.infoTimeoutMs = ms;\n    },\n    discover: async function (): Promise<MintRecommendation[]> {\n      await this.fetchMintInfos();\n      await this.fetchReviews();\n      return this.recommendations;\n    },\n    startSubscriptions: function () {\n      if (this.subsActive) return;\n      this.init();\n      this.hydrateFromDb();\n      const subInfos = this.ndk.subscribe(\n        { kinds: [38172 as NDKKind] } as NDKFilter,\n        { closeOnEose: false, groupable: false }\n      );\n      subInfos.on(\"event\", async (ev: NDKEvent) => {\n        await this.handleMintInfoEvent(ev);\n        try {\n          const u = ev.tags.find(\n            (t) => t[0] === \"u\" && (t[2] === \"cashu\" || t.length >= 2)\n          )?.[1];\n          if (typeof u === \"string\" && u.startsWith(\"http\")) {\n            // Kick off HTTP info fetch (concurrency-limited via scheduler)\n            void this.scheduleHttpInfoFetches([u], 20, 100, this.infoTimeoutMs);\n          }\n        } catch {}\n        void this.rebuildAggregates();\n      });\n      const subReviews = this.ndk.subscribe(\n        { kinds: [38000 as NDKKind] } as NDKFilter,\n        { closeOnEose: false, groupable: false }\n      );\n      subReviews.on(\"event\", async (ev: NDKEvent) => {\n        await this.handleReviewEvent(ev);\n        void this.rebuildAggregates();\n      });\n      this.subsActive = true;\n    },\n    requestMintHttpInfo: async function (url: string, timeoutMs?: number) {\n      try {\n        await this.ensureDbInitialized();\n        const existing = await (this.db as MintReviewsDB).httpInfo.get(url);\n        const nowSec = Math.floor(Date.now() / 1000);\n        const interval = this.httpInfoFetchIntervalSeconds || 0;\n        const isFresh =\n          !!existing &&\n          !!existing.info &&\n          !!existing.fetchedAt &&\n          nowSec - existing.fetchedAt < interval;\n        if (isFresh) {\n          await this.rebuildAggregates();\n          return;\n        }\n        if (this.inflightInfo.has(url)) return;\n        this.inflightInfo.add(url);\n        const ms = timeoutMs ?? this.infoTimeoutMs;\n        const tempMint = { url, keys: [], keysets: [] } as any;\n        const mod = await import(\"src/stores/mints\");\n        console.log(\"Fetching HTTP info for mint\", url);\n        // Start timeout timer only when we actually fire the HTTP request\n        if (!this.infoTimers.has(url)) {\n          const id = setTimeout(async () => {\n            try {\n              const existing = await (this.db as MintReviewsDB).httpInfo.get(\n                url\n              );\n              const row: HttpInfoRow = {\n                url,\n                info: existing?.info ?? null,\n                fetchedAt: Math.floor(Date.now() / 1000) ?? 0,\n                error: false,\n              };\n              await (this.db as MintReviewsDB).httpInfo.put(row);\n              void this.rebuildAggregates();\n            } catch {}\n            this.infoTimers.delete(url);\n          }, ms);\n          this.infoTimers.set(url, id);\n        }\n        const info = await new (mod as any).MintClass(tempMint).api.getInfo();\n        console.log(\"HTTP info for mint\", url, info.name);\n        const row: HttpInfoRow = {\n          url,\n          info,\n          fetchedAt: Math.floor(Date.now() / 1000),\n          error: false,\n        };\n        // unset error in localstore too:\n        await (this.db as MintReviewsDB).httpInfo.put(row);\n        // Update in-memory cache only; do not persist info to localStorage\n        this.httpInfoByUrl.set(url, info);\n        // Rebuild aggregates to reflect fresh fetchedAt and clear error\n        await this.rebuildAggregates();\n        const t = this.infoTimers.get(url);\n        if (t) clearTimeout(t);\n        this.infoTimers.delete(url);\n        // done\n      } catch {\n        console.log(\"Error fetching HTTP info for mint\", url);\n        // Immediately persist error state (do not wait for timer)\n        try {\n          const existing = await (this.db as MintReviewsDB).httpInfo.get(url);\n          const nowSec = Math.floor(Date.now() / 1000);\n          // Failure: keep existing info/fetchedAt if present, only set error\n          const row: HttpInfoRow = {\n            url,\n            info: existing?.info ?? null,\n            fetchedAt: existing?.fetchedAt ?? nowSec,\n            error: true,\n          };\n          await (this.db as MintReviewsDB).httpInfo.put(row);\n        } catch {}\n        const t = this.infoTimers.get(url);\n        if (t) clearTimeout(t);\n        this.infoTimers.delete(url);\n        await this.rebuildAggregates();\n      } finally {\n        this.inflightInfo.delete(url);\n      }\n    },\n    scheduleHttpInfoFetches: async function (\n      urls: string[],\n      concurrency: number = 20,\n      delayMs: number = 100,\n      timeoutMs?: number\n    ) {\n      try {\n        await this.ensureDbInitialized();\n        const nowSec = Math.floor(Date.now() / 1000);\n        const interval = this.httpInfoFetchIntervalSeconds || 0;\n        const seen = new Set<string>();\n        const toFetch: string[] = [];\n        for (const u of urls) {\n          if (typeof u !== \"string\" || !u.startsWith(\"http\")) continue;\n          if (seen.has(u)) continue;\n          seen.add(u);\n          // Skip if a fetch is already in-flight for this URL\n          if (this.inflightInfo.has(u)) continue;\n          const existing = await (this.db as MintReviewsDB).httpInfo.get(u);\n          const fresh =\n            !!existing &&\n            !!existing.fetchedAt &&\n            nowSec - existing.fetchedAt < interval;\n          if (!fresh) toFetch.push(u);\n        }\n        if (!toFetch.length) return;\n        let idx = 0;\n        const delay = (ms: number) => new Promise((res) => setTimeout(res, ms));\n        const worker = async () => {\n          while (true) {\n            const i = idx++;\n            if (i >= toFetch.length) break;\n            const u = toFetch[i];\n            try {\n              await this.requestMintHttpInfo(u, timeoutMs);\n            } catch {}\n            await delay(delayMs);\n          }\n        };\n        const workers = Array.from(\n          { length: Math.min(concurrency, toFetch.length) },\n          () => worker()\n        );\n        await Promise.all(workers);\n      } catch {}\n    },\n    refetchStaleHttpInfoForKnownMints: async function () {\n      try {\n        await this.ensureDbInitialized();\n        const urls = this.recommendations.map((r) => r.url);\n        await this.scheduleHttpInfoFetches(urls, 10, 100);\n      } catch {}\n    },\n    // Expose getters for HTTP info from in-memory cache\n    getHttpInfoForUrl: function (url: string): any | undefined {\n      return this.httpInfoByUrl.get(url);\n    },\n    hasHttpInfo: function (url: string): boolean {\n      return this.httpInfoByUrl.has(url);\n    },\n    handleMintInfoEvent: async function (ev: NDKEvent) {\n      try {\n        if (ev.kind !== 38172) return;\n        const u = ev.tags.find(\n          (t) => t[0] === \"u\" && (t[2] === \"cashu\" || t.length >= 2)\n        )?.[1];\n        if (!u || typeof u !== \"string\" || !u.startsWith(\"http\")) return;\n        let content: any = undefined;\n        try {\n          content = ev.content ? JSON.parse(ev.content) : undefined;\n        } catch {}\n        const row: InfoRow = {\n          url: u,\n          pubkey: ev.pubkey,\n          d: ev.tagValue(\"d\") || \"\",\n          content,\n          created_at: ev.created_at || 0,\n        };\n        await (this.db as MintReviewsDB).infos.add(row);\n      } catch {}\n    },\n    upsertReviewForUrl: async function (url: string, review: MintReview) {\n      if (!url || !url.startsWith(\"http\")) return;\n      const list = this.urlReviews.get(url) || [];\n      if (list.some((r) => r.eventId === review.eventId)) return;\n      const withoutSameAuthor = list.filter((r) => r.pubkey !== review.pubkey);\n      withoutSameAuthor.push(review);\n      withoutSameAuthor.sort(\n        (a, b) => (a.created_at || 0) - (b.created_at || 0)\n      );\n      this.urlReviews.set(url, withoutSameAuthor);\n      await this.persistReviewRow(url, review);\n    },\n    handleReviewEvent: async function (ev: NDKEvent) {\n      try {\n        if (ev.kind !== 38000) return;\n        const kTag = ev.tags.find((t) => t[0] === \"k\");\n        if (!kTag || kTag[1] !== \"38172\") return;\n        const uTags = ev.tags.filter(\n          (t) => t[0] === \"u\" && (t[2] === \"cashu\" || t.length >= 2)\n        );\n        if (!uTags.length) return;\n        const { rating, comment } = parseRatingAndComment(ev.content || \"\");\n        const review: MintReview = {\n          eventId: ev.id,\n          pubkey: ev.pubkey,\n          created_at: ev.created_at || 0,\n          rating,\n          comment,\n          raw: ev.rawEvent(),\n        };\n        for (const u of uTags) {\n          const url = u[1];\n          if (typeof url === \"string\" && url.startsWith(\"http\")) {\n            await this.upsertReviewForUrl(url, review);\n          }\n        }\n      } catch {}\n    },\n    rebuildAggregates: async function () {\n      try {\n        await this.ensureDbInitialized();\n        const [reviews, infos, httpRows] = await Promise.all([\n          (this.db as MintReviewsDB).reviews.toArray(),\n          (this.db as MintReviewsDB).infos.toArray(),\n          (this.db as MintReviewsDB).httpInfo.toArray(),\n        ]);\n        const urlSet = new Set<string>();\n        reviews.forEach((r) => r.url && urlSet.add(r.url));\n        infos.forEach((i) => i.url && urlSet.add(i.url));\n        httpRows.forEach((h) => h.url && urlSet.add(h.url));\n\n        // Group reviews by URL\n        const grouped = new Map<string, ReviewRow[]>();\n        for (const r of reviews) {\n          if (!r.url) continue;\n          const list = grouped.get(r.url) || [];\n          list.push(r);\n          grouped.set(r.url, list);\n        }\n\n        const httpByUrl = new Map<string, HttpInfoRow>();\n        // Refresh in-memory cache from Dexie and build quick lookups\n        this.httpInfoByUrl.clear();\n        for (const h of httpRows) {\n          httpByUrl.set(h.url, h);\n          if (h.info) this.httpInfoByUrl.set(h.url, h.info);\n        }\n\n        const recs: MintRecommendation[] = [];\n        for (const url of urlSet) {\n          const list = grouped.get(url) || [];\n          const ratings = list\n            .map((r) => r.rating)\n            .filter((n): n is number => typeof n === \"number\");\n          const avg = ratings.length\n            ? ratings.reduce((a, b) => a + b, 0) / ratings.length\n            : null;\n          const http = httpByUrl.get(url);\n          recs.push({\n            url,\n            reviewsCount: list.length,\n            averageRating: avg,\n            reviews: [],\n            info: http?.info ?? undefined,\n            error: !!http?.error || false,\n            lastHttpInfoFetchAt: http?.fetchedAt ?? undefined,\n          });\n        }\n        recs.sort(\n          (a, b) =>\n            b.reviewsCount - a.reviewsCount ||\n            (b.averageRating || 0) - (a.averageRating || 0)\n        );\n        this.recommendations = recs;\n      } catch {}\n    },\n    persistReviewRow: async function (url: string, review: MintReview) {\n      try {\n        await this.ensureDbInitialized();\n        const row: ReviewRow = {\n          eventId: review.eventId,\n          url,\n          pubkey: review.pubkey,\n          created_at: review.created_at,\n          rating: review.rating,\n          comment: review.comment,\n          raw: review.raw,\n        };\n        await (this.db as MintReviewsDB).reviews.put(row);\n      } catch {}\n    },\n    getReviewsForUrl: async function (url: string): Promise<MintReview[]> {\n      try {\n        await this.ensureDbInitialized();\n        if (!url) return [];\n        const rows = await (this.db as MintReviewsDB).reviews\n          .where(\"url\")\n          .equals(url)\n          .sortBy(\"created_at\");\n        const list = rows.map(\n          (r) =>\n            ({\n              eventId: r.eventId,\n              pubkey: r.pubkey,\n              created_at: r.created_at,\n              rating: r.rating,\n              comment: r.comment,\n              raw: r.raw,\n            } as MintReview)\n        );\n        list.sort((a, b) => (a.created_at || 0) - (b.created_at || 0));\n        this.urlReviews.set(url, list);\n        await this.rebuildAggregates();\n        return list;\n      } catch {\n        return [];\n      }\n    },\n    getAverageForUrl: function (url: string): number | null {\n      const rec = this.recommendations.find((r) => r.url === url);\n      return rec ? rec.averageRating : null;\n    },\n    getCountForUrl: function (url: string): number {\n      const rec = this.recommendations.find((r) => r.url === url);\n      return rec ? rec.reviewsCount : 0;\n    },\n    clearAllDatabases: async function () {\n      try {\n        await this.ensureDbInitialized();\n        await Promise.all([\n          (this.db as MintReviewsDB).reviews.clear(),\n          (this.db as MintReviewsDB).infos.clear(),\n          (this.db as MintReviewsDB).httpInfo.clear(),\n        ]);\n      } catch {}\n      this.urlReviews.clear();\n      this.dbHydrated = false;\n      await this.rebuildAggregates();\n    },\n  },\n});\n\n// Dexie DB\nclass MintReviewsDB extends Dexie {\n  reviews!: Dexie.Table<ReviewRow, string>;\n  infos!: Dexie.Table<InfoRow, number>;\n  httpInfo!: Dexie.Table<HttpInfoRow, string>;\n  constructor() {\n    super(\"mintReviews\");\n    this.version(1).stores({\n      reviews: \"eventId, url, created_at\",\n    });\n    this.version(2).stores({\n      infos: \"++id, url, created_at\",\n      httpInfo: \"url\",\n    });\n  }\n}\n\ntype ReviewRow = {\n  eventId: string;\n  url: string;\n  pubkey: string;\n  created_at: number;\n  rating: number | null;\n  comment: string;\n  raw: any;\n};\n\ntype InfoRow = {\n  url: string;\n  pubkey: string;\n  d: string;\n  content?: any;\n  created_at: number;\n};\n\ntype HttpInfoRow = {\n  url: string;\n  info: any | null;\n  fetchedAt: number; // unix seconds\n  error?: boolean;\n};\n"
  },
  {
    "path": "src/stores/mints.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { useWorkersStore } from \"./workers\";\nimport { notifyError, notifySuccess } from \"src/js/notify\";\nimport {\n  Mint,\n  MintKeys,\n  Proof,\n  SerializedBlindedSignature,\n  MintKeyset,\n  GetInfoResponse,\n} from \"@cashu/cashu-ts\";\nimport { useUiStore } from \"./ui\";\nimport { ref, watch } from \"vue\";\nimport { useProofsStore } from \"./proofs\";\nimport { i18n } from \"src/boot/i18n\";\nimport { useSettingsStore } from \"./settings\";\nimport { useNostrMintBackupStore } from \"./nostrMintBackup\";\nimport { bytesToHex } from \"@noble/hashes/utils\"; // already an installed dependency\n\nexport type StoredMint = {\n  url: string;\n  keys: MintKeys[];\n  keysets: MintKeyset[];\n  nickname?: string;\n  info?: GetInfoResponse;\n  errored?: boolean;\n  motdDismissed?: boolean;\n  multinutSelected?: boolean;\n  lastInfoUpdated?: string;\n  lastKeysetsUpdated?: string;\n  // initialize api: new Mint(url) on activation\n};\n\nexport class MintClass {\n  mint: StoredMint;\n  constructor(mint: StoredMint) {\n    this.mint = mint;\n  }\n  get api() {\n    return new Mint(this.mint.url);\n  }\n  get proofs() {\n    const proofsStore = useProofsStore();\n    return proofsStore.proofs.filter((p) =>\n      this.mint.keysets.map((k) => k.id).includes(p.id)\n    );\n  }\n  get allBalances() {\n    // return an object with all balances for each unit\n    const balances: Record<string, number> = {};\n    this.units.forEach((unit) => {\n      balances[unit] = this.unitBalance(unit);\n    });\n    return balances;\n  }\n\n  get keysets() {\n    return this.mint.keysets.filter((k) => k.active);\n  }\n\n  get units() {\n    return this.mint.keysets\n      .map((k) => k.unit)\n      .filter((value, index, self) => self.indexOf(value) === index);\n  }\n\n  unitKeysets(unit: string): MintKeyset[] {\n    return this.mint.keysets.filter((k) => k.unit === unit);\n  }\n\n  unitProofs(unit: string): WalletProof[] {\n    const proofsStore = useProofsStore();\n    const unitKeysets = this.unitKeysets(unit);\n    return proofsStore.proofs.filter(\n      (p) => unitKeysets.map((k) => k.id).includes(p.id) && !p.reserved\n    );\n  }\n\n  unitBalance(unit: string) {\n    const proofs = this.unitProofs(unit);\n    return proofs.reduce((sum, p) => sum + p.amount, 0);\n  }\n}\n\n// type that extends type Proof with reserved boolean\nexport type WalletProof = Proof & { reserved: boolean; quote?: string };\n\nexport type Balances = {\n  [unit: string]: number;\n};\n\ntype BlindSignatureAudit = {\n  signature: SerializedBlindedSignature;\n  amount: number;\n  secret: Uint8Array;\n  id: string;\n  r: string;\n};\n\nexport const useMintsStore = defineStore(\"mints\", {\n  state: () => {\n    const t = i18n.global.t;\n    const activeProofs = ref<WalletProof[]>([]);\n    const activeUnit = useLocalStorage<string>(\"cashu.activeUnit\", \"sat\");\n    const activeMintUrl = useLocalStorage<string>(\"cashu.activeMintUrl\", \"\");\n    const addMintData = ref({\n      url: \"\",\n      nickname: \"\",\n    });\n    const mints = useLocalStorage(\"cashu.mints\", [] as StoredMint[]);\n    const showAddMintDialog = ref(false);\n    const addMintBlocking = ref(false);\n    const showRemoveMintDialog = ref(false);\n    const showMintInfoDialog = ref(false);\n    const showEditMintDialog = ref(false);\n\n    const uiStoreGlobal: any = useUiStore();\n    const settingsStoreGlobal: any = useSettingsStore();\n\n    // Watch for changes in activeMintUrl and activeUnit\n    watch([activeMintUrl, activeUnit], async () => {\n      const proofsStore = useProofsStore();\n      console.log(\n        `watcher: activeMintUrl: ${activeMintUrl.value}, activeUnit: ${activeUnit.value}`\n      );\n      await proofsStore.updateActiveProofs();\n    });\n\n    return {\n      t,\n      activeProofs,\n      activeUnit,\n      activeMintUrl,\n      addMintData,\n      mints,\n      showAddMintDialog,\n      addMintBlocking,\n      showRemoveMintDialog,\n      showMintInfoDialog,\n      showEditMintDialog,\n      uiStoreGlobal,\n      settingsStoreGlobal,\n    };\n  },\n  getters: {\n    multiMints({ activeUnit }) {\n      return this.mints.filter((m) => {\n        try {\n          const version = m.info?.version;\n          if (!version) return false;\n\n          const regex = /^(Nutshell)\\/(\\d+)\\.(\\d+)\\.(\\d+)/; // Regex to match \"Nutshell/version\"\n          const match = version.match(regex);\n          if (!match || match[1] !== \"Nutshell\") return false;\n          if (parseInt(match[2]) === 0 && parseInt(match[3]) < 17) return false; // If < 0.17.* then not viable\n\n          const nut15 = m.info?.nuts[15];\n          const viableMint = nut15?.methods.find(\n            (m) => m.method === \"bolt11\" && m.unit === activeUnit\n          );\n          const balance = new MintClass(m).unitBalance(activeUnit);\n          if (nut15 && viableMint && balance > 0) return true;\n          else return false;\n        } catch (e) {\n          console.error(`${e}`);\n          return false;\n        }\n      });\n    },\n    totalUnitBalance({ activeUnit }): number {\n      const proofsStore = useProofsStore();\n      const allUnitKeysets = this.mints\n        .map((m) => m.keysets)\n        .flat()\n        .filter((k) => k.unit === activeUnit);\n      const balance = proofsStore.proofs\n        .filter((p) => allUnitKeysets.map((k) => k.id).includes(p.id))\n        .filter((p) => !p.reserved)\n        .reduce((sum, p) => sum + p.amount, 0);\n      this.uiStoreGlobal.lastBalanceCached = balance;\n      return balance;\n    },\n    activeBalance(): number {\n      return this.activeProofs\n        .flat()\n        .reduce((sum, el) => (sum += el.amount), 0);\n    },\n    activeKeysets({ activeMintUrl, activeUnit }): MintKeyset[] {\n      const unitKeysets = this.mints\n        .find((m) => m.url === activeMintUrl)\n        ?.keysets?.filter((k) => k.unit === activeUnit);\n      if (!unitKeysets) {\n        return [];\n      }\n      return unitKeysets;\n    },\n    activeKeys({ activeMintUrl, activeUnit }): MintKeys[] {\n      const unitKeys = this.mints\n        .find((m) => m.url === activeMintUrl)\n        ?.keys?.filter((k) => k.unit === activeUnit);\n      if (!unitKeys) {\n        return [];\n      }\n      return unitKeys;\n    },\n    activeInfo({ activeMintUrl }): GetInfoResponse {\n      return (\n        this.mints.find((m) => m.url === activeMintUrl)?.info ||\n        ({} as GetInfoResponse)\n      );\n    },\n    activeUnitLabel({ activeUnit }): string {\n      if (activeUnit == \"sat\") {\n        if (this.settingsStoreGlobal.bip177BitcoinSymbol) {\n          return \"₿\";\n        } else {\n          return \"SAT\";\n        }\n      } else if (activeUnit == \"usd\") {\n        return \"USD\";\n      } else if (activeUnit == \"eur\") {\n        return \"EUR\";\n      } else if (activeUnit == \"msat\") {\n        return \"mSAT\";\n      } else {\n        return activeUnit;\n      }\n    },\n    activeUnitCurrencyMultiplyer({ activeUnit }): number {\n      if (activeUnit == \"usd\") {\n        return 100;\n      } else if (activeUnit == \"eur\") {\n        return 100;\n      } else {\n        return 1;\n      }\n    },\n    allMintKeysets: function () {\n      return [].concat(...this.mints.map((m) => m.keysets));\n    },\n  },\n  actions: {\n    activeMint() {\n      const mint = this.mints.find((m) => m.url === this.activeMintUrl);\n      if (mint) {\n        return new MintClass(mint);\n      } else {\n        if (this.mints.length) {\n          console.error(\n            \"No active mint. This should not happen. switching to first one.\"\n          );\n          this.activateMintUrl(this.mints[0].url, false, true);\n          return new MintClass(this.mints[0]);\n        }\n        throw new Error(\"No active mint\");\n      }\n    },\n    mintUnitProofs(mint: StoredMint, unit: string): WalletProof[] {\n      const proofsStore = useProofsStore();\n      const unitKeysets = mint.keysets.filter((k) => k.unit === unit);\n      return proofsStore.proofs.filter(\n        (p) => unitKeysets.map((k) => k.id).includes(p.id) && !p.reserved\n      );\n    },\n    mintUnitKeysets(mint: StoredMint, unit: string): MintKeyset[] {\n      return mint.keysets.filter((k) => k.unit === unit);\n    },\n    toggleUnit: function () {\n      const units = this.activeMint().units;\n      this.activeUnit =\n        units[(units.indexOf(this.activeUnit) + 1) % units.length];\n      return this.activeUnit;\n    },\n    toggleActiveUnitForMint(mint: StoredMint) {\n      // method to set the active unit to one that is supported by `mint`\n      const mintClass = new MintClass(mint);\n      if (\n        !this.activeUnit ||\n        mintClass.allBalances[this.activeUnit] == undefined\n      ) {\n        this.activeUnit = mintClass.units[0];\n      }\n    },\n    updateMint(oldMint: StoredMint, newMint: StoredMint) {\n      const index = this.mints.findIndex((m) => m.url === oldMint.url);\n      this.mints[index] = newMint;\n    },\n    updateMintMultinutSelection(mintUrl: string, selected: boolean) {\n      const mint = this.mints.find((m) => m.url === mintUrl);\n      if (mint) {\n        mint.multinutSelected = selected;\n      }\n    },\n    getKeysForKeyset: async function (keyset_id: string): Promise<MintKeys> {\n      const mint = this.mints.find((m) => m.url === this.activeMintUrl);\n      if (mint) {\n        const keys = mint.keys?.find((k) => k.id === keyset_id);\n        if (keys) {\n          return keys;\n        } else {\n          throw new Error(\"Keys not found\");\n        }\n      } else {\n        throw new Error(\"Mint not found\");\n      }\n    },\n    addMint: async function (\n      addMintData: { url: string; nickname?: string },\n      verbose = false\n    ): Promise<StoredMint> {\n      let url = addMintData.url;\n      this.addMintBlocking = true;\n      try {\n        // sanitize url\n        const sanitizeUrl = (url: string): string => {\n          let cleanedUrl = url.trim().replace(/\\/+$/, \"\");\n          if (!/^[a-z]+:\\/\\//.test(cleanedUrl)) {\n            // Check for any protocol followed by \"://\"\n            cleanedUrl = \"https://\" + cleanedUrl;\n          }\n          return cleanedUrl;\n        };\n        url = sanitizeUrl(url);\n\n        const mintToAdd: StoredMint = {\n          url: url,\n          keys: [],\n          keysets: [],\n          nickname: addMintData.nickname,\n        };\n\n        // we have no mints at all\n        if (this.mints.length === 0) {\n          this.mints = [mintToAdd];\n        } else if (this.mints.filter((m) => m.url === url).length === 0) {\n          // we don't have this mint yet\n          // add mint to this.mints so it can be activated in\n          this.mints.push(mintToAdd);\n        } else {\n          // we already have this mint\n          if (verbose) {\n            notifySuccess(this.t(\"wallet.mint.notifications.already_added\"));\n          }\n          return mintToAdd;\n        }\n        await this.activateMint(mintToAdd, false, true);\n        if (verbose) {\n          await notifySuccess(this.t(\"wallet.mint.notifications.added\"));\n        }\n\n        // Trigger Nostr backup if enabled\n        this.triggerNostrBackup();\n\n        return mintToAdd;\n      } catch (error) {\n        // activation failed, we remove the mint again from local storage\n        this.mints = this.mints.filter((m) => m.url !== url);\n        throw error;\n      } finally {\n        this.showAddMintDialog = false;\n        this.addMintBlocking = false;\n      }\n    },\n    activateMintUrl: async function (\n      url: string,\n      verbose = false,\n      force = false,\n      unit: string | undefined = undefined\n    ) {\n      const mint = this.mints.filter((m) => m.url === url)[0];\n      if (mint) {\n        await this.activateMint(mint, verbose, force);\n        if (unit) {\n          await this.activateUnit(unit, verbose);\n        }\n      } else {\n        notifyError(\n          this.t(\"wallet.mint.notifications.not_found\"),\n          this.t(\"wallet.mint.notifications.activation_failed\")\n        );\n      }\n    },\n    activateUnit: async function (unit: string, verbose = false) {\n      if (unit === this.activeUnit) {\n        return;\n      }\n      const uIStore = useUiStore();\n      await uIStore.lockMutex();\n      const mint = this.mints.find((m) => m.url === this.activeMintUrl);\n      if (!mint) {\n        notifyError(\n          this.t(\"wallet.mint.notifications.no_active_mint\"),\n          this.t(\"wallet.mint.notifications.unit_activation_failed\")\n        );\n        return;\n      }\n      const mintClass = new MintClass(mint);\n      if (mintClass.units.includes(unit)) {\n        this.activeUnit = unit;\n      } else {\n        notifyError(\n          this.t(\"wallet.mint.notifications.unit_not_supported\"),\n          this.t(\"wallet.mint.notifications.unit_activation_failed\")\n        );\n      }\n      await uIStore.unlockMutex();\n      const worker = useWorkersStore();\n      worker.clearAllWorkers();\n    },\n    updateMintInfoAndKeys: async function (mint: StoredMint) {\n      const newMintInfo = await this.fetchMintInfo(mint);\n      this.triggerMintInfoMotdChanged(newMintInfo, mint);\n      mint = await this.fetchMintKeys(mint);\n\n      const mintToUpdate = this.mints.filter((m) => m.url === mint.url)[0];\n      mintToUpdate.errored = false;\n      return mint;\n    },\n    activateMint: async function (\n      mint: StoredMint,\n      verbose = false,\n      force = false\n    ) {\n      if (mint.url === this.activeMintUrl && !force) {\n        return;\n      }\n      const workers = useWorkersStore();\n      const uIStore = useUiStore();\n      // we need to stop workers because they will reset the activeMint again\n      workers.clearAllWorkers();\n\n      // create new mint.api instance because we can't store it in local storage\n      const previousUrl = this.activeMintUrl;\n      await uIStore.lockMutex();\n      try {\n        mint = await this.updateMintInfoAndKeys(mint);\n        this.toggleActiveUnitForMint(mint);\n        if (verbose) {\n          await notifySuccess(this.t(\"wallet.mint.notifications.activated\"));\n        }\n        this.activeMintUrl = mint.url;\n        console.log(\"### activateMint: Mint activated: \", this.activeMintUrl);\n      } catch (error: any) {\n        // restore previous values because the activation errored\n        // this.activeMintUrl = previousUrl;\n        let err_msg = this.t(\"wallet.mint.notifications.could_not_connect\");\n        if (error.message.length) {\n          err_msg = err_msg + ` ${error.message}.`;\n        }\n        await notifyError(\n          err_msg,\n          this.t(\"wallet.mint.notifications.activation_failed\")\n        );\n        this.mints.filter((m) => m.url === mint.url)[0].errored = true;\n        throw error;\n      } finally {\n        await uIStore.unlockMutex();\n      }\n    },\n    checkMintInfoMotdChanged(newMintInfo: GetInfoResponse, mint: StoredMint) {\n      // if mint doesn't have info yet, we don't need to trigger the motd change\n      if (!this.mints.find((m) => m.url === mint.url)?.info) {\n        return false;\n      }\n      const motd = newMintInfo.motd;\n      if (motd !== this.mints.filter((m) => m.url === mint.url)[0].info?.motd) {\n        return true;\n      }\n      return false;\n    },\n    triggerMintInfoMotdChanged(\n      newMintInfo: GetInfoResponse,\n      mint: StoredMint,\n      navigate = true\n    ) {\n      if (!this.checkMintInfoMotdChanged(newMintInfo, mint)) {\n        return;\n      }\n      // set motd_viewed to false\n      this.mints.filter((m) => m.url === mint.url)[0].motdDismissed = false;\n\n      // Navigate to mint details page with mint URL as query parameter\n      if (navigate) {\n        window.location.href = `/mintdetails?mintUrl=${encodeURIComponent(\n          mint.url\n        )}`;\n      }\n    },\n    fetchMintInfo: async function (mint: StoredMint) {\n      try {\n        const mintClass = new MintClass(mint);\n        const data = await mintClass.api.getInfo();\n\n        // if we have this mint in localstorage, update it\n        const storedMint = this.mints.find((m) => m.url === mint.url);\n        if (storedMint) {\n          storedMint.info = data;\n          storedMint.lastInfoUpdated = new Date().toISOString();\n        }\n        return data;\n      } catch (error: any) {\n        console.error(error);\n        try {\n          // notifyApiError(error, this.t(\"wallet.mint.notifications.could_not_get_info\"));\n        } catch {}\n        throw error;\n      }\n    },\n    checkForMintKeysetIdCollisions: async function (\n      mintToAdd: StoredMint,\n      keysets: MintKeyset[]\n    ) {\n      // check if there are any keysets with the same id in another mint\n      const allKeysets = this.mints\n        .filter((m) => m.url !== mintToAdd.url) // exclude the mint we are adding\n        .map((m) => m.keysets)\n        .flat();\n      const collisions = keysets.filter((k) =>\n        allKeysets.map((k) => k.id).includes(k.id)\n      );\n      // perform the same check for the integer representation of the keyset id\n      function keysetIdToBigInt(id: string): bigint {\n        if (/^[0-9a-fA-F]+$/.test(id)) {\n          return BigInt(`0x${id}`) % BigInt(2 ** 31 - 1);\n        } else {\n          const bin = atob(id);\n          const hex = bytesToHex(Uint8Array.from(bin, (c) => c.charCodeAt(0)));\n          return BigInt(`0x${hex}`) % BigInt(2 ** 31 - 1);\n        }\n      }\n      const allKeysetsIdsBigInt = allKeysets.map((k) => keysetIdToBigInt(k.id));\n      const hasCollisions = keysets.some((k) =>\n        allKeysetsIdsBigInt.includes(keysetIdToBigInt(k.id))\n      );\n      if (hasCollisions) {\n        const errorMessage = this.t(\n          \"wallet.mint.notifications.mint_validation_error\"\n        );\n        throw new Error(errorMessage);\n      }\n      return true;\n    },\n    fetchMintKeys: async function (mint: StoredMint): Promise<StoredMint> {\n      try {\n        const mintClass = new MintClass(mint);\n        const keysets = await this.fetchMintKeysets(mint);\n        // if we do not have any keys yet, fetch them\n        if (mint.keys.length === 0 || mint.keys.length == undefined) {\n          const keys = await mintClass.api.getKeys();\n          // store keys in mint and update local storage\n          this.mints.filter((m) => m.url === mint.url)[0].keys = keys.keysets;\n        }\n        // reload mint from local storage\n        mint = this.mints.filter((m) => m.url === mint.url)[0];\n\n        // for each keyset we do not have keys for, fetch keys\n        for (const keyset of keysets) {\n          if (!mint.keys.find((k) => k.id === keyset.id)) {\n            const keys = await mintClass.api.getKeys(keyset.id);\n            // store keys in mint and update local storage\n            this.mints\n              .filter((m) => m.url === mint.url)[0]\n              .keys.push(keys.keysets[0]);\n          }\n        }\n\n        this.mints.filter((m) => m.url === mint.url)[0].lastKeysetsUpdated =\n          new Date().toISOString();\n        // return the mint with keys set\n        return this.mints.filter((m) => m.url === mint.url)[0];\n      } catch (error: any) {\n        console.error(error);\n        try {\n          // notifyApiError(error, this.t(\"wallet.mint.notifications.could_not_get_keys\"));\n        } catch {}\n        throw error;\n      }\n    },\n    fetchMintKeysets: async function (mint: StoredMint) {\n      // fetches and stores keysets for a mint\n      try {\n        const mintClass = new MintClass(mint);\n        const data = await mintClass.api.getKeySets();\n        const keysets = data.keysets;\n        if (keysets.length > 0) {\n          // check for keyset id collisions with other mints\n          await this.checkForMintKeysetIdCollisions(mint, keysets);\n          // store keysets in mint and update local storage\n          // merge new keysets with existing ones instead of overwriting\n          const storedMint = this.mints.find((m) => m.url === mint.url);\n          if (storedMint) {\n            const existingKeysets = storedMint.keysets || [];\n            const mergedKeysets = [...existingKeysets];\n\n            // Add or update keysets\n            for (const newKeyset of keysets) {\n              const existingIndex = mergedKeysets.findIndex(\n                (k) => k.id === newKeyset.id\n              );\n              if (existingIndex !== -1) {\n                // Update existing keyset\n                mergedKeysets[existingIndex] = newKeyset;\n              } else {\n                // Add new keyset\n                mergedKeysets.push(newKeyset);\n              }\n            }\n\n            storedMint.keysets = mergedKeysets;\n          }\n        }\n        return keysets;\n      } catch (error: any) {\n        console.error(error);\n        throw error;\n      }\n    },\n    removeMint: async function (url: string) {\n      this.mints = this.mints.filter((m) => m.url !== url);\n      if (url === this.activeMintUrl) {\n        this.activeMintUrl = \"\";\n      }\n      // todo: we always reset to the first mint, improve this\n      if (this.mints.length > 0) {\n        await this.activateMint(this.mints[0], false);\n      }\n      notifySuccess(this.t(\"wallet.mint.notifications.removed\"));\n\n      // Trigger Nostr backup if enabled\n      this.triggerNostrBackup();\n    },\n    assertMintError: function (response: { error?: any }, verbose = true) {\n      if (response.error != null) {\n        if (verbose) {\n          notifyError(\n            response.error,\n            this.t(\"wallet.mint.notifications.error\")\n          );\n        }\n        throw new Error(`Mint error: ${response.error}`);\n      }\n    },\n\n    // Trigger Nostr backup when mints change\n    triggerNostrBackup: async function () {\n      try {\n        const nostrMintBackupStore = useNostrMintBackupStore();\n\n        if (nostrMintBackupStore.enabled && nostrMintBackupStore.needsBackup) {\n          setTimeout(async () => {\n            try {\n              await nostrMintBackupStore.backupMintsToNostr();\n            } catch (error) {\n              console.error(\"Failed to backup mints to Nostr:\", error);\n            }\n          }, 1000);\n        }\n      } catch (error) {\n        console.error(\"Failed to trigger Nostr backup:\", error);\n      }\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/nostr.ts",
    "content": "import { defineStore } from \"pinia\";\nimport NDK, {\n  NDKEvent,\n  NDKSigner,\n  NDKNip07Signer,\n  NDKNip46Signer,\n  NDKFilter,\n  NDKPrivateKeySigner,\n  NostrEvent,\n  NDKKind,\n  NDKRelaySet,\n  NDKRelay,\n  NDKTag,\n  ProfilePointer,\n} from \"@nostr-dev-kit/ndk\";\nimport { nip04, nip19, nip44 } from \"nostr-tools\";\nimport { bytesToHex, hexToBytes } from \"@noble/hashes/utils\"; // already an installed dependency\nimport { useWalletStore } from \"./wallet\";\nimport { generateSecretKey, getPublicKey } from \"nostr-tools\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { useSettingsStore } from \"./settings\";\nimport { useReceiveTokensStore } from \"./receiveTokensStore\";\nimport {\n  getEncodedTokenV4,\n  PaymentRequestPayload,\n  Token,\n} from \"@cashu/cashu-ts\";\nimport { useTokensStore } from \"./tokens\";\nimport {\n  notifyApiError,\n  notifyError,\n  notifySuccess,\n  notifyWarning,\n  notify,\n} from \"../js/notify\";\nimport { useSendTokensStore } from \"./sendTokensStore\";\nimport { usePRStore } from \"./payment-request\";\nimport token from \"../js/token\";\nimport { HistoryToken } from \"./tokens\";\n\ntype NostrEventLog = {\n  id: string;\n  created_at: number;\n};\n\nexport enum SignerType {\n  NIP07 = \"NIP07\",\n  NIP46 = \"NIP46\",\n  PRIVATEKEY = \"PRIVATEKEY\",\n  SEED = \"SEED\",\n}\n\nexport const useNostrStore = defineStore(\"nostr\", {\n  state: () => ({\n    connected: false,\n    pubkey: useLocalStorage<string>(\"cashu.ndk.pubkey\", \"\"),\n    relays: useLocalStorage<string[]>(\n      \"cashu.nostr.relays\",\n      useSettingsStore().defaultNostrRelays\n    ),\n    ndk: {} as NDK,\n    signerType: useLocalStorage<SignerType>(\n      \"cashu.ndk.signerType\",\n      SignerType.SEED\n    ),\n    nip07signer: {} as NDKNip07Signer,\n    nip46Token: useLocalStorage<string>(\"cashu.ndk.nip46Token\", \"\"),\n    nip46signer: {} as NDKNip46Signer,\n    privateKeySignerPrivateKey: useLocalStorage<string>(\n      \"cashu.ndk.privateKeySignerPrivateKey\",\n      \"\"\n    ),\n    seedSignerPrivateKey: useLocalStorage<string>(\n      \"cashu.ndk.seedSignerPrivateKey\",\n      \"\"\n    ),\n    seedSignerPublicKey: useLocalStorage<string>(\n      \"cashu.ndk.seedSignerPublicKey\",\n      \"\"\n    ),\n    seedSigner: {} as NDKPrivateKeySigner,\n    seedSignerPrivateKeyNsec: \"\",\n    privateKeySigner: {} as NDKPrivateKeySigner,\n    signer: {} as NDKSigner,\n    initialized: false,\n    lastEventTimestamp: useLocalStorage<number>(\n      \"cashu.ndk.lastEventTimestamp\",\n      0\n    ),\n    nip17EventIdsWeHaveSeen: useLocalStorage<NostrEventLog[]>(\n      \"cashu.ndk.nip17EventIdsWeHaveSeen\",\n      []\n    ),\n  }),\n  getters: {\n    seedSignerPrivateKeyNsec: (state) => {\n      const sk = hexToBytes(state.seedSignerPrivateKey);\n      return nip19.nsecEncode(sk);\n    },\n    nprofile: (state) => {\n      const profile: ProfilePointer = {\n        pubkey: state.pubkey,\n        relays: state.relays,\n      };\n      return nip19.nprofileEncode(profile);\n    },\n    seedSignerNprofile: (state) => {\n      const profile: ProfilePointer = {\n        pubkey: state.seedSignerPublicKey,\n        relays: state.relays,\n      };\n      return nip19.nprofileEncode(profile);\n    },\n  },\n  actions: {\n    initNdkReadOnly: function () {\n      this.ndk = new NDK({ explicitRelayUrls: this.relays });\n      this.ndk.connect();\n      this.connected = true;\n    },\n    initSignerIfNotSet: async function () {\n      if (!this.initialized) {\n        await this.initSigner();\n      }\n    },\n    initSigner: async function () {\n      if (this.signerType === SignerType.NIP07) {\n        await this.initNip07Signer();\n      } else if (this.signerType === SignerType.NIP46) {\n        await this.initNip46Signer();\n      } else if (this.signerType === SignerType.PRIVATEKEY) {\n        await this.initPrivateKeySigner();\n      } else {\n        await this.initWalletSeedPrivateKeySigner();\n      }\n      this.initialized = true;\n    },\n    setSigner: function (signer: NDKSigner) {\n      this.signer = signer;\n      this.ndk = new NDK({ signer: signer, explicitRelayUrls: this.relays });\n    },\n    signDummyEvent: async function (): Promise<NDKEvent> {\n      const ndkEvent = new NDKEvent();\n      ndkEvent.kind = 1;\n      ndkEvent.content = \"Hello, world!\";\n      const sig = await ndkEvent.sign(this.signer);\n      console.log(`nostr signature: ${sig})`);\n      const eventString = JSON.stringify(ndkEvent.rawEvent());\n      console.log(`nostr event: ${eventString}`);\n      return ndkEvent;\n    },\n    setPubkey: function (pubkey: string) {\n      console.log(\"Setting pubkey to\", pubkey);\n      this.pubkey = pubkey;\n    },\n    checkNip07Signer: async function (): Promise<boolean> {\n      const signer = new NDKNip07Signer();\n      try {\n        await signer.user();\n        return true;\n      } catch (e) {\n        return false;\n      }\n    },\n    initNip07Signer: async function () {\n      const signer = new NDKNip07Signer();\n      const user = await signer.blockUntilReady();\n      this.signerType = SignerType.NIP07;\n      this.setSigner(signer);\n      this.setPubkey(user.pubkey);\n    },\n    initNip46Signer: async function (nip46Token?: string) {\n      const ndk = new NDK({ explicitRelayUrls: this.relays });\n      if (!nip46Token && !this.nip46Token.length) {\n        nip46Token = (await prompt(\n          \"Enter your NIP-46 connection string\"\n        )) as string;\n        if (!nip46Token) {\n          return;\n        }\n        this.nip46Token = nip46Token;\n      } else {\n        if (nip46Token) {\n          this.nip46Token = nip46Token;\n        }\n      }\n      const signer = new NDKNip46Signer(ndk, this.nip46Token);\n      this.signerType = SignerType.NIP46;\n      this.setSigner(signer);\n      // If the backend sends an auth_url event, open that URL as a popup so the user can authorize the app\n      signer.on(\"authUrl\", (url) => {\n        window.open(url, \"auth\", \"width=600,height=600\");\n      });\n      // wait until the signer is ready\n      const loggedinUser = await signer.blockUntilReady();\n      alert(\"You are now logged in as \" + loggedinUser.npub);\n      this.setPubkey(loggedinUser.pubkey);\n    },\n    resetNip46Signer: async function () {\n      this.nip46Token = \"\";\n      await this.initWalletSeedPrivateKeySigner();\n    },\n    initPrivateKeySigner: async function (nsec?: string) {\n      let privateKeyBytes: Uint8Array;\n      if (!nsec && !this.privateKeySignerPrivateKey.length) {\n        nsec = (await prompt(\"Enter your nsec\")) as string;\n        if (!nsec) {\n          return;\n        }\n        privateKeyBytes = nip19.decode(nsec).data as Uint8Array;\n      } else {\n        if (nsec) {\n          privateKeyBytes = nip19.decode(nsec).data as Uint8Array;\n        } else {\n          privateKeyBytes = hexToBytes(this.privateKeySignerPrivateKey);\n        }\n      }\n      this.privateKeySigner = new NDKPrivateKeySigner(\n        this.privateKeySignerPrivateKey\n      );\n      this.privateKeySignerPrivateKey = bytesToHex(privateKeyBytes);\n      this.signerType = SignerType.PRIVATEKEY;\n      this.setSigner(this.privateKeySigner);\n      const publicKeyHex = getPublicKey(privateKeyBytes);\n      this.setPubkey(publicKeyHex);\n    },\n    resetPrivateKeySigner: async function () {\n      this.privateKeySignerPrivateKey = \"\";\n      await this.initWalletSeedPrivateKeySigner();\n    },\n    walletSeedGenerateKeyPair: async function () {\n      const walletStore = useWalletStore();\n      const sk = walletStore.seed.slice(0, 32);\n      const walletPublicKeyHex = getPublicKey(sk); // `pk` is a hex string\n      const walletPrivateKeyHex = bytesToHex(sk);\n      this.seedSignerPrivateKey = walletPrivateKeyHex;\n      this.seedSignerPublicKey = walletPublicKeyHex;\n      this.seedSigner = new NDKPrivateKeySigner(this.seedSignerPrivateKey);\n    },\n    initWalletSeedPrivateKeySigner: async function () {\n      await this.walletSeedGenerateKeyPair();\n      // TODO: remove duplicate privateKeysigner\n      this.privateKeySigner = this.seedSigner;\n      this.signerType = SignerType.SEED;\n      this.setSigner(this.privateKeySigner);\n      this.setPubkey(this.seedSignerPublicKey);\n    },\n    fetchEventsFromUser: async function () {\n      const filter: NDKFilter = { kinds: [1], authors: [this.pubkey] };\n      return await this.ndk.fetchEvents(filter);\n    },\n\n    sendNip04DirectMessage: async function (\n      recipient: string,\n      message: string\n    ) {\n      const randomPrivateKey = generateSecretKey();\n      const randomPublicKey = getPublicKey(randomPrivateKey);\n      // const randomPrivateKey = hexToBytes(this.seedSignerPrivateKey);\n      // const randomPublicKey = this.pubkey;\n      const ndk = new NDK({\n        explicitRelayUrls: this.relays,\n        signer: new NDKPrivateKeySigner(bytesToHex(randomPrivateKey)),\n      });\n      const event = new NDKEvent(ndk);\n      ndk.connect();\n      event.kind = NDKKind.EncryptedDirectMessage;\n      event.content = await nip04.encrypt(randomPrivateKey, recipient, message);\n      event.tags = [[\"p\", recipient]];\n      event.sign();\n      try {\n        await event.publish();\n        notifySuccess(\"NIP-04 event published\");\n      } catch (e) {\n        console.error(e);\n        notifyError(\"Could not publish NIP-04 event\");\n      }\n    },\n    subscribeToNip04DirectMessages: async function () {\n      await this.walletSeedGenerateKeyPair();\n      await this.initNdkReadOnly();\n      let nip04DirectMessageEvents: Set<NDKEvent> = new Set();\n      const fetchEventsPromise = new Promise<Set<NDKEvent>>((resolve) => {\n        if (!this.lastEventTimestamp) {\n          this.lastEventTimestamp = Math.floor(Date.now() / 1000);\n        }\n        console.log(\n          `### Subscribing to NIP-04 direct messages to ${this.seedSignerPublicKey} since ${this.lastEventTimestamp}`\n        );\n        this.ndk.connect();\n        const sub = this.ndk.subscribe(\n          {\n            kinds: [NDKKind.EncryptedDirectMessage],\n            \"#p\": [this.seedSignerPublicKey],\n            since: this.lastEventTimestamp,\n          } as NDKFilter,\n          { closeOnEose: false, groupable: false }\n        );\n        sub.on(\"event\", (event: NDKEvent) => {\n          console.log(\"event\");\n          nip04\n            .decrypt(\n              hexToBytes(this.seedSignerPrivateKey),\n              event.pubkey,\n              event.content\n            )\n            .then((content) => {\n              console.log(\"NIP-04 DM from\", event.pubkey);\n              console.log(\"Content:\", content);\n              nip04DirectMessageEvents.add(event);\n              this.lastEventTimestamp = Math.floor(Date.now() / 1000);\n              this.parseMessageForEcash(content);\n            });\n        });\n      });\n      try {\n        nip04DirectMessageEvents = await fetchEventsPromise;\n      } catch (error) {\n        console.error(\"Error fetching contact events:\", error);\n      }\n    },\n    sendNip17DirectMessageToNprofile: async function (\n      nprofile: string,\n      message: string\n    ) {\n      const result = nip19.decode(nprofile);\n      const pubkey: string = (result.data as ProfilePointer).pubkey;\n      const relays: string[] | undefined = (result.data as ProfilePointer)\n        .relays;\n      this.sendNip17DirectMessage(pubkey, message, relays);\n    },\n    randomTimeUpTo2DaysInThePast: function () {\n      return Math.floor(Date.now() / 1000) - Math.floor(Math.random() * 172800);\n    },\n    sendNip17DirectMessage: async function (\n      recipient: string,\n      message: string,\n      relays?: string[]\n    ) {\n      await this.walletSeedGenerateKeyPair();\n      const randomPrivateKey = generateSecretKey();\n      const randomPublicKey = getPublicKey(randomPrivateKey);\n\n      const dmEvent = new NDKEvent();\n      dmEvent.kind = 14;\n      dmEvent.content = message;\n      dmEvent.tags = [[\"p\", recipient]];\n      dmEvent.created_at = Math.floor(Date.now() / 1000);\n      dmEvent.pubkey = this.seedSignerPublicKey;\n      dmEvent.id = dmEvent.getEventHash();\n      const dmEventString = JSON.stringify(await dmEvent.toNostrEvent());\n\n      const seedNdk = new NDK({\n        signer: this.seedSigner,\n        explicitRelayUrls: this.relays,\n      });\n      const sealEvent = new NDKEvent(seedNdk);\n      sealEvent.kind = 13;\n      sealEvent.content = nip44.v2.encrypt(\n        dmEventString,\n        nip44.v2.utils.getConversationKey(this.seedSignerPrivateKey, recipient)\n      );\n      sealEvent.created_at = this.randomTimeUpTo2DaysInThePast();\n      sealEvent.pubkey = this.seedSignerPublicKey;\n      sealEvent.id = sealEvent.getEventHash();\n      sealEvent.sig = await sealEvent.sign();\n      const sealEventString = JSON.stringify(await sealEvent.toNostrEvent());\n\n      const randomNdk = new NDK({\n        explicitRelayUrls: relays ?? this.relays,\n        signer: new NDKPrivateKeySigner(bytesToHex(randomPrivateKey)),\n      });\n      const wrapEvent = new NDKEvent(randomNdk);\n      wrapEvent.kind = 1059;\n      wrapEvent.tags = [[\"p\", recipient]];\n      wrapEvent.content = nip44.v2.encrypt(\n        sealEventString,\n        nip44.v2.utils.getConversationKey(\n          bytesToHex(randomPrivateKey),\n          recipient\n        )\n      );\n      wrapEvent.created_at = this.randomTimeUpTo2DaysInThePast();\n      wrapEvent.pubkey = randomPublicKey;\n      wrapEvent.id = wrapEvent.getEventHash();\n      wrapEvent.sig = await wrapEvent.sign();\n\n      try {\n        randomNdk.connect();\n        await wrapEvent.publish();\n      } catch (e) {\n        console.error(e);\n        notifyError(\"Could not publish NIP-17 event\");\n      }\n    },\n    subscribeToNip17DirectMessages: async function () {\n      await this.walletSeedGenerateKeyPair();\n      await this.initNdkReadOnly();\n      let nip17DirectMessageEvents: Set<NDKEvent> = new Set();\n      const fetchEventsPromise = new Promise<Set<NDKEvent>>((resolve) => {\n        if (!this.lastEventTimestamp) {\n          this.lastEventTimestamp = Math.floor(Date.now() / 1000);\n        }\n        const since = this.lastEventTimestamp - 172800; // last 2 days\n        console.log(\n          `### Subscribing to NIP-17 direct messages to ${this.seedSignerPublicKey} since ${since}`\n        );\n        this.ndk.connect();\n        const sub = this.ndk.subscribe(\n          {\n            kinds: [1059 as NDKKind],\n            \"#p\": [this.seedSignerPublicKey],\n            since: since,\n          } as NDKFilter,\n          { closeOnEose: false, groupable: false }\n        );\n\n        sub.on(\"event\", (wrapEvent: NDKEvent) => {\n          const eventLog = {\n            id: wrapEvent.id,\n            created_at: wrapEvent.created_at,\n          } as NostrEventLog;\n          if (this.nip17EventIdsWeHaveSeen.find((e) => e.id === wrapEvent.id)) {\n            // console.log(`### Already seen NIP-17 event ${wrapEvent.id} (time: ${wrapEvent.created_at})`);\n            return;\n          } else {\n            console.log(`### New event ${wrapEvent.id}`);\n            this.nip17EventIdsWeHaveSeen.push(eventLog);\n            // remove all events older than 10 days to keep the list small\n            const fourDaysAgo =\n              Math.floor(Date.now() / 1000) - 10 * 24 * 60 * 60;\n            this.nip17EventIdsWeHaveSeen = this.nip17EventIdsWeHaveSeen.filter(\n              (e) => e.created_at > fourDaysAgo\n            );\n          }\n          let dmEvent: NDKEvent;\n          let content: string;\n          try {\n            const wappedContent = nip44.v2.decrypt(\n              wrapEvent.content,\n              nip44.v2.utils.getConversationKey(\n                this.seedSignerPrivateKey,\n                wrapEvent.pubkey\n              )\n            );\n            const sealEvent = JSON.parse(wappedContent) as NostrEvent;\n            const dmEventString = nip44.v2.decrypt(\n              sealEvent.content,\n              nip44.v2.utils.getConversationKey(\n                this.seedSignerPrivateKey,\n                sealEvent.pubkey\n              )\n            );\n            dmEvent = JSON.parse(dmEventString) as NDKEvent;\n            content = dmEvent.content;\n            console.log(\"### NIP-17 DM from\", dmEvent.pubkey);\n            console.log(\"Content:\", content);\n          } catch (e) {\n            console.error(e);\n            return;\n          }\n          nip17DirectMessageEvents.add(dmEvent);\n          this.lastEventTimestamp = Math.floor(Date.now() / 1000);\n          this.parseMessageForEcash(content);\n        });\n      });\n      try {\n        nip17DirectMessageEvents = await fetchEventsPromise;\n      } catch (error) {\n        console.error(\"Error fetching contact events:\", error);\n      }\n    },\n    parseMessageForEcash: async function (message: string) {\n      // first check if the message can be converted to a json and then to a PaymentRequestPayload\n      try {\n        const payload = JSON.parse(message) as PaymentRequestPayload;\n        if (payload) {\n          const receiveStore = useReceiveTokensStore();\n          const prStore = usePRStore();\n          const sendTokensStore = useSendTokensStore();\n          const tokensStore = useTokensStore();\n          const proofs = payload.proofs;\n          const mint = payload.mint;\n          const unit = payload.unit;\n          const token = {\n            proofs: proofs,\n            mint: mint,\n            unit: unit,\n          } as Token;\n\n          const tokenStr = getEncodedTokenV4(token);\n\n          const tokenInHistory = tokensStore.tokenAlreadyInHistory(tokenStr);\n          if (tokenInHistory && tokenInHistory.amount > 0) {\n            console.log(\"### incoming token already in history\");\n            return;\n          }\n          const historyId = await this.addPendingTokenToHistory(\n            tokenStr,\n            false,\n            payload.id\n          );\n          try {\n            if (historyId) {\n              prStore.registerIncomingPaymentForRequest(\n                payload.id ?? \"\",\n                historyId\n              );\n            }\n          } catch (e) {\n            console.error(\"Failed to register incoming payment to PR:\", e);\n          }\n          receiveStore.receiveData.tokensBase64 = tokenStr;\n          sendTokensStore.showSendTokens = false;\n          const knowThisMint = receiveStore.knowThisMintOfTokenJson(token);\n          if (prStore.receivePaymentRequestsAutomatically && knowThisMint) {\n            const success = await receiveStore.receiveIfDecodes();\n            if (success) {\n              prStore.showPRDialog = false;\n            } else {\n              notifyWarning(\"Could not receive incoming payment\");\n            }\n          } else {\n            prStore.showPRDialog = false;\n            receiveStore.showReceiveTokens = true;\n          }\n          return;\n        }\n      } catch (e) {\n        // console.log(\"### parsing message for ecash failed\");\n        return;\n      }\n\n      console.log(\"### parsing message for ecash\", message);\n      const receiveStore = useReceiveTokensStore();\n      const words = message.split(\" \");\n      const tokens = words.filter((word) => {\n        return word.startsWith(\"cashuA\") || word.startsWith(\"cashuB\");\n      });\n      for (const tokenStr of tokens) {\n        receiveStore.receiveData.tokensBase64 = tokenStr;\n        receiveStore.showReceiveTokens = true;\n        await this.addPendingTokenToHistory(tokenStr);\n      }\n    },\n    addPendingTokenToHistory: function (\n      tokenStr: string,\n      verbose = true,\n      paymentRequestId?: string\n    ): string | undefined {\n      const receiveStore = useReceiveTokensStore();\n      const tokensStore = useTokensStore();\n      if (tokensStore.tokenAlreadyInHistory(tokenStr)) {\n        notifySuccess(\"Ecash already in history\");\n        receiveStore.showReceiveTokens = false;\n        return undefined;\n      }\n      const decodedToken = token.decode(tokenStr);\n      if (decodedToken == undefined) {\n        throw Error(\"could not decode token\");\n      }\n      // get amount from decodedToken.token.proofs[..].amount\n      const amount = token\n        .getProofs(decodedToken)\n        .reduce((sum, el) => (sum += el.amount), 0);\n\n      const id = tokensStore.addPendingToken({\n        amount: amount,\n        token: tokenStr,\n        mint: token.getMint(decodedToken),\n        unit: token.getUnit(decodedToken),\n        paymentRequestId,\n      });\n      receiveStore.showReceiveTokens = false;\n      // show success notification\n      if (verbose) {\n        notifySuccess(\"Ecash added to history.\");\n      }\n      return id;\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/nostrMintBackup.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { bytesToHex } from \"@noble/hashes/utils\";\nimport { sha256 } from \"@noble/hashes/sha256\";\nimport { getPublicKey } from \"nostr-tools\";\nimport { mnemonicToSeedSync } from \"@scure/bip39\";\nimport NDK, {\n  NDKEvent,\n  NDKFilter,\n  NDKPrivateKeySigner,\n} from \"@nostr-dev-kit/ndk\";\nimport { nip44 } from \"nostr-tools\";\nimport { useWalletStore } from \"./wallet\";\nimport { useMintsStore } from \"./mints\";\nimport { useNostrStore } from \"./nostr\";\nimport { notify, notifyError, notifySuccess } from \"../js/notify\";\nimport { useSettingsStore } from \"./settings\";\n\n// NIP kind for mint backup events\nconst MINT_BACKUP_KIND = 30078;\n\ntype MintBackupData = {\n  mints: string[];\n  timestamp: number;\n};\n\ntype DiscoveredMint = {\n  url: string;\n  timestamp: number;\n  selected: boolean;\n};\n\nexport const useNostrMintBackupStore = defineStore(\"nostrMintBackup\", {\n  state: () => ({\n    // Last backup timestamp\n    lastBackupTimestamp: useLocalStorage<number>(\n      \"cashu.nostrMintBackup.lastBackupTimestamp\",\n      0\n    ),\n\n    // Discovered mints from nostr\n    discoveredMints: [] as DiscoveredMint[],\n\n    // Loading states\n    backupInProgress: false,\n    searchInProgress: false,\n\n    // Derived private key for mint backups\n    mintBackupPrivateKey: \"\",\n    mintBackupPublicKey: \"\",\n  }),\n\n  getters: {\n    // Get enabled state from settings store\n    enabled: (): boolean => {\n      const settingsStore = useSettingsStore();\n      return settingsStore.nostrMintBackupEnabled;\n    },\n\n    // Get the current mint URLs\n    currentMintUrls: (): string[] => {\n      const mintsStore = useMintsStore();\n      return mintsStore.mints.map((mint) => mint.url);\n    },\n\n    // Check if backup is needed (mints have changed since last backup)\n    needsBackup: (state): boolean => {\n      const settingsStore = useSettingsStore();\n      if (!settingsStore.nostrMintBackupEnabled) return false;\n\n      const mintsStore = useMintsStore();\n      const currentMints = mintsStore.mints.map((mint) => mint.url).sort();\n\n      // If no mints, no backup needed\n      if (currentMints.length === 0) return false;\n\n      // If never backed up, need backup\n      if (state.lastBackupTimestamp === 0) return true;\n\n      // Check if mints have changed (this is a simple check, you might want to store the last backed up mints)\n      return true; // For now, always allow backup\n    },\n\n    // Get conversation key for encryption\n    conversationKey: (state): Uint8Array | null => {\n      if (!state.mintBackupPrivateKey || !state.mintBackupPublicKey)\n        return null;\n      return nip44.v2.utils.getConversationKey(\n        state.mintBackupPrivateKey,\n        state.mintBackupPublicKey\n      );\n    },\n  },\n\n  actions: {\n    // Initialize backup keys from wallet seed\n    async initializeBackupKeys(): Promise<void> {\n      const walletStore = useWalletStore();\n      // Derive a deterministic private key from wallet seed for mint backup\n      const { privateKeyHex, publicKeyHex } =\n        await this.initializeBackupKeysFromMnemonic(walletStore.mnemonic);\n      this.mintBackupPrivateKey = privateKeyHex;\n      this.mintBackupPublicKey = publicKeyHex;\n    },\n\n    // Initialize backup keys from custom mnemonic (for restore)\n    async initializeBackupKeysFromMnemonic(\n      mnemonic: string\n    ): Promise<{ privateKeyHex: string; publicKeyHex: string }> {\n      // Derive seed from mnemonic\n      const seed: Uint8Array = mnemonicToSeedSync(mnemonic);\n      const domainSeparator = new TextEncoder().encode(\"cashu-mint-backup\");\n      const combinedData = new Uint8Array(seed.length + domainSeparator.length);\n      combinedData.set(seed);\n      combinedData.set(domainSeparator, seed.length);\n\n      // Use sha256 of combined data as private key\n      const privateKeyBytes = sha256(combinedData);\n      const privateKeyHex = bytesToHex(privateKeyBytes);\n      const publicKeyHex = getPublicKey(privateKeyBytes);\n\n      return { privateKeyHex, publicKeyHex };\n    },\n\n    // Create and publish mint backup event\n    async backupMintsToNostr(verbose: boolean = false): Promise<void> {\n      const settingsStore = useSettingsStore();\n      if (!settingsStore.nostrMintBackupEnabled) {\n        console.log(\"Nostr mint backup is disabled\");\n        return;\n      }\n\n      if (this.backupInProgress) {\n        console.log(\"Backup already in progress\");\n        return;\n      }\n\n      this.backupInProgress = true;\n\n      try {\n        // Initialize keys if not already done\n        if (!this.mintBackupPrivateKey) {\n          await this.initializeBackupKeys();\n        }\n\n        const settingsStore = useSettingsStore();\n        const mintsStore = useMintsStore();\n\n        // Check if relays are configured\n        if (\n          !settingsStore.defaultNostrRelays ||\n          settingsStore.defaultNostrRelays.length === 0\n        ) {\n          const errorMsg = \"No Nostr relays configured\";\n          console.error(errorMsg);\n          if (verbose) {\n            notifyError(`Failed to backup mint list to Nostr: ${errorMsg}`);\n          }\n          throw new Error(errorMsg);\n        }\n\n        const currentMints = mintsStore.mints.map((mint) => mint.url);\n\n        if (currentMints.length === 0) {\n          console.log(\"No mints to backup\");\n          return;\n        }\n\n        // Create backup data\n        const backupData: MintBackupData = {\n          mints: currentMints,\n          timestamp: Math.floor(Date.now() / 1000),\n        };\n\n        // Encrypt the backup data\n        const conversationKey = nip44.v2.utils.getConversationKey(\n          this.mintBackupPrivateKey,\n          this.mintBackupPublicKey\n        );\n        const encryptedContent = nip44.v2.encrypt(\n          JSON.stringify(backupData),\n          conversationKey\n        );\n\n        // Create NDK instance\n        const ndk = new NDK({\n          explicitRelayUrls: settingsStore.defaultNostrRelays,\n          signer: new NDKPrivateKeySigner(this.mintBackupPrivateKey),\n        });\n\n        await ndk.connect();\n\n        // Create the event\n        const event = new NDKEvent(ndk);\n        event.kind = MINT_BACKUP_KIND;\n        event.content = encryptedContent;\n        event.tags = [\n          [\"d\", \"mint-list\"], // replaceable event identifier\n          [\"client\", \"cashu.me\"],\n        ];\n        event.created_at = backupData.timestamp;\n        event.pubkey = this.mintBackupPublicKey;\n\n        // Sign and publish\n        await event.sign();\n        await event.publish();\n\n        this.lastBackupTimestamp = backupData.timestamp;\n        if (verbose) {\n          notifySuccess(\"Mint list backed up to Nostr successfully\");\n        }\n\n        console.log(\"Mint backup published to Nostr:\", event.id);\n      } catch (error) {\n        console.error(\"Failed to backup mints to Nostr:\", error);\n        // Only show error notification if verbose is true\n        // This prevents showing errors for automatic backups that fail silently\n        if (verbose) {\n          notifyError(\n            \"Failed to backup mint list to Nostr: \" + (error as Error).message\n          );\n        }\n        throw error;\n      } finally {\n        this.backupInProgress = false;\n      }\n    },\n\n    // Search for mint backups on Nostr\n    async searchMintsOnNostr(mnemonic: string): Promise<DiscoveredMint[]> {\n      this.searchInProgress = true;\n      this.discoveredMints = [];\n\n      try {\n        const settingsStore = useSettingsStore();\n\n        // Derive keys from provided mnemonic\n        const { publicKeyHex } = await this.initializeBackupKeysFromMnemonic(\n          mnemonic\n        );\n\n        // Create read-only NDK instance\n        const ndk = new NDK({\n          explicitRelayUrls: settingsStore.defaultNostrRelays,\n        });\n\n        await ndk.connect();\n\n        // Search for mint backup events from this pubkey\n        const filter: NDKFilter = {\n          kinds: [MINT_BACKUP_KIND],\n          authors: [publicKeyHex],\n          \"#d\": [\"mint-list\"],\n          limit: 10,\n        };\n\n        const events = await ndk.fetchEvents(filter);\n        console.log(`Found ${events.size} mint backup events`);\n\n        const allDiscoveredMints: DiscoveredMint[] = [];\n\n        for (const event of events) {\n          try {\n            // Decrypt event content\n            const { privateKeyHex } =\n              await this.initializeBackupKeysFromMnemonic(mnemonic);\n            const conversationKey = nip44.v2.utils.getConversationKey(\n              privateKeyHex,\n              publicKeyHex\n            );\n            const decryptedContent = nip44.v2.decrypt(\n              event.content,\n              conversationKey\n            );\n\n            const backupData: MintBackupData = JSON.parse(decryptedContent);\n\n            // Add discovered mints\n            for (const mintUrl of backupData.mints) {\n              const existingMint = allDiscoveredMints.find(\n                (m) => m.url === mintUrl\n              );\n              if (!existingMint) {\n                allDiscoveredMints.push({\n                  url: mintUrl,\n                  timestamp: backupData.timestamp,\n                  selected: false,\n                });\n              } else if (backupData.timestamp > existingMint.timestamp) {\n                existingMint.timestamp = backupData.timestamp;\n              }\n            }\n          } catch (decryptError) {\n            console.error(\"Failed to decrypt backup event:\", decryptError);\n          }\n        }\n\n        // Sort by timestamp (newest first)\n        allDiscoveredMints.sort((a, b) => b.timestamp - a.timestamp);\n\n        this.discoveredMints = allDiscoveredMints;\n\n        if (allDiscoveredMints.length > 0) {\n          notify(`Found ${allDiscoveredMints.length} mint(s) in Nostr backups`);\n        } else {\n          notify(\"No mint backups found on Nostr for this seed phrase\");\n        }\n\n        return allDiscoveredMints;\n      } catch (error) {\n        console.error(\"Failed to search mints on Nostr:\", error);\n        notifyError(\n          \"Failed to search for mint backups: \" + (error as Error).message\n        );\n        throw error;\n      } finally {\n        this.searchInProgress = false;\n      }\n    },\n\n    // Add selected mints to the wallet\n    async addSelectedMintsToWallet(\n      selectedMints: DiscoveredMint[]\n    ): Promise<void> {\n      const mintsStore = useMintsStore();\n\n      try {\n        let addedCount = 0;\n\n        for (const mint of selectedMints) {\n          if (!mint.selected) continue;\n\n          try {\n            // Check if mint already exists\n            const existing = mintsStore.mints.find((m) => m.url === mint.url);\n            if (existing) {\n              console.log(`Mint ${mint.url} already exists, skipping`);\n              continue;\n            }\n\n            // Add the mint\n            await mintsStore.addMint({ url: mint.url }, false);\n            addedCount++;\n          } catch (error) {\n            console.error(`Failed to add mint ${mint.url}:`, error);\n            notifyError(\n              `Failed to add mint ${mint.url}: ${(error as Error).message}`\n            );\n          }\n        }\n\n        if (addedCount > 0) {\n          notifySuccess(`Added ${addedCount} mint(s) to your wallet`);\n        } else {\n          notify(\"No new mints were added\");\n        }\n      } catch (error) {\n        console.error(\"Failed to add selected mints:\", error);\n        notifyError(\n          \"Failed to add selected mints: \" + (error as Error).message\n        );\n        throw error;\n      }\n    },\n\n    // Toggle selection of a discovered mint\n    toggleMintSelection(mintUrl: string): void {\n      const mint = this.discoveredMints.find((m) => m.url === mintUrl);\n      if (mint) {\n        mint.selected = !mint.selected;\n      }\n    },\n\n    // Select all discovered mints\n    selectAllMints(): void {\n      this.discoveredMints.forEach((mint) => {\n        mint.selected = true;\n      });\n    },\n\n    // Deselect all discovered mints\n    deselectAllMints(): void {\n      this.discoveredMints.forEach((mint) => {\n        mint.selected = false;\n      });\n    },\n\n    // Enable the backup feature\n    async enableBackup(): Promise<void> {\n      const settingsStore = useSettingsStore();\n      settingsStore.nostrMintBackupEnabled = true;\n\n      // Initialize backup keys\n      await this.initializeBackupKeys();\n\n      // Perform initial backup if needed\n      if (this.needsBackup) {\n        await this.backupMintsToNostr(true);\n      }\n    },\n\n    // Disable the backup feature\n    disableBackup(): void {\n      const settingsStore = useSettingsStore();\n      settingsStore.nostrMintBackupEnabled = false;\n    },\n\n    // Force backup (regardless of whether it's needed)\n    async forceBackup(): Promise<void> {\n      const settingsStore = useSettingsStore();\n      const wasEnabled = settingsStore.nostrMintBackupEnabled;\n      settingsStore.nostrMintBackupEnabled = true;\n\n      try {\n        await this.backupMintsToNostr(true);\n      } finally {\n        settingsStore.nostrMintBackupEnabled = wasEnabled;\n      }\n    },\n\n    // Clear discovered mints\n    clearDiscoveredMints(): void {\n      this.discoveredMints = [];\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/nostrUser.ts",
    "content": "import { defineStore } from \"pinia\";\nimport NDK, { NDKEvent, NDKFilter, NDKKind } from \"@nostr-dev-kit/ndk\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { useNostrStore } from \"./nostr\";\nimport Dexie from \"dexie\";\n\ntype NostrProfile = {\n  name?: string;\n  display_name?: string;\n  picture?: string;\n  image?: string; // sometimes used instead of picture\n  about?: string;\n  nip05?: string;\n  lud16?: string;\n};\n\nexport const useNostrUserStore = defineStore(\"nostrUser\", {\n  state: () => ({\n    pubkey: useLocalStorage<string>(\"cashu.nostrUser.pubkey\", \"\"),\n    profile: null as NostrProfile | null,\n    follows: [] as string[],\n    wotHopsByPubkey: {} as Record<string, number>,\n    lastUpdatedAt: 0,\n    ndkConnected: false,\n    wotLoading: false,\n    wotMaxHops: 2,\n    profileRefreshIntervalSeconds: 60, // 1 minute\n    dbInitialized: false,\n    crawlProcessed: 0,\n    crawlTotal: 0,\n    crawlCheckpointNextIndex: 0,\n    crawlCheckpointTotal: 0,\n    wotCancelRequested: false,\n    defaultWoTSeedPubkey:\n      \"04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9\",\n    defaultShallowWoTPubkeys: [\n      \"04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9\", // ODELL\n      \"50d94fc2d8580c682b071a542f8b1e31a200b0508bab95a33bef0855df281d63\", // Calle\n      \"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2\", // jack\n      \"b33bf9e97b78f35694a02e6bbef8e77059373e42b0a85a63f25a50ebfdadf50d\", // Minibits\n      \"1afe0c74e3d7784eba93a5e3fa554a6eeb01928d12739ae8ba4832786808e36d\", // AmericanHodl\n      \"c48e29f04b482cc01ca1f9ef8c86ef8318c059e0e9353235162f080f26e14c11\", // Walker\n      \"c43bbb58e2e6bc2f9455758257f6ba5329107bd4e8274068c2936c69d9980b7d\", // roya\n    ],\n  }),\n  getters: {\n    displayName(state): string {\n      const p = state.profile || {};\n      const name = (p.display_name || p.name || \"\").trim();\n      return (\n        name ||\n        (state.pubkey\n          ? `${state.pubkey.slice(0, 8)}…${state.pubkey.slice(-4)}`\n          : \"\")\n      );\n    },\n    wotCount(state): number {\n      return Object.keys(state.wotHopsByPubkey || {}).length;\n    },\n    isInWebOfTrust:\n      (state) =>\n      (pk: string): boolean => {\n        return pk in state.wotHopsByPubkey;\n      },\n    getHop:\n      (state) =>\n      (pk: string): number | null => {\n        return state.wotHopsByPubkey[pk] ?? null;\n      },\n    hasCrawlCheckpoint(state): boolean {\n      return (\n        typeof state.crawlCheckpointNextIndex === \"number\" &&\n        typeof state.crawlCheckpointTotal === \"number\" &&\n        state.crawlCheckpointTotal > 0 &&\n        state.crawlCheckpointNextIndex > 0 &&\n        state.crawlCheckpointNextIndex < state.crawlCheckpointTotal\n      );\n    },\n  },\n  actions: {\n    initDb: async function () {\n      if (db.isOpen()) return;\n      await db.open();\n    },\n    ensureDbInitialized: async function () {\n      if (this.dbInitialized) return;\n      await this.initDb();\n      // Load persisted data\n      const [follows, wot, last, nextIdx, hop1Saved] = await Promise.all([\n        db.follows.toArray(),\n        db.wot.toArray(),\n        db.meta.get(\"lastUpdatedAt\"),\n        db.meta.get(\"wot.crawl.nextIndex\"),\n        db.meta.get(\"wot.crawl.hop1\"),\n      ]);\n      this.follows = follows.map((f) => f.pubkey);\n      this.wotHopsByPubkey = Object.fromEntries(\n        wot.map((e) => [e.pubkey, e.hop])\n      );\n      this.lastUpdatedAt = (last?.value as number) || 0;\n      this.crawlCheckpointNextIndex = (nextIdx?.value as number) || 0;\n      this.crawlCheckpointTotal = Array.isArray(hop1Saved?.value)\n        ? (hop1Saved?.value as string[]).length\n        : 0;\n      this.dbInitialized = true;\n    },\n    ensureNdk: function (): NDK {\n      // Use the global NDK instance managed by the nostr store to avoid stuck fetches\n      const nostr = useNostrStore();\n      if (!nostr.connected || !nostr.ndk) nostr.initNdkReadOnly();\n      return nostr.ndk as unknown as NDK;\n    },\n    sleep: async function (ms: number) {\n      return await new Promise<void>((resolve) => setTimeout(resolve, ms));\n    },\n    setPubkey: function (pubkey: string) {\n      this.pubkey = pubkey || \"\";\n    },\n    updateUserProfile: async function (force = false) {\n      await this.ensureDbInitialized();\n      if (!this.pubkey) return;\n      const now = Math.floor(Date.now() / 1000);\n      if (\n        !force &&\n        now - this.lastUpdatedAt < this.profileRefreshIntervalSeconds\n      )\n        return; // throttle to profileRefreshIntervalSeconds seconds\n      console.log(\n        `[nostrUser] Updating user profile for ${this.pubkey} (force=${force})`\n      );\n      await this.fetchProfile();\n      await this.fetchFollows();\n      this.lastUpdatedAt = now;\n      await db.meta.put({ key: \"lastUpdatedAt\", value: now });\n    },\n    fetchProfile: async function () {\n      if (!this.pubkey) return;\n      const ndk = this.ensureNdk();\n      const filter: NDKFilter = {\n        kinds: [NDKKind.Metadata],\n        authors: [this.pubkey],\n        limit: 1,\n      };\n      try {\n        console.log(`[nostrUser] Fetching kind:0 profile for ${this.pubkey}`);\n        const events = await ndk.fetchEvents(filter);\n        let latest: NDKEvent | undefined;\n        events.forEach((e) => {\n          if (!latest || (e.created_at || 0) > (latest.created_at || 0))\n            latest = e;\n        });\n        if (latest) {\n          try {\n            const content = JSON.parse(latest.content || \"{}\");\n            this.profile = content as NostrProfile;\n          } catch {}\n        }\n      } catch {}\n    },\n    fetchFollowsOf: async function (pk: string): Promise<string[]> {\n      const ndk = this.ensureNdk();\n      const filter: NDKFilter = {\n        kinds: [NDKKind.Contacts],\n        authors: [pk],\n        limit: 1,\n      };\n      try {\n        console.log(`[nostrUser] Fetching follows (kind:3) for ${pk}`);\n        const events = await ndk.fetchEvents(filter);\n        let latest: NDKEvent | undefined;\n        events.forEach((e) => {\n          if (!latest || (e.created_at || 0) > (latest.created_at || 0))\n            latest = e;\n        });\n        if (!latest) return [];\n        const follows = (latest.tags || [])\n          .filter((t) => t[0] === \"p\" && typeof t[1] === \"string\")\n          .map((t) => t[1]);\n        console.log(`[nostrUser] Fetched ${follows.length} follows for ${pk}`);\n        return Array.from(new Set(follows));\n      } catch {\n        return [];\n      }\n    },\n    fetchFollows: async function () {\n      await this.ensureDbInitialized();\n      if (!this.pubkey) return;\n      try {\n        console.log(`[nostrUser] Fetching follows (kind:3) for ${this.pubkey}`);\n        const follows = await this.fetchFollowsOf(this.pubkey);\n        this.follows = follows;\n        console.log(\n          `[nostrUser] Fetched ${follows.length} follows for ${this.pubkey}`\n        );\n        await db.follows.clear();\n        if (follows.length) {\n          await db.follows.bulkPut(follows.map((pk) => ({ pubkey: pk })));\n        }\n        // Immediately reflect 1-hop WOT with follows\n        const next: Record<string, number> = { ...this.wotHopsByPubkey };\n        for (const pk of follows) {\n          if (pk && pk !== this.pubkey) next[pk] = 1;\n        }\n        this.wotHopsByPubkey = next;\n        if (Object.keys(next).length) {\n          // Upsert WOT entries with hop=1\n          const wotRows = Object.entries(next).map(([pubkey]) => ({\n            pubkey,\n            hop: next[pubkey],\n          }));\n          await db.wot.bulkPut(wotRows);\n        }\n      } catch {}\n    },\n    shallowCrawlWebOfTrust: async function () {\n      // call 1-hop crawlWebOfTrust for each of the defaultShallowWoTPubkeys\n      for (const pubkey of this.defaultShallowWoTPubkeys) {\n        await this.crawlWebOfTrust(1, pubkey);\n      }\n    },\n    crawlWebOfTrust: async function (\n      maxHops: number | undefined = undefined,\n      sourcePubKey: string | undefined = undefined\n    ) {\n      await this.ensureDbInitialized();\n      maxHops = maxHops || this.wotMaxHops;\n      const nostr = useNostrStore();\n      const source =\n        sourcePubKey ||\n        (nostr.signerType === \"SEED\" ? this.defaultWoTSeedPubkey : this.pubkey);\n      if (!source) return;\n      if (this.wotLoading) return;\n      this.wotLoading = true;\n      this.wotCancelRequested = false;\n      try {\n        console.log(\n          `[nostrUser] Crawling web of trust from ${source} up to ${maxHops} hops…`\n        );\n        // Determine resume vs fresh crawl\n        const hop1Saved = (await db.meta.get(\"wot.crawl.hop1\"))?.value as\n          | string[]\n          | undefined;\n        const nextIndexSaved = (await db.meta.get(\"wot.crawl.nextIndex\"))\n          ?.value as number | undefined;\n        let hop1: string[] = [];\n        let startIndex = 0;\n        if (\n          Array.isArray(hop1Saved) &&\n          typeof nextIndexSaved === \"number\" &&\n          nextIndexSaved >= 0 &&\n          nextIndexSaved < hop1Saved.length\n        ) {\n          // Resume\n          hop1 = hop1Saved;\n          startIndex = nextIndexSaved;\n        } else {\n          // Fresh start from source's follows\n          const baseFollows =\n            source === this.pubkey\n              ? this.follows.length\n                ? this.follows\n                : await this.fetchFollowsOf(source)\n              : await this.fetchFollowsOf(source);\n          hop1 = Array.from(new Set(baseFollows));\n          startIndex = 0;\n          await db.meta.put({ key: \"wot.crawl.hop1\", value: hop1 });\n          await db.meta.put({ key: \"wot.crawl.nextIndex\", value: 0 });\n        }\n\n        const wot: Record<string, number> = {};\n        // Ensure 1 hop are reflected immediately\n        for (const pk of hop1) {\n          if (pk && pk !== source) wot[pk] = 1;\n        }\n        // Update progress counters for UI\n        this.crawlTotal = hop1.length;\n        this.crawlProcessed = startIndex;\n        this.crawlCheckpointTotal = hop1.length;\n        this.crawlCheckpointNextIndex = startIndex;\n        // Commit 1-hop immediately, keeping shortest hop in both state and DB\n        const mergedInitial: Record<string, number> = {\n          ...this.wotHopsByPubkey,\n        };\n        for (const [k, v] of Object.entries(wot)) {\n          mergedInitial[k] = Math.min(mergedInitial[k] ?? v, v);\n        }\n        this.wotHopsByPubkey = mergedInitial;\n        if (Object.keys(wot).length) {\n          await db.wot.bulkPut(\n            Object.keys(wot).map((pubkey) => ({\n              pubkey,\n              hop: this.wotHopsByPubkey[pubkey],\n            }))\n          );\n        }\n\n        if (maxHops >= 2 && hop1.length) {\n          // Sequentially fetch to avoid blocking the UI; short delay between requests\n          const stepDelayMs = 20; // ~1 frame\n          for (let i = startIndex; i < hop1.length; i++) {\n            if (this.wotCancelRequested) break;\n            const pk1 = hop1[i];\n            const followsOfFollow = await this.fetchFollowsOf(pk1);\n            for (const pk2 of followsOfFollow) {\n              if (!pk2 || pk2 === source) continue;\n              if (!(pk2 in wot)) wot[pk2] = 2;\n            }\n            this.crawlProcessed = i + 1;\n            // Persist checkpoint so we can resume later\n            await db.meta.put({ key: \"wot.crawl.nextIndex\", value: i + 1 });\n            this.crawlCheckpointNextIndex = i + 1;\n            if (this.crawlProcessed % 3 === 0) {\n              // Periodically update state so UI reflects progress\n              const merged: Record<string, number> = {\n                ...this.wotHopsByPubkey,\n              };\n              for (const [k, v] of Object.entries(wot)) {\n                merged[k] = Math.min(merged[k] ?? v, v);\n              }\n              this.wotHopsByPubkey = merged;\n              await db.wot.bulkPut(\n                Object.keys(wot).map((pubkey) => ({\n                  pubkey,\n                  hop: this.wotHopsByPubkey[pubkey],\n                }))\n              );\n            }\n            await this.sleep(stepDelayMs);\n          }\n        }\n        // Merge with existing to retain shorter hops and previous entries\n        const merged: Record<string, number> = { ...this.wotHopsByPubkey };\n        for (const [k, v] of Object.entries(wot)) {\n          merged[k] = Math.min(merged[k] ?? v, v);\n        }\n        this.wotHopsByPubkey = merged;\n        if (Object.keys(wot).length) {\n          await db.wot.bulkPut(\n            Object.keys(wot).map((pubkey) => ({\n              pubkey,\n              hop: this.wotHopsByPubkey[pubkey],\n            }))\n          );\n        }\n        if (!this.wotCancelRequested) {\n          console.log(\n            `[nostrUser] Crawl complete. Known pubkeys: ${\n              Object.keys(this.wotHopsByPubkey).length\n            }`\n          );\n          // Clear checkpoint upon completion\n          await db.meta.delete(\"wot.crawl.hop1\");\n          await db.meta.delete(\"wot.crawl.nextIndex\");\n          this.crawlCheckpointNextIndex = 0;\n          this.crawlCheckpointTotal = 0;\n        } else {\n          console.log(\n            `[nostrUser] Crawl cancelled at ${this.crawlProcessed}/${this.crawlTotal}`\n          );\n        }\n      } finally {\n        this.wotLoading = false;\n        // Reset progress counters when done\n        if (!this.wotCancelRequested) {\n          this.crawlTotal = 0;\n          this.crawlProcessed = 0;\n        }\n        this.wotCancelRequested = false;\n      }\n    },\n    cancelCrawl: function () {\n      if (!this.wotLoading) return;\n      this.wotCancelRequested = true;\n    },\n    resetWebOfTrust: async function () {\n      await this.ensureDbInitialized();\n      if (this.wotLoading) return;\n      this.wotHopsByPubkey = {};\n      await db.wot.clear();\n      await db.meta.delete(\"wot.crawl.hop1\");\n      await db.meta.delete(\"wot.crawl.nextIndex\");\n      this.crawlCheckpointNextIndex = 0;\n      this.crawlCheckpointTotal = 0;\n    },\n    clearAllDatabases: function () {\n      db.wot.clear();\n      db.follows.clear();\n      db.meta.clear();\n    },\n  },\n});\n\n// Dexie DB setup for web-of-trust persistence\nclass NostrUserDB extends Dexie {\n  wot!: Dexie.Table<{ pubkey: string; hop: number }, string>;\n  follows!: Dexie.Table<{ pubkey: string }, string>;\n  meta!: Dexie.Table<{ key: string; value: any }, string>;\n  constructor() {\n    super(\"nostrUserDB\");\n    this.version(1).stores({\n      wot: \"pubkey\",\n      follows: \"pubkey\",\n      meta: \"key\",\n    });\n  }\n}\n\nconst db = new NostrUserDB();\n"
  },
  {
    "path": "src/stores/npcv2.ts",
    "content": "import { defineStore } from \"pinia\";\nimport NDK, { NDKEvent } from \"@nostr-dev-kit/ndk\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { nip19 } from \"nostr-tools\";\nimport { useWalletStore } from \"./wallet\";\nimport { notifyApiError, notifyError, notifySuccess } from \"../js/notify\";\nimport { MintQuoteState } from \"@cashu/cashu-ts\";\nimport { useNostrStore } from \"../stores/nostr\";\nimport { date } from \"quasar\";\nimport { useMintsStore } from \"./mints\";\n\ntype NPCUser = {\n  lockQuote: boolean;\n  mintUrl: string;\n  name?: string;\n  pubkey: string;\n};\n\ntype NPCV2InfoReponse =\n  | {\n      error: true;\n      message: string;\n    }\n  | {\n      error: false;\n      data: {\n        user: NPCUser;\n      };\n    };\n\ntype NPCV2UsernameReponse =\n  | { error: true; message: string }\n  | { error: false; data: { user: NPCUser } };\n\ntype NPCQuote = {\n  createdAt: number;\n  paidAt: number;\n  expiresAt: number;\n  mintUrl: string;\n  quoteId: string;\n  request: string;\n  amount: number;\n  state: string;\n  locked: boolean;\n};\n\ntype NPCQuoteResponse =\n  | {\n      error: true;\n      message: string;\n    }\n  | {\n      error: false;\n      data: {\n        quotes: NPCQuote[];\n      };\n      metadata: { limit: number; total: number; since?: number };\n    };\n\ntype UsernameQuote = { username: string; creq: string };\n\nconst NIP98Kind = 27235;\n\nexport const useNPCV2Store = defineStore(\"npcV2\", {\n  state: () => ({\n    npcV2Enabled: useLocalStorage<boolean>(\"cashu.npc.v2.enabled\", false),\n    npcV2ClaimAutomatically: useLocalStorage<boolean>(\n      \"cashu.npc.v2.claimAutomatically\",\n      true\n    ),\n    npcV2LastCheck: useLocalStorage<number>(\"cashu.npc.v2.lastCheck\", null),\n    npcV2Address: useLocalStorage<string>(\"cashu.npc.v2.address\", \"\"),\n    npcV2Mint: useLocalStorage<string>(\"cashu.npc.v2.mint\", null),\n    npcV2Domain: \"\",\n    npcV2BaseURL: useLocalStorage<string>(\n      \"cashu.npc.v2.baseURL\",\n      \"https://npubx.cash\"\n    ),\n    npcV2Loading: false,\n    // ndk: new NDK(),\n    // signer: {} as NDKPrivateKeySigner,\n  }),\n  getters: {},\n  actions: {\n    generateNPCV2Connection: async function () {\n      if (!this.npcV2Enabled) {\n        return;\n      }\n      const nostrStore = useNostrStore();\n      const mintsStore = useMintsStore();\n      if (!nostrStore.pubkey) {\n        return;\n      }\n      const walletPublicKeyHex = nostrStore.pubkey;\n      this.npcV2Domain = new URL(this.npcV2BaseURL).hostname;\n      this.npcV2Address =\n        nip19.npubEncode(walletPublicKeyHex) + \"@\" + this.npcV2Domain;\n      this.npcV2Loading = true;\n      try {\n        const previousAddress = this.npcV2Address;\n        const info = await this.getV2Info();\n        if (info.name) {\n          const usernameAddress = info.name + \"@\" + this.npcV2Domain;\n          if (previousAddress !== usernameAddress) {\n            notifySuccess(`Logged in as ${info.name}`);\n          }\n          this.npcV2Address = usernameAddress;\n        }\n        if (mintsStore.mints.map((m) => m.url).includes(info.mintUrl)) {\n          this.npcV2Mint = info.mintUrl;\n        } else if (mintsStore.activeMintUrl) {\n          await this.changeMintUrl(mintsStore.activeMintUrl);\n        } else {\n          await mintsStore.addMint({ url: info.mintUrl });\n          this.npcV2Mint = info.mintUrl;\n        }\n      } catch (e) {\n        if (e instanceof Error) {\n          notifyApiError(e);\n        }\n        console.log(e);\n      } finally {\n        this.npcV2Loading = false;\n      }\n    },\n    getV2Info: async function (): Promise<{\n      name?: string;\n      mintUrl: string;\n      lockQuote: boolean;\n      pubkey: string;\n    }> {\n      try {\n        const response = await this.sendAuthedRequest(\n          `${this.npcV2BaseURL}/api/v2/user/info`\n        );\n        const info: NPCV2InfoReponse = await response.json();\n        if (info.error) {\n          notifyError(info.message);\n          throw new Error(info.message);\n        }\n        return info.data.user;\n      } catch (e) {\n        console.error(e);\n        return {\n          mintUrl: \"\",\n          name: \"\",\n          pubkey: \"\",\n          lockQuote: false,\n        };\n      }\n    },\n    changeMintUrl: async function (mintUrl: string) {\n      const mintstore = useMintsStore();\n      if (!mintstore.mints.find((m) => m.url === mintUrl)) {\n        notifyError(\n          `Please make sure ${mintUrl} is added to your wallet first!`,\n          \"Could not update npubx.cash mint\"\n        );\n        return;\n      }\n      try {\n        const res = await this.sendAuthedRequest(\n          `${this.npcV2BaseURL}/api/v2/user/mint`,\n          {\n            headers: {\n              \"Content-Type\": \"application/json\",\n            },\n            method: \"PATCH\",\n            body: JSON.stringify({ mint_url: mintUrl }),\n          }\n        );\n        const data = await res.json();\n        if (data.error) {\n          throw new Error(data.message);\n        }\n        this.npcV2Mint = data.data.user.mintUrl;\n      } catch (e) {\n        console.log(e);\n        if (e instanceof Error) {\n          notifyError(e.message);\n        } else {\n          notifyError(\"Something went wrong!\");\n        }\n      }\n    },\n    getLatestQuotes: async function () {\n      if (!this.npcV2Enabled) {\n        return;\n      }\n      const walletStore = useWalletStore();\n      const since = this.npcV2LastCheck ? `?since=${this.npcV2LastCheck}` : \"\";\n\n      const quoteUrl = `${this.npcV2BaseURL}/api/v2/wallet/quotes`;\n      try {\n        const response = await this.sendAuthedRequest(\n          quoteUrl + since,\n          undefined,\n          quoteUrl\n        );\n        const resData: NPCQuoteResponse = await response.json();\n        if (resData.error) {\n          return;\n        }\n        let latestQuoteTime: number | undefined = undefined;\n        resData.data.quotes.forEach(async (quote) => {\n          if (\n            walletStore.invoiceHistory.find((i) => i.quote === quote.quoteId)\n          ) {\n            return;\n          }\n          if (!latestQuoteTime || latestQuoteTime < quote.createdAt) {\n            latestQuoteTime = quote.createdAt;\n          }\n          walletStore.invoiceHistory.push({\n            label: \"Zap\",\n            mint: quote.mintUrl,\n            memo: \"\",\n            bolt11: quote.request,\n            amount: quote.amount,\n            quote: quote.quoteId,\n            date: date.formatDate(\n              new Date(quote.createdAt * 1000),\n              \"YYYY-MM-DD HH:mm:ss\"\n            ),\n            status: \"pending\",\n            unit: \"sat\",\n            mintQuote: {\n              request: quote.request,\n              quote: quote.quoteId,\n              state: MintQuoteState.PAID,\n              expiry: quote.expiresAt,\n              amount: quote.amount,\n              unit: \"sat\",\n            },\n          });\n          if (this.npcV2ClaimAutomatically) {\n            await walletStore.mintOnPaid(quote.quoteId);\n          }\n        });\n        if (latestQuoteTime) {\n          this.npcV2LastCheck = latestQuoteTime;\n        }\n      } catch (e) {\n        console.error(e);\n        return;\n      }\n    },\n    getUsernameQuote: async function (\n      username: string\n    ): Promise<UsernameQuote> {\n      const res = await this.sendAuthedRequest(\n        `${this.npcV2BaseURL}/api/v2/user/username`,\n        {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({ username }),\n        }\n      );\n      const data = (await res.json()) as NPCV2UsernameReponse;\n      if (data.error) {\n        if (res.status === 402) {\n          const paymentHeader = res.headers.get(\"X-Cashu\");\n          if (!paymentHeader) {\n            throw new Error(\"Unexpected reply without payment request\");\n          }\n          return { username, creq: paymentHeader };\n        }\n        throw new Error(data.message);\n      }\n      throw new Error(\"Unexpected reply without payment request\");\n    },\n    setUsername: async function (username: string, token: string) {\n      try {\n        const res = await this.sendAuthedRequest(\n          `${this.npcV2BaseURL}/api/v2/user/username`,\n          {\n            method: \"POST\",\n            headers: { \"Content-Type\": \"application/json\", \"X-Cashu\": token },\n            body: JSON.stringify({ username }),\n          }\n        );\n        const data = (await res.json()) as NPCV2UsernameReponse;\n        if (data.error) {\n          throw new Error(data.message);\n        }\n        this.npcV2Address = `${data.data.user.name}@${this.npcV2Domain}`;\n      } catch (e) {\n        console.log(e);\n        if (e instanceof Error) {\n          notifyError(e.message);\n        }\n      }\n    },\n    sendAuthedRequest: async function (\n      url: string,\n      opts?: RequestInit,\n      authUrl?: string\n    ) {\n      const authHeader = await this.generateNip98Event(\n        authUrl || url,\n        opts?.method || \"GET\"\n      );\n      return fetch(url, {\n        ...opts,\n        headers: { ...opts?.headers, authorization: `Nostr ${authHeader}` },\n      });\n    },\n    generateNip98Event: async function (\n      url: string,\n      method: string\n    ): Promise<string> {\n      const nostrStore = useNostrStore();\n      await nostrStore.initSignerIfNotSet();\n      const nip98Event = new NDKEvent(new NDK());\n      nip98Event.kind = NIP98Kind;\n      nip98Event.content = \"\";\n      nip98Event.tags = [\n        [\"u\", url],\n        [\"method\", method],\n      ];\n      await nip98Event.sign(nostrStore.signer);\n      const eventString = JSON.stringify(nip98Event.rawEvent());\n      return btoa(eventString);\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/npubcash.ts",
    "content": "import { defineStore } from \"pinia\";\nimport NDK, { NDKEvent } from \"@nostr-dev-kit/ndk\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { nip19 } from \"nostr-tools\";\nimport { useWalletStore } from \"./wallet\";\nimport { useReceiveTokensStore } from \"./receiveTokensStore\";\nimport { notifyApiError, notifyError, notifySuccess } from \"../js/notify\";\nimport token from \"../js/token\";\nimport { useTokensStore } from \"../stores/tokens\";\nimport { useNostrStore } from \"../stores/nostr\";\n// type NPCConnection = {\n//   walletPublicKey: string,\n//   walletPrivateKey: string,\n// }\n\ntype NPCInfo = {\n  mintUrl: string;\n  npub: string;\n  username: string;\n  error?: string;\n};\n\ntype NPCBalance = {\n  error: string;\n  data: number;\n};\n\ntype NPCClaim = {\n  error: string;\n  data: {\n    token: string;\n  };\n};\n\ntype NPCWithdrawl = {\n  id: number;\n  claim_ids: number[];\n  created_at: number;\n  pubkey: string;\n  amount: number;\n};\n\ntype NPCWithdrawals = {\n  error: string;\n  data: {\n    count: number;\n    withdrawals: Array<NPCWithdrawl>;\n  };\n};\n\nconst NIP98Kind = 27235;\n\nexport const useNPCStore = defineStore(\"npc\", {\n  state: () => ({\n    npcEnabled: useLocalStorage<boolean>(\"cashu.npc.enabled\", false),\n    npcLastCheck: useLocalStorage<number>(\"cashu.npc.lastCheck\", null),\n    automaticClaim: useLocalStorage<boolean>(\"cashu.npc.automaticClaim\", true),\n    // npcConnections: useLocalStorage<NPCConnection[]>(\"cashu.npc.connections\", []),\n    npcAddress: useLocalStorage<string>(\"cashu.npc.address\", \"\"),\n    npcDomain: useLocalStorage<string>(\"cashu.npc.domain\", \"npub.cash\"),\n    baseURL: useLocalStorage<string>(\"cashu.npc.baseURL\", \"https://npub.cash\"),\n    npcLoading: false,\n    // ndk: new NDK(),\n    // signer: {} as NDKPrivateKeySigner,\n  }),\n  getters: {},\n  actions: {\n    generateNPCConnection: async function () {\n      const nostrStore = useNostrStore();\n      if (!nostrStore.pubkey) {\n        return;\n      }\n      const walletPublicKeyHex = nostrStore.pubkey;\n      console.log(\n        \"Lightning address for wallet:\",\n        nip19.npubEncode(walletPublicKeyHex) + \"@\" + this.npcDomain\n      );\n      console.log(\"npub:\", nip19.npubEncode(walletPublicKeyHex));\n      this.baseURL = `https://${this.npcDomain}`;\n      const previousAddress = this.npcAddress;\n      this.npcAddress =\n        nip19.npubEncode(walletPublicKeyHex) + \"@\" + this.npcDomain;\n      if (!this.npcEnabled) {\n        return;\n      }\n      // get info\n      this.npcLoading = true;\n      try {\n        const info = await this.getInfo();\n        if (info.error) {\n          notifyError(info.error);\n          return;\n        }\n        // log info\n        console.log(info);\n        if (info.username) {\n          const usernameAddress = info.username + \"@\" + this.npcDomain;\n          if (previousAddress !== usernameAddress) {\n            notifySuccess(`Logged in as ${info.username}`);\n          }\n          this.npcAddress = usernameAddress;\n        }\n      } catch (e: any) {\n        notifyApiError(e);\n      } finally {\n        this.npcLoading = false;\n      }\n    },\n    generateNip98Event: async function (\n      url: string,\n      method: string,\n      body?: string\n    ): Promise<string> {\n      const nostrStore = useNostrStore();\n      await nostrStore.initSignerIfNotSet();\n      const nip98Event = new NDKEvent(new NDK());\n      nip98Event.kind = NIP98Kind;\n      nip98Event.content = \"\";\n      nip98Event.tags = [\n        [\"u\", url],\n        [\"method\", method],\n      ];\n      // TODO: if body is set, add 'payload' tag with sha256 hash of body\n      const sig = await nip98Event.sign(nostrStore.signer);\n      const eventString = JSON.stringify(nip98Event.rawEvent());\n      // encode the eventString to base64\n      return btoa(eventString);\n    },\n    getInfo: async function (): Promise<NPCInfo> {\n      const authHeader = await this.generateNip98Event(\n        `${this.baseURL}/api/v1/info`,\n        \"GET\"\n      );\n      try {\n        const response = await fetch(`${this.baseURL}/api/v1/info`, {\n          method: \"GET\",\n          headers: {\n            Authorization: `Nostr ${authHeader}`,\n          },\n        });\n        const info: NPCInfo = await response.json();\n        return info;\n      } catch (e) {\n        console.error(e);\n        return {\n          mintUrl: \"\",\n          npub: \"\",\n          username: \"\",\n        };\n      }\n    },\n    claimAllTokens: async function () {\n      if (!this.npcEnabled) {\n        return;\n      }\n      const receiveStore = useReceiveTokensStore();\n      const npubCashBalance = await this.getBalance();\n      console.log(\"npub.cash balance: \" + npubCashBalance);\n      if (npubCashBalance > 0) {\n        notifySuccess(`You have ${npubCashBalance} sats on npub.cash`);\n        const token = await this.getClaim();\n        if (token) {\n          // add token to history first\n          this.addPendingTokenToHistory(token);\n          receiveStore.receiveData.tokensBase64 = token;\n          if (this.automaticClaim) {\n            try {\n              // redeem token automatically\n              const walletStore = useWalletStore();\n              await walletStore.redeem();\n            } catch {\n              // if it doesn't work, show the receive window\n              receiveStore.showReceiveTokens = true;\n            }\n          } else {\n            receiveStore.showReceiveTokens = true;\n          }\n        }\n      }\n    },\n    tokenAlreadyInHistory: function (tokenStr: string) {\n      const tokensStore = useTokensStore();\n      return (\n        tokensStore.historyTokens.find((t) => t.token === tokenStr) !==\n        undefined\n      );\n    },\n    addPendingTokenToHistory: function (tokenStr: string) {\n      const receiveStore = useReceiveTokensStore();\n      if (this.tokenAlreadyInHistory(tokenStr)) {\n        notifySuccess(\"Ecash already in history\");\n        receiveStore.showReceiveTokens = false;\n        return;\n      }\n      const tokensStore = useTokensStore();\n      const decodedToken = token.decode(tokenStr);\n      if (decodedToken == undefined) {\n        throw Error(\"could not decode token\");\n      }\n      // get amount from decodedToken.token.proofs[..].amount\n      const amount = token\n        .getProofs(decodedToken)\n        .reduce((sum, el) => (sum += el.amount), 0);\n\n      const mintUrl = token.getMint(decodedToken);\n      const unit = token.getUnit(decodedToken);\n      tokensStore.addPendingToken({\n        label: \"Zaps\",\n        amount: amount,\n        token: tokenStr,\n        mint: mintUrl,\n        unit: unit,\n      });\n      receiveStore.showReceiveTokens = false;\n    },\n\n    getBalance: async function (): Promise<number> {\n      const authHeader = await this.generateNip98Event(\n        `${this.baseURL}/api/v1/balance`,\n        \"GET\"\n      );\n      try {\n        const response = await fetch(`${this.baseURL}/api/v1/balance`, {\n          method: \"GET\",\n          headers: {\n            Authorization: `Nostr ${authHeader}`,\n          },\n        });\n        // deserialize the response to NPCBalance\n        const balance: NPCBalance = await response.json();\n        if (balance.error) {\n          return 0;\n        }\n        return balance.data;\n      } catch (e) {\n        console.error(e);\n        return 0;\n      }\n    },\n    getClaim: async function (): Promise<string> {\n      const authHeader = await this.generateNip98Event(\n        `${this.baseURL}/api/v1/claim`,\n        \"GET\"\n      );\n      try {\n        const response = await fetch(`${this.baseURL}/api/v1/claim`, {\n          method: \"GET\",\n          headers: {\n            Authorization: `Nostr ${authHeader}`,\n          },\n        });\n        // deserialize the response to NPCClaim\n        const claim: NPCClaim = await response.json();\n        if (claim.error) {\n          return \"\";\n        }\n        return claim.data.token;\n      } catch (e) {\n        console.error(e);\n        return \"\";\n      }\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/nwc.ts",
    "content": "import { defineStore } from \"pinia\";\nimport NDK, {\n  NDKEvent,\n  NDKFilter,\n  NDKPrivateKeySigner,\n  NDKKind,\n  NDKSubscription,\n} from \"@nostr-dev-kit/ndk\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { bytesToHex } from \"@noble/hashes/utils\"; // already an installed dependency\nimport { nip04, generateSecretKey, getPublicKey } from \"nostr-tools\";\nimport { useMintsStore } from \"./mints\";\nimport { useWalletStore, InvoiceHistory } from \"./wallet\";\nimport { useProofsStore } from \"./proofs\";\nimport { notifyWarning } from \"../js/notify\";\nimport { useNostrStore } from \"./nostr\";\nimport { decode as decodeBolt11 } from \"light-bolt11-decoder\";\n\ntype NWCConnection = {\n  walletPublicKey: string;\n  walletPrivateKey: string;\n  connectionSecret: string;\n  connectionPublicKey: string;\n  allowanceLeft: number;\n};\n\ntype NWCCommand = {\n  method: string;\n  params: any;\n};\n\ntype NWCTransaction = {\n  type: string;\n  invoice: string;\n  description: string | null;\n  preimage: string | null;\n  payment_hash: string | null;\n  amount: number;\n  fees_paid: number | null;\n  created_at: number;\n  settled_at: number | null;\n  expires_at: number | null;\n};\n\ntype NWCResult = {\n  result_type: string;\n  result: any;\n};\n\ntype NWCError = {\n  result_type: string;\n  error: {\n    code: string;\n    message: string;\n  };\n};\n\nconst NWCKind = {\n  NWCInfo: 13194,\n  NWCRequest: 23194,\n  NWCResponse: 23195,\n};\n\nexport const useNWCStore = defineStore(\"nwc\", {\n  state: () => ({\n    nwcEnabled: useLocalStorage<boolean>(\"cashu.nwc.enabled\", false),\n    connections: useLocalStorage<NWCConnection[]>(\"cashu.nwc.connections\", []),\n    seenCommandsUntil: useLocalStorage<number>(\n      \"cashu.nwc.seenCommandsUntil\",\n      0\n    ),\n    supportedMethods: [\n      \"pay_invoice\",\n      \"make_invoice\",\n      \"get_balance\",\n      \"get_info\",\n      \"list_transactions\",\n      \"lookup_invoice\",\n    ],\n    blocking: false,\n    ndk: new NDK(),\n    subscriptions: [] as NDKSubscription[],\n    showNWCDialog: false,\n    showNWCData: { connection: {} as NWCConnection, connectionString: \"\" },\n  }),\n  getters: {},\n  actions: {\n    // ––––---------- NWC Command Handlers ––––----------\n    handleGetInfo: async function (nwcCommand: NWCCommand) {\n      console.log(\"### get_info\", nwcCommand.method);\n      return {\n        result_type: \"get_info\",\n        result: {\n          alias: \"Cashu.me\",\n          color: \"#FF0000\",\n          pubkey: this.connections[0].walletPublicKey,\n          network: \"mainnet\",\n          block_height: 1,\n          block_hash: \"blockchain disrespectoor\",\n          methods: this.supportedMethods,\n        },\n      };\n    },\n    handleGetBalance: async function (nwcCommand: NWCCommand) {\n      const mintsStore = useMintsStore() as any;\n      console.log(\"### get_balance\", nwcCommand.method);\n      return {\n        result_type: \"get_balance\",\n        result: {\n          balance: mintsStore.totalUnitBalance * 1000,\n        },\n      };\n    },\n    handlePayInvoice: async function (nwcCommand: NWCCommand) {\n      const invoice = nwcCommand.params.invoice;\n      const amountMsat = nwcCommand.params.amount;\n      console.log(\"### pay_invoice\", nwcCommand.method);\n      console.log(\"### invoice\", invoice);\n      console.log(\"### amountMsat\", amountMsat);\n      // pay invoice\n      const walletStore = useWalletStore();\n      const proofsStore = useProofsStore();\n      const mintStore = useMintsStore();\n      try {\n        await walletStore.decodeRequest(invoice);\n      } catch (e) {\n        console.log(\"### error decoding invoice\", e);\n        return {\n          result_type: nwcCommand.method,\n          error: { code: \"INTERNAL\", message: \"Invalid invoice\" },\n        } as NWCError;\n      }\n      // expect that the melt quote was requested\n      if (\n        walletStore.payInvoiceData.meltQuote.response.amount == 0 ||\n        walletStore.payInvoiceData.meltQuote.error\n      ) {\n        notifyWarning(\"NWC: Error requesting melt quote\");\n        return {\n          result_type: nwcCommand.method,\n          error: { code: \"INTERNAL\", message: \"Error requesting melt quote\" },\n        } as NWCError;\n      }\n      const maximumAmount =\n        walletStore.payInvoiceData.meltQuote.response.amount +\n        walletStore.payInvoiceData.meltQuote.response.fee_reserve;\n      if (mintStore.activeUnit != \"sat\") {\n        notifyWarning(\"NWC: Active unit must be sats\");\n        return {\n          result_type: nwcCommand.method,\n          error: { code: \"INTERNAL\", message: \"Your active must be sats\" },\n        } as NWCError;\n      }\n      if (maximumAmount > this.connections[0].allowanceLeft) {\n        notifyWarning(\"NWC: Allowance exceeded\");\n        return {\n          result_type: nwcCommand.method,\n          error: { code: \"QUOTA_EXCEEDED\", message: \"Your quota has exceeded\" },\n        } as NWCError;\n      }\n      try {\n        const meltData = await walletStore.meltInvoiceData();\n        const paidAmount =\n          walletStore.payInvoiceData.meltQuote.response.amount +\n          walletStore.payInvoiceData.meltQuote.response.fee_reserve -\n          proofsStore.sumProofs(meltData.change ?? []);\n        this.connections[0].allowanceLeft -= paidAmount;\n        return {\n          result_type: nwcCommand.method,\n          result: {\n            // preimage: meltData.preimage,\n          },\n        };\n      } catch (e) {\n        return {\n          result_type: nwcCommand.method,\n          error: { code: \"INTERNAL\", message: \"Could not pay invoice\" },\n        } as NWCError;\n      }\n    },\n    handleMakeInvoice: async function (nwcCommand: NWCCommand) {\n      const { amount, description, expiry } = nwcCommand.params;\n      console.log(\"### make_invoice\");\n      console.log(\"### amount\", amount); // msats\n      console.log(\"### description\", description);\n      console.log(\"### expiry\", expiry); // seconds\n      // make invoice\n      const walletStore = useWalletStore();\n      const wallet = await walletStore.activeWallet();\n      const quote = await walletStore.requestMint(amount / 1000, wallet);\n      if (!quote) {\n        // requesting mint invoice can fail if no mint was selected yet\n        // the error will have been shown as a notification\n        // TODO: make requestMint throw and return useful message\n        return {\n          result_type: nwcCommand.method,\n          error: {\n            code: \"INTERNAL\",\n            message: \"failed to request mint invoice\",\n          },\n        };\n      }\n\n      walletStore.mintOnPaid(quote.quote, false, true);\n\n      return {\n        result_type: nwcCommand.method,\n        result: {\n          type: \"incoming\",\n          invoice: quote?.request,\n          description,\n          amount,\n        },\n      };\n    },\n    handleListTransactions: async function (nwcCommand: NWCCommand) {\n      console.log(\"### list_transactions\", nwcCommand.method);\n      const walletStore = useWalletStore();\n      const from = nwcCommand.params.from || 0;\n      const until = nwcCommand.params.until || Math.floor(Date.now() / 1000);\n      const limit = nwcCommand.params.limit || 10;\n      const offset = nwcCommand.params.offset || 0;\n      const unpaid = nwcCommand.params.unpaid || false;\n      const type = nwcCommand.params.type || undefined;\n\n      const invoiceHistory = walletStore.invoiceHistory;\n      const transactionsHistory = invoiceHistory\n        .filter((invoice) => {\n          const date = new Date(invoice.date);\n          const created_at = Math.floor(date.getTime() / 1000);\n          if (from && created_at < from) {\n            return false;\n          }\n          if (until && created_at > until) {\n            return false;\n          }\n          if (type && type == \"incoming\" && invoice.amount < 0) {\n            return false;\n          }\n          if (type && type == \"outgoing\" && invoice.amount > 0) {\n            return false;\n          }\n          if (unpaid && invoice.status == \"paid\") {\n            return false;\n          }\n          return true;\n        })\n        .slice(offset, offset + limit);\n      // now create an array \"transactions\" out of nwcTransaction from transactionsHistory\n      //\n      // type = \"incoming\" if amount > 0 else \"outgoing\"\n      // amount = abs(amount)\n      // created_at = unix timestamp of date\n      // settled_at = unix timestamp of date if status == \"paid\" else null\n\n      // According to the NWC spec (NIP47): \"Transactions are returned in descending order of creation time.\"\n      const transactions = transactionsHistory\n        .map(this.mapToNwcTransaction)\n        .sort((a, b) => b.created_at - a.created_at);\n\n      return {\n        result_type: \"list_transactions\",\n        result: {\n          transactions: transactions,\n        },\n      };\n    },\n    handleLookupInvoice: async function (nwcCommand: NWCCommand) {\n      let hash = nwcCommand.params.payment_hash;\n      if (!hash) {\n        const bolt11 = nwcCommand.params.invoice;\n        const decoded = bolt11 ? decodeBolt11(bolt11) : null;\n        // @ts-ignore\n        hash = decoded?.sections.find((s) => s.name === \"payment_hash\")?.value;\n      }\n      if (!hash) {\n        return {\n          result_type: nwcCommand.method,\n          error: { code: \"OTHER\", message: \"invoice or payment_hash required\" },\n        };\n      }\n\n      console.log(\"### lookup_invoice\");\n      const walletStore = useWalletStore();\n      const invoiceHistory = walletStore.invoiceHistory;\n\n      for (const inv of invoiceHistory) {\n        const decoded = decodeBolt11(nwcCommand.params.invoice);\n        // @ts-ignore\n        const invHash = decoded.sections.find(\n          (s) => s.name === \"payment_hash\"\n        )?.value;\n        if (invHash === hash) {\n          return {\n            result_type: nwcCommand.method,\n            result: this.mapToNwcTransaction(inv),\n          };\n        }\n      }\n\n      return {\n        result_type: nwcCommand.method,\n        error: {\n          code: \"NOT_FOUND\",\n          message: \"invoice not found\",\n        },\n      };\n    },\n    mapToNwcTransaction(invoice: InvoiceHistory): NWCTransaction {\n      const type = invoice.amount > 0 ? \"incoming\" : \"outgoing\";\n      const amount = Math.abs(invoice.amount) * 1000;\n      const created_at = Math.floor(new Date(invoice.date).getTime() / 1000);\n      const settled_at =\n        invoice.status == \"paid\"\n          ? Math.floor(new Date(invoice.date).getTime() / 1000)\n          : null;\n      return {\n        type: type,\n        invoice: invoice.bolt11,\n        description: invoice.memo,\n        amount: amount,\n        fees_paid: 0,\n        created_at: created_at,\n        settled_at: settled_at,\n      } as NWCTransaction;\n    },\n    // ––––---------- NWC Connection ––––----------\n    replyNWC: async function (\n      result: NWCResult | NWCError,\n      event: NDKEvent,\n      conn: NWCConnection\n    ) {\n      // reply to NWC with result\n      const replyEvent = new NDKEvent(event.ndk);\n      replyEvent.kind = 23195;\n      console.log(\"### replying with\", JSON.stringify(result));\n      replyEvent.content = await nip04.encrypt(\n        conn.walletPrivateKey,\n        event.author.pubkey,\n        JSON.stringify(result)\n      );\n      replyEvent.tags = [\n        [\"p\", event.author.pubkey],\n        [\"e\", event.id],\n      ];\n      console.log(\"### replyEvent\", replyEvent);\n      console.log(\"### replying to\", event.id);\n      // await this.ndk.publish(replyEvent);\n      await replyEvent.publish();\n    },\n    parseNWCCommand: async function (\n      command: string,\n      event: NDKEvent,\n      conn: NWCConnection\n    ) {\n      // parse command to JSON object {method: 'pay_invoice', params: {invoice: '1234'}}\n      const nwcCommand: NWCCommand = JSON.parse(command);\n      let result: NWCResult | NWCError;\n      console.log(\"### nwcCommand\", nwcCommand);\n      // parse \"get_info\" without params\n      if (nwcCommand.method == \"get_info\") {\n        result = await this.handleGetInfo(nwcCommand);\n      } else if (nwcCommand.method == \"get_balance\") {\n        result = await this.handleGetBalance(nwcCommand);\n      } else if (nwcCommand.method == \"pay_invoice\") {\n        if (this.blocking) {\n          result = {\n            result_type: nwcCommand.method,\n            error: {\n              code: \"INTERNAL\",\n              message: \"Already processing a payment.\",\n            },\n          } as NWCError;\n        }\n        this.blocking = true;\n        try {\n          result = await this.handlePayInvoice(nwcCommand);\n        } catch (e) {\n          return;\n        } finally {\n          this.blocking = false;\n        }\n      } else if (nwcCommand.method === \"make_invoice\") {\n        result = await this.handleMakeInvoice(nwcCommand);\n      } else if (nwcCommand.method == \"list_transactions\") {\n        result = await this.handleListTransactions(nwcCommand);\n      } else if (nwcCommand.method === \"lookup_invoice\") {\n        result = await this.handleLookupInvoice(nwcCommand);\n      } else {\n        console.log(\"### method not supported\", nwcCommand.method);\n        result = {\n          result_type: nwcCommand.method,\n          error: { code: \"NOT_IMPLEMENTED\", message: \"Method not supported\" },\n        } as NWCError;\n      }\n      await this.replyNWC(result, event, conn);\n    },\n    getConnectionString: function (connection: NWCConnection) {\n      const walletPublicKeyHex = connection.walletPublicKey;\n      const connectionSecretHex = connection.connectionSecret;\n      const nostrStore = useNostrStore();\n      return `nostr+walletconnect://${walletPublicKeyHex}?relay=${nostrStore.relays.join(\n        \"&relay=\"\n      )}&secret=${connectionSecretHex}`;\n    },\n    generateNWCConnection: async function () {\n      let conn: NWCConnection;\n      // NOTE: we only support one connection for now\n      if (!this.connections.length) {\n        const sk = generateSecretKey(); // `sk` is a Uint8Array\n        const walletPublicKeyHex = getPublicKey(sk); // `pk` is a hex string\n        const walletPrivateKeyHex = bytesToHex(sk);\n\n        const connectionSecret = generateSecretKey();\n        const connectionPublicKeyHex = getPublicKey(connectionSecret);\n        const connectionSecretHex = bytesToHex(connectionSecret);\n        conn = {\n          walletPublicKey: walletPublicKeyHex,\n          walletPrivateKey: walletPrivateKeyHex,\n          connectionSecret: connectionSecretHex,\n          connectionPublicKey: connectionPublicKeyHex,\n          allowanceLeft: 1000,\n        } as NWCConnection;\n        this.connections = this.connections.concat(conn);\n      } else {\n        conn = this.connections[0];\n      }\n\n      const walletSigner = new NDKPrivateKeySigner(conn.walletPrivateKey);\n      // close and delete all old subscriptions\n      this.unsubscribeNWC();\n      const nostrStore = useNostrStore();\n      this.ndk = new NDK({\n        explicitRelayUrls: nostrStore.relays,\n        signer: walletSigner,\n      });\n      this.ndk.connect();\n\n      const nip47InfoEvent = new NDKEvent(this.ndk as NDK);\n      nip47InfoEvent.kind = NWCKind.NWCInfo;\n      nip47InfoEvent.content = this.supportedMethods.join(\" \");\n      try {\n        // let's fetch the info event from the relay to see if we need to republish it\n        // use NWCKind.NWCInfo as an integer here\n        const filterInfoEvent: NDKFilter = {\n          kinds: [NWCKind.NWCInfo],\n          authors: [conn.walletPublicKey],\n        };\n        const eventsInfoEvent = await this.ndk.fetchEvents(filterInfoEvent);\n        if (eventsInfoEvent.size === 0) {\n          await nip47InfoEvent.publish();\n          console.log(\"### published nip47InfoEvent\", nip47InfoEvent);\n        } else {\n          console.log(\"### nip47InfoEvent already published\");\n        }\n      } catch (e) {\n        console.log(\"### could not publish nip47InfoEvent\", nip47InfoEvent);\n        console.log(\"### error\", e);\n      }\n    },\n    listenToNWCCommands: async function () {\n      // if (!this.connections.length) {\n      //   await this.generateNWCConnection()\n      // }\n      await this.generateNWCConnection();\n      // we only support one connection for now\n      const conn = this.connections[0];\n\n      const currentUnitTime = Math.floor(Date.now() / 1000);\n      const subscribeSince = currentUnitTime - 60; // 1 minute\n      const filter = {\n        kinds: [NWCKind.NWCRequest as NDKKind],\n        since: subscribeSince,\n        authors: [conn.connectionPublicKey],\n        \"#p\": [conn.walletPublicKey],\n      } as NDKFilter;\n      const sub = this.ndk.subscribe(filter);\n      const nostrStore = useNostrStore();\n      console.log(\"### subscribing to NWC on relays: \", nostrStore.relays);\n      this.subscriptions.push(sub);\n\n      sub.on(\"eose\", () =>\n        console.log(\"All relays have reached the end of the event stream\")\n      );\n      sub.on(\"close\", () => console.log(\"Subscription closed\"));\n\n      sub.on(\"event\", async (event) => {\n        // console.log(\"### event\", event)\n        // console.log('### event.kind', event.kind)\n        // console.log('### event.id', event.id)\n        // console.log('### event.author.pubkey', event.author.pubkey)\n        // console.log(\"### event.tagValue('p')\", event.tagValue(\"p\"))\n        // console.log(\"### event.tagValue('e')\", event.tagValue(\"e\"))\n        // console.log(\"### event.content\", event.content)\n        if (event.kind != NWCKind.NWCRequest) {\n          return; // ignore non-NWC events\n        }\n        if (!this.nwcEnabled) {\n          console.log(\"### Received NWC command but NWC is disabled\");\n          return;\n        }\n        // check if the events date is after the last seen command\n        if (event.created_at <= this.seenCommandsUntil) {\n          return;\n        }\n        this.seenCommandsUntil = event.created_at;\n\n        console.log(\"### NWC request!\");\n        console.log(\"### event\", event);\n        const decryptedContent = await nip04.decrypt(\n          conn.connectionSecret,\n          conn.walletPublicKey,\n          event.content\n        );\n        // console.log(\"### decryptedContent\", decryptedContent)\n        await this.parseNWCCommand(decryptedContent, event, conn);\n      });\n    },\n    unsubscribeNWC: function () {\n      console.log(\"### unsubscribing from NWC\");\n      for (const sub of this.subscriptions) {\n        sub.stop();\n      }\n      this.subscriptions = [];\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/p2pk.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { generateSecretKey, getPublicKey, nip19 } from \"nostr-tools\";\nimport { bytesToHex } from \"@noble/hashes/utils\"; // already an installed dependency\nimport { WalletProof } from \"stores/mints\";\nimport token from \"src/js/token\";\n\ntype P2PKKey = {\n  publicKey: string;\n  privateKey: string;\n  used: boolean;\n  usedCount: number;\n};\n\nexport const useP2PKStore = defineStore(\"p2pk\", {\n  state: () => ({\n    p2pkKeys: useLocalStorage<P2PKKey[]>(\"cashu.P2PKKeys\", []),\n    showP2PkButtonInDrawer: useLocalStorage<boolean>(\n      \"cashu.p2pk.showP2PkButtonInDrawer\",\n      false\n    ),\n    showP2PKDialog: false,\n    showP2PKData: {} as P2PKKey,\n  }),\n  getters: {},\n  actions: {\n    haveThisKey: function (key: string) {\n      return this.p2pkKeys.filter((m) => m.publicKey == key).length > 0;\n    },\n    maybeConvertNpub: function (key: string) {\n      // Check and convert npub to P2PK\n      if (key && key.startsWith(\"npub1\")) {\n        const { type, data } = nip19.decode(key);\n        if (type === \"npub\" && data.length === 64) {\n          key = \"02\" + data;\n        }\n      }\n      return key;\n    },\n    isValidPubkey: function (key: string) {\n      key = this.maybeConvertNpub(key);\n      return key && key.length == 66;\n    },\n    setPrivateKeyUsed: function (key: string) {\n      const thisKeys = this.p2pkKeys.filter((k) => k.privateKey == key);\n      if (thisKeys.length) {\n        thisKeys[0].used = true;\n        thisKeys[0].usedCount += 1;\n      }\n    },\n    showKeyDetails: function (key: string) {\n      const thisKeys = this.p2pkKeys.filter((k) => k.publicKey == key);\n      if (thisKeys.length) {\n        this.showP2PKData = JSON.parse(JSON.stringify(thisKeys[0]));\n        this.showP2PKDialog = true;\n      }\n    },\n    showLastKey: function () {\n      if (this.p2pkKeys.length) {\n        this.showP2PKData = JSON.parse(\n          JSON.stringify(this.p2pkKeys[this.p2pkKeys.length - 1])\n        );\n        this.showP2PKDialog = true;\n      }\n    },\n    importNsec: async function () {\n      const nsec = (await prompt(\"Enter your nsec\")) as string;\n      if (!nsec || !nsec.startsWith(\"nsec1\")) {\n        console.log(\"input was not an nsec\");\n        return;\n      }\n      const sk = nip19.decode(nsec).data as Uint8Array; // `sk` is a Uint8Array\n      const pk = \"02\" + getPublicKey(sk); // `pk` is a hex string\n      const skHex = bytesToHex(sk);\n      if (this.haveThisKey(pk)) {\n        console.log(\"nsec already exists in p2pk keystore\");\n        return;\n      }\n      const keyPair: P2PKKey = {\n        publicKey: pk,\n        privateKey: skHex,\n        used: false,\n        usedCount: 0,\n      };\n      this.p2pkKeys = this.p2pkKeys.concat(keyPair);\n    },\n    generateKeypair: function () {\n      const sk = generateSecretKey(); // `sk` is a Uint8Array\n      const pk = \"02\" + getPublicKey(sk); // `pk` is a hex string\n      const skHex = bytesToHex(sk);\n      const keyPair: P2PKKey = {\n        publicKey: pk,\n        privateKey: skHex,\n        used: false,\n        usedCount: 0,\n      };\n      this.p2pkKeys = this.p2pkKeys.concat(keyPair);\n    },\n    getSecretP2PKPubkey: function (secret: string): string {\n      try {\n        const secretObject = JSON.parse(secret);\n        if (secretObject[0] != \"P2PK\" || secretObject[1][\"data\"] == undefined) {\n          console.log(\"not p2pk locked\");\n          return \"\"; // not p2pk locked\n        }\n        // Get all the p2pk secret data\n        const now = Math.floor(Date.now() / 1000); // unix TS\n        const { data, tags } = secretObject[1];\n        const locktimeTag = tags && tags.find((tag) => tag[0] === \"locktime\");\n        const locktime = locktimeTag ? parseInt(locktimeTag[1], 10) : Infinity; // Permanent lock if not set\n        const refundTag = tags && tags.find((tag) => tag[0] === \"refund\");\n        const refundKeys =\n          refundTag && refundTag.length > 1 ? refundTag.slice(1) : [];\n        const pubkeysTag = tags && tags.find((tag) => tag[0] === \"pubkeys\");\n        const pubkeys =\n          pubkeysTag && pubkeysTag.length > 1 ? pubkeysTag.slice(1) : [];\n        const n_sigsTag = tags && tags.find((tag) => tag[0] === \"n_sigs\");\n        const n_sigs = n_sigsTag ? parseInt(n_sigsTag[1], 10) : undefined;\n        // If locktime is in the future, return first owned additional 'pubkeys'\n        // match if multisig ('n_sigs'), otherwise return the main key ('data')\n        if (locktime > now) {\n          console.log(\"p2pk token - locktime is active\");\n          if (n_sigs && n_sigs >= 1) {\n            for (const pk of pubkeys) {\n              if (this.haveThisKey(pk)) return pk;\n            }\n          }\n          return data; // Main lock key (shows locked state)\n        }\n        // If locktime expired, return first owned 'refund' key match or\n        // or just return the first refund key to show token is locked\n        if (refundKeys.length > 0) {\n          console.log(\"p2pk token - locked to refund keys\");\n          for (const pk of refundKeys) {\n            if (this.haveThisKey(pk)) return pk;\n          }\n          return refundKeys[0]; // First refund key (shows locked state)\n        }\n        console.log(\"p2pk token - lock has expired\");\n      } catch {}\n      return \"\"; // Token is not locked / secret is not P2PK\n    },\n    isLocked: function (proofs: WalletProof[]) {\n      const secrets = proofs.map((p) => p.secret);\n      for (const secret of secrets) {\n        try {\n          if (this.getSecretP2PKPubkey(secret)) {\n            return true;\n          }\n        } catch {}\n      }\n      return false;\n    },\n    isLockedToUs: function (proofs: WalletProof[]) {\n      const secrets = proofs.map((p) => p.secret);\n      for (const secret of secrets) {\n        const pubkey = this.getSecretP2PKPubkey(secret);\n        if (pubkey) {\n          return this.haveThisKey(pubkey);\n        }\n      }\n    },\n    getPrivateKeyForP2PKEncodedToken: async function (\n      encodedToken: string\n    ): Promise<string> {\n      const decodedToken = await token.decodeFull(encodedToken);\n      if (!decodedToken) {\n        return \"\";\n      }\n      const proofs = token.getProofs(decodedToken);\n      if (!this.isLocked(proofs) || !this.isLockedToUs(proofs)) {\n        return \"\";\n      }\n\n      const secrets = proofs.map((p) => p.secret);\n      for (const secret of secrets) {\n        const pubkey = this.getSecretP2PKPubkey(secret);\n        if (pubkey && this.haveThisKey(pubkey)) {\n          // NOTE: we assume all tokens are locked to the same key here!\n          return this.p2pkKeys.filter((m) => m.publicKey == pubkey)[0]\n            .privateKey;\n        }\n      }\n      return \"\";\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/payment-request.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { useWalletStore } from \"./wallet\";\nimport {\n  decodePaymentRequest,\n  PaymentRequest,\n  PaymentRequestPayload,\n  PaymentRequestTransport,\n  PaymentRequestTransportType,\n} from \"@cashu/cashu-ts\";\nimport { useMintsStore } from \"./mints\";\nimport { useSendTokensStore } from \"./sendTokensStore\";\nimport { useNostrStore } from \"./nostr\";\nimport { useTokensStore } from \"./tokens\";\nimport type { HistoryToken } from \"./tokens\";\nimport token from \"src/js/token\";\nimport {\n  notify,\n  notifyError,\n  notifySuccess,\n  notifyWarning,\n} from \"src/js/notify\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nexport type OurPaymentRequest = {\n  id: string; // UUID from PaymentRequest\n  encoded: string;\n  unit?: string;\n  mints?: string[];\n  memo?: string;\n  createdAt: string;\n  receivedPaymentIds: string[]; // HistoryToken ids mapped to this PR\n};\n\nexport const usePRStore = defineStore(\"payment-request\", {\n  state: () => ({\n    showPRDialog: false,\n    showPRKData: \"\" as string,\n    enablePaymentRequest: useLocalStorage<boolean>(\"cashu.pr.enable\", true),\n    receivePaymentRequestsAutomatically: useLocalStorage<boolean>(\n      \"cashu.pr.receive\",\n      false\n    ),\n    ourPaymentRequests: useLocalStorage<OurPaymentRequest[]>(\n      \"cashu.pr.ours\",\n      []\n    ),\n    selectedPRIndex: useLocalStorage<number>(\"cashu.pr.selected_index\", 0),\n  }),\n  getters: {\n    currentPaymentRequest(state): OurPaymentRequest | undefined {\n      if (!state.ourPaymentRequests.length) return undefined;\n      const idx = Math.min(\n        Math.max(0, state.selectedPRIndex ?? 0),\n        state.ourPaymentRequests.length - 1\n      );\n      return state.ourPaymentRequests[idx];\n    },\n  },\n  actions: {\n    newPaymentRequest(\n      amount?: number,\n      memo?: string,\n      mintUrl?: string,\n      forceNew: boolean = false\n    ) {\n      const walletStore = useWalletStore();\n      // If not forcing a new request and we already have at least one,\n      // do not auto-create a new one; just show the currently selected.\n      if (!forceNew && this.ourPaymentRequests.length > 0) {\n        const current =\n          this.currentPaymentRequest || this.ourPaymentRequests[0];\n        this.showPRKData = current.encoded;\n        return;\n      }\n      this.showPRKData = this.createPaymentRequest(amount, memo, mintUrl);\n    },\n    createPaymentRequest: function (\n      amount?: number,\n      memo?: string,\n      mintUrl?: string\n    ) {\n      const nostrStore = useNostrStore();\n      const mintStore = useMintsStore();\n      const tags = [[\"n\", \"17\"]];\n      const transport = [\n        {\n          type: PaymentRequestTransportType.NOSTR,\n          target: nostrStore.seedSignerNprofile,\n          tags: tags,\n        },\n      ] as PaymentRequestTransport[];\n      const uuid = uuidv4().split(\"-\")[0];\n      const paymentRequest = new PaymentRequest(\n        transport,\n        uuid,\n        amount,\n        mintStore.activeUnit,\n        mintUrl?.length\n          ? mintStore.activeMintUrl\n            ? [mintStore.activeMintUrl]\n            : undefined\n          : undefined,\n        memo\n      );\n      const encoded = paymentRequest.toEncodedRequest();\n      this.ensureStoredRequest(paymentRequest, encoded, memo);\n      this.showPRKData = encoded;\n      return encoded;\n    },\n    ensureStoredRequest(\n      request: PaymentRequest,\n      encoded: string,\n      memo?: string\n    ) {\n      const existIdx = this.ourPaymentRequests.findIndex(\n        (r) => r.id === request.id\n      );\n      const entry: OurPaymentRequest = {\n        id: request.id,\n        encoded,\n        unit: request.unit,\n        mints: request.mints,\n        memo,\n        createdAt: new Date().toISOString(),\n        receivedPaymentIds: [],\n      };\n      if (existIdx >= 0) {\n        // Update encoded/memo/unit/mints in case changed\n        this.ourPaymentRequests[existIdx] = {\n          ...this.ourPaymentRequests[existIdx],\n          ...entry,\n        };\n        this.selectedPRIndex = existIdx;\n      } else {\n        this.ourPaymentRequests.push(entry);\n        this.selectedPRIndex = this.ourPaymentRequests.length - 1;\n      }\n    },\n    selectPrevRequest() {\n      if (!this.ourPaymentRequests.length) return;\n      this.selectedPRIndex =\n        (this.selectedPRIndex - 1 + this.ourPaymentRequests.length) %\n        this.ourPaymentRequests.length;\n      this.showPRKData = this.ourPaymentRequests[this.selectedPRIndex].encoded;\n    },\n    selectNextRequest() {\n      if (!this.ourPaymentRequests.length) return;\n      this.selectedPRIndex =\n        (this.selectedPRIndex + 1) % this.ourPaymentRequests.length;\n      this.showPRKData = this.ourPaymentRequests[this.selectedPRIndex].encoded;\n    },\n    selectRequestByIndex(index: number) {\n      if (!this.ourPaymentRequests.length) return;\n      const idx = Math.min(\n        Math.max(0, index),\n        this.ourPaymentRequests.length - 1\n      );\n      this.selectedPRIndex = idx;\n      this.showPRKData = this.ourPaymentRequests[idx].encoded;\n    },\n    registerIncomingPaymentForRequest(\n      requestId: string,\n      historyTokenId: string\n    ) {\n      const pr = this.ourPaymentRequests.find((r) => r.id === requestId);\n      if (!pr) return;\n      if (!pr.receivedPaymentIds.includes(historyTokenId)) {\n        pr.receivedPaymentIds.push(historyTokenId);\n      }\n    },\n    getPaymentsForRequest(requestId: string) {\n      const tokensStore = useTokensStore();\n      const pr = this.ourPaymentRequests.find((r) => r.id === requestId);\n      if (!pr) return [];\n      return pr.receivedPaymentIds\n        .map((id) => tokensStore.historyTokens.find((t) => t.id === id))\n        .filter((t): t is HistoryToken => !!t);\n    },\n    async decodePaymentRequest(pr: string) {\n      console.log(\"decodePaymentRequest\", pr);\n      const request: PaymentRequest = decodePaymentRequest(pr);\n      console.log(\"decodePaymentRequest\", request);\n      const mintsStore = useMintsStore() as any;\n      // activate the mint in the payment request\n      if (request.mints && request.mints.length > 0) {\n        let foundMint = false;\n        for (const mint of request.mints) {\n          if (mintsStore.mints.find((m) => m.url == mint)) {\n            // await mintsStore.activateMintUrl(mint, false, false, request.unit);\n            mintsStore.activeMintUrl = mint;\n            foundMint = true;\n            break;\n          }\n        }\n        if (!foundMint) {\n          notifyError(`This payment requires using the mint: ${request.mints}`);\n          throw new Error(\n            `This payment requires using the mint: ${request.mints}`\n          );\n        }\n      }\n\n      // activate the unit in the payment request\n      if (request.unit) {\n        // if the activeMint() supports this unit, set it\n        if (mintsStore.activeMint().units.find((u) => u == request.unit)) {\n          mintsStore.activeUnit = request.unit;\n        } else {\n          notifyWarning(\n            `The mint does not support the unit in the payment request: ${request.unit}`\n          );\n        }\n      }\n\n      const sendTokenStore = useSendTokensStore();\n      if (!sendTokenStore.showSendTokens) {\n        // if the sendtokendialog is not currently open, clear all data and then show the send dialog\n        sendTokenStore.clearSendData();\n      }\n      // if the payment request has an amount, set it\n      if (request.amount) {\n        sendTokenStore.sendData.amount =\n          request.amount / mintsStore.activeUnitCurrencyMultiplyer;\n      }\n      // Also make sure this decoded request gets stored (e.g., if user pasted an older one)\n      try {\n        const encoded = pr;\n        this.ensureStoredRequest(request, encoded);\n        this.showPRKData = encoded;\n      } catch (e) {\n        // noop\n      }\n      sendTokenStore.sendData.paymentRequest = request;\n      if (!sendTokenStore.showSendTokens) {\n        // show the send dialog\n        sendTokenStore.showSendTokens = true;\n      }\n    },\n    async parseAndPayPaymentRequest(\n      request: PaymentRequest,\n      tokenStr: string\n    ): Promise<boolean> {\n      const transports: PaymentRequestTransport[] = request.transport ?? [];\n      for (const transport of transports) {\n        if (transport.type == PaymentRequestTransportType.NOSTR) {\n          return await this.payNostrPaymentRequest(\n            request,\n            transport,\n            tokenStr\n          );\n        }\n        if (transport.type == PaymentRequestTransportType.POST) {\n          return await this.payPostPaymentRequest(request, transport, tokenStr);\n        }\n      }\n      throw new Error(\"Unsupported payment request transport.\");\n    },\n    async payNostrPaymentRequest(\n      request: PaymentRequest,\n      transport: PaymentRequestTransport,\n      tokenStr: string\n    ): Promise<boolean> {\n      console.log(\"payNostrPaymentRequest\", request, tokenStr);\n      console.log(\"transport\", transport);\n      const nostrStore = useNostrStore();\n      const decodedToken = await token.decodeFull(tokenStr);\n      if (!decodedToken) {\n        console.error(\"could not decode token\");\n        throw new Error(\"Could not decode ecash token.\");\n      }\n      const proofs = token.getProofs(decodedToken);\n      const mint = token.getMint(decodedToken);\n      const paymentPayload: PaymentRequestPayload = {\n        id: request.id,\n        mint: mint,\n        unit: request.unit || \"\",\n        proofs: proofs,\n      };\n      const paymentPayloadString = JSON.stringify(paymentPayload);\n      try {\n        await nostrStore.sendNip17DirectMessageToNprofile(\n          transport.target,\n          paymentPayloadString\n        );\n      } catch (error) {\n        console.error(\"Error paying payment request:\", error);\n        throw error;\n      }\n      notifySuccess(\"Payment sent\");\n      return true;\n    },\n    async payPostPaymentRequest(\n      request: PaymentRequest,\n      transport: PaymentRequestTransport,\n      tokenStr: string\n    ): Promise<boolean> {\n      console.log(\"payPostPaymentRequest\", request, tokenStr);\n      // get the endpoint from the transport target and make an HTTP POST request with the paymentPayload as the body\n      const decodedToken = await token.decodeFull(tokenStr);\n      if (!decodedToken) {\n        console.error(\"could not decode token\");\n        throw new Error(\"Could not decode ecash token.\");\n      }\n      const proofs = token.getProofs(decodedToken);\n      const unit = token.getUnit(decodedToken);\n      const mint = token.getMint(decodedToken);\n      const paymentPayload: PaymentRequestPayload = {\n        id: request.id,\n        mint: mint,\n        unit: unit,\n        proofs: proofs,\n      };\n      const paymentPayloadString = JSON.stringify(paymentPayload);\n      try {\n        const response = await fetch(transport.target, {\n          headers: {\n            \"Content-Type\": \"application/json\",\n          },\n          method: \"POST\",\n          body: paymentPayloadString,\n        });\n        if (!response.ok) {\n          console.error(\"Error paying payment request:\", response.statusText);\n          throw new Error(response.statusText);\n        }\n        notifySuccess(\"Payment sent\");\n      } catch (error) {\n        console.error(\"Error paying payment request:\", error);\n        throw error;\n      }\n      return true;\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/price.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { useSettingsStore } from \"./settings\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport {\n  notifyApiError,\n  notifyError,\n  notifySuccess,\n  notifyWarning,\n  notify,\n} from \"../js/notify\";\nimport axios from \"axios\";\n\nexport const usePriceStore = defineStore(\"price\", {\n  state: () => ({\n    bitcoinPrice: useLocalStorage(\"cashu.price.bitcoinPrice\", 0 as number),\n    bitcoinPriceLastUpdated: useLocalStorage(\n      \"cashu.price.bitcoinPriceLastUpdated\",\n      0 as number\n    ),\n    bitcoinPriceMinRefreshInterval: 60_000,\n    bitcoinPrices: useLocalStorage(\n      \"cashu.price.bitcoinPrices\",\n      {} as Record<string, number>\n    ),\n  }),\n  actions: {\n    fetchBitcoinPrice: async function () {\n      const settingsStore = useSettingsStore();\n      if (!settingsStore.getBitcoinPrice) {\n        this.bitcoinPrice = 0;\n        this.bitcoinPriceLastUpdated = 0;\n        this.bitcoinPrices = {};\n        console.log(\"Not fetching bitcoin price, disabled in settings\");\n        return;\n      }\n      if (\n        Date.now() - this.bitcoinPriceLastUpdated <\n        this.bitcoinPriceMinRefreshInterval\n      ) {\n        console.log(\n          `Not fetching bitcoin price, last updated ${\n            Date.now() - this.bitcoinPriceLastUpdated\n          }ms ago: ${this.bitcoinPrice}`\n        );\n        return;\n      }\n      try {\n        const { data } = await axios.get(\n          \"https://api.coinbase.com/v2/exchange-rates?currency=BTC\"\n        );\n        this.bitcoinPrices = data.data.rates;\n        // Update the main bitcoinPrice to current selected currency for backward compatibility\n        this.bitcoinPrice =\n          data.data.rates[settingsStore.bitcoinPriceCurrency] ||\n          data.data.rates.USD;\n        this.bitcoinPriceLastUpdated = Date.now();\n      } catch (error) {\n        console.error(\"Failed to fetch bitcoin price:\", error);\n        notifyError(\"Failed to fetch bitcoin price\");\n      }\n    },\n    updateBitcoinPriceForCurrentCurrency: function () {\n      const settingsStore = useSettingsStore();\n      // Update the main bitcoinPrice to reflect the current selected currency\n      this.bitcoinPrice =\n        this.bitcoinPrices[settingsStore.bitcoinPriceCurrency] ||\n        this.bitcoinPrice;\n    },\n  },\n  getters: {\n    currentCurrencyPrice(): number {\n      const settingsStore = useSettingsStore();\n      return (\n        this.bitcoinPrices[settingsStore.bitcoinPriceCurrency] ||\n        this.bitcoinPrice\n      );\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/proofs.ts",
    "content": "import { ref } from \"vue\";\nimport { defineStore } from \"pinia\";\nimport { useMintsStore, WalletProof } from \"./mints\";\nimport { cashuDb, CashuDexie, useDexieStore } from \"./dexie\";\nimport {\n  Proof,\n  getEncodedToken,\n  getEncodedTokenV4,\n  Token,\n} from \"@cashu/cashu-ts\";\nimport { liveQuery } from \"dexie\";\n\nexport const useProofsStore = defineStore(\"proofs\", {\n  state: () => {\n    const proofs = ref<WalletProof[]>([]);\n\n    liveQuery(() => cashuDb.proofs.toArray()).subscribe({\n      next: (newProofs) => {\n        proofs.value = newProofs;\n        updateActiveProofs();\n      },\n      error: (err) => {\n        console.error(err);\n      },\n    });\n\n    // Function to update activeProofs\n    const updateActiveProofs = async () => {\n      const mintStore = useMintsStore();\n      const currentMint = mintStore.mints.find(\n        (m) => m.url === mintStore.activeMintUrl\n      );\n      if (!currentMint) {\n        mintStore.activeProofs = [];\n        return;\n      }\n\n      const unitKeysets = currentMint?.keysets?.filter(\n        (k) => k.unit === mintStore.activeUnit\n      );\n      if (!unitKeysets || unitKeysets.length === 0) {\n        mintStore.activeProofs = [];\n        return;\n      }\n\n      const keysetIds = unitKeysets.map((k) => k.id);\n      const activeProofs = await cashuDb.proofs\n        .where(\"id\")\n        .anyOf(keysetIds)\n        .toArray()\n        .then((proofs) => {\n          return proofs.filter((p) => !p.reserved);\n        });\n      mintStore.activeProofs = activeProofs;\n    };\n\n    return {\n      proofs,\n      updateActiveProofs,\n    };\n  },\n  actions: {\n    sumProofs: function (proofs: Proof[]) {\n      return proofs.reduce((s, t) => (s += t.amount), 0);\n    },\n    getProofs: async function (): Promise<WalletProof[]> {\n      return await cashuDb.proofs.toArray();\n    },\n    setReserved: async function (\n      proofs: Proof[],\n      reserved: boolean = true,\n      quote?: string\n    ) {\n      const setQuote: string | undefined = reserved ? quote : undefined;\n      await cashuDb.transaction(\"rw\", cashuDb.proofs, async () => {\n        for (const p of proofs) {\n          await cashuDb.proofs\n            .where(\"secret\")\n            .equals(p.secret)\n            .modify((pr) => {\n              pr.reserved = reserved;\n              pr.quote = setQuote;\n            });\n        }\n      });\n    },\n    proofsToWalletProofs(proofs: Proof[], quote?: string): WalletProof[] {\n      return proofs.map((p) => {\n        return {\n          ...p,\n          reserved: false,\n          quote: quote,\n        } as WalletProof;\n      });\n    },\n    async addProofs(proofs: Proof[], quote?: string) {\n      const walletProofs = this.proofsToWalletProofs(proofs);\n      await cashuDb.transaction(\"rw\", cashuDb.proofs, async () => {\n        walletProofs.forEach(async (p) => {\n          await cashuDb.proofs.add(p);\n        });\n      });\n    },\n    async removeProofs(proofs: Proof[]) {\n      const walletProofs = this.proofsToWalletProofs(proofs);\n      await cashuDb.transaction(\"rw\", cashuDb.proofs, async () => {\n        walletProofs.forEach(async (p) => {\n          await cashuDb.proofs.delete(p.secret);\n        });\n      });\n    },\n    async getProofsForQuote(quote: string): Promise<WalletProof[]> {\n      return await cashuDb.proofs.where(\"quote\").equals(quote).toArray();\n    },\n    getUnreservedProofs: function (proofs: WalletProof[]) {\n      return proofs.filter((p) => !p.reserved);\n    },\n    serializeProofs: function (proofs: Proof[]): string {\n      const mintStore = useMintsStore();\n      // unique keyset IDs of proofs\n      const uniqueIds = [...new Set(proofs.map((p) => p.id))];\n      // keysets with these uniqueIds\n      const keysets = mintStore.mints.flatMap((m) =>\n        m.keysets.filter((k) => uniqueIds.includes(k.id))\n      );\n      if (keysets.length === 0) {\n        throw new Error(\"No keysets found for proofs\");\n      }\n      // mints that have any of the keyset.id\n      const mints = mintStore.mints.filter((m) =>\n        m.keysets.some((k) => uniqueIds.includes(k.id))\n      );\n      if (mints.length === 0) {\n        throw new Error(\"No mints found for proofs\");\n      }\n      // unit of keysets\n      const unit = keysets[0].unit;\n      const token = {\n        mint: mints[0].url,\n        proofs: proofs,\n        unit: unit,\n      } as Token;\n      try {\n        return getEncodedTokenV4(token);\n      } catch (e) {\n        console.log(\"Could not encode TokenV4, defaulting to TokenV3\", e);\n        return getEncodedToken(token);\n      }\n\n      // // what we put into the JSON\n      // let mintsJson = mints.map((m) => [{ url: m.url, ids: m.keysets }][0]);\n      // let tokenV3 = {\n      //   token: [{ proofs: proofs, mint: mintsJson[0].url }],\n      //   unit: unit,\n      // };\n      // return \"cashuA\" + btoa(JSON.stringify(tokenV3));\n    },\n    getProofsMint: function (proofs: WalletProof[]) {\n      const mintStore = useMintsStore();\n      // unique keyset IDs of proofs\n      const uniqueIds = [...new Set(proofs.map((p) => p.id))];\n      // mints that have any of the keyset IDs\n      const mints_keysets = mintStore.mints.filter((m) =>\n        m.keysets.some((k) => uniqueIds.includes(k.id))\n      );\n      // what we put into the JSON\n      const mints = mints_keysets.map(\n        (m) => [{ url: m.url, ids: m.keysets }][0]\n      );\n      return mints[0];\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/receiveTokensStore.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { StoredMint, useMintsStore } from \"./mints\";\nimport { useUiStore } from \"./ui\";\nimport { useP2PKStore } from \"./p2pk\";\nimport { useWalletStore } from \"./wallet\";\nimport token from \"src/js/token\";\nimport { useTokensStore } from \"./tokens\";\nimport {\n  notifyError,\n  notifySuccess,\n  notify,\n  notifyWarning,\n} from \"../js/notify\";\nimport { getDecodedTokenBinary, getEncodedToken, Token } from \"@cashu/cashu-ts\";\nimport { useSwapStore } from \"./swap\";\n\nexport const useReceiveTokensStore = defineStore(\"receiveTokensStore\", {\n  state: () => ({\n    showReceiveTokens: false,\n    watchClipboardPaste: false,\n    receiveData: {\n      tokensBase64: \"\",\n      p2pkPrivateKey: \"\",\n    },\n    scanningCard: false,\n  }),\n  actions: {\n    decodeToken: function (encodedToken: string) {\n      let decodedToken = undefined;\n      try {\n        decodedToken = token.decode(encodedToken);\n      } catch (error) {}\n      return decodedToken;\n    },\n    knowThisMintOfTokenJson: function (tokenJson: Token) {\n      const mintStore = useMintsStore();\n      const uniqueIds = [\n        ...new Set(token.getProofs(tokenJson).map((p) => p.id)),\n      ];\n      return mintStore.mints\n        .map((m) => m.url)\n        .includes(token.getMint(tokenJson));\n    },\n    receiveToken: async function (encodedToken: string) {\n      const mintStore = useMintsStore();\n      const walletStore = useWalletStore();\n      const receiveStore = useReceiveTokensStore();\n      const uiStore = useUiStore();\n      console.log(\"### receive tokens\", receiveStore.receiveData.tokensBase64);\n\n      if (receiveStore.receiveData.tokensBase64.length == 0) {\n        throw new Error(\"no tokens provided.\");\n      }\n\n      // get the private key for the token we want to receive if it is locked with P2PK\n      receiveStore.receiveData.p2pkPrivateKey =\n        await useP2PKStore().getPrivateKeyForP2PKEncodedToken(\n          receiveStore.receiveData.tokensBase64\n        );\n\n      const tokenJson = await token.decodeFull(\n        receiveStore.receiveData.tokensBase64\n      );\n      if (tokenJson == undefined) {\n        throw new Error(\"no tokens provided.\");\n      }\n      // check if we have all mints\n      if (!this.knowThisMintOfTokenJson(tokenJson)) {\n        // add the mint\n        await mintStore.addMint({ url: token.getMint(tokenJson) });\n      }\n      // redeem the token\n      await walletStore.redeem();\n      receiveStore.showReceiveTokens = false;\n      uiStore.closeDialogs();\n    },\n    receiveIfDecodes: async function () {\n      try {\n        const decodedToken = this.decodeToken(this.receiveData.tokensBase64);\n        if (decodedToken) {\n          await this.receiveToken(this.receiveData.tokensBase64);\n          return true;\n        }\n      } catch (error) {\n        console.error(error);\n        return false;\n      }\n    },\n    meltTokenToMint: async function (encodedToken: string, mint: StoredMint) {\n      const receiveStore = useReceiveTokensStore();\n      const mintStore = useMintsStore();\n      const uiStore = useUiStore();\n      const tokenJson = await token.decodeFull(encodedToken);\n      if (tokenJson == undefined) {\n        throw new Error(\"no tokens provided.\");\n      }\n      // check if we have all mints\n      if (!this.knowThisMintOfTokenJson(tokenJson)) {\n        // add the mint\n        await mintStore.addMint({ url: token.getMint(tokenJson) });\n      }\n      await useSwapStore().meltProofsToMint(tokenJson, mint);\n      receiveStore.showReceiveTokens = false;\n      uiStore.closeDialogs();\n    },\n    pasteToParseDialog: async function (verbose = false) {\n      const text = await useUiStore().pasteFromClipboard();\n      if (this.decodeToken(text)) {\n        const tokensStore = useTokensStore();\n        const historyToken = tokensStore.tokenAlreadyInHistory(text);\n\n        if (\n          historyToken &&\n          (historyToken.amount > 0 || historyToken.status === \"paid\")\n        ) {\n          if (verbose) notify(\"Token already in history.\");\n          return false;\n        }\n        this.receiveData.tokensBase64 = text;\n        return true;\n      } else {\n        // notifyWarning(\"Invalid token\");\n        return false;\n      }\n    },\n    toggleScanner: function () {\n      const receiveStore = useReceiveTokensStore();\n      const tokenStore = useTokensStore();\n      const uiStore = useUiStore();\n      if (this.scanningCard === false) {\n        try {\n          this.ndef = new window.NDEFReader();\n          this.controller = new AbortController();\n          const signal = this.controller.signal;\n          this.ndef\n            .scan({ signal })\n            .then(() => {\n              console.log(\"> Scan started\");\n\n              this.ndef.addEventListener(\"readingerror\", () => {\n                console.error(\"Cannot read data from the NFC tag.\");\n                notifyError(\"Cannot read data from the NFC tag.\");\n                this.controller.abort();\n                this.scanningCard = false;\n              });\n\n              this.ndef.addEventListener(\n                \"reading\",\n                ({ message, serialNumber }) => {\n                  try {\n                    const record = message.records[0];\n                    const recordType = record.recordType;\n                    let tokenStr = \"\";\n                    switch (recordType) {\n                      case \"text\": {\n                        const text = new TextDecoder().decode(record.data);\n                        if (!text.startsWith(\"cashu\")) {\n                          throw new Error(\n                            \"text does not contain a cashu token\"\n                          );\n                        }\n                        tokenStr = text;\n                        break;\n                      }\n                      case \"url\": {\n                        const url = new TextDecoder().decode(record.data);\n                        const i = url.indexOf(\"#token=cashu\");\n                        if (i === -1) {\n                          throw new Error(\"URL does not contain a cashu token\");\n                        }\n                        tokenStr = url.substring(i + 7);\n                        break;\n                      }\n                      case \"mime\": {\n                        if (record.mediaType !== \"application/octet-stream\") {\n                          throw new Error(\"binary data expected\");\n                        }\n                        const data = new Uint8Array(record.data.buffer);\n                        const prefix = String.fromCharCode(...data.slice(0, 4));\n                        if (prefix !== \"craw\") {\n                          throw new Error(\n                            \"binary data does not contain a cashu token\"\n                          );\n                        }\n                        const token = getDecodedTokenBinary(data);\n                        tokenStr = getEncodedToken(token);\n                        break;\n                      }\n                      default:\n                        throw new Error(`unsupported recordType ${recordType}`);\n                    }\n                    const historyToken =\n                      tokenStore.tokenAlreadyInHistory(tokenStr);\n                    if (!historyToken || historyToken.status === \"pending\") {\n                      receiveStore.receiveData.tokensBase64 = tokenStr;\n                      receiveStore.showReceiveTokens = true;\n                      uiStore.closeDialogs();\n                    } else {\n                      notify(\"Token already in history.\");\n                    }\n                  } catch (err) {\n                    console.error(`Something went wrong! ${err}`);\n                    notifyError(`Something went wrong! ${err}`);\n                  }\n                  this.controller.abort();\n                  this.scanningCard = false;\n                }\n              );\n              this.scanningCard = true;\n            })\n            .catch((error) => {\n              console.error(`Scan error: ${error.message}`);\n              notifyError(`Scan error: ${error.message}`);\n            });\n        } catch (error) {\n          console.error(`NFC error: ${error.message}`);\n          notifyError(`NFC error: ${error.message}`);\n        }\n      } else {\n        this.controller.abort();\n        this.scanningCard = false;\n      }\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/restore.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { generateSecretKey, getPublicKey } from \"nostr-tools\";\nimport { bytesToHex } from \"@noble/hashes/utils\"; // already an installed dependency\nimport { useWalletStore } from \"./wallet\";\nimport { Mint, Wallet, CheckStateEnum, Proof } from \"@cashu/cashu-ts\";\nimport { useMintsStore } from \"./mints\";\nimport { notify, notifyError, notifySuccess } from \"src/js/notify\";\nimport { useUiStore } from \"./ui\";\nimport { useProofsStore } from \"./proofs\";\nimport { i18n } from \"../boot/i18n\";\n\nconst BATCH_SIZE = 200;\nconst MAX_GAP = 2;\n\nexport const useRestoreStore = defineStore(\"restore\", {\n  state: () => ({\n    showRestoreDialog: useLocalStorage<boolean>(\n      \"cashu.restore.showRestoreDialog\",\n      false\n    ),\n    restoringState: false,\n    restoringMint: \"\",\n    mnemonicToRestore: useLocalStorage<string>(\n      \"cashu.restore.mnemonicToRestore\",\n      \"\"\n    ),\n    restoreProgress: 0,\n    restoreCounter: 0,\n    restoreStatus: \"\",\n  }),\n  getters: {},\n  actions: {\n    restoreMint: async function (url: string) {\n      this.restoringState = true;\n      this.restoringMint = url;\n      this.restoreProgress = 0;\n      this.restoreCounter = 0;\n      this.restoreStatus = \"\";\n      try {\n        await this._restoreMint(url);\n      } catch (error) {\n        notifyError(\n          i18n.global.t(\"restore.restore_mint_error_text\", { error })\n        );\n      } finally {\n        this.restoringState = false;\n        this.restoringMint = \"\";\n        this.restoreProgress = 0;\n      }\n    },\n    _restoreMint: async function (url: string) {\n      if (this.mnemonicToRestore.length === 0) {\n        notifyError(i18n.global.t(\"restore.mnemonic_error_text\"));\n        return;\n      }\n      this.restoreProgress = 0;\n      const walletStore = useWalletStore();\n      const proofsStore = useProofsStore();\n      const mintStore = useMintsStore();\n      await mintStore.activateMintUrl(url);\n\n      const mnemonic = this.mnemonicToRestore;\n      this.restoreStatus = i18n.global.t(\"restore.prepare_info_text\");\n      const mint = new Mint(url);\n      const keysets = (await mint.getKeySets()).keysets;\n      let restoredSomething = false;\n\n      // Calculate total steps for progress calculation\n      let totalSteps = keysets.length * MAX_GAP;\n      let currentStep = 0;\n\n      for (const keyset of keysets) {\n        console.log(`Restoring keyset ${keyset.id} with unit ${keyset.unit}`);\n        const bip39Seed = walletStore.mnemonicToSeedSync(mnemonic);\n        const wallet = new Wallet(mint, {\n          bip39seed: bip39Seed,\n          unit: keyset.unit,\n        });\n        await wallet.loadMint();\n        let start = 0;\n        let emptyBatchCount = 0;\n        let restoreProofs: Proof[] = [];\n\n        while (emptyBatchCount < MAX_GAP) {\n          console.log(`Restoring proofs ${start} to ${start + BATCH_SIZE}`);\n          let proofs: Proof[] = [];\n          try {\n            proofs = (\n              await wallet.restore(start, BATCH_SIZE, { keysetId: keyset.id })\n            ).proofs;\n          } catch (error) {\n            console.error(`Error restoring proofs: ${error}`);\n            proofs = [];\n          }\n          if (proofs.length === 0) {\n            console.log(`No proofs found for keyset ${keyset.id}`);\n            emptyBatchCount++;\n          } else {\n            console.log(\n              `> Restored ${proofs.length} proofs with sum ${proofs.reduce(\n                (s, p) => s + p.amount,\n                0\n              )}`\n            );\n            restoreProofs = restoreProofs.concat(proofs);\n            emptyBatchCount = 0;\n            this.restoreCounter += proofs.length;\n            totalSteps += 1;\n          }\n          this.restoreStatus = i18n.global.t(\n            \"restore.restored_proofs_for_keyset_info_text\",\n            {\n              restoreCounter: this.restoreCounter,\n              keysetId: keyset.id,\n            }\n          );\n          start += BATCH_SIZE;\n\n          currentStep++;\n          this.restoreProgress = currentStep / totalSteps;\n        }\n\n        let restoredProofs: Proof[] = [];\n        for (let i = 0; i < restoreProofs.length; i += BATCH_SIZE) {\n          this.restoreStatus = i18n.global.t(\n            \"restore.checking_proofs_for_keyset_info_text\",\n            {\n              startIndex: i,\n              endIndex: i + BATCH_SIZE,\n              keysetId: keyset.id,\n            }\n          );\n          const checkRestoreProofs = restoreProofs.slice(i, i + BATCH_SIZE);\n          const proofStates = await wallet.checkProofsStates(\n            checkRestoreProofs\n          );\n          const spentProofs = checkRestoreProofs.filter(\n            (p, i) => proofStates[i].state === CheckStateEnum.SPENT\n          );\n          const spentProofsSecrets = spentProofs.map((p) => p.secret);\n          const unspentProofs = checkRestoreProofs.filter(\n            (p) => !spentProofsSecrets.includes(p.secret)\n          );\n          if (unspentProofs.length > 0) {\n            console.log(\n              `Found ${\n                unspentProofs.length\n              } unspent proofs with sum ${unspentProofs.reduce(\n                (s, p) => s + p.amount,\n                0\n              )}`\n            );\n          }\n          const newProofs = unspentProofs.filter(\n            (p) => !proofsStore.proofs.some((pr) => pr.secret === p.secret)\n          );\n          await useProofsStore().addProofs(newProofs);\n          restoredProofs = restoredProofs.concat(newProofs);\n          currentStep++;\n          this.restoreProgress = currentStep / totalSteps;\n        }\n        const restoredAmount = restoredProofs.reduce((s, p) => s + p.amount, 0);\n        const restoredAmountStr = useUiStore().formatCurrency(\n          restoredAmount,\n          keyset.unit\n        );\n        if (restoredAmount > 0) {\n          notifySuccess(\n            i18n.global.t(\"restore.restored_amount_success_text\", {\n              amount: restoredAmountStr,\n            })\n          );\n          restoredSomething = true;\n        }\n      }\n      if (!restoredSomething) {\n        notify(i18n.global.t(\"restore.no_proofs_info_text\"));\n      }\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/sendTokensStore.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { decodePaymentRequest, PaymentRequest } from \"@cashu/cashu-ts\";\nimport { HistoryToken } from \"./tokens\";\n\nexport const useSendTokensStore = defineStore(\"sendTokensStore\", {\n  state: () => ({\n    showSendTokens: false,\n    showLockInput: false,\n    sendData: {\n      amount: null,\n      historyAmount: null,\n      memo: \"\",\n      tokens: \"\",\n      tokensBase64: \"\",\n      p2pkPubkey: \"\",\n      paymentRequest: undefined,\n      historyToken: undefined,\n    } as {\n      amount: number | null;\n      historyAmount: number | null;\n      memo: string;\n      tokens: string;\n      tokensBase64: string;\n      p2pkPubkey: string;\n      paymentRequest?: PaymentRequest;\n      historyToken: HistoryToken | undefined;\n    },\n  }),\n  actions: {\n    clearSendData() {\n      this.sendData.amount = null;\n      this.sendData.historyAmount = null;\n      this.sendData.memo = \"\";\n      this.sendData.tokens = \"\";\n      this.sendData.tokensBase64 = \"\";\n      this.sendData.p2pkPubkey = \"\";\n      this.sendData.paymentRequest = undefined;\n      this.sendData.historyToken = undefined;\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/settings.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { useLocalStorage } from \"@vueuse/core\";\n\nconst defaultNostrRelays = [\n  \"wss://relay.damus.io\",\n  \"wss://relay.8333.space/\",\n  \"wss://nos.lol\",\n  \"wss://relay.primal.net\",\n];\n\nexport const useSettingsStore = defineStore(\"settings\", {\n  state: () => {\n    return {\n      getBitcoinPrice: useLocalStorage<boolean>(\n        \"cashu.settings.getBitcoinPrice\",\n        true\n      ),\n      bitcoinPriceCurrency: useLocalStorage<string>(\n        \"cashu.settings.bitcoinPriceCurrency\",\n        \"USD\"\n      ),\n      checkSentTokens: useLocalStorage<boolean>(\n        \"cashu.settings.checkSentTokens\",\n        true\n      ),\n      checkIncomingInvoices: useLocalStorage<boolean>(\n        \"cashu.settings.checkIncomingInvoices\",\n        true\n      ),\n      periodicallyCheckIncomingInvoices: useLocalStorage<boolean>(\n        \"cashu.settings.periodicallyCheckIncomingInvoices\",\n        true\n      ),\n      checkInvoicesOnStartup: useLocalStorage<boolean>(\n        \"cashu.settings.checkInvoicesOnStartup\",\n        true\n      ),\n      useWebsockets: useLocalStorage<boolean>(\n        \"cashu.settings.useWebsockets\",\n        true\n      ),\n      defaultNostrRelays: useLocalStorage<string[]>(\n        \"cashu.settings.defaultNostrRelays\",\n        defaultNostrRelays\n      ),\n      includeFeesInSendAmount: useLocalStorage<boolean>(\n        \"cashu.settings.includeFeesInSendAmount\",\n        false\n      ),\n      nfcEncoding: useLocalStorage<string>(\n        \"cashu.settings.nfcEncoding\",\n        \"weburl\"\n      ),\n      useNumericKeyboard: useLocalStorage<boolean>(\n        \"cashu.settings.useNumericKeyboard\",\n        false\n      ),\n      enableReceiveSwaps: useLocalStorage<boolean>(\n        \"cashu.settings.enableReceiveSwaps\",\n        true\n      ),\n      showNfcButtonInDrawer: useLocalStorage(\n        \"cashu.ui.showNfcButtonInDrawer\",\n        true\n      ),\n      autoPasteEcashReceive: useLocalStorage(\n        \"cashu.settings.autoPasteEcashReceive\",\n        true\n      ),\n      auditorEnabled: useLocalStorage<boolean>(\n        \"cashu.settings.auditorEnabled\",\n        true\n      ),\n      auditorUrl: useLocalStorage<string>(\n        \"cashu.settings.auditorUrl\",\n        \"https://audit.8333.space\"\n      ),\n      auditorApiUrl: useLocalStorage<string>(\n        \"cashu.settings.auditorApiUrl\",\n        \"https://api.audit.8333.space\"\n      ),\n      bip177BitcoinSymbol: useLocalStorage<boolean>(\n        \"cashu.settings.bip177\",\n        true\n      ),\n      multinutEnabled: useLocalStorage<boolean>(\n        \"cashu.settings.multinutEnabled\",\n        false\n      ),\n      nostrMintBackupEnabled: useLocalStorage<boolean>(\n        \"cashu.settings.nostrMintBackupEnabled\",\n        true\n      ),\n    };\n  },\n});\n"
  },
  {
    "path": "src/stores/storage.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { useWalletStore } from \"./wallet\";\nimport { useMintsStore } from \"./mints\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { notifyError, notifySuccess } from \"../js/notify\";\nimport { useTokensStore } from \"./tokens\";\nimport { currentDateStr } from \"src/js/utils\";\nimport { useProofsStore } from \"./proofs\";\n\nexport const useStorageStore = defineStore(\"storage\", {\n  state: () => ({\n    lastLocalStorageCleanUp: useLocalStorage(\n      \"cashu.lastLocalStorageCleanUp\",\n      new Date()\n    ),\n  }),\n  actions: {\n    restoreFromBackup: async function (backup: any) {\n      const proofsStore = useProofsStore();\n      if (!backup) {\n        notifyError(\"Unrecognized Backup Format!\");\n      } else {\n        const keys = Object.keys(backup);\n        for (const key of keys) {\n          // we treat some keys differently *magic*\n          if (key === \"cashu.dexie.db.proofs\") {\n            const proofs = JSON.parse(backup[key]);\n            await proofsStore.addProofs(proofs);\n          } else {\n            localStorage.setItem(key, backup[key]);\n          }\n        }\n        notifySuccess(\"Backup restored\");\n        window.location.reload();\n      }\n    },\n    exportWalletState: async function () {\n      const jsonToSave: any = {};\n      for (let i = 0; i < localStorage.length; i++) {\n        const k = localStorage.key(i);\n        if (!k) {\n          continue;\n        }\n        const v = localStorage.getItem(k);\n        jsonToSave[k] = v;\n      }\n      // proofs table *magic*\n      const proofs = await useProofsStore().getProofs();\n      jsonToSave[\"cashu.dexie.db.proofs\"] = JSON.stringify(proofs);\n\n      const textToSave = JSON.stringify(jsonToSave);\n      const textToSaveAsBlob = new Blob([textToSave], {\n        type: \"text/plain\",\n      });\n      const textToSaveAsURL = window.URL.createObjectURL(textToSaveAsBlob);\n\n      const fileName = `cashu_me_backup_${currentDateStr()}.json`;\n      const downloadLink = document.createElement(\"a\");\n      downloadLink.download = fileName;\n      downloadLink.innerHTML = \"Download File\";\n      downloadLink.href = textToSaveAsURL;\n      downloadLink.onclick = function () {\n        document.body.removeChild(event.target);\n      };\n      downloadLink.style.display = \"none\";\n      document.body.appendChild(downloadLink);\n      downloadLink.click();\n      notifySuccess(\"Wallet backup exported\");\n    },\n    checkLocalStorage: async function () {\n      const needsCleanup = this.checkLocalStorageQuota();\n      if (needsCleanup) {\n        this.cleanUpLocalStorage(true);\n      } else {\n        this.cleanUpLocalStorageScheduler();\n      }\n    },\n    checkLocalStorageQuota: function (): boolean {\n      // determine if the user might have exceeded the local storage quota\n      // store 10kb of data in local storage to check if it fails\n      const localStorageSize = JSON.stringify(localStorage).length;\n      console.log(`Local storage size: ${localStorageSize} bytes`);\n      const data = new Array(10240).join(\"x\");\n      try {\n        localStorage.setItem(\"cashu.test\", data);\n        localStorage.removeItem(\"cashu.test\");\n        return false;\n      } catch (e) {\n        console.log(\"Local storage quota exceeded\");\n        notifyError(\n          \"Local storage quota exceeded. Clean up your local storage.\"\n        );\n        return true;\n      }\n    },\n    cleanUpLocalStorageScheduler: function () {\n      const cleanUpInterval = 1000 * 60 * 60 * 24 * 7; // 7 day\n      const lastCleanUp = this.lastLocalStorageCleanUp;\n      if (\n        !lastCleanUp ||\n        isNaN(new Date(lastCleanUp).getTime()) ||\n        new Date().getTime() - new Date(lastCleanUp).getTime() > cleanUpInterval\n      ) {\n        console.log(`Last clean up: ${lastCleanUp}, cleaning up local storage`);\n        this.cleanUpLocalStorage();\n      }\n    },\n    cleanUpLocalStorage: function (verbose = false) {\n      const walletStore = useWalletStore();\n      const tokenStore = useTokensStore();\n      const localStorageSizeBefore = JSON.stringify(localStorage).length;\n\n      // delete cashu.spentProofs from local storage\n      localStorage.removeItem(\"cashu.spentProofs\");\n\n      // from all paid invoices in this.invoiceHistory, delete the oldest so that only max 100 remain\n      const max_history = 200;\n      const paidInvoices = walletStore.invoiceHistory.filter(\n        (i) => i.status == \"paid\"\n      );\n\n      if (paidInvoices.length > max_history) {\n        const sortedInvoices = paidInvoices.sort((a, b) => {\n          return new Date(a.date).getTime() - new Date(b.date).getTime();\n        });\n        const deleteInvoices = sortedInvoices.slice(\n          0,\n          sortedInvoices.length - max_history\n        );\n        walletStore.invoiceHistory = walletStore.invoiceHistory.filter(\n          (i) => !deleteInvoices.includes(i)\n        );\n      }\n\n      // walk through the oldest paid tokenStore.historyTokens and delete the token\n      const paidTokens = tokenStore.historyTokens.filter(\n        (t) => t.status == \"paid\"\n      );\n\n      if (paidTokens.length > max_history) {\n        const sortedTokens = paidTokens.sort((a, b) => {\n          return new Date(a.date).getTime() - new Date(b.date).getTime();\n        });\n        const deleteTokens = sortedTokens.slice(\n          0,\n          sortedTokens.length - max_history\n        );\n        for (let i = 0; i < deleteTokens.length; i++) {\n          deleteTokens[i].token = undefined;\n        }\n      }\n\n      const localStorageSizeAfter = JSON.stringify(localStorage).length;\n      const localStorageSizeDiff =\n        localStorageSizeBefore - localStorageSizeAfter;\n      console.log(`Cleaned up ${localStorageSizeDiff} bytes of local storage`);\n      if (localStorageSizeDiff > 0 && verbose) {\n        notifySuccess(`Cleaned up ${localStorageSizeDiff} bytes`);\n      }\n      this.lastLocalStorageCleanUp = new Date();\n    },\n  },\n  getters: {\n    canPasteFromClipboard() {\n      return (\n        window.isSecureContext &&\n        navigator.clipboard &&\n        navigator.clipboard.readText\n      );\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/store-flag.d.ts",
    "content": "/* eslint-disable */\n// THIS FEATURE-FLAG FILE IS AUTOGENERATED,\n//  REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING\nimport \"quasar/dist/types/feature-flag\";\n\ndeclare module \"quasar/dist/types/feature-flag\" {\n  interface QuasarFeatureFlags {\n    store: true;\n  }\n}\n"
  },
  {
    "path": "src/stores/swap.ts",
    "content": "import { defineStore } from \"pinia\";\nimport {\n  PaymentRequest,\n  Token,\n  MeltQuoteBolt11Response,\n} from \"@cashu/cashu-ts\";\nimport { StoredMint, useMintsStore } from \"./mints\";\nimport { useWalletStore } from \"./wallet\";\nimport { useProofsStore } from \"./proofs\";\nimport { notifyError, notifyWarning } from \"../js/notify\";\nimport token from \"src/js/token\";\nimport { i18n } from \"../boot/i18n\";\n\n/**\n * The tokens store handles everything related to tokens and proofs\n */\n\ntype SwapAmountData = {\n  fromUrl: string | undefined;\n  toUrl: string | undefined;\n  amount: number;\n};\n\nexport type HistoryToken = {\n  status: \"paid\" | \"pending\";\n  amount: number;\n  date: string;\n  token?: string;\n  mint: string;\n  unit: string;\n  paymentRequest?: PaymentRequest;\n  fee?: number;\n  meltQuote?: MeltQuoteBolt11Response;\n  paidDate?: string;\n};\n\nexport const useSwapStore = defineStore(\"swap\", {\n  state: () => ({\n    swapAmountData: {} as SwapAmountData,\n    swapBlocking: false,\n  }),\n  actions: {\n    //\n    mintAmountSwap: async function (swapAmountData: SwapAmountData) {\n      const walletStore = useWalletStore();\n      const mintStore = useMintsStore();\n      if (this.swapBlocking) {\n        notifyWarning(i18n.global.t(\"swap.in_progress_warning_text\"));\n        return;\n      }\n      if (!swapAmountData.fromUrl || !swapAmountData.toUrl) {\n        notifyError(i18n.global.t(\"swap.invalid_swap_data_error_text\"));\n        return;\n      }\n      this.swapBlocking = true;\n      try {\n        // get invoice\n        // await mintStore.activateMintUrl(swapAmountData.toUrl);\n        const toWallet = await walletStore.mintWallet(\n          swapAmountData.toUrl,\n          mintStore.activeUnit,\n          true\n        );\n        const mintQuote = await walletStore.requestMint(\n          swapAmountData.amount,\n          toWallet\n        );\n\n        // pay invoice\n        const fromWallet = await walletStore.mintWallet(\n          swapAmountData.fromUrl,\n          mintStore.activeUnit,\n          true\n        );\n        const meltQuote = await walletStore.meltQuote(\n          fromWallet,\n          mintQuote.request\n        );\n        const mint = mintStore.mints.find(\n          (m) => m.url === swapAmountData.fromUrl\n        );\n        if (!mint) {\n          throw new Error(\"mint not found\");\n        }\n        const mintProofs = mintStore.mintUnitProofs(mint, fromWallet.unit);\n        await walletStore.melt(mintProofs, meltQuote, fromWallet);\n\n        // settle invoice on other side\n        await walletStore.checkInvoice(mintQuote.quote);\n      } catch (e) {\n        console.error(\"Error swapping\", e);\n        notifyError(i18n.global.t(\"swap.swap_error_text\"));\n      } finally {\n        this.swapBlocking = false;\n      }\n    },\n    meltToMintFees: async function (tokenJson: Token) {\n      const proofsStore = useProofsStore();\n      const walletStore = useWalletStore();\n      const fromMintUrl = token.getMint(tokenJson);\n      const unit = token.getUnit(tokenJson);\n      const tokenAmount = proofsStore.sumProofs(token.getProofs(tokenJson));\n      let meltAmount = tokenAmount - Math.max(2, Math.ceil(tokenAmount * 0.02));\n      try {\n        // walletStore.mintWallet(fromMintUrl, unit); will fail if we don't have fromMintUrl yet\n        const fromWallet = await walletStore.mintWallet(fromMintUrl, unit);\n        const proofs = token.getProofs(tokenJson);\n        meltAmount -= fromWallet.getFeesForProofs(proofs);\n      } catch (e) {}\n      return tokenAmount - meltAmount;\n    },\n    meltProofsToMint: async function (tokenJson: Token, mint: StoredMint) {\n      const proofsStore = useProofsStore();\n      const walletStore = useWalletStore();\n      if (this.swapBlocking) {\n        notifyWarning(i18n.global.t(\"swap.in_progress_warning_text\"));\n        return;\n      }\n\n      this.swapBlocking = true;\n      try {\n        const tokenAmount = proofsStore.sumProofs(token.getProofs(tokenJson));\n        let meltAmount =\n          tokenAmount - Math.max(2, Math.ceil(tokenAmount * 0.02));\n        const unit = token.getUnit(tokenJson);\n        const fromMintUrl = token.getMint(tokenJson);\n        const fromWallet = await walletStore.mintWallet(\n          fromMintUrl,\n          unit,\n          true\n        );\n        const toWallet = await walletStore.mintWallet(mint.url, unit, true);\n        const proofs = token.getProofs(tokenJson);\n        meltAmount -= fromWallet.getFeesForProofs(proofs);\n\n        const mintQuote = await walletStore.requestMint(meltAmount, toWallet);\n        const meltQuote = await walletStore.meltQuote(\n          fromWallet,\n          mintQuote.request\n        );\n        await walletStore.melt(proofs, meltQuote, fromWallet);\n\n        await walletStore.checkInvoice(mintQuote.quote);\n      } catch (e) {\n        console.error(\"Error swapping\", e);\n        notifyError(i18n.global.t(\"swap.swap_error_text\"));\n      } finally {\n        this.swapBlocking = false;\n      }\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/tokens.ts",
    "content": "import { useLocalStorage } from \"@vueuse/core\";\nimport { date } from \"quasar\";\nimport { defineStore } from \"pinia\";\nimport {\n  PaymentRequest,\n  Proof,\n  Token,\n  MeltQuoteBolt11Response,\n} from \"@cashu/cashu-ts\";\nimport token from \"src/js/token\";\nimport { v4 as uuidv4 } from \"uuid\";\n\n/**\n * The tokens store handles everything related to tokens and proofs\n */\n\nexport type HistoryToken = {\n  id: string;\n  status: \"paid\" | \"pending\";\n  amount: number;\n  date: string;\n  token: string;\n  mint: string;\n  unit: string;\n  paymentRequest?: PaymentRequest;\n  fee?: number;\n  label?: string; // Add label field for custom naming\n  meltQuote?: MeltQuoteBolt11Response;\n  paidDate?: string;\n  paymentRequestId?: string; // If created in response to a payment request\n};\n\nexport const useTokensStore = defineStore(\"tokens\", {\n  state: () => ({\n    historyTokens: useLocalStorage(\"cashu.historyTokens\", [] as HistoryToken[]),\n  }),\n  actions: {\n    /**\n     * @param {{amount: number, token: string, mint: string, unit: string}} param0\n     */\n    addPaidToken({\n      amount,\n      token,\n      mint,\n      unit,\n      fee,\n      paymentRequest,\n      label,\n      paymentRequestId,\n    }: {\n      amount: number;\n      token: string;\n      mint: string;\n      unit: string;\n      fee?: number;\n      paymentRequest?: PaymentRequest;\n      label?: string;\n      paymentRequestId?: string;\n    }): string {\n      const id = uuidv4();\n      this.historyTokens.push({\n        id,\n        status: \"paid\",\n        amount,\n        date: currentDateStr(),\n        token,\n        mint,\n        unit,\n        fee,\n        paymentRequest,\n        label,\n        paymentRequestId,\n      } as HistoryToken);\n      return id;\n    },\n    addPendingToken({\n      amount,\n      token,\n      mint,\n      unit,\n      fee,\n      paymentRequest,\n      label,\n      paymentRequestId,\n    }: {\n      amount: number;\n      token: string;\n      mint: string;\n      unit: string;\n      fee?: number;\n      paymentRequest?: PaymentRequest;\n      label?: string;\n      paymentRequestId?: string;\n    }): string {\n      const id = uuidv4();\n      this.historyTokens.push({\n        id,\n        status: \"pending\",\n        amount,\n        date: currentDateStr(),\n        token: token,\n        mint,\n        unit,\n        fee,\n        paymentRequest,\n        label,\n        paymentRequestId,\n      });\n      return id;\n    },\n    editHistoryToken(\n      tokenToEdit: string,\n      options?: {\n        newAmount?: number;\n        addAmount?: number;\n        newStatus?: \"paid\" | \"pending\";\n        newToken?: string;\n        newFee?: number;\n      }\n    ): HistoryToken | undefined {\n      const index = this.historyTokens.findIndex(\n        (t) => t.token === tokenToEdit\n      );\n      if (index >= 0) {\n        if (options) {\n          if (options.newToken) {\n            this.historyTokens[index].token = options.newToken;\n          }\n          if (options.newAmount) {\n            this.historyTokens[index].amount =\n              options.newAmount * Math.sign(this.historyTokens[index].amount);\n          }\n          if (options.addAmount) {\n            if (this.historyTokens[index].amount > 0) {\n              this.historyTokens[index].amount += options.addAmount;\n            } else {\n              this.historyTokens[index].amount -= options.addAmount;\n            }\n          }\n          if (options.newStatus) {\n            this.historyTokens[index].status = options.newStatus;\n          }\n          if (options.newFee) {\n            this.historyTokens[index].fee = options.newFee;\n          }\n        }\n\n        return this.historyTokens[index];\n      }\n\n      return undefined;\n    },\n    setTokenPaid(token: string) {\n      const index = this.historyTokens.findIndex(\n        (t) => t.token === token && t.status == \"pending\"\n      );\n      if (index >= 0) {\n        this.historyTokens[index].status = \"paid\";\n      }\n    },\n    deleteToken(token: string) {\n      const index = this.historyTokens.findIndex((t) => t.token === token);\n      if (index >= 0) {\n        this.historyTokens.splice(index, 1);\n      }\n    },\n    tokenAlreadyInHistory(tokenStr: string): HistoryToken | undefined {\n      return this.historyTokens.find((t) => t.token === tokenStr);\n    },\n  },\n});\n\nfunction currentDateStr() {\n  return date.formatDate(new Date(), \"YYYY-MM-DD HH:mm:ss\");\n}\n"
  },
  {
    "path": "src/stores/ui.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { useMintsStore } from \"./mints\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport {\n  notifyApiError,\n  notifyError,\n  notifySuccess,\n  notifyWarning,\n  notify,\n} from \"../js/notify\";\nimport { Clipboard } from \"@capacitor/clipboard\";\nimport { Haptics, ImpactStyle } from \"@capacitor/haptics\";\n\nimport ts from \"typescript\";\nimport { useSettingsStore } from \"./settings\";\n\nconst unitTickerShortMap = {\n  sat: \"sats\",\n  usd: \"USD\",\n  eur: \"EUR\",\n  msat: \"msats\",\n};\n\nexport const useUiStore = defineStore(\"ui\", {\n  state: () => ({\n    hideBalance: useLocalStorage<boolean>(\"cashu.ui.hideBalance\", false),\n    tickerLong: \"Satoshis\",\n    showInvoiceDetails: false,\n    showCreateInvoiceDialog: false,\n    showSendDialog: false,\n    showReceiveDialog: false,\n    showReceiveEcashDrawer: false,\n    showNumericKeyboard: false,\n    activityOrb: false,\n    tab: useLocalStorage(\"cashu.ui.tab\", \"history\" as string),\n    expandHistory: useLocalStorage(\"cashu.ui.expandHistory\", true as boolean),\n    globalMutexLock: false,\n    showDebugConsole: useLocalStorage(\"cashu.ui.showDebugConsole\", false),\n    lastBalanceCached: useLocalStorage(\"cashu.ui.lastBalanceCached\", 0),\n    multinutExperimentalWarningDismissed: useLocalStorage(\n      \"cashu.ui.multinutExperimentalWarningDismissed\",\n      false\n    ),\n  }),\n  actions: {\n    closeDialogs() {\n      this.showInvoiceDetails = false;\n      this.showCreateInvoiceDialog = false;\n      this.showSendDialog = false;\n      this.showReceiveDialog = false;\n      this.showReceiveEcashDrawer = false;\n    },\n    async lockMutex() {\n      const nRetries = 10;\n      const retryInterval = 500;\n      let retries = 0;\n\n      while (this.globalMutexLock) {\n        if (retries >= nRetries) {\n          notify(\"Please try again.\");\n          throw new Error(\"Failed to acquire global mutex lock\");\n        }\n        retries++;\n        await new Promise((resolve) => setTimeout(resolve, retryInterval));\n      }\n\n      this.globalMutexLock = true;\n    },\n    unlockMutex() {\n      this.globalMutexLock = false;\n    },\n    triggerActivityOrb() {\n      this.activityOrb = true;\n    },\n    setTab(tab: string) {\n      this.tab = tab;\n    },\n    formatSat: function (value: number) {\n      // convert value to integer\n      if (useSettingsStore().bip177BitcoinSymbol) {\n        if (value >= 0) {\n          return \"₿\" + new Intl.NumberFormat(navigator.language).format(value);\n        } else {\n          return (\n            \"-₿\" +\n            new Intl.NumberFormat(navigator.language).format(Math.abs(value))\n          );\n        }\n      }\n      return new Intl.NumberFormat(navigator.language).format(value) + \" sat\";\n    },\n    fromMsat: function (value: number) {\n      return new Intl.NumberFormat(navigator.language).format(value) + \" msat\";\n    },\n    formatCurrency: function (\n      value: number,\n      currency: string,\n      showBalance = false\n    ) {\n      if (currency == undefined) {\n        currency = \"sat\";\n      }\n      if (useUiStore().hideBalance && !showBalance) {\n        return \"****\";\n      }\n      if (currency == \"sat\") return this.formatSat(value);\n      if (currency == \"msat\") return this.fromMsat(value);\n      if (currency == \"usd\") value = value / 100;\n      if (currency == \"eur\") value = value / 100;\n      return new Intl.NumberFormat(navigator.language, {\n        style: \"currency\",\n        currency: currency,\n      }).format(value);\n      // + \" \" +\n      // currency.toUpperCase()\n    },\n    toggleDebugConsole() {\n      this.showDebugConsole = !this.showDebugConsole;\n      if (this.showDebugConsole) {\n        this.enableDebugConsole();\n      } else {\n        this.disableDebugConsole();\n      }\n    },\n    enableDebugConsole() {\n      if (!this.showDebugConsole) {\n        return;\n      }\n      // enable debug terminal\n      const script = document.createElement(\"script\");\n      script.src = \"//cdn.jsdelivr.net/npm/eruda\";\n      document.body.appendChild(script);\n      script.onload = function () {\n        // @ts-ignore\n        eruda.init();\n      };\n    },\n    disableDebugConsole() {\n      // @ts-ignore\n      document.querySelector(\"#eruda\").remove();\n    },\n    pasteFromClipboard: async function () {\n      let text = \"\";\n      // @ts-ignore\n      if (window?.Capacitor) {\n        const { value } = await Clipboard.read();\n        text = value;\n      } else {\n        text = await navigator.clipboard.readText();\n      }\n      return text;\n    },\n    vibrate: async function () {\n      // @ts-ignore\n      if (window.Capacitor) {\n        // Haptics.impact({ style: ImpactStyle.Light });\n        Haptics.vibrate({ duration: 200 });\n      } else {\n        navigator.vibrate(200);\n      }\n    },\n  },\n  getters: {\n    tickerShort() {\n      const unit = useMintsStore().activeUnit;\n      if (unit == \"sat\" && useSettingsStore().bip177BitcoinSymbol) {\n        return \"₿\";\n      } else {\n        return unitTickerShortMap[unit as keyof typeof unitTickerShortMap];\n      }\n    },\n    ndefSupported(): boolean {\n      //console.log(`window.Capacitor.getPlatform() = ${window.Capacitor.getPlatform()}`)\n      // @ts-ignore\n      if (window.Capacitor.getPlatform() !== \"web\") {\n        return false;\n      }\n      return \"NDEFReader\" in globalThis;\n    },\n    canPasteFromClipboard() {\n      return (\n        window.isSecureContext &&\n        navigator.clipboard &&\n        navigator.clipboard.readText\n      );\n    },\n    webShareSupported(): boolean {\n      return \"share\" in navigator;\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/wallet.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { currentDateStr } from \"src/js/utils\";\nimport { useMintsStore, WalletProof, MintClass } from \"./mints\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { useProofsStore } from \"./proofs\";\nimport { HistoryToken, useTokensStore } from \"./tokens\";\nimport { useReceiveTokensStore } from \"./receiveTokensStore\";\nimport { useUiStore } from \"src/stores/ui\";\nimport { useP2PKStore } from \"src/stores/p2pk\";\nimport { useSendTokensStore } from \"src/stores/sendTokensStore\";\nimport { usePRStore } from \"./payment-request\";\nimport { useWorkersStore } from \"./workers\";\nimport { useInvoicesWorkerStore } from \"./invoicesWorker\";\nimport * as nobleSecp256k1 from \"@noble/secp256k1\";\nimport { bytesToHex } from \"@noble/hashes/utils\";\n\nimport _ from \"underscore\";\nimport token from \"src/js/token\";\nimport {\n  notifyApiError,\n  notifyError,\n  notifySuccess,\n  notifyWarning,\n  notify,\n} from \"src/js/notify\";\nimport {\n  Wallet,\n  Proof,\n  MintQuoteBolt11Request,\n  MeltQuoteBolt11Request,\n  MintQuoteBolt11Response,\n  MeltQuoteBolt11Response,\n  CheckStateEnum,\n  MeltQuoteState,\n  MintQuoteState,\n  ProofState,\n  KeyChain,\n  // ConsoleLogger,\n} from \"@cashu/cashu-ts\";\n// @ts-ignore\nimport * as bolt11Decoder from \"light-bolt11-decoder\";\nimport { bech32 } from \"bech32\";\nimport axios from \"axios\";\nimport { date } from \"quasar\";\n\n// bip39 requires Buffer\n// import { Buffer } from 'buffer';\n// window.Buffer = Buffer;\nimport { generateMnemonic, mnemonicToSeedSync } from \"@scure/bip39\";\nimport { wordlist } from \"@scure/bip39/wordlists/english\";\nimport { useSettingsStore } from \"./settings\";\nimport { usePriceStore } from \"./price\";\nimport { useI18n } from \"vue-i18n\";\nimport {\n  isLegacyRetailQR,\n  translateLegacyQRToLightningAddress,\n} from \"src/js/legacy-qr\";\n// HACK: this is a workaround so that the catch block in the melt function does not throw an error when the user exits the app\n// before the payment is completed. This is necessary because the catch block in the melt function would otherwise remove all\n// quotes from the invoiceHistory and the user would not be able to pay the invoice again after reopening the app.\nlet isUnloading = false;\nwindow.addEventListener(\"beforeunload\", () => {\n  isUnloading = true;\n});\n\ntype Invoice = {\n  amount: number;\n  bolt11: string;\n  quote: string;\n  memo: string;\n};\n\nexport type InvoiceHistory = Invoice & {\n  date: string;\n  status: \"pending\" | \"paid\";\n  mint: string;\n  unit: string;\n  mintQuote?: MintQuoteBolt11Response;\n  meltQuote?: MeltQuoteBolt11Response;\n  label?: string; // Add label field for custom naming\n  privKey?: string; // Private key, if the quote is locked\n  paidDate?: string;\n};\n\ntype KeysetCounter = {\n  id: string;\n  counter: number;\n};\n\nconst receiveStore = useReceiveTokensStore();\nconst tokenStore = useTokensStore();\nconst proofsStore = useProofsStore();\n\nexport const useWalletStore = defineStore(\"wallet\", {\n  state: () => {\n    const { t } = useI18n();\n    return {\n      t: t,\n      mnemonic: useLocalStorage(\"cashu.mnemonic\", \"\"),\n      invoiceHistory: useLocalStorage(\n        \"cashu.invoiceHistory\",\n        [] as InvoiceHistory[]\n      ),\n      keysetCounters: useLocalStorage(\n        \"cashu.keysetCounters\",\n        [] as KeysetCounter[]\n      ),\n      oldMnemonicCounters: useLocalStorage(\n        \"cashu.oldMnemonicCounters\",\n        [] as { mnemonic: string; keysetCounters: KeysetCounter[] }[]\n      ),\n      invoiceData: {} as InvoiceHistory,\n      activeWebsocketConnections: 0,\n      payInvoiceData: {\n        blocking: false,\n        bolt11: \"\",\n        show: false,\n        fee_paid: 0,\n        meltQuote: {\n          payload: {\n            unit: \"\",\n            request: \"\",\n          } as MeltQuoteBolt11Request,\n          response: {\n            quote: \"\",\n            amount: 0,\n            fee_reserve: 0,\n          } as MeltQuoteBolt11Response,\n          error: \"\",\n        },\n        invoice: {\n          sat: 0,\n          memo: \"\",\n          bolt11: \"\",\n        } as { sat: number; memo: string; bolt11: string } | null,\n        lnurlpay: {\n          domain: \"\",\n          callback: \"\",\n          minSendable: 0,\n          maxSendable: 0,\n          metadata: {},\n          successAction: {},\n          routes: [],\n          tag: \"\",\n          lightningAddress: \"\",\n        },\n        lnurlauth: {},\n        input: {\n          request: \"\",\n          amount: undefined,\n          comment: \"\",\n          quote: \"\",\n        } as {\n          request: string;\n          amount: number | undefined;\n          comment: string;\n          quote: string;\n        },\n      },\n    };\n  },\n  getters: {\n    seed(): Uint8Array {\n      return mnemonicToSeedSync(this.mnemonic);\n    },\n  },\n  actions: {\n    setMnemonicFromUser: function (mnemonic: string) {\n      this.mnemonic = mnemonic.trim().toLowerCase(); // normalize\n    },\n    /**\n     * Returns a fully initialised Wallet for the active mint.\n     * Calls loadMint internally, so is safe for all wallet operations.\n     */\n    async activeWallet(updateKeysets: boolean = false): Promise<Wallet> {\n      const mints = useMintsStore() as any;\n      return this.mintWallet(\n        mints.activeMintUrl,\n        mints.activeUnit,\n        updateKeysets\n      );\n    },\n    async mintWallet(\n      url: string,\n      unit: string,\n      updateKeysets: boolean = false\n    ): Promise<Wallet> {\n      // short-lived wallet for mint operations\n      // note: the unit of the wallet will be activeUnit by default,\n      // overwrite wallet.unit if needed\n      const mints = useMintsStore() as any;\n      let storedMint = mints.mints.find((m: any) => m.url === url);\n      if (!storedMint) {\n        throw new Error(\"mint not found\");\n      }\n      // if updateKeysets is true and keysetsLastFetched is older than 1 hour, fetch the keysets for the mint\n      const ONE_HOUR = 60 * 60 * 1000;\n      const lastUpdated = storedMint.lastKeysetsUpdated\n        ? new Date(storedMint.lastKeysetsUpdated).getTime()\n        : 0;\n      const mintNeedsUpdate =\n        updateKeysets && lastUpdated < Date.now() - ONE_HOUR;\n      if (mintNeedsUpdate) {\n        console.log(\"updating mint info and keys for mint\", storedMint.url);\n        try {\n          await mints.updateMintInfoAndKeys(storedMint);\n          // Re-fetch mint after update to get fresh keysets\n          storedMint = mints.mints.find((m: any) => m.url === url);\n        } catch (error: any) {\n          console.error(\"Failed to update mint info/keys:\", error);\n          // Continue with potentially stale keysets rather than failing\n        }\n      }\n      return this.createWalletInstance(storedMint, url, unit, mints);\n    },\n    // Synchronous wallet creation for non-critical operations (e.g., fee calculation display)\n    // Use mintWallet() with updateKeysets=true for critical operations\n    mintWalletSync(url: string, unit: string): Wallet {\n      const mints = useMintsStore() as any;\n      const storedMint = mints.mints.find((m: any) => m.url === url);\n      if (!storedMint) {\n        throw new Error(\"mint not found\");\n      }\n      return this.createWalletInstance(storedMint, url, unit, mints);\n    },\n    createWalletInstance(\n      storedMint: any,\n      url: string,\n      unit: string,\n      mints: any\n    ): Wallet {\n      if (this.mnemonic == \"\") {\n        this.mnemonic = generateMnemonic(wordlist);\n      }\n      const bip39seed = mnemonicToSeedSync(this.mnemonic);\n      const counterInit = Object.fromEntries(\n        this.keysetCounters.map(({ id, counter }) => [id, counter])\n      );\n      const wallet = new Wallet(url, {\n        unit,\n        bip39seed,\n        counterInit,\n        // logger: new ConsoleLogger(\"debug\"),\n      });\n      // Load the caches\n      const unitKeysets = mints.mintUnitKeysets(storedMint, unit);\n      const keychainCache = KeyChain.mintToCacheDTO(\n        unit,\n        url,\n        unitKeysets,\n        storedMint.keys\n      );\n      wallet.loadMintFromCache(storedMint.info, keychainCache);\n      return wallet;\n    },\n    mnemonicToSeedSync: function (mnemonic: string): Uint8Array {\n      return mnemonicToSeedSync(mnemonic);\n    },\n    newMnemonic: function () {\n      // store old mnemonic and keysetCounters\n      const oldMnemonicCounters = this.oldMnemonicCounters;\n      const keysetCounters = this.keysetCounters;\n      oldMnemonicCounters.push({ mnemonic: this.mnemonic, keysetCounters });\n      this.keysetCounters = [];\n      this.mnemonic = generateMnemonic(wordlist);\n    },\n    keysetCounter: function (id: string) {\n      const keysetCounter = this.keysetCounters.find((c) => c.id === id);\n      if (keysetCounter) {\n        return keysetCounter.counter;\n      } else {\n        this.keysetCounters.push({ id, counter: 1 });\n        return 1;\n      }\n    },\n    increaseKeysetCounter: function (id: string, by: number) {\n      const keysetCounter = this.keysetCounters.find((c) => c.id === id);\n      if (keysetCounter) {\n        keysetCounter.counter += by;\n      } else {\n        const newCounter = { id, counter: by } as KeysetCounter;\n        this.keysetCounters.push(newCounter);\n      }\n    },\n    getKeyset(\n      mintUrl: string | null = null,\n      unit: string | null = null\n    ): string {\n      unit = unit || useMintsStore().activeUnit;\n      mintUrl = mintUrl || useMintsStore().activeMintUrl;\n      const mint = useMintsStore().mints.find((m) => m.url === mintUrl);\n      if (!mint) {\n        throw new Error(\"mint not found\");\n      }\n      const mintClass = new MintClass(mint);\n      // const mintStore = useMintsStore();\n      const keysets = mint.keysets;\n      if (keysets == null || keysets.length == 0) {\n        throw new Error(\"no keysets found.\");\n      }\n      const unitKeysets = mintClass.unitKeysets(unit);\n      if (unitKeysets == null || unitKeysets.length == 0) {\n        console.error(\"no keysets found for unit\", unit);\n        throw new Error(\"no keysets found for unit\");\n      }\n      // select the keyset id\n      // const keyset_id = unitKeysets[0].id;\n      // rules for selection:\n      // - filter all keysets that are active=true\n      // - order by id (whether it is hex or base64)\n      // - order by input_fee_ppk (ascending) TODO: this is not implemented yet\n      // - select the first one\n      const activeKeysets = unitKeysets.filter((k) => k.active);\n      const hexKeysets = activeKeysets.filter((k) => k.id.startsWith(\"00\"));\n      const base64Keysets = activeKeysets.filter((k) => !k.id.startsWith(\"00\"));\n      const sortedKeysets = hexKeysets.concat(base64Keysets);\n      // const sortedKeysets = _.sortBy(activeKeysets, k => [k.id, k.input_fee_ppk])\n      if (sortedKeysets.length == 0) {\n        console.error(\"no active keysets found for unit\", unit);\n        throw new Error(\"no active keysets found for unit\");\n      }\n      return sortedKeysets[0].id;\n    },\n    /**\n     * Sets an invoice status to paid\n     */\n    setInvoicePaid(quoteId: string) {\n      const invoice = this.invoiceHistory.find((i) => i.quote === quoteId);\n      if (!invoice) return;\n      invoice.status = \"paid\";\n      invoice.paidDate = currentDateStr();\n    },\n    splitAmount: function (value: number) {\n      // returns optimal 2^n split\n      const chunks: Array<number> = [];\n      for (let i = 0; i < 32; i++) {\n        const mask: number = 1 << i;\n        if ((value & mask) !== 0) {\n          chunks.push(Math.pow(2, i));\n        }\n      }\n      return chunks;\n    },\n    coinSelectSpendBase64: function (\n      proofs: WalletProof[],\n      amount: number\n    ): WalletProof[] {\n      const base64Proofs = proofs.filter((p) => !p.id.startsWith(\"00\"));\n      if (base64Proofs.length > 0) {\n        base64Proofs.sort((a, b) => b.amount - a.amount);\n        let sum = 0;\n        const selectedProofs: WalletProof[] = [];\n        for (let i = 0; i < base64Proofs.length; i++) {\n          const proof = base64Proofs[i];\n          sum += proof.amount;\n          selectedProofs.push(proof);\n          if (sum >= amount) {\n            return selectedProofs;\n          }\n        }\n        return [];\n      }\n      return [];\n    },\n    coinSelect: function (\n      proofs: WalletProof[],\n      wallet: Wallet,\n      amount: number,\n      includeFees: boolean = false\n    ): WalletProof[] {\n      if (proofs.reduce((s, t) => (s += t.amount), 0) < amount) {\n        // there are not enough proofs to pay the amount\n        return [];\n      }\n      const { send: selectedProofs, keep: _ } = wallet.selectProofsToSend(\n        proofs,\n        amount,\n        includeFees\n      );\n      const selectedWalletProofs = selectedProofs.map((p) => {\n        return { ...p, reserved: false } as WalletProof;\n      });\n      return selectedWalletProofs;\n    },\n    spendableProofs: function (\n      proofs: WalletProof[],\n      amount: number\n    ): WalletProof[] {\n      const proofsStore = useProofsStore();\n      const spendableProofs = proofsStore.getUnreservedProofs(proofs);\n      if (proofsStore.sumProofs(spendableProofs) < amount) {\n        throw Error(this.t(\"wallet.notifications.balance_too_low\"));\n      }\n      return spendableProofs;\n    },\n    getFeesForProofs: function (proofs: Proof[]): number {\n      const mints = useMintsStore() as any;\n      const wallet = this.mintWalletSync(mints.activeMintUrl, mints.activeUnit);\n      return wallet.getFeesForProofs(proofs);\n    },\n    sendToLock: async function (\n      proofs: WalletProof[],\n      wallet: Wallet,\n      amount: number,\n      receiverPubkey: string\n    ) {\n      const spendableProofs = this.spendableProofs(proofs, amount);\n      const proofsToSend = this.coinSelect(\n        spendableProofs,\n        wallet,\n        amount,\n        true\n      );\n      const keysetId = this.getKeyset(wallet.mint.mintUrl, wallet.unit);\n      const { keep: keepProofs, send: sendProofs } = await wallet.ops\n        .send(amount, proofsToSend)\n        .keyset(keysetId)\n        .asP2PK({ pubkey: receiverPubkey })\n        .run();\n      const proofsStore = useProofsStore();\n      await proofsStore.removeProofs(proofsToSend);\n      // note: we do not store sendProofs in the proofs store but\n      // expect from the caller to store it in the history\n      await proofsStore.addProofs(keepProofs);\n      return { keepProofs, sendProofs };\n    },\n    send: async function (\n      proofs: WalletProof[],\n      wallet: Wallet,\n      amount: number,\n      invalidate: boolean = false,\n      includeFees: boolean = false\n    ): Promise<{ keepProofs: Proof[]; sendProofs: Proof[] }> {\n      /*\n      splits proofs so the user can keep firstProofs, send scndProofs.\n      then sets scndProofs as reserved.\n\n      if invalidate, scndProofs (the one to send) are invalidated\n      */\n      const proofsStore = useProofsStore();\n      const uIStore = useUiStore();\n      let proofsToSend: WalletProof[] = [];\n      const keysetId = this.getKeyset(wallet.mint.mintUrl, wallet.unit);\n      await uIStore.lockMutex();\n      try {\n        const spendableProofs = this.spendableProofs(proofs, amount);\n\n        proofsToSend = this.coinSelect(\n          spendableProofs,\n          wallet,\n          amount,\n          includeFees\n        );\n        const totalAmount = proofsToSend.reduce((s, t) => (s += t.amount), 0);\n        const fees = includeFees ? wallet.getFeesForProofs(proofsToSend) : 0;\n        const targetAmount = amount + fees;\n\n        let keepProofs: Proof[] = [];\n        let sendProofs: Proof[] = [];\n\n        if (totalAmount != targetAmount) {\n          // we need to swap!\n          // get a new wallet with potentially updated keysets / info\n          const swapWallet = await this.mintWallet(\n            wallet.mint.mintUrl,\n            wallet.unit,\n            true\n          );\n          const counter = this.keysetCounter(keysetId);\n          proofsToSend = this.coinSelect(\n            spendableProofs,\n            swapWallet,\n            targetAmount,\n            true\n          );\n          ({ keep: keepProofs, send: sendProofs } = await swapWallet.ops\n            .send(targetAmount, proofsToSend)\n            .asDeterministic(counter)\n            .keyset(keysetId)\n            .proofsWeHave(spendableProofs)\n            .run());\n          this.increaseKeysetCounter(\n            keysetId,\n            keepProofs.length + sendProofs.length\n          );\n          await proofsStore.addProofs(keepProofs);\n          await proofsStore.addProofs(sendProofs);\n\n          // make sure we don't delete any proofs that were returned\n          const proofsToSendNotReturned = proofsToSend\n            .filter((p) => !sendProofs.find((s) => s.secret === p.secret))\n            .filter((p) => !keepProofs.find((k) => k.secret === p.secret));\n          await proofsStore.removeProofs(proofsToSendNotReturned);\n        } else if (totalAmount == targetAmount) {\n          keepProofs = [];\n          sendProofs = proofsToSend;\n        } else {\n          throw new Error(\"could not split proofs.\");\n        }\n\n        await proofsStore.setReserved(sendProofs, true);\n        if (invalidate) {\n          await proofsStore.removeProofs(sendProofs);\n        }\n        return { keepProofs, sendProofs };\n      } catch (error: any) {\n        await proofsStore.setReserved(proofsToSend, false);\n        console.error(error);\n        notifyApiError(error);\n        this.handleOutputsHaveAlreadyBeenSignedError(keysetId, error);\n        throw error;\n      } finally {\n        uIStore.unlockMutex();\n      }\n    },\n    redeem: async function () {\n      /*\n      Receives a token that is prepared in the receiveToken – it is not yet in the history\n      */\n      const uIStore = useUiStore();\n      const mintStore = useMintsStore();\n      const p2pkStore = useP2PKStore();\n      const wasReceiveDialogVisible = receiveStore.showReceiveTokens;\n\n      if (receiveStore.receiveData.tokensBase64.length == 0) {\n        throw new Error(\"no tokens provided.\");\n      }\n      const tokenJson = await token.decodeFull(\n        receiveStore.receiveData.tokensBase64\n      );\n      if (tokenJson == undefined) {\n        throw new Error(\"no tokens provided.\");\n      }\n      const proofs = token.getProofs(tokenJson);\n      if (proofs.length == 0) {\n        throw new Error(\"no proofs found.\");\n      }\n      const inputAmount = proofs.reduce((s, t) => (s += t.amount), 0);\n      let fee = 0;\n      const mintInToken = token.getMint(tokenJson);\n      const unitInToken = token.getUnit(tokenJson);\n\n      const historyToken = {\n        amount: inputAmount,\n        token: receiveStore.receiveData.tokensBase64,\n        unit: unitInToken,\n        mint: mintInToken,\n        fee: fee,\n      };\n      const mintWallet = await this.mintWallet(\n        historyToken.mint,\n        historyToken.unit,\n        true\n      );\n      const mint = mintStore.mints.find((m) => m.url === historyToken.mint);\n      if (!mint) {\n        throw new Error(\"mint not found\");\n      }\n      await uIStore.lockMutex();\n      try {\n        // redeem\n        const keysetId = this.getKeyset(historyToken.mint, historyToken.unit);\n        const counter = this.keysetCounter(keysetId);\n        const privkey = receiveStore.receiveData.p2pkPrivateKey;\n        let proofs: Proof[];\n        try {\n          proofs = await mintWallet.ops\n            .receive(receiveStore.receiveData.tokensBase64)\n            .asDeterministic(counter)\n            .privkey(privkey)\n            .proofsWeHave(mintStore.mintUnitProofs(mint, historyToken.unit))\n            .run();\n          await proofsStore.addProofs(proofs);\n          this.increaseKeysetCounter(keysetId, proofs.length);\n        } catch (error: any) {\n          console.error(error);\n          this.handleOutputsHaveAlreadyBeenSignedError(keysetId, error);\n          throw new Error(\"Error receiving tokens: \" + error);\n        }\n\n        p2pkStore.setPrivateKeyUsed(privkey);\n\n        const outputAmount = proofs.reduce((s, t) => (s += t.amount), 0);\n\n        // if token is already in history, set to paid, else add to history\n        if (\n          tokenStore.historyTokens.find(\n            (t) =>\n              t.token === receiveStore.receiveData.tokensBase64 && t.amount > 0\n          )\n        ) {\n          tokenStore.setTokenPaid(receiveStore.receiveData.tokensBase64);\n        } else {\n          // if this is a self-sent token, we will find an outgoing token with the inverse amount\n          if (\n            tokenStore.historyTokens.find(\n              (t) =>\n                t.token === receiveStore.receiveData.tokensBase64 &&\n                t.amount < 0\n            )\n          ) {\n            tokenStore.setTokenPaid(receiveStore.receiveData.tokensBase64);\n          }\n          fee = inputAmount - outputAmount;\n          historyToken.fee = fee;\n          historyToken.amount = outputAmount;\n          tokenStore.addPaidToken(historyToken as any);\n        }\n        useUiStore().vibrate();\n        let message = this.t(\"wallet.notifications.received\", {\n          amount: uIStore.formatCurrency(outputAmount, historyToken.unit),\n        });\n        if (fee > 0) {\n          message += this.t(\"wallet.notifications.fee\", {\n            fee: uIStore.formatCurrency(fee, historyToken.unit),\n          });\n        }\n        notifySuccess(message);\n        if (wasReceiveDialogVisible) {\n          receiveStore.showReceiveTokens = false;\n          uIStore.closeDialogs();\n        }\n      } catch (error: any) {\n        console.error(error);\n        notifyApiError(error);\n        throw error;\n      } finally {\n        uIStore.unlockMutex();\n      }\n      // }\n    },\n\n    // /mint\n    /**\n     * Ask the mint to generate an invoice for the given amount\n     * Upon paying the request, the mint will credit the wallet with\n     * cashu tokens\n     */\n    requestMint: async function (\n      amount: number,\n      mintWallet: Wallet\n    ): Promise<MintQuoteBolt11Response> {\n      try {\n        await mintWallet.loadMint(); // defensive\n        const { supported: nut20supported } = mintWallet\n          .getMintInfo()\n          .isSupported(20);\n        const privkey = nut20supported\n          ? bytesToHex(nobleSecp256k1.utils.randomPrivateKey())\n          : undefined;\n        const pubkey = nut20supported\n          ? bytesToHex(nobleSecp256k1.getPublicKey(privkey!!, true))\n          : undefined;\n        const payload: MintQuoteBolt11Request = {\n          amount: amount,\n          unit: mintWallet.unit,\n          pubkey: pubkey,\n        };\n        const data = await mintWallet.mint.createMintQuoteBolt11(payload);\n        this.invoiceData.amount = amount;\n        this.invoiceData.bolt11 = data.request;\n        this.invoiceData.quote = data.quote;\n        this.invoiceData.date = currentDateStr();\n        this.invoiceData.status = \"pending\";\n        this.invoiceData.mint = mintWallet.mint.mintUrl;\n        this.invoiceData.unit = mintWallet.unit;\n        this.invoiceData.mintQuote = data as MintQuoteBolt11Response;\n        this.invoiceData.privKey = privkey;\n        this.invoiceHistory.push({\n          ...this.invoiceData,\n        });\n        return data as MintQuoteBolt11Response;\n      } catch (error: any) {\n        console.error(error);\n        notifyApiError(\n          error,\n          this.t(\"wallet.notifications.could_not_request_mint\")\n        );\n        throw error;\n      } finally {\n      }\n    },\n    mint: async function (invoice: InvoiceHistory, verbose: boolean = true) {\n      const proofsStore = useProofsStore();\n      const mintStore = useMintsStore();\n      const uIStore = useUiStore();\n      const keysetId = this.getKeyset(invoice.mint, invoice.unit);\n      const mintWallet = await this.mintWallet(\n        invoice.mint,\n        invoice.unit,\n        true\n      );\n      const mint = mintStore.mints.find((m) => m.url === invoice.mint);\n      if (!mint) {\n        throw new Error(\"mint not found\");\n      }\n\n      await uIStore.lockMutex();\n      try {\n        // first we check if the mint quote is paid\n        const mintQuote = await mintWallet.checkMintQuoteBolt11(invoice.quote);\n        invoice.mintQuote = mintQuote as MintQuoteBolt11Response;\n        console.log(\"### mint(): mintQuote\", mintQuote);\n        switch (mintQuote.state) {\n          case MintQuoteState.PAID:\n            break;\n          case MintQuoteState.UNPAID:\n            if (verbose) {\n              notify(this.t(\"wallet.notifications.invoice_still_pending\"));\n            }\n            throw new Error(\"invoice pending.\");\n          case MintQuoteState.ISSUED:\n            throw new Error(\"invoice already issued.\");\n          default:\n            throw new Error(\"unknown state.\");\n        }\n        // MintQuoteState must be PAID\n        const counter = this.keysetCounter(keysetId);\n        const proofs = await mintWallet.ops\n          .mintBolt11(invoice.amount, invoice.mintQuote!!)\n          .keyset(keysetId)\n          .asDeterministic(counter)\n          .proofsWeHave(mintStore.mintUnitProofs(mint, invoice.unit))\n          .privkey(invoice.privKey as string)\n          .run();\n        this.increaseKeysetCounter(keysetId, proofs.length);\n        await proofsStore.addProofs(proofs);\n\n        // update UI\n        this.setInvoicePaid(invoice.quote);\n        useInvoicesWorkerStore().removeInvoiceFromChecker(invoice.quote);\n\n        return proofs;\n      } catch (error: any) {\n        console.error(error);\n        if (verbose) {\n          notifyApiError(error);\n        }\n        this.handleOutputsHaveAlreadyBeenSignedError(keysetId, error);\n        throw error;\n      } finally {\n        uIStore.unlockMutex();\n      }\n    },\n    // get a melt quote for the current invoice data\n    meltQuoteInvoiceData: async function () {\n      // choose active wallet with active mint and unit\n      const mintWallet = await this.activeWallet();\n      // throw an error if this.payInvoiceData.blocking is true\n      if (this.payInvoiceData.blocking) {\n        throw new Error(\"already processing an melt quote.\");\n      }\n      this.payInvoiceData.blocking = true;\n      this.payInvoiceData.meltQuote.error = \"\";\n      try {\n        const mintStore = useMintsStore();\n        if (this.payInvoiceData.input.request == \"\") {\n          throw new Error(\"no invoice provided.\");\n        }\n        const payload: MeltQuoteBolt11Request = {\n          unit: mintStore.activeUnit,\n          request: this.payInvoiceData.input.request,\n        };\n        this.payInvoiceData.meltQuote.payload = payload;\n        const data = await this.meltQuote(mintWallet, payload.request);\n        mintStore.assertMintError(data as any);\n        this.payInvoiceData.meltQuote.response = data;\n        return data;\n      } catch (error: any) {\n        this.payInvoiceData.meltQuote.error = error;\n        console.error(error);\n        notifyApiError(error);\n        throw error;\n      } finally {\n        this.payInvoiceData.blocking = false;\n      }\n    },\n    meltQuote: async function (\n      wallet: Wallet,\n      request: string,\n      mpp_amount: number | undefined = undefined\n    ): Promise<MeltQuoteBolt11Response> {\n      const mintStore = useMintsStore();\n      let data;\n      if (mpp_amount) {\n        data = await wallet.createMultiPathMeltQuote(\n          request,\n          mpp_amount * 1000\n        );\n      } else {\n        data = await wallet.createMeltQuoteBolt11(request);\n      }\n\n      mintStore.assertMintError(data as any);\n      return data;\n    },\n    meltInvoiceData: async function (silent?: boolean) {\n      if (this.payInvoiceData.invoice == null) {\n        throw new Error(\"no invoice provided.\");\n      }\n      const quote = this.payInvoiceData.meltQuote.response;\n      if (quote == null) {\n        throw new Error(\"no quote found.\");\n      }\n      const request = this.payInvoiceData.invoice.bolt11;\n      if (\n        this.invoiceHistory.find(\n          (i) => i.bolt11 === request && i.amount < 0 && i.status === \"paid\"\n        )\n      ) {\n        notifyError(\"Invoice already paid.\");\n        throw new Error(\"invoice already paid.\");\n      }\n\n      // Construct active wallet manually as we need mintStore anyway.\n      const mintStore = useMintsStore();\n      const mintWallet = await this.mintWallet(\n        mintStore.activeMintUrl,\n        mintStore.activeUnit,\n        true\n      );\n      return await this.melt(mintStore.activeProofs, quote, mintWallet, silent);\n    },\n    melt: async function (\n      proofs: WalletProof[],\n      quote: MeltQuoteBolt11Response,\n      mintWallet: Wallet,\n      silent?: boolean,\n      releaseMutex?: boolean\n    ) {\n      const uIStore = useUiStore();\n      const proofsStore = useProofsStore();\n\n      console.log(\"#### melt()\");\n      const amount = quote.amount + quote.fee_reserve;\n      let countChangeOutputs = 0;\n      const keysetId = this.getKeyset(mintWallet.mint.mintUrl, mintWallet.unit);\n      let keysetCounterIncrease = 0;\n\n      // start melt\n      let sendProofs: Proof[] = [];\n      try {\n        const { sendProofs: _sendProofs } = await this.send(\n          proofs,\n          mintWallet,\n          amount,\n          false,\n          true\n        );\n        sendProofs = _sendProofs;\n        if (sendProofs.length == 0) {\n          throw new Error(\"could not split proofs.\");\n        }\n      } catch (error: any) {\n        console.error(error);\n        if (!silent) notifyApiError(error, \"Payment failed\");\n        throw error;\n      }\n\n      await uIStore.lockMutex();\n      try {\n        await this.addOutgoingPendingInvoiceToHistory(\n          quote,\n          mintWallet.mint.mintUrl,\n          mintWallet.unit\n        );\n        await proofsStore.setReserved(sendProofs, true, quote.quote);\n\n        // NUT-08 blank outputs for change\n        const counter = this.keysetCounter(keysetId);\n\n        // QUIRK: we increase the keyset counter by sendProofs and the maximum number of possible change outputs\n        // this way, in case the user exits the app before meltProofs is completed, the returned change outputs won't cause a \"outputs already signed\" error\n        // if the payment fails, we decrease the counter again\n        this.increaseKeysetCounter(keysetId, sendProofs.length);\n        if (quote.fee_reserve > 0) {\n          countChangeOutputs = Math.ceil(Math.log2(quote.fee_reserve)) || 1;\n          this.increaseKeysetCounter(keysetId, countChangeOutputs);\n          keysetCounterIncrease += countChangeOutputs;\n        }\n\n        uIStore.triggerActivityOrb();\n\n        // NOTE: if the user exits the app while we're in the API call, JS will emit an error that we would catch below!\n        // We have to handle that case in the catch block below\n        if (releaseMutex) uIStore.unlockMutex(); // Momentarely release the mutex (needed for concurrent melts)\n        let data;\n        try {\n          data = await mintWallet.ops\n            .meltBolt11(quote, sendProofs)\n            .keyset(keysetId)\n            .asDeterministic(counter)\n            .run();\n          // store melt quote in invoice history\n          this.updateOutgoingInvoiceInHistory(\n            data.quote as MeltQuoteBolt11Response\n          );\n        } catch (error) {\n          throw error;\n        } finally {\n          if (releaseMutex) await uIStore.lockMutex();\n        }\n\n        if (data.quote.state != MeltQuoteState.PAID) {\n          throw new Error(\"Invoice not paid.\");\n        }\n\n        // NUT-08 get change\n        if (data.change != null) {\n          const changeProofs = data.change;\n          console.log(\n            \"## Received change: \" + proofsStore.sumProofs(changeProofs)\n          );\n          await proofsStore.addProofs(changeProofs);\n        }\n\n        // delete spent tokens from db\n        await proofsStore.removeProofs(sendProofs);\n\n        const amount_paid = amount - proofsStore.sumProofs(data.change ?? []);\n        useUiStore().vibrate();\n        if (!silent) {\n          notifySuccess(\n            this.t(\"wallet.notifications.paid_lightning\", {\n              amount: uIStore.formatCurrency(amount_paid, mintWallet.unit),\n            })\n          );\n        }\n        console.log(\n          `#### pay lightning: ${amount_paid} ${mintWallet.unit} paid`\n        );\n\n        this.updateOutgoingInvoiceInHistory(quote, {\n          status: \"paid\",\n          amount: -amount_paid,\n        });\n\n        this.payInvoiceData.invoice = { sat: 0, memo: \"\", bolt11: \"\" };\n        this.payInvoiceData.show = false;\n        return data;\n      } catch (error: any) {\n        if (isUnloading) {\n          // NOTE: An error is thrown when the user exits the app while the payment is in progress.\n          // do not handle the error if the user exits the app\n          throw error;\n        }\n        // get quote and check state\n        const meltQuote = await mintWallet.mint.checkMeltQuoteBolt11(\n          quote.quote\n        );\n        // store melt quote in invoice history\n        this.updateOutgoingInvoiceInHistory(\n          meltQuote as MeltQuoteBolt11Response\n        );\n\n        if (\n          meltQuote.state == MeltQuoteState.PAID ||\n          meltQuote.state == MeltQuoteState.PENDING\n        ) {\n          console.log(\n            \"### melt: error, but quote is paid or pending. not rolling back.\"\n          );\n          this.payInvoiceData.show = false;\n          notify(this.t(\"wallet.notifications.payment_pending_refresh\"));\n          throw error;\n        }\n\n        // roll back proof management and keyset counter\n        await proofsStore.setReserved(sendProofs, false);\n        this.increaseKeysetCounter(keysetId, -keysetCounterIncrease);\n        this.removeOutgoingInvoiceFromHistory(quote.quote);\n\n        console.error(error);\n        this.handleOutputsHaveAlreadyBeenSignedError(keysetId, error);\n        if (!silent) notifyApiError(error, \"Payment failed\");\n        throw error;\n      } finally {\n        uIStore.unlockMutex();\n      }\n    },\n    // /check\n    checkProofsSpendable: async function (\n      proofs: Proof[],\n      wallet: Wallet,\n      update_history = false\n    ) {\n      /*\n      checks with the mint whether an array of proofs is still\n      spendable or already invalidated\n      */\n      const uIStore = useUiStore();\n      const proofsStore = useProofsStore();\n      const tokenStore = useTokensStore();\n      if (proofs.length == 0) {\n        return;\n      }\n      try {\n        uIStore.triggerActivityOrb();\n        const { spent: spentProofs } = await wallet.groupProofsByState(proofs);\n        if (spentProofs.length) {\n          await proofsStore.removeProofs(spentProofs);\n          // update UI\n          const serializedProofs = proofsStore.serializeProofs(spentProofs);\n          if (serializedProofs == null) {\n            throw new Error(\"could not serialize proofs.\");\n          }\n          if (update_history) {\n            tokenStore.addPaidToken({\n              amount: -proofsStore.sumProofs(spentProofs),\n              token: serializedProofs,\n              unit: wallet.unit,\n              mint: wallet.mint.mintUrl,\n            });\n          }\n        }\n        // return spent proofs\n        return spentProofs;\n      } catch (error: any) {\n        console.error(error);\n        notifyApiError(error);\n        throw error;\n      }\n    },\n    checkTokenSpendable: async function (\n      historyToken: HistoryToken,\n      verbose: boolean = true\n    ) {\n      /*\n      checks whether a base64-encoded token (from the history table) has been spent already.\n      if it is spent, the appropraite entry in the history table is set to paid.\n      */\n      const uIStore = useUiStore();\n      const mintStore = useMintsStore();\n      const tokenStore = useTokensStore();\n      const proofsStore = useProofsStore();\n\n      const tokenJson = await token.decodeFull(historyToken.token);\n      if (tokenJson == undefined) {\n        throw new Error(\"no tokens provided.\");\n      }\n      const proofs = token.getProofs(tokenJson);\n      const mintWallet = await this.mintWallet(\n        historyToken.mint,\n        historyToken.unit\n      );\n\n      const mint = mintStore.mints.find((m) => m.url === historyToken.mint);\n      if (!mint) {\n        throw new Error(\"mint not found\");\n      }\n      const spentProofs = await this.checkProofsSpendable(proofs, mintWallet);\n      if (spentProofs != undefined && spentProofs.length == proofs.length) {\n        // all proofs are spent, set token to paid\n        tokenStore.setTokenPaid(historyToken.token);\n      } else if (\n        spentProofs != undefined &&\n        spentProofs.length &&\n        spentProofs.length < proofs.length\n      ) {\n        // not all proofs are spent, we remove the spent part of the token from the history\n        const spentAmount = proofsStore.sumProofs(spentProofs);\n        const serializedSpentProofs = proofsStore.serializeProofs(spentProofs);\n        const unspentProofs = proofs.filter(\n          (p) => !spentProofs.find((sp) => sp.secret === p.secret)\n        );\n        const unspentAmount = proofsStore.sumProofs(unspentProofs);\n        const serializedUnspentProofs =\n          proofsStore.serializeProofs(unspentProofs);\n\n        if (serializedSpentProofs && serializedUnspentProofs) {\n          const historyToken2 = tokenStore.editHistoryToken(\n            historyToken.token,\n            {\n              newAmount: spentAmount,\n              newStatus: \"paid\",\n              newToken: serializedSpentProofs,\n            }\n          );\n          // add all unspent proofs back to the history\n          // QUICK: we use the historyToken object here because we don't know if the transaction is incoming or outgoing (we don't know the sign of the amount)\n          if (historyToken2) {\n            tokenStore.addPendingToken({\n              amount: unspentAmount * Math.sign(historyToken2.amount),\n              token: serializedUnspentProofs,\n              unit: historyToken2.unit,\n              mint: historyToken2.mint,\n            });\n          }\n        }\n      }\n      if (spentProofs != undefined && spentProofs.length) {\n        useUiStore().vibrate();\n        const proofStore = useProofsStore();\n        notifySuccess(\n          this.t(\"wallet.notifications.sent\", {\n            amount: uIStore.formatCurrency(\n              proofStore.sumProofs(spentProofs),\n              historyToken.unit\n            ),\n          })\n        );\n      } else {\n        console.log(\"### token not paid yet\");\n        if (verbose) {\n          notify(this.t(\"wallet.notifications.token_still_pending\"));\n        }\n        return false;\n      }\n      return true;\n    },\n    checkInvoice: async function (\n      quote: string,\n      verbose = true,\n      hideInvoiceDetailsOnMint = true\n    ) {\n      const uIStore = useUiStore();\n      uIStore.triggerActivityOrb();\n      const mintStore = useMintsStore();\n      const invoice = this.invoiceHistory.find((i) => i.quote === quote);\n      if (!invoice) {\n        throw new Error(\"invoice not found\");\n      }\n      const mintWallet = await this.mintWallet(invoice.mint, invoice.unit);\n      const mint = mintStore.mints.find((m) => m.url === invoice.mint);\n      if (!mint) {\n        throw new Error(\"mint not found\");\n      }\n      try {\n        // check the state first\n        const state = (await mintWallet.checkMintQuoteBolt11(quote)).state;\n        if (state == MintQuoteState.ISSUED) {\n          this.setInvoicePaid(quote);\n          return;\n        }\n        if (state != MintQuoteState.PAID) {\n          console.log(\"### mintQuote not paid yet\");\n          if (verbose) {\n            notify(this.t(\"wallet.notifications.invoice_still_pending\"));\n          }\n          throw new Error(`invoice state not paid: ${state}`);\n        }\n        const proofs = await this.mint(invoice, verbose);\n        if (hideInvoiceDetailsOnMint) {\n          uIStore.showInvoiceDetails = false;\n        }\n        useUiStore().vibrate();\n        notifySuccess(\n          this.t(\"wallet.notifications.received_lightning\", {\n            amount: uIStore.formatCurrency(invoice.amount, invoice.unit),\n          })\n        );\n        return proofs;\n      } catch (error) {\n        // if (verbose) {\n        //   notify(\"Invoice still pending\");\n        // }\n        console.log(\"Invoice still pending\", invoice.quote);\n        throw error;\n      }\n    },\n    checkOutgoingInvoice: async function (quote: string, verbose = true) {\n      const uIStore = useUiStore();\n      const mintStore = useMintsStore();\n      const invoice = this.invoiceHistory.find((i) => i.quote === quote);\n      if (!invoice) {\n        throw new Error(\"invoice not found\");\n      }\n      const mintWallet = await this.mintWallet(invoice.mint, invoice.unit);\n      const mint = mintStore.mints.find((m) => m.url === invoice.mint);\n      if (!mint) {\n        throw new Error(\"mint not found\");\n      }\n      const proofs: Proof[] = await proofsStore.getProofsForQuote(quote);\n      try {\n        // this is an outgoing invoice, we first do a getMintQuote to check if the invoice is paid\n        const meltQuote = await mintWallet.mint.checkMeltQuoteBolt11(quote);\n        this.updateOutgoingInvoiceInHistory(\n          meltQuote as MeltQuoteBolt11Response\n        );\n        if (meltQuote.state == MeltQuoteState.PENDING) {\n          console.log(\"### mintQuote not paid yet\");\n          if (verbose) {\n            notify(this.t(\"wallet.notifications.invoice_still_pending\"));\n          }\n          throw new Error(\"invoice not paid yet.\");\n        } else if (meltQuote.state == MeltQuoteState.UNPAID) {\n          // we assume that the payment failed and we unset the proofs as reserved\n          await useProofsStore().setReserved(proofs, false);\n          this.removeOutgoingInvoiceFromHistory(quote);\n          notifyWarning(\n            this.t(\"wallet.notifications.lightning_payment_failed\")\n          );\n        } else if (meltQuote.state == MeltQuoteState.PAID) {\n          // if the invoice is paid, we check if all proofs are spent and if so, we invalidate them and set the invoice state in the history to \"paid\"\n          const spentProofs = await this.checkProofsSpendable(\n            proofs,\n            mintWallet,\n            true\n          );\n          if (spentProofs != undefined && spentProofs.length == proofs.length) {\n            useUiStore().vibrate();\n            notifySuccess(\n              this.t(\"wallet.notifications.sent\", {\n                amount: uIStore.formatCurrency(\n                  useProofsStore().sumProofs(proofs),\n                  invoice.unit\n                ),\n              })\n            );\n          }\n          // set invoice in history to paid\n          this.setInvoicePaid(quote);\n        }\n      } catch (error: any) {\n        if (verbose) {\n          notifyApiError(error);\n        }\n        console.log(\"Could not check quote\", invoice.quote, error);\n        throw error;\n      }\n    },\n    onTokenPaid: async function (historyToken: HistoryToken) {\n      const sendTokensStore = useSendTokensStore();\n      const uIStore = useUiStore();\n      const tokenJson = await token.decodeFull(historyToken.token);\n      const mintStore = useMintsStore();\n      const settingsStore = useSettingsStore();\n      if (!settingsStore.checkSentTokens) {\n        console.log(\n          \"settingsStore.checkSentTokens is disabled, skipping token check\"\n        );\n        return;\n      }\n      const mint = mintStore.mints.find((m) => m.url === historyToken.mint);\n      if (!mint) {\n        throw new Error(\"mint not found\");\n      }\n      if (\n        !settingsStore.useWebsockets ||\n        !mint.info?.nuts[17]?.supported ||\n        !mint.info?.nuts[17]?.supported.find(\n          (s) =>\n            s.method == \"bolt11\" &&\n            s.unit == historyToken.unit &&\n            s.commands.indexOf(\"proof_state\") != -1\n        )\n      ) {\n        console.log(\n          \"Websockets not supported, kicking off token check worker.\"\n        );\n        useWorkersStore().checkTokenSpendableWorker(historyToken);\n        return;\n      }\n      try {\n        console.log(\"onTokenPaid kicking off websocket\");\n        if (tokenJson == undefined) {\n          throw new Error(\"no tokens provided.\");\n        }\n        const proofs = token.getProofs(tokenJson);\n        const oneProof = [proofs[0]];\n        this.activeWebsocketConnections++;\n        uIStore.triggerActivityOrb();\n        const wallet = await this.activeWallet();\n        const unsub = await wallet.on.proofStateUpdates(\n          oneProof,\n          async (proofState: ProofState) => {\n            console.log(`Websocket: proof state updated: ${proofState.state}`);\n            if (proofState.state == CheckStateEnum.SPENT) {\n              const tokenSpent = await this.checkTokenSpendable(historyToken);\n              if (tokenSpent) {\n                sendTokensStore.showSendTokens = false;\n                unsub();\n              }\n            }\n          },\n          async (error: any) => {\n            console.error(error);\n            notifyApiError(error);\n            throw error;\n          }\n        );\n      } catch (error) {\n        console.error(\n          \"Error in websocket subscription. Starting invoices worker.\",\n          error\n        );\n        useWorkersStore().checkTokenSpendableWorker(historyToken);\n      } finally {\n        this.activeWebsocketConnections--;\n      }\n    },\n    mintOnPaid: async function (\n      quote: string,\n      verbose = true,\n      kickOffInvoiceChecker = true,\n      hideInvoiceDetailsOnMint = true\n    ) {\n      const mintStore = useMintsStore();\n      const settingsStore = useSettingsStore();\n      if (!settingsStore.checkIncomingInvoices) {\n        console.log(\n          \"settingsStore.checkIncomingInvoices is disabled, skipping invoice check\"\n        );\n        return;\n      }\n      const invoice = this.invoiceHistory.find((i) => i.quote === quote);\n      if (!invoice) {\n        throw new Error(\"invoice not found\");\n      }\n      const mintWallet = await this.mintWallet(invoice.mint, invoice.unit);\n      const mint = mintStore.mints.find((m) => m.url === invoice.mint);\n\n      if (!mint) {\n        throw new Error(\"mint not found\");\n      }\n      // add to checker before we try a websocket\n      if (kickOffInvoiceChecker) {\n        if (useSettingsStore().periodicallyCheckIncomingInvoices) {\n          console.log(`Adding quote ${quote} to long-polling checker.`);\n          useInvoicesWorkerStore().addInvoiceToChecker(quote);\n        } else if (useSettingsStore().checkIncomingInvoices) {\n          console.log(`Adding quote ${quote} to old worker checker.`);\n          useWorkersStore().invoiceCheckWorker(quote);\n        }\n      }\n\n      if (\n        !settingsStore.useWebsockets ||\n        !mint.info?.nuts[17]?.supported ||\n        !mint.info?.nuts[17]?.supported.find(\n          (s) =>\n            s.method == \"bolt11\" &&\n            s.unit == invoice.unit &&\n            s.commands.indexOf(\"bolt11_mint_quote\") != -1\n        )\n      ) {\n        console.log(\"Websockets not supported.\");\n        return;\n      }\n      const uIStore = useUiStore();\n      try {\n        this.activeWebsocketConnections++;\n        uIStore.triggerActivityOrb();\n        const unsub = await mintWallet.on.mintQuotePaid(\n          quote,\n          async (_mintQuoteResponse: MintQuoteBolt11Response) => {\n            console.log(\"Websocket: mint quote paid.\");\n            let proofs;\n            try {\n              proofs = await this.mint(invoice, false);\n            } catch (error: any) {\n              console.error(error);\n              // notifyApiError(error);\n              throw error;\n            }\n\n            if (hideInvoiceDetailsOnMint) {\n              uIStore.showInvoiceDetails = false;\n            }\n            useUiStore().vibrate();\n            notifySuccess(\n              this.t(\"wallet.notifications.received_lightning\", {\n                amount: uIStore.formatCurrency(invoice.amount, invoice.unit),\n              })\n            );\n            unsub();\n            return proofs;\n          },\n          async (error: any) => {\n            if (verbose) {\n              notifyApiError(error);\n            }\n            console.log(\"Invoice still pending\", invoice.quote);\n            throw error;\n          }\n        );\n      } catch (error) {\n        console.log(\"Error in websocket subscription\", error);\n      } finally {\n        this.activeWebsocketConnections--;\n      }\n    },\n    ////////////// UI HELPERS //////////////\n    addOutgoingPendingInvoiceToHistory: async function (\n      quote: MeltQuoteBolt11Response,\n      mint: string,\n      unit: string\n    ) {\n      this.invoiceHistory.push({\n        amount: -(quote.amount + quote.fee_reserve),\n        bolt11: this.payInvoiceData.input.request,\n        quote: quote.quote,\n        memo: \"Outgoing invoice\",\n        date: currentDateStr(),\n        status: \"pending\",\n        mint: mint,\n        unit: unit,\n        meltQuote: quote,\n      });\n    },\n    removeOutgoingInvoiceFromHistory: function (quote: string) {\n      const index = this.invoiceHistory.findIndex((i) => i.quote === quote);\n      if (index >= 0) {\n        this.invoiceHistory.splice(index, 1);\n      }\n    },\n    updateOutgoingInvoiceInHistory: function (\n      quote: MeltQuoteBolt11Response,\n      options?: { status?: \"pending\" | \"paid\"; amount?: number }\n    ) {\n      this.invoiceHistory\n        .filter((i) => i.quote === quote.quote)\n        .forEach((i) => {\n          if (options) {\n            if (options.status) {\n              i.status = options.status;\n              if (options.status === \"paid\") {\n                i.paidDate = currentDateStr();\n              }\n            }\n            if (options.amount) {\n              i.amount = options.amount;\n            }\n            i.meltQuote = quote;\n          }\n        });\n    },\n    checkPendingTokens: async function (verbose: boolean = true) {\n      const tokenStore = useTokensStore();\n      const last_n = 5;\n      let i = 0;\n      // invert for loop\n      for (const t of tokenStore.historyTokens.slice().reverse()) {\n        if (i >= last_n) {\n          break;\n        }\n        if (t.status === \"pending\" && t.amount < 0 && t.token) {\n          console.log(\"### checkPendingTokens\", t.token);\n          this.checkTokenSpendable(t, verbose);\n          i += 1;\n        }\n      }\n    },\n    handleBolt11Invoice: async function () {\n      this.payInvoiceData.show = true;\n      let invoice;\n      try {\n        invoice = bolt11Decoder.decode(this.payInvoiceData.input.request);\n      } catch (error) {\n        notifyWarning(\n          this.t(\"wallet.notifications.failed_to_decode_invoice\"),\n          undefined,\n          3000\n        );\n        this.payInvoiceData.show = false;\n        throw error;\n      }\n      const cleanInvoice = {\n        bolt11: invoice.paymentRequest,\n        memo: \"\",\n        msat: 0,\n        sat: 0,\n        fsat: 0,\n        hash: \"\",\n        description: \"\",\n        timestamp: 0,\n        expireDate: \"\",\n        expired: false,\n      };\n      _.each(invoice.sections, (tag) => {\n        if (_.isObject(tag) && _.has(tag, \"name\")) {\n          if (tag.name === \"amount\") {\n            cleanInvoice.msat = parseInt(tag.value, 10);\n            cleanInvoice.sat = parseInt(tag.value, 10) / 1000;\n            cleanInvoice.fsat = cleanInvoice.sat;\n          } else if (tag.name === \"payment_hash\") {\n            cleanInvoice.hash = tag.value;\n          } else if (tag.name === \"description\") {\n            cleanInvoice.description = tag.value;\n          } else if (tag.name === \"timestamp\") {\n            cleanInvoice.timestamp = tag.value;\n          } else if (tag.name === \"expiry\") {\n            const expireDate = new Date(\n              (cleanInvoice.timestamp + tag.value) * 1000\n            );\n            cleanInvoice.expireDate = date.formatDate(\n              expireDate,\n              \"YYYY-MM-DDTHH:mm:ss.SSSZ\"\n            );\n            cleanInvoice.expired = false; // TODO\n          }\n        }\n      });\n\n      this.payInvoiceData.invoice = Object.freeze(cleanInvoice);\n      // get quote for this request\n      await this.meltQuoteInvoiceData();\n    },\n    handleCashuToken: function () {\n      this.payInvoiceData.show = false;\n      receiveStore.showReceiveTokens = true;\n    },\n    handleP2PK: function (req: string) {\n      const sendTokenStore = useSendTokensStore();\n      sendTokenStore.sendData.p2pkPubkey = req;\n      sendTokenStore.showSendTokens = true;\n    },\n    handlePaymentRequest: async function (req: string) {\n      const prStore = usePRStore();\n      await prStore.decodePaymentRequest(req);\n    },\n    decodeRequest: async function (req: string) {\n      const p2pkStore = useP2PKStore();\n      req = req.trim();\n      this.payInvoiceData.input.request = req;\n      if (req.toLowerCase().startsWith(\"lnbc\")) {\n        this.payInvoiceData.input.request = req;\n        await this.handleBolt11Invoice();\n      } else if (req.toLowerCase().startsWith(\"lightning:\")) {\n        this.payInvoiceData.input.request = req.slice(10);\n        await this.handleBolt11Invoice();\n      } else if (req.toLowerCase().startsWith(\"bitcoin:\")) {\n        try {\n          const url = new URL(\n            req.replace(/^bitcoin:/i, \"bitcoin://placeholder/\")\n          );\n          const creq = url.searchParams.get(\"creq\");\n          const lightning = url.searchParams.get(\"lightning\");\n          if (creq) {\n            this.payInvoiceData.input.request = creq;\n            await this.handlePaymentRequest(creq);\n          } else if (lightning) {\n            this.payInvoiceData.input.request = lightning;\n            if (lightning.toLowerCase().startsWith(\"lnurl1\")) {\n              await this.lnurlPayFirst(lightning);\n            } else {\n              await this.handleBolt11Invoice();\n            }\n          }\n        } catch {\n          const creqMatch = req.match(/[?&]creq=([^&]+)/i);\n          const lightningMatch = req.match(/[?&]lightning=([^&]+)/i);\n          if (creqMatch) {\n            this.payInvoiceData.input.request = creqMatch[1];\n            await this.handlePaymentRequest(creqMatch[1]);\n          } else if (lightningMatch) {\n            this.payInvoiceData.input.request = lightningMatch[1];\n            await this.handleBolt11Invoice();\n          }\n        }\n      } else if (req.toLowerCase().startsWith(\"lnurl:\")) {\n        this.payInvoiceData.input.request = req.slice(6);\n        await this.lnurlPayFirst(this.payInvoiceData.input.request);\n      } else if (req.indexOf(\"lightning=lnurl1\") !== -1) {\n        this.payInvoiceData.input.request = req\n          .split(\"lightning=\")[1]\n          .split(\"&\")[0];\n        await this.lnurlPayFirst(this.payInvoiceData.input.request);\n      } else if (\n        req.toLowerCase().startsWith(\"lnurl1\") ||\n        req.match(/[\\w.+-~_]+@[\\w.+-~_]/)\n      ) {\n        this.payInvoiceData.input.request = req;\n        await this.lnurlPayFirst(this.payInvoiceData.input.request);\n      } else if (req.startsWith(\"cashuA\") || req.startsWith(\"cashuB\")) {\n        // parse cashu tokens from a pasted token\n        receiveStore.receiveData.tokensBase64 = req;\n        this.handleCashuToken();\n      } else if (req.indexOf(\"token=cashu\") !== -1) {\n        // parse cashu tokens from a URL like https://example.com#token=cashu...\n        const token = req.slice(req.indexOf(\"token=cashu\") + 6);\n        receiveStore.receiveData.tokensBase64 = token;\n        this.handleCashuToken();\n      } else if (p2pkStore.isValidPubkey(req)) {\n        this.handleP2PK(req);\n      } else if (req.startsWith(\"http\")) {\n        const mintStore = useMintsStore();\n        mintStore.addMintData = { url: req, nickname: \"\" };\n      } else if (\n        req.toLowerCase().startsWith(\"creqa\") ||\n        req.toLowerCase().startsWith(\"creqb\")\n      ) {\n        await this.handlePaymentRequest(req);\n      } else if (isLegacyRetailQR(req)) {\n        // Try to convert legacy retail QR code (EMV format) to Lightning Address\n        const lightningAddress = translateLegacyQRToLightningAddress(req);\n        if (lightningAddress) {\n          // Process as Lightning Address (LNURL)\n          this.payInvoiceData.input.request = lightningAddress;\n          await this.lnurlPayFirst(lightningAddress);\n        } else {\n          // Not a supported merchant QR code\n          notifyWarning(\n            this.t(\"wallet.notifications.unsupported_legacy_qr\"),\n            this.t(\"wallet.notifications.legacy_qr_not_supported\")\n          );\n        }\n      }\n      const uiStore = useUiStore();\n      uiStore.closeDialogs();\n    },\n    lnurlPayFirst: async function (address: string) {\n      let host;\n      let data;\n      if (address.split(\"@\").length == 2) {\n        const [user, lnaddresshost] = address.split(\"@\");\n        host = `https://${lnaddresshost}/.well-known/lnurlp/${user}`;\n        const resp = await axios.get(host); // Moved it here: we don't want 2 potential calls\n        data = resp.data;\n      } else if (address.toLowerCase().slice(0, 6) === \"lnurl1\") {\n        const decoded = bech32.decode(address, 20000);\n        const words = bech32.fromWords(decoded.words);\n        const uint8Array = new Uint8Array(words);\n        host = new TextDecoder().decode(uint8Array);\n\n        const resp = await axios.get(host);\n        data = resp.data;\n      }\n      if (host == undefined) {\n        notifyError(\n          this.t(\"wallet.notifications.invalid_lnurl\"),\n          this.t(\"wallet.notifications.lnurl_error\")\n        );\n        return;\n      }\n      if (data.tag == \"payRequest\") {\n        this.payInvoiceData.lnurlpay = data;\n        this.payInvoiceData.lnurlpay.domain = host\n          .split(\"https://\")[1]\n          .split(\"/\")[0];\n        // Store lightning address if it was a lightning address (not a LNURL)\n        if (address.split(\"@\").length == 2) {\n          this.payInvoiceData.lnurlpay.lightningAddress = address;\n        } else {\n          this.payInvoiceData.lnurlpay.lightningAddress = \"\";\n        }\n        if (\n          this.payInvoiceData.lnurlpay.maxSendable ==\n          this.payInvoiceData.lnurlpay.minSendable\n        ) {\n          this.payInvoiceData.input.amount =\n            this.payInvoiceData.lnurlpay.maxSendable / 1000;\n        }\n        this.payInvoiceData.invoice = null;\n        this.payInvoiceData.input = {\n          request: \"\",\n          amount: undefined,\n          comment: \"\",\n          quote: \"\",\n        };\n        this.payInvoiceData.show = true;\n      }\n    },\n    lnurlPaySecond: async function () {\n      const mintStore = useMintsStore();\n      let amount = this.payInvoiceData.input.amount;\n      if (amount == null) {\n        notifyError(\n          this.t(\"wallet.notifications.no_amount\"),\n          this.t(\"wallet.notifications.lnurl_error\")\n        );\n        return;\n      }\n      if (this.payInvoiceData.lnurlpay == null) {\n        notifyError(\n          this.t(\"wallet.notifications.no_lnurl_data\"),\n          this.t(\"wallet.notifications.lnurl_error\")\n        );\n        return;\n      }\n      if (\n        this.payInvoiceData.lnurlpay.tag == \"payRequest\" &&\n        this.payInvoiceData.lnurlpay.minSendable <= amount * 1000 &&\n        this.payInvoiceData.lnurlpay.maxSendable >= amount * 1000\n      ) {\n        if (mintStore.activeUnit == \"usd\") {\n          const priceUsd = usePriceStore().bitcoinPrice;\n          if (priceUsd == 0) {\n            notifyError(\n              this.t(\"wallet.notifications.no_price_data\"),\n              this.t(\"wallet.notifications.lnurl_error\")\n            );\n            return;\n          }\n          const satPrice = 1 / (priceUsd / 1e8);\n          const usdAmount = amount;\n          amount = Math.floor(usdAmount * satPrice);\n        }\n        const callback = this.payInvoiceData.lnurlpay.callback;\n        const separator = callback.includes(\"?\") ? \"&\" : \"?\";\n        const { data } = await axios.get(\n          `${callback}${separator}amount=${amount * 1000}`\n        );\n        // check http error\n        if (data.status == \"ERROR\") {\n          notifyError(data.reason, this.t(\"wallet.notifications.lnurl_error\"));\n          return;\n        }\n        await this.decodeRequest(data.pr);\n      }\n    },\n    initializeMnemonic: function () {\n      if (this.mnemonic == \"\") {\n        this.mnemonic = generateMnemonic(wordlist);\n      }\n      return this.mnemonic;\n    },\n    handleOutputsHaveAlreadyBeenSignedError: function (\n      keysetId: string,\n      error: any\n    ) {\n      if (error.message.includes(\"outputs have already been signed\")) {\n        this.increaseKeysetCounter(keysetId, 10);\n        notify(this.t(\"wallet.notifications.please_try_again\"));\n        return true;\n      }\n      return false;\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/welcome.ts",
    "content": "// src/stores/welcome.ts\nimport { defineStore } from \"pinia\";\nimport { useLocalStorage } from \"@vueuse/core\";\nimport { computed } from \"vue\";\n\nexport type WelcomeState = {\n  showWelcome: boolean;\n  currentSlide: number;\n  seedPhraseValidated: boolean;\n  termsAccepted: boolean;\n  onboardingPath: string; // 'new' | 'recover' | ''\n  seedEnteredValid: boolean;\n  mintSetupCompleted: boolean;\n  ecashRestoreCompleted: boolean;\n};\n\n// Define the Pinia store\nexport const useWelcomeStore = defineStore(\"welcome\", {\n  state: (): WelcomeState => ({\n    showWelcome: useLocalStorage<boolean>(\"cashu.welcome.showWelcome\", true),\n    currentSlide: useLocalStorage<number>(\"cashu.welcome.currentSlide\", 0),\n    seedPhraseValidated: useLocalStorage<boolean>(\n      \"cashu.welcome.seedPhraseValidated\",\n      false\n    ),\n    termsAccepted: useLocalStorage<boolean>(\n      \"cashu.welcome.termsAccepted\",\n      false\n    ),\n    onboardingPath: useLocalStorage<string>(\"cashu.welcome.path\", \"\"),\n    seedEnteredValid: useLocalStorage<boolean>(\n      \"cashu.welcome.seedEnteredValid\",\n      false\n    ),\n    mintSetupCompleted: useLocalStorage<boolean>(\n      \"cashu.welcome.mintSetupCompleted\",\n      false\n    ),\n    ecashRestoreCompleted: useLocalStorage<boolean>(\n      \"cashu.welcome.ecashRestoreCompleted\",\n      false\n    ),\n  }),\n  getters: {\n    // Determines if the current slide is the last one\n    isLastSlide: (state) => {\n      // Slides:\n      // 0 Intro, 1 PWA, 2 Choice,\n      // New: 3 Seed, 4 Mints (no more terms screen)\n      // Recover: 3 SeedIn, 4 Mints, 5 Restore (no more terms screen)\n      if (state.onboardingPath === \"recover\") return state.currentSlide === 5;\n      if (state.onboardingPath === \"new\") return state.currentSlide === 4;\n      // before choosing a path\n      return false;\n    },\n\n    // Determines if the user can proceed to the next slide\n    canProceed: (state) => {\n      // 0 Intro\n      if (state.currentSlide === 0) return true;\n      // 1 PWA\n      if (state.currentSlide === 1) return true;\n      // 2 Choice\n      if (state.currentSlide === 2) return state.onboardingPath !== \"\";\n      // 3 (seed phrase for both paths)\n      if (state.currentSlide === 3) {\n        if (state.onboardingPath === \"new\") return state.seedPhraseValidated;\n        if (state.onboardingPath === \"recover\") return state.seedEnteredValid;\n      }\n      // 4 (mints setup for both paths - last step for \"new\" path)\n      if (state.currentSlide === 4) return state.mintSetupCompleted || true;\n      // 5 (restore step for recover path - last step for \"recover\" path)\n      if (state.currentSlide === 5) {\n        if (state.onboardingPath === \"recover\")\n          return state.ecashRestoreCompleted || true;\n      }\n      return false;\n    },\n\n    // Determines if the user can navigate to the previous slide\n    canGoPrev: (state) => state.currentSlide > 0,\n  },\n  actions: {\n    /**\n     * Initializes the welcome dialog based on local storage.\n     * Should be called when the store is initialized.\n     */\n    initializeWelcome() {\n      if (!this.showWelcome) {\n        window.location.href = \"/\";\n      }\n    },\n\n    /**\n     * Closes the welcome dialog and marks it as seen.\n     */\n    closeWelcome() {\n      this.showWelcome = false;\n      // Reset the slide to the beginning for next time (if welcome is ever shown again)\n      this.currentSlide = 0;\n      // Redirect to home or desired route\n      window.location.href =\n        \"/\" + window.location.search + window.location.hash;\n    },\n    setPath(path: \"new\" | \"recover\") {\n      this.onboardingPath = path;\n    },\n\n    /**\n     * Sets the current slide index.\n     * @param index - The index of the slide to navigate to.\n     */\n    setCurrentSlide(index: number) {\n      this.currentSlide = index;\n    },\n\n    /**\n     * Marks the terms as accepted.\n     */\n    acceptTerms() {\n      this.termsAccepted = true;\n    },\n\n    /**\n     * Validates the seed phrase.\n     */\n    validateSeedPhrase() {\n      this.seedPhraseValidated = true;\n    },\n\n    /**\n     * Resets the welcome dialog state (useful for testing or resetting).\n     */\n    resetWelcome() {\n      this.showWelcome = true;\n      this.currentSlide = 0;\n      this.termsAccepted = false;\n      this.seedPhraseValidated = false;\n      this.onboardingPath = \"\";\n      this.seedEnteredValid = false;\n      this.mintSetupCompleted = false;\n      this.ecashRestoreCompleted = false;\n    },\n\n    /**\n     * Navigates to the previous slide if possible.\n     */\n    goToPrevSlide() {\n      if (this.canGoPrev) {\n        this.currentSlide -= 1;\n      }\n      // Optionally, handle edge cases or emit events\n    },\n\n    /**\n     * Navigates to the next slide if possible.\n     * If on the last slide, it can close the welcome dialog.\n     */\n    goToNextSlide() {\n      if (this.canProceed) {\n        if (this.isLastSlide) {\n          this.closeWelcome();\n        } else {\n          this.currentSlide += 1;\n        }\n      }\n      // Optionally, handle edge cases or emit events\n      console.log(`href: ${window.location.href}`);\n    },\n  },\n});\n"
  },
  {
    "path": "src/stores/workers.ts",
    "content": "import { defineStore } from \"pinia\";\nimport { useWalletStore } from \"src/stores/wallet\"; // invoiceData,\nimport { useUiStore } from \"src/stores/ui\"; // showInvoiceDetails\nimport { useSendTokensStore } from \"src/stores/sendTokensStore\"; // showSendTokens and sendData\nimport { useSettingsStore } from \"./settings\";\nimport { HistoryToken, useTokensStore } from \"./tokens\";\nexport const useWorkersStore = defineStore(\"workers\", {\n  state: () => {\n    return {\n      invoiceCheckListener: null as NodeJS.Timeout | null,\n      tokensCheckSpendableListener: null as NodeJS.Timeout | null,\n      invoiceWorkerRunning: false,\n      tokenWorkerRunning: false,\n      checkInterval: 5000,\n    };\n  },\n  getters: {},\n\n  actions: {\n    clearAllWorkers: function () {\n      if (this.invoiceCheckListener) {\n        clearInterval(this.invoiceCheckListener);\n        this.invoiceWorkerRunning = false;\n      }\n      if (this.tokensCheckSpendableListener) {\n        clearInterval(this.tokensCheckSpendableListener);\n        this.tokenWorkerRunning = false;\n      }\n    },\n    invoiceCheckWorker: async function (quote: string) {\n      const walletStore = useWalletStore();\n      let nInterval = 0;\n      this.clearAllWorkers();\n      this.invoiceCheckListener = setInterval(async () => {\n        try {\n          this.invoiceWorkerRunning = true;\n          nInterval += 1;\n\n          // exit loop after 1m\n          if (nInterval > 12) {\n            console.log(\"### stopping invoice check worker\");\n            this.clearAllWorkers();\n          }\n          console.log(\"### invoiceCheckWorker setInterval\", nInterval);\n\n          // this will throw an error if the invoice is pending\n          await walletStore.checkInvoice(quote, false);\n\n          // only without error (invoice paid) will we reach here\n          console.log(\"### stopping invoice check worker\");\n          this.clearAllWorkers();\n        } catch (error) {\n          console.log(\"invoiceCheckWorker: not paid yet\");\n        }\n      }, this.checkInterval);\n    },\n    checkTokenSpendableWorker: async function (historyToken: HistoryToken) {\n      const settingsStore = useSettingsStore();\n      if (!settingsStore.checkSentTokens) {\n        console.log(\n          \"settingsStore.checkSentTokens is disabled, not kicking off checkTokenSpendableWorker\"\n        );\n        return;\n      }\n      console.log(\"### kicking off checkTokenSpendableWorker\");\n      this.tokenWorkerRunning = true;\n      const walletStore = useWalletStore();\n      const sendTokensStore = useSendTokensStore();\n      let nInterval = 0;\n      this.clearAllWorkers();\n      this.tokensCheckSpendableListener = setInterval(async () => {\n        try {\n          nInterval += 1;\n          // exit loop after 30s\n          if (nInterval > 10) {\n            console.log(\"### stopping token check worker\");\n            this.clearAllWorkers();\n          }\n          console.log(\"### checkTokenSpendableWorker setInterval\", nInterval);\n          const paid = await walletStore.checkTokenSpendable(\n            historyToken,\n            false\n          );\n          if (paid) {\n            console.log(\"### stopping token check worker\");\n            this.clearAllWorkers();\n            sendTokensStore.showSendTokens = false;\n          }\n        } catch (error) {\n          console.log(\"checkTokenSpendableWorker: some error\", error);\n          this.clearAllWorkers();\n        }\n      }, this.checkInterval);\n    },\n  },\n});\n"
  },
  {
    "path": "src-electron/electron-env.d.ts",
    "content": "/* eslint-disable */\n\ndeclare namespace NodeJS {\n  interface ProcessEnv {\n    QUASAR_PUBLIC_FOLDER: string;\n    QUASAR_ELECTRON_PRELOAD: string;\n    APP_URL: string;\n  }\n}\n"
  },
  {
    "path": "src-electron/electron-flag.d.ts",
    "content": "/* eslint-disable */\n// THIS FEATURE-FLAG FILE IS AUTOGENERATED,\n//  REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING\nimport \"quasar/dist/types/feature-flag\";\n\ndeclare module \"quasar/dist/types/feature-flag\" {\n  interface QuasarFeatureFlags {\n    electron: true;\n  }\n}\n"
  },
  {
    "path": "src-electron/electron-main.ts",
    "content": "import { app, BrowserWindow } from \"electron\";\nimport path from \"path\";\nimport os from \"os\";\n\n// needed in case process is undefined under Linux\nconst platform = process.platform || os.platform();\n\nlet mainWindow: BrowserWindow | undefined;\n\nfunction createWindow() {\n  /**\n   * Initial window options\n   */\n  mainWindow = new BrowserWindow({\n    icon: path.resolve(__dirname, \"icons/icon.png\"), // tray icon\n    width: 1000,\n    height: 600,\n    useContentSize: true,\n    webPreferences: {\n      contextIsolation: true,\n      // More info: https://v2.quasar.dev/quasar-cli-vite/developing-electron-apps/electron-preload-script\n      preload: path.resolve(__dirname, process.env.QUASAR_ELECTRON_PRELOAD),\n    },\n  });\n\n  mainWindow.loadURL(process.env.APP_URL);\n\n  if (process.env.DEBUGGING) {\n    // if on DEV or Production with debug enabled\n    mainWindow.webContents.openDevTools();\n  } else {\n    // we're on production; no access to devtools pls\n    mainWindow.webContents.on(\"devtools-opened\", () => {\n      mainWindow?.webContents.closeDevTools();\n    });\n  }\n\n  mainWindow.on(\"closed\", () => {\n    mainWindow = undefined;\n  });\n}\n\napp.whenReady().then(createWindow);\n\napp.on(\"window-all-closed\", () => {\n  if (platform !== \"darwin\") {\n    app.quit();\n  }\n});\n\napp.on(\"activate\", () => {\n  if (mainWindow === undefined) {\n    createWindow();\n  }\n});\n"
  },
  {
    "path": "src-electron/electron-preload.ts",
    "content": "/**\n * This file is used specifically for security reasons.\n * Here you can access Nodejs stuff and inject functionality into\n * the renderer thread (accessible there through the \"window\" object)\n *\n * WARNING!\n * If you import anything from node_modules, then make sure that the package is specified\n * in package.json > dependencies and NOT in devDependencies\n *\n * Example (injects window.myAPI.doAThing() into renderer thread):\n *\n *   import { contextBridge } from 'electron'\n *\n *   contextBridge.exposeInMainWorld('myAPI', {\n *     doAThing: () => {}\n *   })\n *\n * WARNING!\n * If accessing Node functionality (like importing @electron/remote) then in your\n * electron-main.ts you will need to set the following when you instantiate BrowserWindow:\n *\n * mainWindow = new BrowserWindow({\n *   // ...\n *   webPreferences: {\n *     // ...\n *     sandbox: false // <-- to be able to import @electron/remote in preload script\n *   }\n * }\n */\n"
  },
  {
    "path": "src-pwa/custom-service-worker.js",
    "content": "/* eslint-env serviceworker */\n\n/*\n * This file (which will be your service worker)\n * is picked up by the build system ONLY if\n * quasar.config.js > pwa > workboxMode is set to \"injectManifest\"\n */\n\nimport { clientsClaim } from \"workbox-core\";\nimport {\n  precacheAndRoute,\n  cleanupOutdatedCaches,\n  createHandlerBoundToURL,\n} from \"workbox-precaching\";\nimport { registerRoute, NavigationRoute } from \"workbox-routing\";\n\nself.skipWaiting();\nclientsClaim();\n\n// Use with precache injection\nprecacheAndRoute(self.__WB_MANIFEST);\n\ncleanupOutdatedCaches();\n\n// Non-SSR fallback to index.html\n// Production SSR fallback to offline.html (except for dev)\nif (process.env.MODE !== \"ssr\" || process.env.PROD) {\n  registerRoute(\n    new NavigationRoute(\n      createHandlerBoundToURL(process.env.PWA_FALLBACK_HTML),\n      { denylist: [/sw\\.js$/, /workbox-(.)*\\.js$/] }\n    )\n  );\n}\n"
  },
  {
    "path": "src-pwa/manifest.json",
    "content": "{\n  \"name\": \"Cashu.me\",\n  \"short_name\": \"Cashu.me\",\n  \"description\": \"A Cashu Ecash wallet for the web.\",\n  \"display\": \"standalone\",\n  \"orientation\": \"portrait\",\n  \"background_color\": \"#000000\",\n  \"theme_color\": \"#000000\",\n  \"protocol_handlers\": [\n    {\n      \"protocol\": \"web+cashu\",\n      \"url\": \"/?token=%s\"\n    },\n    {\n      \"protocol\": \"web+lightning\",\n      \"url\": \"/?lightning=%s\"\n    }\n  ],\n  \"icons\": [\n    {\n      \"src\": \"icons/icon-128x128.png\",\n      \"sizes\": \"128x128\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable\"\n    },\n    {\n      \"src\": \"icons/icon-192x192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable\"\n    },\n    {\n      \"src\": \"icons/icon-256x256.png\",\n      \"sizes\": \"256x256\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable\"\n    },\n    {\n      \"src\": \"icons/icon-384x384.png\",\n      \"sizes\": \"384x384\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable\"\n    },\n    {\n      \"src\": \"icons/icon-512x512.png\",\n      \"sizes\": \"512x512\",\n      \"type\": \"image/png\",\n      \"purpose\": \"maskable\"\n    },\n    {\n      \"src\": \"icons/icon-512x512.png\",\n      \"sizes\": \"512x512\",\n      \"type\": \"image/png\",\n      \"purpose\": \"any\"\n    }\n  ],\n  \"screenshots\": [\n    {\n      \"src\": \"/screenshots/narrow-1.png\",\n      \"form_factor\": \"narrow\",\n      \"sizes\": \"542x942\",\n      \"type\": \"image/png\",\n      \"label\": \"fullscreen view\"\n    },\n    {\n      \"src\": \"/screenshots/narrow-2.png\",\n      \"form_factor\": \"narrow\",\n      \"sizes\": \"542x942\",\n      \"type\": \"image/png\",\n      \"label\": \"fullscreen view\"\n    },\n    {\n      \"src\": \"/screenshots/wide-1.png\",\n      \"form_factor\": \"wide\",\n      \"sizes\": \"1910x932\",\n      \"type\": \"image/png\",\n      \"label\": \"fullscreen view\"\n    },\n    {\n      \"src\": \"/screenshots/wide-2.png\",\n      \"form_factor\": \"wide\",\n      \"sizes\": \"1910x932\",\n      \"type\": \"image/png\",\n      \"label\": \"fullscreen view\"\n    }\n  ]\n}\n"
  },
  {
    "path": "src-pwa/pwa-flag.d.ts",
    "content": "/* eslint-disable */\n// THIS FEATURE-FLAG FILE IS AUTOGENERATED,\n//  REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING\nimport \"quasar/dist/types/feature-flag\";\n\ndeclare module \"quasar/dist/types/feature-flag\" {\n  interface QuasarFeatureFlags {\n    pwa: true;\n  }\n}\n"
  },
  {
    "path": "src-pwa/register-service-worker.js",
    "content": "import { register } from \"register-service-worker\";\n\n// The ready(), registered(), cached(), updatefound() and updated()\n// events passes a ServiceWorkerRegistration instance in their arguments.\n// ServiceWorkerRegistration: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration\n\nregister(process.env.SERVICE_WORKER_FILE, {\n  // The registrationOptions object will be passed as the second argument\n  // to ServiceWorkerContainer.register()\n  // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register#Parameter\n\n  // registrationOptions: { scope: './' },\n\n  ready(/* registration */) {\n    // console.log('Service worker is active.')\n  },\n\n  registered(/* registration */) {\n    // console.log('Service worker has been registered.')\n  },\n\n  cached(/* registration */) {\n    // console.log('Content has been cached for offline use.')\n  },\n\n  updatefound(/* registration */) {\n    // console.log('New content is downloading.')\n  },\n\n  updated(/* registration */) {\n    // console.log('New content is available; please refresh.')\n  },\n\n  offline() {\n    // console.log('No internet connection found. App is running in offline mode.')\n  },\n\n  error(/* err */) {\n    // console.error('Error during service worker registration:', err)\n  },\n});\n"
  },
  {
    "path": "test/vitest/__tests__/bip39seed.test.ts",
    "content": "import { test, describe, expect } from \"vitest\";\nimport {\n  mnemonicToSeedSync,\n  validateMnemonic,\n  mnemonicToSeed,\n} from \"@scure/bip39\";\nimport { wordlist } from \"@scure/bip39/wordlists/english\";\n\ndescribe(\"mnemonicToSeedSync\", () => {\n  test(\"converts same mnemonic consistently\", () => {\n    const mnem =\n      \"legal winner thank year wave sausage worth useful legal winner thank yellow\";\n    expect(validateMnemonic(mnem, wordlist)).toBeTruthy();\n    const seed1 = mnemonicToSeedSync(mnem);\n    const seed2 = mnemonicToSeedSync(mnem);\n    expect(seed1).toEqual(seed2);\n  });\n  test(\"converts same mnemonic consistently sync/async\", async () => {\n    const mnem =\n      \"legal winner thank year wave sausage worth useful legal winner thank yellow\";\n    expect(validateMnemonic(mnem, wordlist)).toBeTruthy();\n    const seed1 = await mnemonicToSeed(mnem);\n    const seed2 = mnemonicToSeedSync(mnem);\n    expect(seed1).toEqual(seed2);\n  });\n  test(\"varies with capitalization\", () => {\n    const mnem =\n      \"legal winner thank year wave sausage worth useful legal winner thank yellow\"; // [w]inner\n    const mNem =\n      \"legal Winner thank year wave sausage worth useful legal winner thank yellow\"; // [W]inner\n    expect(validateMnemonic(mnem, wordlist)).toBeTruthy();\n    expect(validateMnemonic(mNem, wordlist)).toBeFalsy();\n    const lowerSeed = mnemonicToSeedSync(mnem);\n    const mixedSeed = mnemonicToSeedSync(mNem);\n    expect(lowerSeed).not.toEqual(mixedSeed);\n  });\n  test(\"fails with extra/missing spacing\", () => {\n    const mnem1 =\n      \"legal  winner thank year wave sausage worth useful legal winner thank yellow\"; // 2 spaces\n    const mnem2 =\n      \"legalwinner thank year wave sausage worth useful legal winner thank yellow\"; // missing space\n    const mnem3 =\n      \" legalwinner thank year wave sausage worth useful legal winner thank yellow \"; // untrimmed\n    expect(validateMnemonic(mnem1, wordlist)).toBeFalsy();\n    expect(validateMnemonic(mnem2, wordlist)).toBeFalsy();\n    expect(validateMnemonic(mnem3, wordlist)).toBeFalsy();\n    expect(() => mnemonicToSeedSync(mnem1)).toThrow();\n    expect(() => mnemonicToSeedSync(mnem2)).toThrow();\n    expect(() => mnemonicToSeedSync(mnem3)).toThrow();\n  });\n  test(\"converts any words/order does not matter\", () => {\n    const mnem1 =\n      \"legal thank winner year wave sausage worth useful legal winner yellow thank\"; // invalid checksum\n    expect(validateMnemonic(mnem1, wordlist)).toBeFalsy();\n    expect(() => mnemonicToSeedSync(mnem1)).not.toThrow();\n    const mnem2 = \"a b c d e f g h i j k l\"; // 12 \"words\"\n    expect(validateMnemonic(mnem2, wordlist)).toBeFalsy();\n    expect(() => mnemonicToSeedSync(mnem2)).not.toThrow();\n    const mnem3 = \"lega winn than year wave saus wort usef lega winn than yell\"; // first 4\n    expect(validateMnemonic(mnem3, wordlist)).toBeFalsy();\n    expect(() => mnemonicToSeedSync(mnem3)).not.toThrow();\n    expect(mnemonicToSeedSync(mnem1)).not.toEqual(mnemonicToSeedSync(mnem3));\n  });\n});\n"
  },
  {
    "path": "test/vitest/setup-file.js",
    "content": "// This file will be run before each test file\nimport { createPinia, setActivePinia } from \"pinia\";\n\n// Initialize Pinia globally at module load time\nconsole.log(\"Vitest setup file is running\");\nsetActivePinia(createPinia()); // Runs immediately, before imports\n\n// Still keep beforeEach to reset Pinia state between tests\nimport { beforeEach } from \"vitest\";\nbeforeEach(() => {\n  console.log(\"Setting up Pinia\");\n  setActivePinia(createPinia()); // Fresh instance for each test\n});\n\n// Mock localStorage\nconst localStorageMock = (function () {\n  let store = {};\n  return {\n    getItem: function (key) {\n      return store[key] || null;\n    },\n    setItem: function (key, value) {\n      store[key] = value.toString();\n    },\n    removeItem: function (key) {\n      delete store[key];\n    },\n    clear: function () {\n      store = {};\n    },\n  };\n})();\n\nObject.defineProperty(window, \"localStorage\", {\n  value: localStorageMock,\n  writable: true,\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"@quasar/app-vite/tsconfig-preset\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"ES2020\",\n    \"paths\": {\n      \"src/*\": [\"src/*\"],\n      \"app/*\": [\"*\"],\n      \"components/*\": [\"src/components/*\"],\n      \"layouts/*\": [\"src/layouts/*\"],\n      \"pages/*\": [\"src/pages/*\"],\n      \"assets/*\": [\"src/assets/*\"],\n      \"boot/*\": [\"src/boot/*\"],\n      \"stores/*\": [\"src/stores/*\"],\n      \"vue$\": [\"node_modules/vue/dist/vue.runtime.esm-bundler.js\"]\n    }\n  }\n}\n"
  },
  {
    "path": "types/light-bolt11-decoder/index.d.ts",
    "content": "declare module \"light-bolt11-decoder\" {\n  export interface DecodedSection {\n    name?: string;\n    value?: any;\n    [key: string]: any;\n  }\n\n  export interface DecodedBolt11 {\n    paymentRequest: string;\n    sections: DecodedSection[];\n  }\n\n  export function decode(paymentRequest: string): DecodedBolt11;\n}\n"
  },
  {
    "path": "vitest.config.js",
    "content": "import { defineConfig } from \"vitest/config\";\nimport vue from \"@vitejs/plugin-vue\";\nimport { quasar, transformAssetUrls } from \"@quasar/vite-plugin\";\nimport jsconfigPaths from \"vite-jsconfig-paths\";\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  build: {\n    target: \"esnext\",\n  },\n  optimizeDeps: {\n    esbuildOptions: {\n      target: \"esnext\",\n    },\n  },\n  test: {\n    environment: \"happy-dom\",\n    setupFiles: \"test/vitest/setup-file.js\",\n    include: [\n      // Matches vitest tests in any subfolder of 'src' or into 'test/vitest/__tests__'\n      // Matches all files with extension 'js', 'jsx', 'ts' and 'tsx'\n      \"src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}\",\n      \"test/vitest/__tests__/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}\",\n    ],\n  },\n  plugins: [\n    vue({\n      template: { transformAssetUrls },\n    }),\n    quasar({\n      sassVariables: \"src/quasar-variables.scss\",\n    }),\n    jsconfigPaths(),\n  ],\n});\n"
  }
]