Compare commits

..
Author SHA1 Message Date
Forte11andGitHub 7ec4e4eebc Merge pull request #122 from Forte11Cuba/chore/bump-0.3.0
Release APK / build (push) Canceled after 0s
chore: bump version to 0.3.0+4
2026-04-21 05:01:46 -06:00
Forte11Cuba 92cdf4ec2d chore: bump version to 0.3.0+4 2026-04-21 05:00:56 -06:00
Forte11andGitHub b9d57b69b0 Merge pull request #121 from Forte11Cuba/chore/settings-recover-icon-and-order
chore(settings): align recover icon and reorder wallet section
2026-04-21 04:56:07 -06:00
Forte11Cuba 4f83d9c3a1 chore(settings): align recover icon and reorder wallet section 2026-04-21 04:52:12 -06:00
Forte11andGitHub 6246d09876 Merge pull request #120 from Forte11Cuba/refactor/mint-quote-recovery-by-id
refactor: recover mint quotes by quote_id (closes #118)
2026-04-19 07:43:20 -06:00
Forte11Cuba 73a0261af6 fix: run mint-quote recovery before checkAllMintQuotes 2026-04-19 06:18:51 -06:00
Forte11Cuba dd9450c0cc refactor: recover mint quotes by quote_id (closes #118) 2026-04-19 06:03:21 -06:00
Forte11andGitHub 09565aa3ed Merge pull request #119 from Forte11Cuba/fix/mint-issued-event-race-condition
fix: deliver mint Issued event before metadata await (closes #115)
2026-04-19 05:09:08 -06:00
Forte11Cuba e3fdd46df8 fix: deliver mint Issued event before metadata await (closes #115) 2026-04-19 05:00:05 -06:00
Forte11andGitHub f4d15fd2ab Merge pull request #117 from Forte11Cuba/fix/mints-list-show-all-supported-units
fix: show all supported units on mints list, dim zero-balance pills
2026-04-18 05:15:34 -06:00
Forte11Cuba 7bc9639e89 fix: show all supported units on mints list, dim zero-balance pills 2026-04-18 03:55:18 -06:00
Forte11andGitHub e11778ff0a Merge pull request #116 from Forte11Cuba/feat/observe-only-reconcile-and-settled-state
fix: observe-only reconcile + settled state for offline sends
2026-04-18 03:44:17 -06:00
Forte11Cuba d5d4b3a6ea fix: address CodeRabbit review 2026-04-18 03:29:51 -06:00
Forte11Cuba a0f9ca738b fix: observe-only reconcile + settled state for offline sends 2026-04-18 02:09:50 -06:00
Forte11andGitHub ca63c56ecf Merge pull request #114 from Forte11Cuba/feat/reclaim-pending-send-from-history
feat: add cancel/reclaim button for pending sends (online + offline)
2026-04-18 01:05:04 -06:00
Forte11Cuba 5cdce99154 fix: address CodeRabbit review on #87 2026-04-17 07:52:38 -06:00
Forte11Cuba f734b35670 feat: add cancel/reclaim button for pending sends (online + offline) 2026-04-17 07:12:33 -06:00
Forte11andGitHub 5eef9ce130 Merge pull request #113 from Forte11Cuba/feat/paste-from-clipboard-in-scanner
feat: add paste-from-clipboard button to QR scanner
2026-04-17 06:04:53 -06:00
Forte11Cuba f2951d2bef feat: add paste-from-clipboard button to QR scanner 2026-04-17 05:07:53 -06:00
Forte11andGitHub 8015b4817b Merge pull request #112 from Forte11Cuba/fix/home-action-arrows-as-icons
fix: render home send/receive arrows as Lucide icons
2026-04-17 04:58:57 -06:00
Forte11Cuba cc1531f3d5 fix: render home send/receive arrows as Lucide icons 2026-04-17 03:55:09 -06:00
Forte11andGitHub aaef4b1cdc Merge pull request #111 from Forte11Cuba/fix/modal-bottom-sheet-safe-area
fix: bottom sheet modals overlap system navigation bar
2026-04-17 03:45:46 -06:00
Forte11Cuba dacec9ea22 fix: bottom sheet modals overlap system navigation bar 2026-04-17 03:39:41 -06:00
Forte11andGitHub 4b8edf9669 Merge pull request #110 from Forte11Cuba/fix/reclaim-per-unit-and-cancel-melt
fix: aggregate reclaimPendingProofs by unit and replace broken cancel Melt/cancelSend
2026-04-15 20:05:54 -06:00
Forte11Cuba f458cfad34 fix: aggregate reclaimPendingProofs by unit and replace broken cancelMelt/cancelSend 2026-04-15 17:37:27 -06:00
Forte11Cuba 428f2c1f45 fix: aggregate reclaimPendingProofs by unit and replace broken cancelMelt/cancelSend 2026-04-15 17:19:38 -06:00
Forte11andGitHub 9a14af33a2 Merge pull request #107 from Forte11Cuba/fix/melt-proofs-lost-and-min-amount-validation
fix: recover reserved proofs on melt failure and validate minimum viable amounts
2026-04-15 06:10:08 -06:00
Forte11Cuba 770868a773 fix: add fee pre-validation to swap and log cancelMelt failures 2026-04-15 04:48:03 -06:00
Forte11Cuba dc78f8958c fix: recover reserved proofs on melt failure and validate minimum viable amounts 2026-04-15 04:20:53 -06:00
Forte11andGitHub 03ba0e532f Merge pull request #103 from Forte11Cuba/refactor/split-settings-screen
refactor: split settings_screen.dart into separate files
2026-04-12 16:34:06 -06:00
Forte11Cuba a6a4365a0d fix: remove unnecessary async from synchronous _loadMints 2026-04-12 16:33:26 -06:00
Forte11Cuba ac23574ae2 fix: count only mints that actually recovered tokens in restore message 2026-04-12 16:07:24 -06:00
Forte11Cuba 87f8e52036 fix: use addPostFrameCallback for safe context access in RecoverTokensModal 2026-04-12 15:56:26 -06:00
Forte11Cuba eb9319fca0 refactor: split settings_screen.dart into separate files 2026-04-12 15:43:08 -06:00
Forte11andGitHub a9e4272762 Merge pull request #102 from Forte11Cuba/feat/privacy-screen
feat: add privacy policy screen with humorous tone
2026-04-11 03:01:53 -06:00
Forte11Cuba f66668f0ec fix: normalize privacyBody split to filter empty lines 2026-04-11 02:32:46 -06:00
Forte11Cuba 100a4943eb fix: handle privacyConclusion lines robustly for any locale line count 2026-04-11 02:27:46 -06:00
Forte11Cuba 5b1317c3e4 fix: address CodeRabbit review Portuguese register, scrollable layout 2026-04-11 02:22:28 -06:00
Forte11Cuba d85b6562dc feat: add privacy policy screen with humorous tone 2026-04-11 01:32:43 -06:00
Forte11andGitHub 56585319cc Merge pull request #101 from Forte11Cuba/feat/swap-chart-improvements
feat: improve swap price chart — real data only, 24h stats, min/max
2026-04-11 00:54:52 -06:00
Forte11Cuba 79ca30c5ed fix: filter non-finite and zero price samples before rendering chart 2026-04-11 00:37:57 -06:00
Forte11Cuba 0bd988a7a6 fix: address CodeRabbit review handle 2 samples, use i18n template for minMax 2026-04-11 00:31:53 -06:00
Forte11Cuba 218d798d2a feat: improve swap price chart — real data only, 24h stats, min/max 2026-04-11 00:19:43 -06:00
Forte11andGitHub e8d8789413 Merge pull request #100 from Forte11Cuba/feat/swap-screen
feat: add swap screen UI (sats -> USD) with price chart
2026-04-10 04:47:37 -06:00
Forte11Cuba a2866d035a fix: truncate conversion amounts with floor instead of round to prevent overpaying 2026-04-10 04:44:32 -06:00
Forte11Cuba 1a8e3e601b fix: show mint name in AppBar, make confirmation sheet non-dismissible, hardcode Cuba Bitcoin mint 2026-04-10 04:06:40 -06:00
Forte11Cuba b3dde35141 fix: add try/catch to _loadBalances, localize fee/total labels, remove redundant ternary 2026-04-10 03:48:58 -06:00
Forte11Cuba 8541222da6 fix: remove redundant ternary, capture provider ref outside async callback 2026-04-10 03:26:49 -06:00
Forte11Cuba d6085f7754 fix: improve swap reliability — remove unsafe tx fallback, add mint stream timeout and issued handler 2026-04-10 03:18:27 -06:00
Forte11Cuba d2035ba416 feat: implement swap logic (sats <-> USD) with bidirectional input, fee preview, and mint validation 2026-04-10 02:57:55 -06:00
Forte11Cuba 42bab5d18a feat: add swap screen UI (sats -> USD) with price chart 2026-04-09 04:54:48 -06:00
Forte11andGitHub 6c05785391 Merge pull request #99 from Forte11Cuba/chore/upgrade-cdk-v0.16.0
docs: update CDK version to 0.16.0 in README
2026-04-09 03:57:31 -06:00
Forte11Cuba 65224314e1 docs: update CDK version to 0.16.0 in README 2026-04-09 03:53:02 -06:00
Forte11andGitHub 2534cf5d1e Merge pull request #98 from Forte11Cuba/chore/upgrade-cdk-v0.16.0
chore: upgrade CDK from v0.15.1 to v0.16.0
2026-04-05 20:32:14 -06:00
Forte11Cuba 262b049c20 chore: upgrade CDK from v0.15.1 to v0.16.0 2026-04-05 20:09:55 -06:00
Forte11Cuba 697f6fa555 chore: upgrade CDK from v0.15.1 to v0.16.0 2026-04-05 19:56:34 -06:00
Forte11andGitHub 02fec140e1 Merge pull request #97 from Forte11Cuba/release/v0.2.0
Release APK / build (push) Canceled after 0s
chore: bump version to v0.2.0+3
2026-04-05 16:08:16 -06:00
Forte11Cuba 7cc92d6732 chore: bump version to v0.2.0+3 2026-04-05 15:51:01 -06:00
Forte11andGitHub 013560d25b Merge pull request #96 from Forte11Cuba/chore/rename-package-id
chore: rename package identifier from com.example.elcaju to me.elcaju
2026-04-03 18:44:20 -06:00
Forte11Cuba d19a58df84 chore: rename package identifier from com.example.elcaju to me.elcaju 2026-04-03 17:51:08 -06:00
Forte11andGitHub fd64d2f269 Merge pull request #95 from Forte11Cuba/fix/mint-http-polling-fallback
fix: add HTTP polling fallback for mint quote payment detection
2026-04-03 17:46:23 -06:00
Forte11Cuba 426b63456d fix: move mint logic outside timeout to prevent expired race 2026-04-03 17:25:38 -06:00
Forte11Cuba 4d2ad39323 fix: add HTTP polling fallback for mint quote payment detection 2026-04-03 16:56:22 -06:00
Forte11andGitHub 060c6acd4e Merge pull request #94 from Forte11Cuba/fix/refresh-mint-units-on-startup
fix: refresh mint units on startup via NUT-04
2026-04-03 16:48:24 -06:00
Forte11Cuba 14da243b89 fix: wrap _saveMints in try-catch inside async void method 2026-04-03 16:20:23 -06:00
Forte11Cuba 4f91a2be1c fix: validate activeUnit after refreshing mint units 2026-04-03 15:48:02 -06:00
Forte11Cuba ab13e8f6c2 fix: refresh mint units on startup via NUT-04 2026-04-03 15:40:51 -06:00
Forte11andGitHub f431f825c5 Merge pull request #93 from Forte11Cuba/fix/cache-balance-stream-and-sync-controller
fix: cache balance stream and use sync StreamController
2026-04-03 15:30:20 -06:00
Forte11Cuba b56aa5ce62 fix: clear balance stream cache in deleteDatabase() and reset() 2026-04-03 15:25:11 -06:00
Forte11Cuba c69de2b41b fix: cache balance stream and use sync StreamController 2026-04-03 15:05:22 -06:00
Forte11andGitHub e5285058a8 Merge pull request #91 from Forte11Cuba/fix/send-token-metadata-race-condition
fix: use deterministic transaction ID for send/mint metadata
2026-04-02 01:58:26 -06:00
Forte11Cuba b01b658b93 fix: add mounted guards after async mintTokens call 2026-04-02 01:52:53 -06:00
Forte11Cuba 6596aa963a refactor: log mint tx ID computation failure instead of swallowing 2026-04-02 01:52:14 -06:00
Forte11Cuba ed309d18e8 refactor: add debug logs for tx ID computation failures 2026-04-02 01:08:36 -06:00
Forte11Cuba 1b30d9836b fix: make transaction ID computation best-effort after committed send 2026-04-01 22:54:05 -06:00
Forte11Cuba 588a127f9a refactor: use Option<String> for SendResult.transaction_id 2026-04-01 20:14:11 -06:00
Forte11Cuba 362e45e40f fix: use deterministic transaction ID for send/mint metadata 2026-04-01 19:42:17 -06:00
Forte11andGitHub fae57cc46e Merge pull request #90 from Forte11Cuba/fix/disable-nostr-payment-request-nip17-bug
fix: disable Cashu/Nostr payment request until CDK fixes NIP-17 sender pubkey
2026-04-01 16:45:30 -06:00
Forte11Cuba 80ab0fd9e1 refactor: remove dead code paths while Nostr is disabled 2026-04-01 16:38:02 -06:00
Forte11Cuba 29895f4079 fix: treat Lightning mint error as terminal without Nostr fallback 2026-04-01 14:45:40 -06:00
Forte11Cuba 41fa32d1b4 refactor: hide mode toggle and simplify QR content while Nostr is disabled 2026-04-01 14:33:58 -06:00
Forte11Cuba 63ce12d745 fix: defer QR display until Lightning invoice is ready 2026-04-01 14:10:39 -06:00
Forte11Cuba 7245dbb669 fix: disable Cashu/Nostr payment request until CDK fixes NIP-17 sender pubkey 2026-04-01 13:59:04 -06:00
Forte11andGitHub e124e0839a Merge pull request #89 from Forte11Cuba/feat/mint-websocket-subscription
feat: replace 3s polling with CDK WebSocket subscription (NUT-17) for mint quote
2026-04-01 12:42:25 -06:00
Forte11Cuba 1a31b47c6b fix: handle expired-quote timeout and emit Issued on recovery path 2026-04-01 12:39:17 -06:00
Forte11Cuba a5cd50af13 feat: replace 3s polling with CDK WebSocket subscription (NUT-17) for mint quote 2026-04-01 12:24:12 -06:00
Forte11andGitHub 66119f6b56 Merge pull request #88 from Forte11Cuba/fix/receive-already-signed-retry
fix: handle 'already signed' error in receive with counter resync
2026-03-31 03:42:05 -06:00
Forte11Cuba 484f73a33b fix: only reclaim proofs explicitly reported as Unspent by mint 2026-03-31 03:36:16 -06:00
Forte11Cuba 67204a12f8 fix: handle 'already signed' error in receive with counter resync 2026-03-31 02:36:43 -06:00
Forte11andGitHub b4a62fe9e5 Merge pull request #86 from Forte11Cuba/fix/offline-send-token-v4
fix: use CDK to create valid V4 tokens for offline send
2026-03-31 01:49:26 -06:00
Forte11Cuba 638b288b7e fix: prevent ProofService leak and snapshot selectedTotal 2026-03-31 00:42:17 -06:00
Forte11Cuba 357cc6827f fix: prevent ProofService close during in-flight token creation 2026-03-31 00:31:11 -06:00
Forte11Cuba 682ec4ba7d fix: log rollback error instead of swallowing silently 2026-03-30 23:36:52 -06:00
Forte11Cuba 03cf1d2119 fix: rollback proofs to UNSPENT if offline token creation fails 2026-03-30 23:30:00 -06:00
Forte11Cuba 2309a2edda fix: use CDK to create valid V4 tokens for offline send 2026-03-30 23:09:40 -06:00
Forte11andGitHub ac1ac11075 Merge pull request #85 from Forte11Cuba/fix/dispose-crash-modal-overlap
fix: dispose crash and modal overlap
2026-03-30 00:20:13 -06:00
Forte11Cuba aafa9f97eb fix: dispose crash and modal overlap 2026-03-30 00:10:34 -06:00
Forte11andGitHub 7d3fe5ac58 Merge pull request #84 from Forte11Cuba/fix/nostr-listener-fail-to-post
fix: reduce Nostr listener cancellation delay from 5s to 1s
2026-03-29 23:53:06 -06:00
Forte11Cuba b9c01ce865 fix: reduce Nostr listener cancellation delay from 5s to 1s 2026-03-29 23:45:36 -06:00
Forte11andGitHub 679aa6be5f Merge pull request #83 from Forte11Cuba/feat/nostr-key-persistence
feat: persist Nostr ephemeral keys for payment request recovery
2026-03-29 23:35:54 -06:00
Forte11Cuba c44a17efbd fix: address CodeRabbit review on Nostr key persistence 2026-03-29 23:30:56 -06:00
Forte11Cuba 27a2baefcc feat: persist Nostr ephemeral keys for payment request recovery 2026-03-29 22:49:13 -06:00
Forte11andGitHub e23c5748ce Merge pull request #82 from Forte11Cuba/fix/qr-logo-overlay
feat: add logo overlay to QR codes in RequestScreen
2026-03-29 21:54:15 -06:00
Forte11Cuba c9781b2ff2 refactor: extract bitcoinOrange to AppColors constants 2026-03-29 21:53:50 -06:00
Forte11Cuba b9485d3296 feat: add logo overlay to QR codes in RequestScreen 2026-03-29 21:31:08 -06:00
Forte11andGitHub c222fff82e Merge pull request #81 from Forte11Cuba/feat/nut18-receiver-ui
feat: add RequestScreen UI with unified payment (Phase 2)
2026-03-29 21:22:01 -06:00
Forte11Cuba f839031340 fix: align scanner invoiceOnly handler with BIP-321 validation 2026-03-29 21:18:16 -06:00
Forte11Cuba 41b8743b29 fix: address CodeRabbit Phase 2 review 2026-03-29 21:05:55 -06:00
Forte11Cuba 056f4c1286 feat: add RequestScreen UI with unified payment (Phase 2) 2026-03-29 20:33:00 -06:00
Forte11andGitHub cfed450521 Merge pull request #79 from Forte11Cuba/feat/nut18-receiver-bridge
feat: expose create_payment_request() and wait_for_nostr_payment() in Rust bridge
2026-03-29 18:56:06 -06:00
Forte11Cuba 438603c97b fix: validate wallet matches handle before starting listener 2026-03-29 18:29:23 -06:00
Forte11Cuba c92c438efe fix: skip spawning listener if Dart stream is already closed 2026-03-29 15:22:23 -06:00
Forte11Cuba 7d5efcfb75 fix: apply nitpick improvements from CodeRabbit 2026-03-29 14:19:04 -06:00
Forte11Cuba bb942ef72c fix: return error on Dart stream cancellation instead of Ok(0) 2026-03-29 14:07:45 -06:00
Forte11Cuba b28a529ffc fix: validate incoming payment and reject empty relay list 2026-03-29 13:45:20 -06:00
Forte11Cuba 1ff3a6ec16 feat: address CodeRabbit review feedback 2026-03-29 13:27:52 -06:00
Forte11Cuba f1b679d912 feat: expose create_payment_request() and wait_for_nostr_payment() in Rust bridge 2026-03-29 13:01:36 -06:00
Forte11andGitHub 8b514337a6 Merge pull request #78 from Forte11Cuba/feat/nut18-payment-requests
feat: implement NUT-18/26 payment request support (payer side)
2026-03-29 04:31:42 -06:00
Forte11Cuba d7f6803f1f fix: normalize mint URLs before acceptance check 2026-03-29 04:25:05 -06:00
Forte11Cuba d91a40c41d fix: address CodeRabbit review feedback 2026-03-29 04:06:56 -06:00
Forte11Cuba 0ed49789d0 feat: implement NUT-18/26 payment request support (payer side) 2026-03-29 03:40:13 -06:00
Forte11andGitHub a41b99644c Merge pull request #76 from Forte11Cuba/fix/p2pk-pending-token-claim
fix: pass P2PK signing key when claiming pending tokens
2026-03-29 02:35:58 -06:00
Forte11Cuba 286eae3812 fix: route pending P2PK claims through receiveToken for full bookkeeping 2026-03-29 01:58:12 -06:00
Forte11Cuba e1e8057be5 fix: pass P2PK signing key when claiming pending tokens (#71) 2026-03-29 01:44:27 -06:00
Forte11andGitHub a1ce50bcd4 Merge pull request #75 from Forte11Cuba/docs/update-readme
docs: update README to reflect current project state
2026-03-29 01:28:57 -06:00
Forte11Cuba e9d245c986 docs: capitalize Git in CI/CD section 2026-03-29 01:28:28 -06:00
Forte11Cuba 9836f8f246 docs: address CodeRabbit review — fix URLs, clarify rustc version, add code block hints 2026-03-29 01:25:06 -06:00
Forte11Cuba 88e1483152 docs: update README to reflect current project state 2026-03-29 01:17:16 -06:00
Forte11andGitHub 0e7ff326cf Merge pull request #74 from Forte11Cuba/feat/flutter-integration-phase2
feat: integrate internal Rust bridge, remove cdk_flutter dependency (Phase 2)
2026-03-28 17:49:07 -06:00
Forte11Cuba 188e26784b fix: update outdated cdk-flutter comment in qr_scanner_widget 2026-03-28 17:47:37 -06:00
Forte11Cuba 989b710cd8 feat: integrate internal Rust bridge, remove cdk_flutter dependency (Phase 2) 2026-03-28 15:04:54 -06:00
Forte11andGitHub 01ea5be578 Merge pull request #73 from Forte11Cuba/feat/rust-api-core
feat: implement Phase 1 Rust API core (keys, token, mint_info, wallet)
2026-03-28 03:59:51 -06:00
Forte11Cuba 07c0a7be89 fix: remove panic paths in TokenDecoder lock handling 2026-03-28 03:45:56 -06:00
Forte11Cuba 1f629d45b3 fix: remove panics in generate_mnemonic and token parsing 2026-03-28 03:30:56 -06:00
Forte11Cuba fa5fd7d98e feat: implement Phase 1 Rust API core (keys, token, mint_info, wallet) 2026-03-28 03:14:40 -06:00
Forte11andGitHub e4f60010f7 Merge pull request #72 from Forte11Cuba/feat/rust-internal-bridge
feat: scaffold internal Rust bridge (elcaju_core)
2026-03-28 02:33:58 -06:00
Forte11Cuba 5d54cb957c fix: correct babash typo from sed double-replacement 2026-03-28 02:33:33 -06:00
Forte11Cuba b930f3c201 fix: use bash consistently across build scripts and podspecs 2026-03-28 02:27:54 -06:00
Forte11Cuba 830c42da93 fix: bash consistency in build_pod.sh 2026-03-28 01:33:37 -06:00
Forte11Cuba e28c68a0ae fix: use bash shebang in build_pod.sh (uses bash-only features) 2026-03-28 01:28:41 -06:00
Forte11Cuba f59090cec8 fix: apply CodeRabbit review feedback 2026-03-28 01:21:12 -06:00
Forte11Cuba 2a092fc9f8 feat: scaffold internal Rust bridge (elcaju_core) 2026-03-28 00:52:48 -06:00
Forte11andGitHub 5fa8583cdf Merge pull request #68 from Forte11Cuba/fix/startup-recovery
fix: add startup recovery for lost proofs and pending operations
2026-03-20 11:37:32 -06:00
Forte11Cuba 9ab78e0ab8 fix: add startup recovery for lost proofs and pending operations 2026-03-20 11:32:16 -06:00
Forte11andGitHub d13b5e8782 Merge pull request #66 from Forte11Cuba/feature/nfc-support
feat: NFC support for Cashu token read/write
2026-03-13 17:34:47 -06:00
Forte11Cuba 76931f7b1d fix: parse URI fragment as key-value pairs instead of substring 2026-03-13 14:09:56 -06:00
Forte11Cuba 115843ce51 fix: guard against infinite loop on zero-data IsoDep response 2026-03-13 12:43:35 -06:00
Forte11Cuba 713f13a46d fix: address CodeRabbit review — await NFC sessions, log errors, validate tokens 2026-03-13 12:30:46 -06:00
Forte11Cuba 560d6fc683 feat: phone-to-phone NFC via HCE with proprietary AID 2026-03-13 12:05:36 -06:00
Forte11Cuba c0f8b166ea feat: NFC support for Cashu token read/write 2026-03-12 10:53:50 -06:00
Forte11andGitHub 6b68367c0d Merge pull request #64 from Forte11Cuba/feature/peanut-emoji-encoding
feat: add peanut emoji encoding for Cashu tokens
2026-03-12 08:33:05 -06:00
Forte11Cuba d80ed4caa7 fix: handle malformed peanut tokens with explicit error message 2026-03-12 08:20:52 -06:00
Forte11Cuba a627ed39f2 fix: guard clipboard callback with mounted check in ReceiveScreen 2026-03-12 08:06:24 -06:00
Forte11Cuba 5d00213d95 fix: peanut decode trimLeft consistency and auto-decode on receive 2026-03-12 07:30:52 -06:00
Forte11Cuba 2e3bb5dcc3 feat: add peanut emoji encoding for Cashu tokens 2026-03-12 06:54:50 -06:00
Forte11andGitHub 749d44650d Merge pull request #63 from Forte11Cuba/cleanup/css-duplicates-and-i18n
fix: auto version from pubspec, remove Cuba Bitcoin badge, LaChispa tappable link
2026-02-26 03:09:30 -06:00
Forte11Cuba 3ff2d6ef84 fix: defensive error handling for version load and LaChispa link 2026-02-26 03:04:58 -06:00
Forte11Cuba 18c72ebf1d fix: auto version from pubspec, remove Cuba Bitcoin badge, LaChispa tappable link 2026-02-26 02:50:40 -06:00
Forte11Cuba f9a70bf1ed ci: add universal APK and stable download URL
Release APK / build (push) Canceled after 0s
2026-02-25 05:27:34 -06:00
Forte11Cuba 22434245f2 bump version to v0.1.0 2026-02-25 04:48:05 -06:00
Forte11Cuba fb76bde5b7 fix: remove stale precompiled .so and ignore jniLibs 2026-02-25 04:46:44 -06:00
Forte11andGitHub 365047e73c Merge pull request #62 from Forte11Cuba/feature/p2pk-qr-scan
feat: add QR scan button for P2PK pubkey input (#59)
2026-02-23 16:31:48 -06:00
Forte11Cuba c374c042c7 fix: add user feedback for invalid QR and camera errors in P2PK scanner 2026-02-23 16:30:30 -06:00
Forte11Cuba 0f7e89a687 feat: add QR scan button for P2PK pubkey input (#59) 2026-02-23 16:21:29 -06:00
Forte11andGitHub 73582c4a69 Merge pull request #61 from Forte11Cuba/fix/cancel-send-on-failure
fix: release reserved proofs when confirmSend fails (#58)
2026-02-23 15:51:19 -06:00
Forte11Cuba 3e9fea34eb fix: guard cancelSend to avoid masking original send error 2026-02-23 15:48:46 -06:00
Forte11Cuba 348ef1d987 fix: release reserved proofs when confirmSend fails (#58) 2026-02-23 15:42:02 -06:00
Forte11andGitHub fc72bb33b2 Merge pull request #57 from Forte11Cuba/feature/enable-p2pk-send
feat: enable P2PK send with CDK 0.15 includeFee support
2026-02-23 14:29:52 -06:00
147 changed files with 42398 additions and 2464 deletions
+8 -1
View File
@@ -41,15 +41,20 @@ jobs:
- name: Generate localizations
run: flutter gen-l10n
- name: Build release APKs
- name: Build release APKs (split per ABI)
run: flutter build apk --release --split-per-abi
- name: Build universal APK
run: flutter build apk --release
- name: Rename APKs
run: |
cd build/app/outputs/flutter-apk
mv app-arm64-v8a-release.apk elcaju-${{ github.ref_name }}-arm64.apk
mv app-armeabi-v7a-release.apk elcaju-${{ github.ref_name }}-armeabi-v7a.apk
mv app-x86_64-release.apk elcaju-${{ github.ref_name }}-x86_64.apk
mv app-release.apk elcaju-${{ github.ref_name }}-universal.apk
cp elcaju-${{ github.ref_name }}-universal.apk elcaju-universal.apk
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
@@ -58,4 +63,6 @@ jobs:
build/app/outputs/flutter-apk/elcaju-${{ github.ref_name }}-arm64.apk
build/app/outputs/flutter-apk/elcaju-${{ github.ref_name }}-armeabi-v7a.apk
build/app/outputs/flutter-apk/elcaju-${{ github.ref_name }}-x86_64.apk
build/app/outputs/flutter-apk/elcaju-${{ github.ref_name }}-universal.apk
build/app/outputs/flutter-apk/elcaju-universal.apk
generate_release_notes: true
+4
View File
@@ -44,3 +44,7 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# Rust/CargoKit compiled native libraries
android/app/src/main/jniLibs/
rust/target/
+141 -115
View File
@@ -1,6 +1,6 @@
## ElCaju 🥜
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Forte11Cuba/elcaju)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Forte11Cuba/elcaju)
*Ask questions about this project using DeepWiki AI*
<p align="center">
@@ -28,7 +28,7 @@
---
## 📱 About ElCaju
## About ElCaju
**ElCaju** is a [Cashu](https://cashu.space) wallet (ecash on Bitcoin) with Cuban identity, brother of [LaChispa](https://github.com/lachispame/) (Lightning). Designed to offer privacy, offline transactions, and a warm tropical experience.
@@ -41,23 +41,31 @@ Cashu is an ecash protocol that enables Bitcoin transactions with maximum privac
---
## Features
## Features
| Feature | Description |
|---------|-------------|
| 🔐 **Total Privacy** | Fungible ecash tokens, no traceable history |
| 📴 **Works Offline** | Store and share tokens without internet |
| **Lightning Bridge** | Deposit and withdraw sats via Lightning Network |
| 🏦 **Multi-Mint** | Connect to multiple Cashu mints simultaneously |
| 💱 **Multi-Unit** | Support for sat, USD, EUR and more |
| 🔑 **Your Seed, Your Money** | Backup with 12 words (BIP39) |
| 🔒 **Optional PIN** | Protect access to your wallet |
| 🌐 **Bilingual** | Spanish and English |
| 🎉 **Friendly UX** | Visual effects and confetti when receiving funds |
| **Total Privacy** | Fungible ecash tokens, no traceable history |
| **Works Offline** | Store, create, and share tokens without internet |
| **Lightning Bridge** | Deposit and withdraw sats via Lightning Network |
| **Multi-Mint** | Connect to unlimited Cashu mints simultaneously |
| **Multi-Unit** | Support for sat, USD, EUR, and any unit offered by mints |
| **P2PK (NUT-10)** | Lock tokens to a Nostr public key — only the recipient can claim |
| **Token Recovery (NUT-13)** | Restore tokens from any mint using your seed phrase |
| **NFC Transfers** | Share tokens via NFC — includes Host Card Emulation (HCE) |
| **Animated QR** | UR-format multi-frame QR codes for large tokens |
| **Peanut Emoji** | Encode tokens as emoji (compatible with cashu.me) |
| **Transaction History** | Full history with filters: Ecash, Lightning, Pending |
| **Pending Tokens** | Save tokens to claim later (up to 50, 30-day expiry) |
| **BTC Price** | Live price via Yadio API with fiat conversion |
| **Your Seed, Your Money** | Backup with 12 words (BIP39) |
| **Optional PIN** | 4-digit PIN to protect wallet access |
| **11 Languages** | ES, EN, PT, FR, RU, DE, IT, KO, ZH, JA, SW |
| **Friendly UX** | Confetti celebrations when receiving funds |
---
## 🚀 Screenshots
## Screenshots
<p align="center">
<img src="assets/img/Captura1.png" width="180"/>
@@ -69,35 +77,42 @@ Cashu is an ecash protocol that enables Bitcoin transactions with maximum privac
---
## 🔧 Technologies
## Technologies
### Frontend
- **Flutter 3.27+**: Cross-platform framework
- **Dart**: Programming language
- **Dart 3.11+**: Programming language
- **Provider**: State management
- **QR Flutter**: QR code generation
- **QR Flutter + Mobile Scanner**: QR generation and scanning
### Backend & Core
- **[cdk-flutter](https://github.com/cashubtc/cdk_flutter)**: Cashu Development Kit (Rust FFI)
- **elcaju_core**: Internal Rust library via [flutter_rust_bridge](https://github.com/fzyzcjy/flutter_rust_bridge) v2.11.1 (FFI)
- **[CDK](https://github.com/cashubtc/cdk) 0.15.1**: Cashu Development Kit (wallet, SQLite, HTTP client, signatory)
- **SQLite**: Local persistence via cdk-sqlite
- **Flutter Secure Storage**: Secure storage for seed and PIN
- **Flutter Secure Storage**: Encrypted storage for seed and PIN
### Protocols
- **Cashu**: NUT-00 to NUT-13 (ecash protocol)
- **Cashu**: NUT-00, 04, 05, 06, 07, 09, 10 (P2PK), 13 (restore)
- **Lightning Network**: Deposits and withdrawals via BOLT11
- **BIP39**: Seed phrase generation
- **BIP39 / BIP32**: Seed phrase generation and key derivation
- **NIP-06**: Nostr key derivation from mnemonic for P2PK
### Native
- **NFC Manager + HCE**: Phone-to-phone token transfers
- **Android NDK 27.0**: Multi-architecture native compilation
- **Cargokit**: Automated Rust build integration
---
## 📦 Installation
## Installation
### Prerequisites
- Flutter SDK (>=3.27.0)
- Dart SDK (>=3.6.0)
- Dart SDK (>=3.11.0)
- Android Studio with Android SDK
- Android NDK 26.3+ (install via Android Studio > SDK Manager > SDK Tools)
- Rust toolchain (>=1.85.0)
- Android NDK 27.0+ (install via Android Studio > SDK Manager > SDK Tools)
- Rust toolchain (edition 2024; rustc >= 1.85.0)
### Clone the Repository
@@ -118,7 +133,7 @@ flutter pub get
# Install Rust if not already installed
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Update to latest stable (requires >= 1.85.0)
# Update to latest stable
rustup update stable
# Install Android targets
@@ -137,61 +152,39 @@ linker = "gcc"
rustflags = ["-C", "link-arg=-fuse-ld=bfd"]
[target.aarch64-linux-android]
linker = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"
linker = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"
[target.armv7-linux-androideabi]
linker = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang"
linker = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang"
[target.x86_64-linux-android]
linker = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android24-clang"
linker = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android24-clang"
[env]
CC_armv7-linux-androideabi = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang"
AR_armv7-linux-androideabi = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
CC_x86_64-linux-android = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android24-clang"
AR_x86_64-linux-android = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
CC_aarch64-linux-android = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"
AR_aarch64-linux-android = "/path/to/Android/Sdk/ndk/26.3.11579264/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
CC_armv7-linux-androideabi = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang"
AR_armv7-linux-androideabi = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
CC_x86_64-linux-android = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android24-clang"
AR_x86_64-linux-android = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
CC_aarch64-linux-android = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang"
AR_aarch64-linux-android = "/path/to/Android/Sdk/ndk/27.0.12077973/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar"
```
> **Note**: Replace `/path/to/Android/Sdk` with your actual Android SDK path (usually `~/Android/Sdk` on Linux or `~/Library/Android/sdk` on macOS).
### Compile Native Libraries
The cdk_flutter native libraries must be compiled manually:
```bash
# Navigate to cdk_flutter rust directory
cd ~/.pub-cache/git/cdk_flutter-*/rust
# Build for each architecture
cargo build --release --target aarch64-linux-android
cargo build --release --target armv7-linux-androideabi
cargo build --release --target x86_64-linux-android
# Copy libraries to project
mkdir -p /path/to/elcaju/android/app/src/main/jniLibs/{arm64-v8a,armeabi-v7a,x86_64}
cp target/aarch64-linux-android/release/libcdk_flutter.so \
/path/to/elcaju/android/app/src/main/jniLibs/arm64-v8a/
cp target/armv7-linux-androideabi/release/libcdk_flutter.so \
/path/to/elcaju/android/app/src/main/jniLibs/armeabi-v7a/
cp target/x86_64-linux-android/release/libcdk_flutter.so \
/path/to/elcaju/android/app/src/main/jniLibs/x86_64/
```
### Run in Development
The Rust library compiles automatically via Cargokit during `flutter run`:
```bash
flutter run
```
> **Note**: The first build will take several minutes while Rust compiles. Subsequent builds are incremental.
### Build for Production
```bash
# Android APK (includes all architectures, ~55MB)
# Android APK (includes all architectures)
flutter build apk --release
# Android App Bundle (recommended for Play Store)
@@ -203,100 +196,123 @@ flutter build appbundle --release
For faster builds targeting only modern devices:
```bash
# Build only arm64 library
cargo build --release --target aarch64-linux-android
# Copy to project
cp target/aarch64-linux-android/release/libcdk_flutter.so \
android/app/src/main/jniLibs/arm64-v8a/
# Build APK (~40MB)
flutter build apk --release
flutter build apk --release --target-platform android-arm64
```
---
## 🏗️ Architecture
## Architecture
```
```text
┌─────────────────────────────────────────┐
│ Flutter UI (Dart)
│ Provider + Streams + Intl
│ Flutter UI (Dart 3.11+)
│ Provider + Streams + Intl (11 langs)
├─────────────────────────────────────────┤
WalletProvider / SettingsProvider
(Multi-unit: Map<mintUrl:unit>)
4 Providers: Wallet, Settings,
Price (Yadio), P2PK (NIP-06)
├─────────────────────────────────────────┤
cdk-flutter (FFI)
│ flutter_rust_bridge v2.11.1 │
flutter_rust_bridge v2.11.1 (FFI)
├─────────────────────────────────────────┤
Rust Core (CDK)
Cashu 0.13.4 + SQLite + Bitcoin
elcaju_core (Rust interno)
CDK 0.16.0 + SQLite + BIP39 + Tokio
└─────────────────────────────────────────┘
```
### Project Structure
```
```text
elcaju/
├── lib/
│ ├── main.dart # Entry point
│ ├── main.dart # Entry point (RustLib.init + Providers)
│ ├── core/
│ │ ├── constants/ # Colors, dimensions
│ │ ├── theme/ # App theme
│ │ ── utils/ # Formatters, validators
│ │ ├── constants/ # Colors, dimensions
│ │ ├── models/ # Proof model
│ │ ── services/ # LNURL, NFC, price, proofs
│ │ ├── theme/ # Material 3 dark theme
│ │ └── utils/ # Formatters, parsers, nostr, p2pk, peanut codec
│ ├── data/ # Pending tokens, transaction metadata storage
│ ├── models/ # P2PK key model (npub/nsec)
│ ├── providers/
│ │ ├── wallet_provider.dart # Wallet logic
│ │ ── settings_provider.dart# Preferences and PIN
│ │ ├── wallet_provider.dart # Multi-mint, multi-unit wallet logic
│ │ ── settings_provider.dart # Preferences, PIN, seed
│ │ ├── price_provider.dart # BTC price cache (Yadio API)
│ │ └── p2pk_provider.dart # Nostr key management (NIP-06)
│ ├── screens/
│ │ ├── 1_splash/ # Loading screen
│ │ ├── 2_onboarding/ # Create/restore wallet
│ │ ├── 3_home/ # Main screen
│ │ ├── 4_receive/ # Receive tokens
│ │ ├── 5_send/ # Send tokens
│ │ ├── 6_mint/ # Deposit via Lightning
│ │ ├── 7_melt/ # Withdraw to Lightning
│ │ ── 8_settings/ # Settings
│ │ ├── 1_splash/ # Loading & initialization
│ │ ├── 2_onboarding/ # Create / restore / backup seed
│ │ ├── 3_home/ # Main dashboard
│ │ ├── 4_receive/ # Receive tokens (paste, QR, NFC)
│ │ ├── 5_send/ # Send tokens + offline mode + share
│ │ ├── 6_mint/ # Lightning deposit
│ │ ├── 7_melt/ # Lightning withdrawal
│ │ ── 8_settings/ # Settings, mints, P2PK keys, language
│ │ ├── 9_history/ # Transaction history with filters
│ │ └── 10_scanner/ # QR code scanner
│ ├── src/rust/ # FFI bridge (auto-generated + API bindings)
│ ├── widgets/
│ │ ├── common/ # Buttons, cards, etc.
│ │ ── effects/ # Confetti and animations
└── l10n/ # Translations ES/EN
└── assets/
└── img/ # Logo and assets
│ │ ├── common/ # Buttons, cards, numpad, backgrounds
│ │ ── effects/ # Confetti animation
│ ├── proof/ # Proof selector (offline send)
│ │ └── scanner/ # QR scanner widget
└── l10n/ # 11 languages (354 translation keys)
├── rust/
│ ├── Cargo.toml # elcaju_core: CDK 0.16.0, flutter_rust_bridge 2.11.1
│ └── src/api/ # wallet, token, keys, mint_info, error
├── rust_builder/ # Cargokit integration (auto Rust compilation)
├── android/ # Android config (NDK 27.0, NFC HCE service)
└── assets/img/ # Logo and screenshots
```
---
## 🔐 Security
## Cashu NUT Support
| NUT | Name | Status |
|-----|------|--------|
| NUT-00 | Cashu Protocol | Supported |
| NUT-04 | Mint (Lightning deposit) | Supported |
| NUT-05 | Melt (Lightning withdrawal) | Supported |
| NUT-06 | Token swap | Supported |
| NUT-07 | Spend conditions (witness) | Supported |
| NUT-09 | Token restore | Supported |
| NUT-10 | P2PK (locked tokens) | Supported |
| NUT-13 | Deterministic secrets (recovery) | Supported |
---
## Security
| Feature | Implementation |
|---------|----------------|
| **Seed Phrase** | Stored with Flutter Secure Storage (native encryption) |
| **PIN** | Secure hash, local verification |
| **Local Data** | SQLite with proofs encrypted by Cashu protocol |
| **Seed Phrase** | BIP39 12 words, stored with Flutter Secure Storage (native encryption) |
| **Key Derivation** | BIP32 + NIP-06 (m/44'/1237'/0'/0/0) for P2PK keys |
| **PIN** | 4-digit PIN with secure hash, local verification |
| **Local Data** | SQLite with proofs managed by CDK |
| **P2PK Keys** | Primary (from seed) + up to 10 imported keys, all encrypted |
| **No Tracking** | We don't collect user data |
| **Open Source** | 100% auditable |
### Important
> ⚠️ **Backup your seed phrase (12 words)**. Without it, you won't be able to recover your funds if you lose your device.
> **Backup your seed phrase (12 words)**. Without it, you won't be able to recover your funds if you lose your device.
---
## 📱 Compatibility
## Compatibility
| Platform | Status | Minimum Version |
|----------|--------|-----------------|
| Android | Supported | API 24 (Android 7.0) |
| 🚧 iOS | Coming soon | iOS 12.0+ |
| 🚧 Web | Coming soon | - |
| Android | Supported | API 24 (Android 7.0) |
| iOS | Coming soon | iOS 11.0+ |
| Web | Coming soon | - |
---
## 🌐 Default Mint
## Default Mint
ElCaju comes preconfigured with the Cuba Bitcoin mint:
```
```text
https://mint.cubabitcoin.org
```
@@ -304,7 +320,16 @@ You can add other Cashu mints from the settings.
---
## 🤝 Contributing
## CI/CD
Automated releases via GitHub Actions (`.github/workflows/release.yml`):
- Triggers on Git tags (`v*`)
- Builds multi-architecture APKs (arm64, armv7, x86_64) + universal APK
- Creates GitHub releases automatically
---
## Contributing
Contributions are welcome!
@@ -324,16 +349,17 @@ Open an [issue](https://github.com/Forte11Cuba/elcaju/issues) with:
---
## 📄 License
## License
This project is under the MIT License - see the [LICENSE](LICENSE) file for more details.
---
## 🙏 Credits
## Credits
- **[Cashu](https://cashu.space)** - Ecash protocol
- **[cdk-flutter](https://github.com/cashubtc/cdk_flutter)** - Cashu Development Kit
- **[CDK](https://github.com/cashubtc/cdk)** - Cashu Development Kit
- **[flutter_rust_bridge](https://github.com/fzyzcjy/flutter_rust_bridge)** - Rust-Dart FFI
- **[Cashu4Community](https://cashu4community.xyz)** - Global Cashu community
- **[Cuba Bitcoin](https://cubabitcoin.org)** - Cuban Bitcoin community
+1 -1
View File
@@ -21,7 +21,7 @@ android {
defaultConfig {
applicationId "me.elcaju"
minSdk 21 // Required for FlutterSecureStorage (EncryptedSharedPreferences)
minSdkVersion flutter.minSdkVersion // Required for FlutterSecureStorage (EncryptedSharedPreferences)
targetSdk flutter.targetSdkVersion
versionCode flutter.versionCode
versionName flutter.versionName
+16
View File
@@ -7,6 +7,9 @@
<!-- Cámara opcional: la app funciona sin ella (pegando desde portapapeles) -->
<uses-feature android:name="android.hardware.camera" android:required="false"/>
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false"/>
<!-- NFC para compartir tokens Cashu -->
<uses-permission android:name="android.permission.NFC"/>
<uses-feature android:name="android.hardware.nfc" android:required="false"/>
<application
android:label="ElCaju"
@@ -34,6 +37,19 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- HCE service: phone emulates NFC tag for phone-to-phone transfers -->
<service
android:name=".NfcHceService"
android:exported="true"
android:permission="android.permission.BIND_NFC_SERVICE">
<intent-filter>
<action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
</intent-filter>
<meta-data
android:name="android.nfc.cardemulation.host_apdu_service"
android:resource="@xml/hce_nfc" />
</service>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
@@ -1,5 +1,60 @@
package me.elcaju
import android.content.ComponentName
import android.nfc.NfcAdapter
import android.nfc.cardemulation.CardEmulation
import android.util.Log
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterActivity()
class MainActivity : FlutterActivity() {
private val CHANNEL = "me.elcaju/nfc_hce"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
when (call.method) {
"setPayload" -> {
val payload = call.argument<ByteArray>("payload")
NfcHceService.ndefPayload = payload
// Force Android to route the NDEF AID to our service
// instead of manufacturer services (Xiaomi Mi Share, etc.)
try {
val adapter = NfcAdapter.getDefaultAdapter(this)
if (adapter != null) {
val cardEmulation = CardEmulation.getInstance(adapter)
cardEmulation.setPreferredService(
this,
ComponentName(this, NfcHceService::class.java)
)
}
} catch (e: Exception) {
Log.w("MainActivity", "setPreferredService failed (non-critical)", e)
}
result.success(true)
}
"clearPayload" -> {
NfcHceService.ndefPayload = null
// Release preferred service routing
try {
val adapter = NfcAdapter.getDefaultAdapter(this)
if (adapter != null) {
val cardEmulation = CardEmulation.getInstance(adapter)
cardEmulation.unsetPreferredService(this)
}
} catch (e: Exception) {
Log.w("MainActivity", "unsetPreferredService failed (non-critical)", e)
}
result.success(true)
}
else -> result.notImplemented()
}
}
}
}
@@ -0,0 +1,140 @@
package me.elcaju
import android.nfc.cardemulation.HostApduService
import android.os.Bundle
import android.util.Log
/**
* Host Card Emulation service that makes the phone act as an NFC Forum Type 4 tag.
* Another phone in reader mode can tap and read the NDEF message we serve.
*
* The payload is set from Flutter via MainActivity's MethodChannel.
*/
class NfcHceService : HostApduService() {
companion object {
private const val TAG = "NfcHceService"
// Shared payload - set by Flutter via MethodChannel
@Volatile
var ndefPayload: ByteArray? = null
private val OK = byteArrayOf(0x90.toByte(), 0x00.toByte())
private val NOT_FOUND = byteArrayOf(0x6A.toByte(), 0x82.toByte())
// Capability Container (CC) file - fixed structure
// MLe=0xFF (255), MLc=0xFF (255), max NDEF=0x70FF (28671)
// Compatible with Numo
private val CC_FILE = byteArrayOf(
0x00, 0x0F, // CC length (15)
0x20, // Mapping version 2.0
0x00, 0xFF.toByte(), // MLe: max read 255 bytes
0x00, 0xFF.toByte(), // MLc: max write 255 bytes
0x04, 0x06, // NDEF File Control TLV
0xE1.toByte(), 0x04, // NDEF file ID
0x70, 0xFF.toByte(), // Max NDEF size: 28671 bytes
0x00, // Read access: open
0xFF.toByte() // Write access: denied
)
}
private var selectedFile: String = "none"
private var ndefFileCache: ByteArray? = null
override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
if (commandApdu.size < 4) return NOT_FOUND
val ins = commandApdu[1]
// SELECT command
if (ins == 0xA4.toByte()) {
return handleSelect(commandApdu)
}
// READ BINARY command
if (ins == 0xB0.toByte()) {
return handleRead(commandApdu)
}
return NOT_FOUND
}
private fun handleSelect(apdu: ByteArray): ByteArray {
// Select ElCaju proprietary AID (F04543414A5500)
// Avoids conflicts with Xiaomi Mi Share / Samsung Beam
if (apdu.size >= 12 && apdu[5] == 0xF0.toByte() && apdu[6] == 0x45.toByte()) {
selectedFile = "app"
Log.d(TAG, "Selected ElCaju Application (proprietary AID)")
return OK
}
// Select NDEF Application (AID: D2760000850101)
if (apdu.size >= 12 && apdu[5] == 0xD2.toByte() && apdu[6] == 0x76.toByte()) {
selectedFile = "app"
Log.d(TAG, "Selected NDEF Application")
return OK
}
// Select by file ID
if (apdu.size >= 7) {
val fileId = ((apdu[5].toInt() and 0xFF) shl 8) or (apdu[6].toInt() and 0xFF)
when (fileId) {
0xE103 -> {
selectedFile = "cc"
Log.d(TAG, "Selected CC file")
return OK
}
0xE104 -> {
selectedFile = "ndef"
// Cache the NDEF file when selected
val payload = ndefPayload
if (payload != null) {
val file = ByteArray(2 + payload.size)
file[0] = (payload.size shr 8).toByte()
file[1] = (payload.size and 0xFF).toByte()
System.arraycopy(payload, 0, file, 2, payload.size)
ndefFileCache = file
Log.d(TAG, "Selected NDEF file (${payload.size} bytes payload)")
} else {
ndefFileCache = null
Log.w(TAG, "Selected NDEF file but no payload set")
}
return OK
}
}
}
return NOT_FOUND
}
private fun handleRead(apdu: ByteArray): ByteArray {
if (apdu.size < 5) return NOT_FOUND
val offset = ((apdu[2].toInt() and 0xFF) shl 8) or (apdu[3].toInt() and 0xFF)
// Le=0x00 means 256 bytes in short APDU encoding
var length = apdu[4].toInt() and 0xFF
if (length == 0) length = 256
val data = when (selectedFile) {
"cc" -> CC_FILE
"ndef" -> ndefFileCache ?: return NOT_FOUND
else -> return NOT_FOUND
}
if (offset >= data.size) {
Log.w(TAG, "Read offset $offset beyond data size ${data.size}")
return NOT_FOUND
}
val end = minOf(offset + length, data.size)
val response = data.copyOfRange(offset, end)
Log.d(TAG, "Read $selectedFile: offset=$offset len=$length returned=${response.size} bytes")
return response + OK
}
override fun onDeactivated(reason: Int) {
Log.d(TAG, "Deactivated (reason=$reason)")
selectedFile = "none"
ndefFileCache = null
}
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="hce_description">ElCaju NFC</string>
</resources>
+12
View File
@@ -0,0 +1,12 @@
<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
android:description="@string/hce_description"
android:requireDeviceUnlock="false">
<aid-group
android:category="other"
android:description="@string/hce_description">
<!-- ElCaju proprietary AID (avoids Xiaomi/Samsung NFC service conflicts) -->
<aid-filter android:name="F04543414A5500" />
<!-- NDEF Tag Application AID (NFC Forum Type 4) -->
<aid-filter android:name="D2760000850101" />
</aid-group>
</host-apdu-service>
+3
View File
@@ -0,0 +1,3 @@
rust_input: crate::api
rust_root: rust/
dart_output: lib/src/rust
+6 -6
View File
@@ -368,7 +368,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -384,7 +384,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -401,7 +401,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -416,7 +416,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
@@ -547,7 +547,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -569,7 +569,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
-1
View File
@@ -2,4 +2,3 @@ arb-dir: lib/l10n
template-arb-file: app_es.arb
output-localization-file: app_localizations.dart
output-class: L10n
synthetic-package: false
+1
View File
@@ -22,6 +22,7 @@ class AppColors {
// === ACCENT COLORS ===
static const Color primaryAction = Color(0xFFE35D33); // Naranja vibrante
static const Color secondaryAction = Color(0xFFFFB74D); // Amarillo pulpa/Bitcoin
static const Color bitcoinOrange = Color(0xFFF7931A); // Bitcoin brand orange
static const Color success = Color(0xFF00E676); // Verde neón
static const Color error = Color(0xFFFF5252); // Rojo suave
static const Color warning = Color(0xFFFFB74D); // Amarillo
+485
View File
@@ -0,0 +1,485 @@
import 'dart:io' show Platform;
import 'dart:typed_data';
import 'package:flutter/services.dart';
import 'package:nfc_manager/nfc_manager.dart';
import 'package:nfc_manager/nfc_manager_android.dart';
import 'package:ndef_record/ndef_record.dart';
/// NFC availability state for UI rendering.
enum NfcState {
/// Device does not have NFC hardware.
unsupported,
/// Device has NFC but it is disabled in settings.
disabled,
/// NFC is supported and enabled.
enabled,
}
/// Service for reading and writing Cashu tokens via NFC.
///
/// Writes tokens as NDEF Text Records for maximum compatibility
/// with cashu.me and Numo. Reads both Text and URI records.
class NfcService {
static const _hceChannel = MethodChannel('me.elcaju/nfc_hce');
// ─── HCE (phone-to-phone) ───
/// Start emulating an NFC tag with the given text payload.
/// The phone will appear as an NFC tag to other devices.
static Future<void> startEmulating(String text) async {
final ndefMessage = _buildNdefTextMessage(text);
await _hceChannel.invokeMethod('setPayload', {'payload': ndefMessage});
}
/// Stop emulating an NFC tag.
static Future<void> stopEmulating() async {
await _hceChannel.invokeMethod('clearPayload');
}
/// Build an NDEF message with a Text Record.
/// Uses Short Record (SR) for payloads ≤ 255 bytes,
/// Long Record for > 255 bytes (compatible with Numo).
static Uint8List _buildNdefTextMessage(String text) {
final textBytes = Uint8List.fromList(text.codeUnits);
final languageCode = Uint8List.fromList('en'.codeUnits);
// NDEF Text Record payload: [status byte][lang code][text]
final recordPayload = Uint8List(1 + languageCode.length + textBytes.length);
recordPayload[0] = languageCode.length;
recordPayload.setRange(1, 1 + languageCode.length, languageCode);
recordPayload.setRange(1 + languageCode.length, recordPayload.length, textBytes);
final payloadLength = recordPayload.length;
final isShortRecord = payloadLength <= 255;
if (isShortRecord) {
// Short Record: flags(1) + typeLen(1) + payloadLen(1) + type(1) + payload
final record = Uint8List(4 + payloadLength);
record[0] = 0xD1; // MB|ME|SR|TNF=well-known
record[1] = 1; // type length
record[2] = payloadLength;
record[3] = 0x54; // 'T'
record.setRange(4, 4 + payloadLength, recordPayload);
return record;
} else {
// Long Record: flags(1) + typeLen(1) + payloadLen(4) + type(1) + payload
final record = Uint8List(7 + payloadLength);
record[0] = 0xC1; // MB|ME|TNF=well-known (no SR flag)
record[1] = 1; // type length
record[2] = (payloadLength >> 24) & 0xFF;
record[3] = (payloadLength >> 16) & 0xFF;
record[4] = (payloadLength >> 8) & 0xFF;
record[5] = payloadLength & 0xFF;
record[6] = 0x54; // 'T'
record.setRange(7, 7 + payloadLength, recordPayload);
return record;
}
}
// ─── Tag read/write ───
/// Check NFC state on the device.
static Future<NfcState> checkState() async {
if (!Platform.isAndroid) return NfcState.unsupported;
try {
final availability = await NfcManager.instance.checkAvailability();
return switch (availability) {
NfcAvailability.enabled => NfcState.enabled,
NfcAvailability.disabled => NfcState.disabled,
NfcAvailability.unsupported => NfcState.unsupported,
};
} catch (_) {
return NfcState.unsupported;
}
}
/// Write a Cashu token to an NFC tag as NDEF Text Record.
static Future<void> startWrite({
required String token,
required void Function() onSuccess,
required void Function(String error) onError,
}) async {
try {
await NfcManager.instance.startSession(
pollingOptions: {NfcPollingOption.iso14443},
onDiscovered: (NfcTag tag) async {
final ndef = NdefAndroid.from(tag);
if (ndef == null || !ndef.isWritable) {
onError('Tag is not writable');
await NfcManager.instance.stopSession();
return;
}
// Build NDEF Text Record: [status byte][language code][text]
final textBytes = Uint8List.fromList(token.codeUnits);
final languageCode = Uint8List.fromList('en'.codeUnits);
final payload = Uint8List(1 + languageCode.length + textBytes.length);
payload[0] = languageCode.length; // status byte (UTF-8, no length)
payload.setRange(1, 1 + languageCode.length, languageCode);
payload.setRange(1 + languageCode.length, payload.length, textBytes);
// Check size
if (payload.length + 7 > ndef.maxSize) {
onError('Token too large for this NFC tag '
'(${payload.length + 7}B > ${ndef.maxSize}B)');
await NfcManager.instance.stopSession();
return;
}
try {
final message = NdefMessage(records: [
NdefRecord(
typeNameFormat: TypeNameFormat.wellKnown,
type: Uint8List.fromList([0x54]), // 'T' = Text Record
identifier: Uint8List(0),
payload: payload,
),
]);
await ndef.writeNdefMessage(message);
onSuccess();
await NfcManager.instance.stopSession();
} catch (e) {
onError(e.toString());
await NfcManager.instance.stopSession();
}
},
);
} catch (e) {
onError(e.toString());
}
}
/// Stop any active NFC session.
static Future<void> stopWrite() async {
await NfcManager.instance.stopSession();
}
/// Start reading NFC tags for Cashu tokens.
/// Tries IsoDep APDU first (phone-to-phone HCE, bypasses manufacturer
/// NFC services), falls back to NDEF (physical tags).
static Future<void> startRead({
required void Function(String token) onTokenRead,
required void Function(String error) onError,
}) async {
try {
await NfcManager.instance.startSession(
pollingOptions: {NfcPollingOption.iso14443},
onDiscovered: (NfcTag tag) async {
try {
final diagnostics = <String>[];
// 1. Try IsoDep first (HCE phone-to-phone)
final isoDep = IsoDepAndroid.from(tag);
if (isoDep != null) {
final (token, isoInfo) = await _readViaIsoDep(isoDep);
if (token != null) {
onTokenRead(token);
await NfcManager.instance.stopSession();
return;
}
diagnostics.add('IsoDep: $isoInfo');
} else {
diagnostics.add('IsoDep: not available');
}
// 2. Fallback: NDEF (physical tags)
final ndef = NdefAndroid.from(tag);
if (ndef != null) {
final message = ndef.cachedNdefMessage ?? await ndef.getNdefMessage();
if (message != null) {
final token = _extractToken(message);
if (token != null) {
onTokenRead(token);
await NfcManager.instance.stopSession();
return;
}
diagnostics.add('NDEF: ${message.records.length} records, no Cashu token');
} else {
diagnostics.add('NDEF: no message');
}
} else {
diagnostics.add('NDEF: not available');
}
onError('No Cashu token found [${diagnostics.join('; ')}]');
await NfcManager.instance.stopSession();
} catch (e) {
onError(e.toString());
await NfcManager.instance.stopSession();
}
},
);
} catch (e) {
onError(e.toString());
}
}
/// Read NDEF from HCE via IsoDep APDU commands (like Numo).
/// Bypasses manufacturer NFC services (Xiaomi, Samsung, etc).
/// Returns (token, diagnosticInfo) tuple.
static Future<(String?, String)> _readViaIsoDep(IsoDepAndroid isoDep) async {
String _hex(Uint8List bytes) =>
bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
try {
// Set longer timeout (5s) to avoid TagLostException
await isoDep.setTimeout(5000);
// Try ElCaju proprietary AID first (avoids Xiaomi/Samsung conflicts)
final selectElCaju = await isoDep.transceive(Uint8List.fromList([
0x00, 0xA4, 0x04, 0x00, 0x07,
0xF0, 0x45, 0x43, 0x41, 0x4A, 0x55, 0x00,
0x00,
]));
final bool elCajuSelected = _isOk(selectElCaju);
// Fallback: standard NDEF Application AID
if (!elCajuSelected) {
final selectApp = await isoDep.transceive(Uint8List.fromList([
0x00, 0xA4, 0x04, 0x00, 0x07,
0xD2, 0x76, 0x00, 0x00, 0x85, 0x01, 0x01,
0x00,
]));
if (!_isOk(selectApp)) {
return (null, 'SELECT APP failed (both AIDs): elcaju=${_hex(selectElCaju)}, ndef=${_hex(selectApp)}');
}
}
// SELECT NDEF file (E104)
final selectNdef = await isoDep.transceive(Uint8List.fromList([
0x00, 0xA4, 0x00, 0x0C, 0x02,
0xE1, 0x04,
]));
if (!_isOk(selectNdef)) {
return (null, 'SELECT NDEF failed: ${_hex(selectNdef)}');
}
// READ NLEN (first 2 bytes)
final nlenResponse = await isoDep.transceive(Uint8List.fromList([
0x00, 0xB0, 0x00, 0x00, 0x02,
]));
if (nlenResponse.length < 4 || !_isOk(nlenResponse)) {
return (null, 'READ NLEN failed: ${_hex(nlenResponse)}');
}
final ndefLen = (nlenResponse[0] << 8) | nlenResponse[1];
if (ndefLen == 0) {
return (null, 'NLEN=0 (no payload set on emitter)');
}
// READ NDEF message in chunks (max 255 bytes per read)
final ndefBytes = BytesBuilder();
var offset = 2; // skip NLEN
var remaining = ndefLen;
while (remaining > 0) {
final chunkSize = remaining > 255 ? 255 : remaining;
final readCmd = Uint8List.fromList([
0x00, 0xB0,
(offset >> 8) & 0xFF,
offset & 0xFF,
chunkSize,
]);
final chunk = await isoDep.transceive(readCmd);
if (chunk.length < 2 || !_isOk(chunk)) {
return (null, 'READ chunk at offset=$offset failed: ${_hex(chunk)}');
}
// Response = data + SW1 SW2 (last 2 bytes are status)
final bytesRead = chunk.length - 2;
if (bytesRead == 0) {
return (null, 'READ chunk at offset=$offset returned no data');
}
ndefBytes.add(chunk.sublist(0, bytesRead));
offset += bytesRead;
remaining -= bytesRead;
}
// Parse NDEF message from raw bytes
final raw = ndefBytes.toBytes();
if (raw.isEmpty) {
return (null, 'read ${ndefLen}B but empty after parsing');
}
final token = _parseNdefAndExtractToken(Uint8List.fromList(raw));
if (token != null) {
return (token, 'OK');
}
return (null, 'NDEF parsed (${raw.length}B) but no Cashu token, hex: ${_hex(Uint8List.fromList(raw.length > 40 ? raw.sublist(0, 40) : raw))}...');
} catch (e) {
final msg = e.toString();
if (msg.contains('TagLostException')) {
return (null, 'connection lost (hold phones together longer)');
}
return (null, 'error: $msg');
}
}
/// Check if APDU response ends with 90 00 (OK).
static bool _isOk(Uint8List response) {
if (response.length < 2) return false;
return response[response.length - 2] == 0x90 &&
response[response.length - 1] == 0x00;
}
/// Parse raw NDEF bytes and extract Cashu token.
static String? _parseNdefAndExtractToken(Uint8List raw) {
if (raw.isEmpty) return null;
var pos = 0;
while (pos < raw.length) {
if (pos + 3 > raw.length) break;
final flags = raw[pos];
final tnf = flags & 0x07;
final isShortRecord = (flags & 0x10) != 0;
final hasIdLength = (flags & 0x08) != 0;
pos++;
final typeLength = raw[pos]; pos++;
int payloadLength;
if (isShortRecord) {
payloadLength = raw[pos]; pos++;
} else {
if (pos + 4 > raw.length) break;
payloadLength = (raw[pos] << 24) | (raw[pos+1] << 16) |
(raw[pos+2] << 8) | raw[pos+3];
pos += 4;
}
int idLength = 0;
if (hasIdLength) {
idLength = raw[pos]; pos++;
}
// Type
if (pos + typeLength > raw.length) break;
final type = raw.sublist(pos, pos + typeLength); pos += typeLength;
// ID (skip)
pos += idLength;
// Payload
if (pos + payloadLength > raw.length) break;
final payload = raw.sublist(pos, pos + payloadLength);
pos += payloadLength;
// Check: Text Record (TNF=0x01, type='T')
if (tnf == 0x01 && typeLength == 1 && type[0] == 0x54) {
if (payload.isNotEmpty) {
final langLen = payload[0] & 0x3F;
if (payload.length > 1 + langLen) {
final text = String.fromCharCodes(payload.sublist(1 + langLen));
if (_isCashuToken(text)) return text;
}
}
}
// Check: URI Record (TNF=0x01, type='U')
if (tnf == 0x01 && typeLength == 1 && type[0] == 0x55) {
if (payload.isNotEmpty) {
const prefixes = ['', 'http://www.', 'https://www.', 'http://', 'https://'];
final prefixIdx = payload[0];
final prefix = prefixIdx < prefixes.length ? prefixes[prefixIdx] : '';
final rest = String.fromCharCodes(payload.sublist(1));
final uri = '$prefix$rest';
final token = _extractTokenFromUri(uri);
if (token != null) return token;
}
}
// If ME (Message End) flag set, stop
if ((flags & 0x40) != 0) break;
}
return null;
}
/// Stop any active NFC read session.
static Future<void> stopRead() async {
await NfcManager.instance.stopSession();
}
/// Extract a Cashu token from an NDEF message.
static String? _extractToken(NdefMessage message) {
for (final record in message.records) {
// Text Record
if (record.typeNameFormat == TypeNameFormat.wellKnown &&
record.type.length == 1 &&
record.type[0] == 0x54) {
final text = _decodeTextRecord(record);
if (text != null && _isCashuToken(text)) {
return text;
}
}
// URI Record
if (record.typeNameFormat == TypeNameFormat.wellKnown &&
record.type.length == 1 &&
record.type[0] == 0x55) {
final uri = _decodeUriRecord(record);
if (uri != null) {
final token = _extractTokenFromUri(uri);
if (token != null) return token;
}
}
}
return null;
}
/// Decode NDEF Text Record payload.
static String? _decodeTextRecord(NdefRecord record) {
if (record.payload.isEmpty) return null;
final languageCodeLength = record.payload[0] & 0x3F;
if (record.payload.length <= 1 + languageCodeLength) return null;
return String.fromCharCodes(
record.payload.sublist(1 + languageCodeLength));
}
/// Decode NDEF URI Record payload.
static String? _decodeUriRecord(NdefRecord record) {
if (record.payload.isEmpty) return null;
const prefixes = [
'', // 0x00
'http://www.', // 0x01
'https://www.', // 0x02
'http://', // 0x03
'https://', // 0x04
];
final prefixIndex = record.payload[0];
final prefix = prefixIndex < prefixes.length ? prefixes[prefixIndex] : '';
final rest = String.fromCharCodes(record.payload.sublist(1));
return '$prefix$rest';
}
/// Extract token from a URL like https://wallet.com/#token=cashuA...
static String? _extractTokenFromUri(String uri) {
try {
final parsed = Uri.parse(uri);
final fragmentParams = Uri.splitQueryString(parsed.fragment);
final fragmentToken = fragmentParams['token'];
if (fragmentToken != null && _isCashuToken(fragmentToken)) {
return fragmentToken;
}
final tokenParam = parsed.queryParameters['token'];
if (tokenParam != null && _isCashuToken(tokenParam)) {
return tokenParam;
}
} catch (_) {}
return null;
}
/// Check if a string looks like a Cashu token or payment request.
static bool _isCashuToken(String text) {
final lower = text.toLowerCase().trim();
return lower.startsWith('cashua') ||
lower.startsWith('cashub') ||
lower.startsWith('creqa') ||
lower.startsWith('creqb') ||
lower.startsWith('bitcoin:');
}
}
+56
View File
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:math';
import 'package:http/http.dart' as http;
/// Servicio para obtener precios de Bitcoin usando Yadio API
@@ -86,4 +87,59 @@ class PriceService {
// Retornar en centavos
return BigInt.from((fiatAmount * 100).round());
}
// --- Blink API (precios históricos) ---
static const _blinkUrl = 'https://api.blink.sv/graphql';
/// Obtiene precios históricos de BTC desde Blink API.
/// range: ONE_DAY, ONE_WEEK, ONE_MONTH, ONE_YEAR, FIVE_YEARS
static Future<List<PricePoint>> getHistoricalPrices({
String range = 'ONE_DAY',
}) async {
try {
final response = await http.post(
Uri.parse(_blinkUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'query':
'query { btcPriceList(range: $range) { price { base offset } timestamp } }',
}),
).timeout(const Duration(seconds: 15));
if (response.statusCode != 200) {
throw Exception('Error HTTP: ${response.statusCode}');
}
final json = jsonDecode(response.body) as Map<String, dynamic>;
final data = json['data'] as Map<String, dynamic>?;
if (data == null) throw Exception('No data in response');
final list = data['btcPriceList'] as List?;
if (list == null || list.isEmpty) {
throw Exception('No historical prices in response');
}
return list.map((point) {
final price = point['price'] as Map<String, dynamic>;
final base = (price['base'] as num).toDouble();
final offset = (price['offset'] as num).toInt();
// base / 10^offset = USD cents → /100 = USD
final priceUsd = base / pow(10, offset) / 100;
final timestamp = (point['timestamp'] as num).toInt();
return PricePoint(timestamp: timestamp, priceUsd: priceUsd);
}).toList();
} catch (e) {
throw Exception('Error obteniendo precios históricos: $e');
}
}
}
/// Punto de precio histórico de BTC
class PricePoint {
final int timestamp;
final double priceUsd;
PricePoint({required this.timestamp, required this.priceUsd});
}
+1 -1
View File
@@ -33,7 +33,7 @@ class ProofService {
final dir = await getApplicationDocumentsDirectory();
final dbPath = '${dir.path}/elcaju_wallet.sqlite';
_db = await openDatabase(dbPath);
_db = await openDatabase(dbPath, singleInstance: false);
return _db!;
}
+20
View File
@@ -0,0 +1,20 @@
/// Builds a BIP-321 unified payment URI.
///
/// Format: BITCOIN:?LIGHTNING=LNBC...&CREQ=CREQB1...
/// All uppercase for optimal QR encoding (alphanumeric mode).
///
/// Ref: NUT-26 "BIP-321 Integration" section.
String buildUnifiedUri({
required String creqB,
String? bolt11,
}) {
final buffer = StringBuffer('bitcoin:?');
if (bolt11 != null) {
buffer.write('lightning=');
buffer.write(bolt11.toUpperCase());
buffer.write('&');
}
buffer.write('creq=');
buffer.write(creqB.toUpperCase());
return buffer.toString();
}
+67 -5
View File
@@ -1,5 +1,7 @@
// Parser para detectar el tipo de dato entrante (QR, clipboard, etc.)
// Soporta: tokens Cashu (A/B), invoices Lightning, URLs de mint, payment requests
// Soporta: tokens Cashu (A/B), invoices Lightning, URLs de mint, payment requests, peanut emoji
import 'peanut_codec.dart';
/// Modo de escaneo
enum ScanMode {
@@ -13,7 +15,7 @@ enum IncomingDataType {
cashuToken, // cashuA... / cashuB...
lightningInvoice, // lnbc... / lntb... / lnbcrt...
mintUrl, // https://...
paymentRequest, // creqA... (post-MVP, Cashu payment request)
paymentRequest, // creqA/creqB/bitcoin:?creq= (NUT-18/26 payment request)
unknown,
}
@@ -57,6 +59,15 @@ class IncomingDataParser {
/// Detecta el tipo de dato y extrae información relevante
static ParsedData parse(String data) {
final trimmed = data.trim();
// Peanut-encoded token (🥜 + variation selectors)
if (PeanutCodec.isPeanut(trimmed)) {
final decoded = PeanutCodec.decode(trimmed);
if (decoded != null) {
return parse(decoded);
}
}
final lower = trimmed.toLowerCase();
// Token Cashu (cashuA... o cashuB...)
@@ -92,8 +103,31 @@ class IncomingDataParser {
);
}
// Payment Request (creqA...)
if (lower.startsWith('creqa')) {
// BIP-321 unified URI: bitcoin:?lightning=...&creq=...
// Detect as paymentRequest but also extract the lightning invoice
if (lower.startsWith('bitcoin:')) {
final bolt11 = _extractBip321Param(trimmed, 'lightning');
final creq = _extractBip321Param(trimmed, 'creq');
if (creq != null) {
return ParsedData(
type: IncomingDataType.paymentRequest,
raw: trimmed,
invoiceBolt11: bolt11,
);
}
// bitcoin: URI with only lightning= (no creq)
if (bolt11 != null) {
return ParsedData(
type: IncomingDataType.lightningInvoice,
raw: trimmed,
invoiceBolt11: bolt11,
);
}
}
// Payment Request: creqA (NUT-18), creqB/CREQB1 (NUT-26)
if (lower.startsWith('creqa') || lower.startsWith('creqb')) {
return ParsedData(
type: IncomingDataType.paymentRequest,
raw: trimmed,
@@ -182,7 +216,35 @@ class IncomingDataParser {
case ScanMode.cashuOnly:
return data.type == IncomingDataType.cashuToken;
case ScanMode.invoiceOnly:
return data.type == IncomingDataType.lightningInvoice;
// Accept pure invoices and BIP-321 URIs that contain a lightning invoice
return data.type == IncomingDataType.lightningInvoice ||
(data.type == IncomingDataType.paymentRequest &&
data.invoiceBolt11 != null);
}
}
/// Extract a query parameter value from a BIP-321 URI.
/// Case-insensitive key matching, handles percent-encoding.
static String? _extractBip321Param(String uri, String key) {
final qIndex = uri.indexOf('?');
if (qIndex < 0) return null;
final query = uri.substring(qIndex + 1);
for (final param in query.split('&')) {
final eqIndex = param.indexOf('=');
if (eqIndex < 0) continue;
final k = param.substring(0, eqIndex);
if (k.toLowerCase() == key.toLowerCase()) {
final rawValue = param.substring(eqIndex + 1);
if (rawValue.isEmpty) continue;
try {
final value = Uri.decodeComponent(rawValue);
if (value.isEmpty) continue;
return value;
} on ArgumentError {
continue;
}
}
}
return null;
}
}
+62
View File
@@ -0,0 +1,62 @@
/// Peanut encoding/decoding for Cashu tokens.
///
/// Encodes Cashu tokens into invisible Unicode Variation Selectors
/// appended to a 🥜 emoji, compatible with cashu.me's implementation.
///
/// Each character of the base64 token is mapped to a Variation Selector:
/// - charCode 0-15 → VS1-VS16 (U+FE00 to U+FE0F)
/// - charCode 16-255 → VS17-VS256 (U+E0100 to U+E01EF)
class PeanutCodec {
static const String _peanutEmoji = '🥜';
/// Encodes a Cashu token string into peanut format (🥜 + variation selectors).
static String encode(String token) {
final buffer = StringBuffer(_peanutEmoji);
for (int i = 0; i < token.length; i++) {
final byte = token.codeUnitAt(i);
if (byte < 16) {
// VS1-VS16: U+FE00 to U+FE0F
buffer.writeCharCode(0xFE00 + byte);
} else if (byte < 256) {
// VS17-VS256: U+E0100 to U+E01EF
buffer.writeCharCode(0xE0100 + (byte - 16));
}
}
return buffer.toString();
}
/// Decodes a peanut-encoded string back to the original Cashu token.
/// Returns null if the input is not valid peanut format.
static String? decode(String peanut) {
final trimmed = peanut.trimLeft();
if (!trimmed.startsWith(_peanutEmoji)) return null;
final buffer = StringBuffer();
final runes = trimmed.runes.toList();
// Skip the first rune (🥜 emoji)
for (int i = 1; i < runes.length; i++) {
final codePoint = runes[i];
if (codePoint >= 0xFE00 && codePoint <= 0xFE0F) {
// VS1-VS16 → byte 0-15
buffer.writeCharCode(codePoint - 0xFE00);
} else if (codePoint >= 0xE0100 && codePoint <= 0xE01EF) {
// VS17-VS256 → byte 16-255
buffer.writeCharCode(codePoint - 0xE0100 + 16);
}
// Skip any other characters (whitespace, etc.)
}
final result = buffer.toString();
return result.isNotEmpty ? result : null;
}
/// Returns true if the string starts with the 🥜 emoji.
static bool isPeanut(String data) {
return data.trimLeft().startsWith(_peanutEmoji);
}
}
+156
View File
@@ -0,0 +1,156 @@
/// Estado del envío dentro del storage propio.
///
/// - [active] : el receptor aún no reclamó; los proofs están PendingSpent
/// localmente. Mostrar con acción de cancelar/reclamar.
/// - [settled] : la reconciliación con el mint confirmó que los proofs
/// fueron gastados (receptor reclamó) o que una reconciliación
/// previa ya los procesó. Mostrar como tx saliente histórica,
/// sin acción de cancel.
enum PendingSendStatus {
active,
settled;
String get wireName => name;
static PendingSendStatus fromWire(String? raw) {
switch (raw) {
case 'settled':
return PendingSendStatus.settled;
case 'active':
case null:
default:
return PendingSendStatus.active;
}
}
}
/// Modelo para rastrear envíos offline.
///
/// Los envíos offline no crean una Transaction en CDK (porque se hacen con
/// selección manual de proofs + createOfflineToken), por lo que necesitamos
/// un storage propio para poder mostrarlos en el historial y permitir
/// cancelar/reclamar por transacción.
///
/// Los proofs se identifican por sus Y values (hex) — mismos que acepta
/// Wallet.reclaimProofsByYs() en el bridge Rust.
class PendingSend {
/// UUID único
final String id;
/// Token codificado completo (cashuA... o cashuB...)
final String encoded;
/// Monto total del envío (suma de los proofs)
final BigInt amount;
/// URL del mint
final String mintUrl;
/// Unidad (sat, usd, eur, etc.)
final String unit;
/// Y values de los proofs incluidos (hex). Se usan para reclamar.
final List<String> proofYs;
/// Fecha de creación del envío
final DateTime createdAt;
/// Memo opcional
final String? memo;
/// Estado del envío. Ver [PendingSendStatus].
final PendingSendStatus status;
/// Timestamp de cuando el envío pasó a [PendingSendStatus.settled].
/// Null mientras esté activo. Se usa para ordenar en el historial.
final DateTime? settledAt;
PendingSend({
required this.id,
required this.encoded,
required this.amount,
required this.mintUrl,
required this.unit,
required List<String> proofYs,
required this.createdAt,
this.memo,
this.status = PendingSendStatus.active,
this.settledAt,
}) : proofYs = List.unmodifiable(proofYs);
bool get isActive => status == PendingSendStatus.active;
bool get isSettled => status == PendingSendStatus.settled;
/// Para ordenar en el historial: si está settled, usar settledAt; si no,
/// usar createdAt. Condicionado en `isSettled` (no sólo en `settledAt != null`)
/// para que una corrupción futura del campo no reubique un record activo.
DateTime get effectiveTimestamp =>
isSettled ? (settledAt ?? createdAt) : createdAt;
PendingSend copyWith({
String? id,
String? encoded,
BigInt? amount,
String? mintUrl,
String? unit,
List<String>? proofYs,
DateTime? createdAt,
String? memo,
PendingSendStatus? status,
DateTime? settledAt,
}) {
return PendingSend(
id: id ?? this.id,
encoded: encoded ?? this.encoded,
amount: amount ?? this.amount,
mintUrl: mintUrl ?? this.mintUrl,
unit: unit ?? this.unit,
proofYs: proofYs ?? this.proofYs,
createdAt: createdAt ?? this.createdAt,
memo: memo ?? this.memo,
status: status ?? this.status,
settledAt: settledAt ?? this.settledAt,
);
}
Map<String, dynamic> toMap() {
return {
'id': id,
'encoded': encoded,
'amount': amount.toString(),
'mint_url': mintUrl,
'unit': unit,
'proof_ys': proofYs.join(','),
'created_at': createdAt.millisecondsSinceEpoch,
'memo': memo,
'status': status.wireName,
'settled_at': settledAt?.millisecondsSinceEpoch,
};
}
factory PendingSend.fromMap(Map<String, dynamic> map) {
final ysRaw = map['proof_ys'] as String? ?? '';
final ys = ysRaw.isEmpty
? <String>[]
: ysRaw.split(',').where((s) => s.isNotEmpty).toList();
final settledAtRaw = map['settled_at'] as int?;
return PendingSend(
id: map['id'] as String,
encoded: map['encoded'] as String,
amount: BigInt.parse(map['amount'] as String),
mintUrl: map['mint_url'] as String,
unit: map['unit'] as String,
proofYs: ys,
createdAt: DateTime.fromMillisecondsSinceEpoch(map['created_at'] as int),
memo: map['memo'] as String?,
status: PendingSendStatus.fromWire(map['status'] as String?),
settledAt: settledAtRaw == null
? null
: DateTime.fromMillisecondsSinceEpoch(settledAtRaw),
);
}
@override
String toString() =>
'PendingSend(id: $id, amount: $amount $unit, mintUrl: $mintUrl, ys: ${proofYs.length}, status: ${status.name})';
}
+214
View File
@@ -0,0 +1,214 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:sqflite_common_ffi/sqflite_ffi.dart';
import 'pending_send.dart';
/// Storage persistente para envíos offline.
///
/// Los envíos offline no crean una Transaction en CDK, así que necesitamos
/// rastrearlos nosotros para poder:
/// - Mostrarlos en el historial como activos (cancelables) o como
/// liquidados (settled, display-only).
/// - Reclamar las proofs por transacción si el receptor no reclamó.
///
/// Mirrors PendingTokenStorage (singleton + cache + SQLite + change stream).
class PendingSendStorage {
static const _dbName = 'pending_sends.db';
static const _tableName = 'pending_sends';
static const _schemaVersion = 2;
Database? _db;
final Map<String, PendingSend> _cache = {};
bool _isInitialized = false;
final StreamController<void> _changesController =
StreamController<void>.broadcast();
Stream<void> get changes => _changesController.stream;
static final PendingSendStorage _instance = PendingSendStorage._internal();
factory PendingSendStorage() => _instance;
PendingSendStorage._internal();
bool get isInitialized => _isInitialized;
int get count => _cache.length;
bool get hasPendingSends => _cache.isNotEmpty;
int get activeCount =>
_cache.values.where((s) => s.isActive).length;
bool get hasActivePendingSends => activeCount > 0;
Future<void> init() async {
if (_isInitialized) return;
if (Platform.isLinux || Platform.isWindows) {
sqfliteFfiInit();
databaseFactory = databaseFactoryFfi;
}
final dir = await getApplicationDocumentsDirectory();
final dbPath = '${dir.path}/$_dbName';
_db = await openDatabase(
dbPath,
version: _schemaVersion,
onCreate: (db, version) async {
await db.execute('''
CREATE TABLE $_tableName (
id TEXT PRIMARY KEY,
encoded TEXT NOT NULL,
amount TEXT NOT NULL,
mint_url TEXT NOT NULL,
unit TEXT NOT NULL,
proof_ys TEXT NOT NULL,
created_at INTEGER NOT NULL,
memo TEXT,
status TEXT NOT NULL DEFAULT 'active',
settled_at INTEGER
)
''');
await db.execute(
'CREATE INDEX idx_ps_mint_unit ON $_tableName(mint_url, unit)',
);
await db.execute(
'CREATE INDEX idx_ps_created_at ON $_tableName(created_at)',
);
await db.execute(
'CREATE INDEX idx_ps_status ON $_tableName(status)',
);
},
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
// v2: status + settled_at. Los records existentes quedan 'active'.
await db.execute(
"ALTER TABLE $_tableName ADD COLUMN status TEXT NOT NULL DEFAULT 'active'",
);
await db.execute(
'ALTER TABLE $_tableName ADD COLUMN settled_at INTEGER',
);
await db.execute(
'CREATE INDEX IF NOT EXISTS idx_ps_status ON $_tableName(status)',
);
}
},
);
await _loadFromDb();
_isInitialized = true;
debugPrint(
'PendingSendStorage inicializado: ${_cache.length} envíos '
'($activeCount activos)',
);
}
Future<void> _loadFromDb() async {
if (_db == null) return;
final results = await _db!.query(_tableName, orderBy: 'created_at DESC');
_cache.clear();
for (final row in results) {
final send = PendingSend.fromMap(row);
_cache[send.id] = send;
}
}
Future<PendingSend> add({
required String id,
required String encoded,
required BigInt amount,
required String mintUrl,
required String unit,
required List<String> proofYs,
String? memo,
}) async {
final send = PendingSend(
id: id,
encoded: encoded,
amount: amount,
mintUrl: mintUrl,
unit: unit,
proofYs: proofYs,
createdAt: DateTime.now(),
memo: memo,
// status default = active
);
// Persistencia primero: si SQLite falla, la cache no queda contaminada
// con un record que el reinicio no va a encontrar.
await _db?.insert(_tableName, send.toMap());
_cache[send.id] = send;
_changesController.add(null);
return send;
}
/// Marca un envío como liquidado (settled). Se usa cuando la reconciliación
/// confirmó que el receptor reclamó o el envío ya no es actionable.
/// Si no existe el record, no hace nada.
Future<void> markSettled(String id) async {
final current = _cache[id];
if (current == null) return;
if (current.isSettled) return; // idempotente
final updated = current.copyWith(
status: PendingSendStatus.settled,
settledAt: DateTime.now(),
);
// Persistencia primero: si SQLite falla, la cache queda activa y un
// próximo reconcile reintenta. Con el orden inverso, la idempotencia
// (`isSettled → early return`) impediría el retry tras una falla.
await _db?.update(
_tableName,
updated.toMap(),
where: 'id = ?',
whereArgs: [id],
);
_cache[id] = updated;
_changesController.add(null);
}
/// Borrado permanente. Reservado para "dismiss manual" (no implementado
/// hoy en UI) o cleanup explícito del wallet (`clear`). Para el caso
/// "receptor reclamó", usar `markSettled` en vez de este.
Future<void> remove(String id) async {
// Persistencia primero: igual que add/markSettled, la cache no se
// adelanta al storage.
await _db?.delete(_tableName, where: 'id = ?', whereArgs: [id]);
_cache.remove(id);
_changesController.add(null);
}
List<PendingSend> listAll() {
final list = _cache.values.toList();
list.sort((a, b) => b.createdAt.compareTo(a.createdAt));
return list;
}
/// Envíos activos (el receptor aún no reclamó). Ordenados por creación
/// descendente. Renderizables en el historial con botón cancel.
List<PendingSend> listActive() {
return _cache.values.where((s) => s.isActive).toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
}
/// Envíos liquidados (el receptor ya reclamó). Ordenados por `settledAt`
/// descendente — representa la línea temporal de finalización.
List<PendingSend> listSettled() {
return _cache.values.where((s) => s.isSettled).toList()
..sort((a, b) => b.effectiveTimestamp.compareTo(a.effectiveTimestamp));
}
PendingSend? get(String id) => _cache[id];
/// Filtra por mint+unit. Útil para mostrar pendientes específicos del
/// wallet activo.
List<PendingSend> listByMintUnit(String mintUrl, String unit) {
return _cache.values
.where((s) => s.mintUrl == mintUrl && s.unit == unit)
.toList()
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
}
Future<void> clear() async {
_cache.clear();
await _db?.delete(_tableName);
_changesController.add(null);
}
}
+84 -5
View File
@@ -63,8 +63,8 @@
"homeTitle": "Start",
"receive": "Empfangen",
"send": "Senden",
"sendAction": "Senden",
"receiveAction": "Empfangen",
"sendAction": "Senden",
"receiveAction": "Empfangen",
"deposit": "Einzahlen",
"withdraw": "Abheben",
"lightning": "Lightning",
@@ -86,6 +86,21 @@
"receiveCashu": "Cashu empfangen",
"pasteTheCashuToken": "Füge den Cashu Token ein:",
"pasteFromClipboard": "Aus Zwischenablage einfügen",
"emptyClipboard": "Zwischenablage leer",
"cancelSend": "Senden abbrechen",
"cancelSendConfirmTitle": "Diesen Versand abbrechen?",
"cancelSendConfirmBody": "Funktioniert nur, wenn der Empfänger das Token noch nicht eingelöst hat. Falls doch, wird der Versand als abgeschlossen markiert.",
"cancelSendSuccess": "Versand abgebrochen. {amount} {unit} zurückerhalten",
"@cancelSendSuccess": {
"placeholders": {
"amount": {"type": "String"},
"unit": {"type": "String"}
}
},
"cancelSendAlreadyClaimed": "Der Empfänger hat das Token bereits eingelöst",
"pendingOfflineSend": "Ausstehender Offline-Versand",
"pendingOfflineSendsHeader": "Nicht eingelöste Offline-Versendungen",
"pendingSendDetailTitle": "Ausstehender Versand",
"validToken": "Token gültig",
"invalidToken": "Ungültiger oder fehlerhafter Token",
"amount": "Betrag:",
@@ -118,6 +133,7 @@
"confirm": "Bestätigen",
"cancel": "Abbrechen",
"insufficientBalance": "Unzureichendes Guthaben",
"feeExceedsAmount": "Die Gebühr übersteigt den Sendebetrag",
"tokenCreationError": "Fehler beim Erstellen des Tokens: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -137,6 +153,20 @@
},
"keepTokenWarning": "Bewahre diesen Token auf, bis der Empfänger ihn einlöst. Wenn du ihn verlierst, verlierst du die Mittel.",
"tokenCopiedToClipboard": "Token in Zwischenablage kopiert",
"copyAsEmoji": "Als Emoji kopieren",
"emojiCopiedToClipboard": "Token als Emoji kopiert 🥜",
"peanutDecodeError": "Emoji-Token konnte nicht dekodiert werden. Möglicherweise beschädigt.",
"nfcWrite": "Auf NFC-Tag schreiben",
"nfcRead": "NFC-Tag lesen",
"nfcHoldNear": "Gerät an NFC-Tag halten...",
"nfcWriteSuccess": "Token auf NFC-Tag geschrieben",
"nfcWriteError": "NFC-Schreibfehler: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "NFC-Lesefehler: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC ist deaktiviert. Aktiviere es in den Einstellungen.",
"nfcUnsupported": "Dieses Gerät unterstützt kein NFC",
"amountToDeposit": "Einzuzahlender Betrag:",
"descriptionOptional": "Beschreibung (optional):",
@@ -213,6 +243,8 @@
"pendingStatus": "Ausstehend",
"receivedStatus": "Empfangen",
"sentStatus": "Gesendet",
"receiving": "Wird empfangen",
"sending": "Wird gesendet",
"now": "Jetzt",
"agoMinutes": "Vor {minutes} Min",
"@agoMinutes": {
@@ -296,7 +328,7 @@
}
},
"close": "Schließen",
"aboutDescription": "Eine Cashu Wallet mit kubanischer DNA für die ganze Welt. Bruder von La Chispa.",
"aboutDescription": "Eine Cashu Wallet mit kubanischer DNA für die ganze Welt. Bruder von LaChispa.",
"couldNotOpenLink": "Link konnte nicht geöffnet werden",
"deleteWalletQuestion": "Wallet löschen?",
@@ -488,7 +520,22 @@
"scanLightningInvoiceHint": "Scanne eine Lightning Rechnung (lnbc...)",
"addMintQuestion": "Diesen Mint hinzufügen?",
"cameraPermissionDenied": "Kamera-Berechtigung verweigert",
"paymentRequestNotSupported": "Zahlungsanfragen werden noch nicht unterstützt",
"paymentRequestTitle": "Zahlungsanfrage",
"paymentRequestFrom": "Anfrage von",
"paymentRequestAmount": "Angeforderter Betrag",
"paymentRequestDescription": "Beschreibung",
"paymentRequestMints": "Akzeptierte Mints",
"paymentRequestAnyMint": "Jeder Mint",
"paymentRequestPay": "Bezahlen",
"paymentRequestPaying": "Bezahle...",
"paymentRequestSuccess": "Zahlung erfolgreich gesendet",
"paymentRequestNoTransport": "Diese Anfrage hat keine konfigurierte Zustellmethode",
"paymentRequestTransport": "Transport",
"paymentRequestMintNotAccepted": "Dein aktiver Mint ist nicht in der Liste der akzeptierten Mints",
"paymentRequestUnitMismatch": "Inkompatible Einheit: Anfrage erfordert {unit}",
"paymentRequestInsufficientBalance": "Unzureichendes Guthaben",
"paymentRequestErrorParsing": "Fehler beim Lesen der Zahlungsanfrage",
"p2pkTitle": "P2PK-Schlüssel",
"p2pkSettingsDescription": "Gesperrtes ecash empfangen",
"p2pkExperimental": "P2PK ist experimentell. Mit Vorsicht verwenden.",
@@ -520,5 +567,37 @@
"p2pkErrorInvalidNsec": "Ungültiger nsec",
"p2pkErrorKeyAlreadyExists": "Dieser Schlüssel existiert bereits",
"p2pkErrorKeyNotFound": "Schlüssel nicht gefunden",
"p2pkErrorCannotDeletePrimary": "Primärschlüssel kann nicht gelöscht werden"
"p2pkErrorCannotDeletePrimary": "Primärschlüssel kann nicht gelöscht werden",
"request": "Anfordern",
"requestPayment": "Zahlung anfordern",
"requestPaymentDescription": "Einheitliche Zahlungsanforderung erstellen",
"generateRequest": "Anforderung erstellen",
"generatingRequest": "Anforderung wird erstellt...",
"requestPaymentReceived": "Zahlung erhalten",
"requestDescriptionHint": "Beschreibung (optional)",
"universal": "Universal",
"copiedToClipboard": "In die Zwischenablage kopiert",
"swap": "Tauschen",
"swapDescription": "Zwischen Sats und USD wechseln",
"swapFrom": "Von",
"swapTo": "Nach",
"swapAction": "Tauschen",
"swapEstimatedFee": "Geschätzte Gebühr",
"swapUseAll": "Alles verwenden",
"swapMinimum": "Minimum: {amount}",
"swapProcessing": "Swap wird verarbeitet...",
"swapSuccess": "Swap abgeschlossen",
"swapErrorInsufficient": "Unzureichendes Guthaben",
"swapErrorExpired": "Angebot abgelaufen",
"swapErrorGeneric": "Swap-Fehler: {error}",
"swapChartUnavailable": "Preis nicht verfügbar · Tippen zum Wiederholen",
"swapChartMinMax": "24h Min: {minPrice} — Max: {maxPrice}",
"privacyPolicy": "Datenschutz",
"privacyTitle": "WIR SAMMELN NICHTS",
"privacyGoodbye": "TSCHÜSS",
"privacyKeepReading": "(lies weiter, wenn du willst…)",
"privacyBody": "Wir wissen nicht, wer du bist\nWir wissen nicht, wie viel du hast\nWir wissen nicht, was du tust",
"privacyConclusion": "Der beste Weg, deine Daten zu schützen,\nist sie nicht zu haben"
}
+96 -5
View File
@@ -53,8 +53,8 @@
"homeTitle": "Home",
"receive": "Receive",
"send": "Send",
"sendAction": "Send",
"receiveAction": "Receive",
"sendAction": "Send",
"receiveAction": "Receive",
"deposit": "Deposit",
"withdraw": "Withdraw",
"lightning": "Lightning",
@@ -76,6 +76,21 @@
"receiveCashu": "Receive Cashu",
"pasteTheCashuToken": "Paste the Cashu token:",
"pasteFromClipboard": "Paste from clipboard",
"emptyClipboard": "Clipboard is empty",
"cancelSend": "Cancel send",
"cancelSendConfirmTitle": "Cancel this send?",
"cancelSendConfirmBody": "Only works if the recipient has not claimed the token yet. If they already did, the send will be marked as completed.",
"cancelSendSuccess": "Send canceled. Recovered {amount} {unit}",
"@cancelSendSuccess": {
"placeholders": {
"amount": {"type": "String"},
"unit": {"type": "String"}
}
},
"cancelSendAlreadyClaimed": "The recipient already claimed the token",
"pendingOfflineSend": "Pending offline send",
"pendingOfflineSendsHeader": "Unclaimed offline sends",
"pendingSendDetailTitle": "Pending send",
"validToken": "Valid token",
"invalidToken": "Invalid or malformed token",
"amount": "Amount:",
@@ -103,6 +118,7 @@
"confirm": "Confirm",
"cancel": "Cancel",
"insufficientBalance": "Insufficient balance",
"feeExceedsAmount": "Fee exceeds the amount to send",
"tokenCreationError": "Error creating token: {error}",
"tokenCreated": "Token created",
@@ -112,6 +128,28 @@
"tokenCashuAnimatedQr": "Cashu Token (animated QR - {fragments} UR fragments)",
"keepTokenWarning": "Keep this token until the recipient claims it. If you lose it, you will lose the funds.",
"tokenCopiedToClipboard": "Token copied to clipboard",
"copyAsEmoji": "Copy as emoji",
"emojiCopiedToClipboard": "Token copied as emoji 🥜",
"peanutDecodeError": "Could not decode emoji token. It may be corrupted.",
"nfcWrite": "Write to NFC tag",
"nfcRead": "Read NFC tag",
"nfcHoldNear": "Hold device near NFC tag...",
"nfcWriteSuccess": "Token written to NFC tag",
"nfcWriteError": "NFC write error: {error}",
"@nfcWriteError": {
"placeholders": {
"error": { "type": "String" }
}
},
"nfcReadError": "NFC read error: {error}",
"@nfcReadError": {
"placeholders": {
"error": { "type": "String" }
}
},
"nfcDisabled": "NFC is disabled. Enable it in Settings.",
"nfcUnsupported": "This device does not support NFC",
"amountToDeposit": "Amount to deposit:",
"descriptionOptional": "Description (optional):",
@@ -171,6 +209,8 @@
"pendingStatus": "Pending",
"receivedStatus": "Received",
"sentStatus": "Sent",
"receiving": "Receiving",
"sending": "Sending",
"now": "Now",
"agoMinutes": "{minutes} min ago",
"agoHours": "{hours} h ago",
@@ -234,7 +274,7 @@
"selectLanguage": "Select language",
"languageChanged": "Language changed to {language}",
"close": "Close",
"aboutDescription": "A Cashu wallet with Cuban DNA for the entire world. Brother of La Chispa.",
"aboutDescription": "A Cashu wallet with Cuban DNA for the entire world. Brother of LaChispa.",
"couldNotOpenLink": "Could not open link",
"deleteWalletQuestion": "Delete wallet?",
@@ -368,7 +408,26 @@
"scanLightningInvoiceHint": "Scan a Lightning invoice (lnbc...)",
"addMintQuestion": "Add this mint?",
"cameraPermissionDenied": "Camera permission denied",
"paymentRequestNotSupported": "Payment requests are not yet supported",
"paymentRequestTitle": "Payment Request",
"paymentRequestFrom": "Request from",
"paymentRequestAmount": "Requested amount",
"paymentRequestDescription": "Description",
"paymentRequestMints": "Accepted mints",
"paymentRequestAnyMint": "Any mint",
"paymentRequestPay": "Pay",
"paymentRequestPaying": "Paying...",
"paymentRequestSuccess": "Payment sent successfully",
"paymentRequestNoTransport": "This request has no delivery method configured",
"paymentRequestTransport": "Transport",
"paymentRequestMintNotAccepted": "Your active mint is not in the list of accepted mints",
"paymentRequestUnitMismatch": "Incompatible unit: request requires {unit}",
"@paymentRequestUnitMismatch": {
"placeholders": {
"unit": { "type": "String" }
}
},
"paymentRequestInsufficientBalance": "Insufficient balance",
"paymentRequestErrorParsing": "Error reading payment request",
"p2pkTitle": "P2PK Keys",
"p2pkSettingsDescription": "Receive locked ecash",
@@ -401,5 +460,37 @@
"p2pkErrorInvalidNsec": "Invalid nsec",
"p2pkErrorKeyAlreadyExists": "This key already exists",
"p2pkErrorKeyNotFound": "Key not found",
"p2pkErrorCannotDeletePrimary": "Cannot delete primary key"
"p2pkErrorCannotDeletePrimary": "Cannot delete primary key",
"request": "Request",
"requestPayment": "Request Payment",
"requestPaymentDescription": "Generate unified payment request",
"generateRequest": "Generate Request",
"generatingRequest": "Generating request...",
"requestPaymentReceived": "Payment received",
"requestDescriptionHint": "Description (optional)",
"universal": "Universal",
"copiedToClipboard": "Copied to clipboard",
"swap": "Swap",
"swapDescription": "Convert between sats and USD",
"swapFrom": "From",
"swapTo": "To",
"swapAction": "Swap",
"swapEstimatedFee": "Estimated fee",
"swapUseAll": "Use all",
"swapMinimum": "Minimum: {amount}",
"swapProcessing": "Processing swap...",
"swapSuccess": "Swap completed",
"swapErrorInsufficient": "Insufficient balance",
"swapErrorExpired": "Quote has expired",
"swapErrorGeneric": "Swap error: {error}",
"swapChartUnavailable": "Price unavailable · Tap to retry",
"swapChartMinMax": "24h Min: {minPrice} — Max: {maxPrice}",
"privacyPolicy": "Privacy policy",
"privacyTitle": "WE COLLECT NOTHING",
"privacyGoodbye": "BYE",
"privacyKeepReading": "(keep reading if you want…)",
"privacyBody": "We don't know who you are\nWe don't know how much you have\nWe don't know what you do",
"privacyConclusion": "The best way to protect your data\nis not to have it"
}
+112 -5
View File
@@ -63,8 +63,8 @@
"homeTitle": "Inicio",
"receive": "Recibir",
"send": "Enviar",
"sendAction": "Enviar",
"receiveAction": "Recibir",
"sendAction": "Enviar",
"receiveAction": "Recibir",
"deposit": "Depositar",
"withdraw": "Retirar",
"lightning": "Lightning",
@@ -86,6 +86,21 @@
"receiveCashu": "Recibir Cashu",
"pasteTheCashuToken": "Pega el token Cashu:",
"pasteFromClipboard": "Pegar del portapapeles",
"emptyClipboard": "Portapapeles vacío",
"cancelSend": "Cancelar envío",
"cancelSendConfirmTitle": "¿Cancelar envío?",
"cancelSendConfirmBody": "Solo funciona si el destinatario aún no reclamó el token. Si ya lo hizo, el envío se marcará como completado.",
"cancelSendSuccess": "Envío cancelado. Recuperaste {amount} {unit}",
"@cancelSendSuccess": {
"placeholders": {
"amount": {"type": "String"},
"unit": {"type": "String"}
}
},
"cancelSendAlreadyClaimed": "El destinatario ya reclamó el token",
"pendingOfflineSend": "Envío offline pendiente",
"pendingOfflineSendsHeader": "Envíos offline no reclamados",
"pendingSendDetailTitle": "Envío pendiente",
"validToken": "Token válido",
"invalidToken": "Token inválido o malformado",
"amount": "Monto:",
@@ -118,6 +133,7 @@
"confirm": "Confirmar",
"cancel": "Cancelar",
"insufficientBalance": "Balance insuficiente",
"feeExceedsAmount": "La comisión supera el monto a enviar",
"tokenCreationError": "Error al crear token: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -137,6 +153,28 @@
},
"keepTokenWarning": "Guarda este token hasta que el receptor lo reclame. Si lo pierdes, perderás los fondos.",
"tokenCopiedToClipboard": "Token copiado al portapapeles",
"copyAsEmoji": "Copiar como emoji",
"emojiCopiedToClipboard": "Token copiado como emoji 🥜",
"peanutDecodeError": "No se pudo decodificar el token emoji. Puede estar corrupto.",
"nfcWrite": "Escribir en tag NFC",
"nfcRead": "Leer tag NFC",
"nfcHoldNear": "Acerca el dispositivo al tag NFC...",
"nfcWriteSuccess": "Token escrito en tag NFC",
"nfcWriteError": "Error NFC al escribir: {error}",
"@nfcWriteError": {
"placeholders": {
"error": { "type": "String" }
}
},
"nfcReadError": "Error NFC al leer: {error}",
"@nfcReadError": {
"placeholders": {
"error": { "type": "String" }
}
},
"nfcDisabled": "NFC está desactivado. Actívalo en Ajustes.",
"nfcUnsupported": "Este dispositivo no soporta NFC",
"amountToDeposit": "Monto a depositar:",
"descriptionOptional": "Descripción (opcional):",
@@ -213,6 +251,8 @@
"pendingStatus": "Pendiente",
"receivedStatus": "Recibido",
"sentStatus": "Enviado",
"receiving": "Recibiendo",
"sending": "Enviando",
"now": "Ahora",
"agoMinutes": "Hace {minutes} min",
"@agoMinutes": {
@@ -296,7 +336,7 @@
}
},
"close": "Cerrar",
"aboutDescription": "Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de La Chispa.",
"aboutDescription": "Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de LaChispa.",
"couldNotOpenLink": "No se pudo abrir el enlace",
"deleteWalletQuestion": "¿Borrar wallet?",
@@ -488,7 +528,26 @@
"scanLightningInvoiceHint": "Escanea un invoice Lightning (lnbc...)",
"addMintQuestion": "¿Agregar este mint?",
"cameraPermissionDenied": "Permiso de cámara denegado",
"paymentRequestNotSupported": "Los payment requests aún no están soportados",
"paymentRequestTitle": "Solicitud de pago",
"paymentRequestFrom": "Solicitud de",
"paymentRequestAmount": "Monto solicitado",
"paymentRequestDescription": "Descripción",
"paymentRequestMints": "Mints aceptados",
"paymentRequestAnyMint": "Cualquier mint",
"paymentRequestPay": "Pagar",
"paymentRequestPaying": "Pagando...",
"paymentRequestSuccess": "Pago enviado correctamente",
"paymentRequestNoTransport": "Esta solicitud no tiene método de entrega configurado",
"paymentRequestTransport": "Transporte",
"paymentRequestMintNotAccepted": "Tu mint activo no está en la lista de mints aceptados",
"paymentRequestUnitMismatch": "Unidad incompatible: la solicitud requiere {unit}",
"@paymentRequestUnitMismatch": {
"placeholders": {
"unit": { "type": "String" }
}
},
"paymentRequestInsufficientBalance": "Balance insuficiente",
"paymentRequestErrorParsing": "Error al leer la solicitud de pago",
"p2pkTitle": "Claves P2PK",
"p2pkSettingsDescription": "Recibir ecash bloqueado",
@@ -521,5 +580,53 @@
"p2pkErrorInvalidNsec": "nsec inválido",
"p2pkErrorKeyAlreadyExists": "Esta clave ya existe",
"p2pkErrorKeyNotFound": "Clave no encontrada",
"p2pkErrorCannotDeletePrimary": "No se puede eliminar la clave principal"
"p2pkErrorCannotDeletePrimary": "No se puede eliminar la clave principal",
"request": "Solicitar",
"requestPayment": "Solicitar pago",
"requestPaymentDescription": "Generar solicitud de pago unificada",
"generateRequest": "Generar solicitud",
"generatingRequest": "Generando solicitud...",
"requestPaymentReceived": "Pago recibido",
"requestDescriptionHint": "Descripción (opcional)",
"universal": "Universal",
"copiedToClipboard": "Copiado al portapapeles",
"swap": "Cambiar",
"swapDescription": "Convertir entre sats y USD",
"swapFrom": "De",
"swapTo": "A",
"swapAction": "Cambiar",
"swapEstimatedFee": "Fee estimado",
"swapUseAll": "Usar todo",
"swapMinimum": "Mínimo: {amount}",
"@swapMinimum": {
"placeholders": {
"amount": { "type": "String" }
}
},
"swapProcessing": "Procesando swap...",
"swapSuccess": "Swap completado",
"swapErrorInsufficient": "Saldo insuficiente",
"swapErrorExpired": "La cotización ha expirado",
"swapErrorGeneric": "Error en el swap: {error}",
"@swapErrorGeneric": {
"placeholders": {
"error": { "type": "String" }
}
},
"swapChartUnavailable": "Precio no disponible · Toca para reintentar",
"swapChartMinMax": "24h Mín: {minPrice} — Máx: {maxPrice}",
"@swapChartMinMax": {
"placeholders": {
"minPrice": { "type": "String" },
"maxPrice": { "type": "String" }
}
},
"privacyPolicy": "Política de privacidad",
"privacyTitle": "NO RECOPILAMOS NADA",
"privacyGoodbye": "ADIÓS",
"privacyKeepReading": "(sigue leyendo si quieres…)",
"privacyBody": "No sabemos quién eres\nNo sabemos cuánto tienes\nNo sabemos qué haces",
"privacyConclusion": "La mejor forma de proteger tus datos\nes no tenerlos"
}
+84 -5
View File
@@ -63,8 +63,8 @@
"homeTitle": "Accueil",
"receive": "Recevoir",
"send": "Envoyer",
"sendAction": "Envoyer",
"receiveAction": "Recevoir",
"sendAction": "Envoyer",
"receiveAction": "Recevoir",
"deposit": "Déposer",
"withdraw": "Retirer",
"lightning": "Lightning",
@@ -86,6 +86,21 @@
"receiveCashu": "Recevoir Cashu",
"pasteTheCashuToken": "Collez le token Cashu :",
"pasteFromClipboard": "Coller depuis le presse-papiers",
"emptyClipboard": "Presse-papiers vide",
"cancelSend": "Annuler l'envoi",
"cancelSendConfirmTitle": "Annuler cet envoi ?",
"cancelSendConfirmBody": "Ne fonctionne que si le destinataire n'a pas encore réclamé le token. S'il l'a déjà fait, l'envoi sera marqué comme terminé.",
"cancelSendSuccess": "Envoi annulé. Récupéré {amount} {unit}",
"@cancelSendSuccess": {
"placeholders": {
"amount": {"type": "String"},
"unit": {"type": "String"}
}
},
"cancelSendAlreadyClaimed": "Le destinataire a déjà réclamé le token",
"pendingOfflineSend": "Envoi hors ligne en attente",
"pendingOfflineSendsHeader": "Envois hors ligne non réclamés",
"pendingSendDetailTitle": "Envoi en attente",
"validToken": "Token valide",
"invalidToken": "Token invalide ou malformé",
"amount": "Montant :",
@@ -118,6 +133,7 @@
"confirm": "Confirmer",
"cancel": "Annuler",
"insufficientBalance": "Solde insuffisant",
"feeExceedsAmount": "Les frais dépassent le montant à envoyer",
"tokenCreationError": "Erreur de création du token : {error}",
"@tokenCreationError": {
"placeholders": {
@@ -137,6 +153,20 @@
},
"keepTokenWarning": "Conservez ce token jusqu'à ce que le destinataire le réclame. Si vous le perdez, vous perdrez les fonds.",
"tokenCopiedToClipboard": "Token copié dans le presse-papiers",
"copyAsEmoji": "Copier en emoji",
"emojiCopiedToClipboard": "Token copié en emoji 🥜",
"peanutDecodeError": "Impossible de décoder le token emoji. Il est peut-être corrompu.",
"nfcWrite": "Écrire sur tag NFC",
"nfcRead": "Lire tag NFC",
"nfcHoldNear": "Approchez l'appareil du tag NFC...",
"nfcWriteSuccess": "Token écrit sur le tag NFC",
"nfcWriteError": "Erreur NFC écriture : {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "Erreur NFC lecture : {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC est désactivé. Activez-le dans les Paramètres.",
"nfcUnsupported": "Cet appareil ne prend pas en charge le NFC",
"amountToDeposit": "Montant à déposer :",
"descriptionOptional": "Description (optionnelle) :",
@@ -213,6 +243,8 @@
"pendingStatus": "En attente",
"receivedStatus": "Reçu",
"sentStatus": "Envoyé",
"receiving": "En réception",
"sending": "En envoi",
"now": "Maintenant",
"agoMinutes": "Il y a {minutes} min",
"@agoMinutes": {
@@ -296,7 +328,7 @@
}
},
"close": "Fermer",
"aboutDescription": "Un portefeuille Cashu avec ADN cubain pour le monde entier. Frère de La Chispa.",
"aboutDescription": "Un portefeuille Cashu avec ADN cubain pour le monde entier. Frère de LaChispa.",
"couldNotOpenLink": "Impossible d'ouvrir le lien",
"deleteWalletQuestion": "Supprimer le portefeuille ?",
@@ -488,7 +520,22 @@
"scanLightningInvoiceHint": "Scannez une facture Lightning (lnbc...)",
"addMintQuestion": "Ajouter ce mint ?",
"cameraPermissionDenied": "Permission de la caméra refusée",
"paymentRequestNotSupported": "Les demandes de paiement ne sont pas encore prises en charge",
"paymentRequestTitle": "Demande de paiement",
"paymentRequestFrom": "Demande de",
"paymentRequestAmount": "Montant demandé",
"paymentRequestDescription": "Description",
"paymentRequestMints": "Mints acceptés",
"paymentRequestAnyMint": "N'importe quel mint",
"paymentRequestPay": "Payer",
"paymentRequestPaying": "Paiement en cours...",
"paymentRequestSuccess": "Paiement envoyé avec succès",
"paymentRequestNoTransport": "Cette demande n'a pas de méthode de livraison configurée",
"paymentRequestTransport": "Transport",
"paymentRequestMintNotAccepted": "Votre mint actif n'est pas dans la liste des mints acceptés",
"paymentRequestUnitMismatch": "Unité incompatible : la demande nécessite {unit}",
"paymentRequestInsufficientBalance": "Solde insuffisant",
"paymentRequestErrorParsing": "Erreur lors de la lecture de la demande de paiement",
"p2pkTitle": "Clés P2PK",
"p2pkSettingsDescription": "Recevoir ecash verrouillé",
"p2pkExperimental": "P2PK est expérimental. Utiliser avec prudence.",
@@ -520,5 +567,37 @@
"p2pkErrorInvalidNsec": "nsec invalide",
"p2pkErrorKeyAlreadyExists": "Cette clé existe déjà",
"p2pkErrorKeyNotFound": "Clé non trouvée",
"p2pkErrorCannotDeletePrimary": "Impossible de supprimer la clé principale"
"p2pkErrorCannotDeletePrimary": "Impossible de supprimer la clé principale",
"request": "Demander",
"requestPayment": "Demander un paiement",
"requestPaymentDescription": "Générer une demande de paiement unifiée",
"generateRequest": "Générer la demande",
"generatingRequest": "Génération en cours...",
"requestPaymentReceived": "Paiement reçu",
"requestDescriptionHint": "Description (facultatif)",
"universal": "Universel",
"copiedToClipboard": "Copié dans le presse-papiers",
"swap": "Échanger",
"swapDescription": "Convertir entre sats et USD",
"swapFrom": "De",
"swapTo": "Vers",
"swapAction": "Échanger",
"swapEstimatedFee": "Frais estimés",
"swapUseAll": "Tout utiliser",
"swapMinimum": "Minimum : {amount}",
"swapProcessing": "Swap en cours...",
"swapSuccess": "Swap terminé",
"swapErrorInsufficient": "Solde insuffisant",
"swapErrorExpired": "Le devis a expiré",
"swapErrorGeneric": "Erreur de swap : {error}",
"swapChartUnavailable": "Prix indisponible · Appuyez pour réessayer",
"swapChartMinMax": "24h Min : {minPrice} — Max : {maxPrice}",
"privacyPolicy": "Politique de confidentialité",
"privacyTitle": "NOUS NE COLLECTONS RIEN",
"privacyGoodbye": "AU REVOIR",
"privacyKeepReading": "(continue à lire si tu veux…)",
"privacyBody": "Nous ne savons pas qui tu es\nNous ne savons pas combien tu as\nNous ne savons pas ce que tu fais",
"privacyConclusion": "La meilleure façon de protéger tes données,\nc'est de ne pas les avoir"
}
+84 -5
View File
@@ -63,8 +63,8 @@
"homeTitle": "Home",
"receive": "Ricevi",
"send": "Invia",
"sendAction": "Invia",
"receiveAction": "Ricevi",
"sendAction": "Invia",
"receiveAction": "Ricevi",
"deposit": "Deposita",
"withdraw": "Preleva",
"lightning": "Lightning",
@@ -86,6 +86,21 @@
"receiveCashu": "Ricevi Cashu",
"pasteTheCashuToken": "Incolla il token Cashu:",
"pasteFromClipboard": "Incolla dagli appunti",
"emptyClipboard": "Appunti vuoti",
"cancelSend": "Annulla invio",
"cancelSendConfirmTitle": "Annullare questo invio?",
"cancelSendConfirmBody": "Funziona solo se il destinatario non ha ancora riscattato il token. Se l'ha già fatto, l'invio sarà contrassegnato come completato.",
"cancelSendSuccess": "Invio annullato. Recuperati {amount} {unit}",
"@cancelSendSuccess": {
"placeholders": {
"amount": {"type": "String"},
"unit": {"type": "String"}
}
},
"cancelSendAlreadyClaimed": "Il destinatario ha già riscattato il token",
"pendingOfflineSend": "Invio offline in sospeso",
"pendingOfflineSendsHeader": "Invii offline non riscattati",
"pendingSendDetailTitle": "Invio in sospeso",
"validToken": "Token valido",
"invalidToken": "Token non valido o malformato",
"amount": "Importo:",
@@ -118,6 +133,7 @@
"confirm": "Conferma",
"cancel": "Annulla",
"insufficientBalance": "Saldo insufficiente",
"feeExceedsAmount": "La commissione supera l'importo da inviare",
"tokenCreationError": "Errore creazione token: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -137,6 +153,20 @@
},
"keepTokenWarning": "Conserva questo token finché il destinatario non lo riscatta. Se lo perdi, perderai i fondi.",
"tokenCopiedToClipboard": "Token copiato negli appunti",
"copyAsEmoji": "Copia come emoji",
"emojiCopiedToClipboard": "Token copiato come emoji 🥜",
"peanutDecodeError": "Impossibile decodificare il token emoji. Potrebbe essere corrotto.",
"nfcWrite": "Scrivi su tag NFC",
"nfcRead": "Leggi tag NFC",
"nfcHoldNear": "Avvicina il dispositivo al tag NFC...",
"nfcWriteSuccess": "Token scritto sul tag NFC",
"nfcWriteError": "Errore NFC scrittura: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "Errore NFC lettura: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC è disattivato. Attivalo nelle Impostazioni.",
"nfcUnsupported": "Questo dispositivo non supporta NFC",
"amountToDeposit": "Importo da depositare:",
"descriptionOptional": "Descrizione (opzionale):",
@@ -213,6 +243,8 @@
"pendingStatus": "In attesa",
"receivedStatus": "Ricevuto",
"sentStatus": "Inviato",
"receiving": "In ricezione",
"sending": "In invio",
"now": "Adesso",
"agoMinutes": "{minutes} min fa",
"@agoMinutes": {
@@ -296,7 +328,7 @@
}
},
"close": "Chiudi",
"aboutDescription": "Un portafoglio Cashu con DNA cubano per il mondo intero. Fratello di La Chispa.",
"aboutDescription": "Un portafoglio Cashu con DNA cubano per il mondo intero. Fratello di LaChispa.",
"couldNotOpenLink": "Impossibile aprire il link",
"deleteWalletQuestion": "Eliminare portafoglio?",
@@ -488,7 +520,22 @@
"scanLightningInvoiceHint": "Scansiona una fattura Lightning (lnbc...)",
"addMintQuestion": "Aggiungere questo mint?",
"cameraPermissionDenied": "Permesso fotocamera negato",
"paymentRequestNotSupported": "Le richieste di pagamento non sono ancora supportate",
"paymentRequestTitle": "Richiesta di pagamento",
"paymentRequestFrom": "Richiesta da",
"paymentRequestAmount": "Importo richiesto",
"paymentRequestDescription": "Descrizione",
"paymentRequestMints": "Mint accettati",
"paymentRequestAnyMint": "Qualsiasi mint",
"paymentRequestPay": "Paga",
"paymentRequestPaying": "Pagamento in corso...",
"paymentRequestSuccess": "Pagamento inviato con successo",
"paymentRequestNoTransport": "Questa richiesta non ha un metodo di consegna configurato",
"paymentRequestTransport": "Trasporto",
"paymentRequestMintNotAccepted": "Il tuo mint attivo non è nella lista dei mint accettati",
"paymentRequestUnitMismatch": "Unità incompatibile: la richiesta richiede {unit}",
"paymentRequestInsufficientBalance": "Saldo insufficiente",
"paymentRequestErrorParsing": "Errore nella lettura della richiesta di pagamento",
"p2pkTitle": "Chiavi P2PK",
"p2pkSettingsDescription": "Ricevi ecash bloccato",
"p2pkExperimental": "P2PK è sperimentale. Usare con cautela.",
@@ -520,5 +567,37 @@
"p2pkErrorInvalidNsec": "nsec non valido",
"p2pkErrorKeyAlreadyExists": "Questa chiave esiste già",
"p2pkErrorKeyNotFound": "Chiave non trovata",
"p2pkErrorCannotDeletePrimary": "Impossibile eliminare la chiave principale"
"p2pkErrorCannotDeletePrimary": "Impossibile eliminare la chiave principale",
"request": "Richiedi",
"requestPayment": "Richiedi pagamento",
"requestPaymentDescription": "Genera richiesta di pagamento unificata",
"generateRequest": "Genera richiesta",
"generatingRequest": "Generazione in corso...",
"requestPaymentReceived": "Pagamento ricevuto",
"requestDescriptionHint": "Descrizione (opzionale)",
"universal": "Universale",
"copiedToClipboard": "Copiato negli appunti",
"swap": "Scambia",
"swapDescription": "Converti tra sats e USD",
"swapFrom": "Da",
"swapTo": "A",
"swapAction": "Scambia",
"swapEstimatedFee": "Commissione stimata",
"swapUseAll": "Usa tutto",
"swapMinimum": "Minimo: {amount}",
"swapProcessing": "Swap in corso...",
"swapSuccess": "Swap completato",
"swapErrorInsufficient": "Saldo insufficiente",
"swapErrorExpired": "Il preventivo è scaduto",
"swapErrorGeneric": "Errore swap: {error}",
"swapChartUnavailable": "Prezzo non disponibile · Tocca per riprovare",
"swapChartMinMax": "24h Min: {minPrice} — Max: {maxPrice}",
"privacyPolicy": "Informativa sulla privacy",
"privacyTitle": "NON RACCOGLIAMO NULLA",
"privacyGoodbye": "CIAO",
"privacyKeepReading": "(continua a leggere se vuoi…)",
"privacyBody": "Non sappiamo chi sei\nNon sappiamo quanto hai\nNon sappiamo cosa fai",
"privacyConclusion": "Il modo migliore per proteggere i tuoi dati\nè non averli"
}
+84 -5
View File
@@ -63,8 +63,8 @@
"homeTitle": "ホーム",
"receive": "受取",
"send": "送金",
"sendAction": "送金",
"receiveAction": "受取",
"sendAction": "送金",
"receiveAction": "受取",
"deposit": "入金",
"withdraw": "出金",
"lightning": "Lightning",
@@ -86,6 +86,21 @@
"receiveCashu": "Cashuを受け取る",
"pasteTheCashuToken": "Cashuトークンを貼り付け:",
"pasteFromClipboard": "クリップボードから貼り付け",
"emptyClipboard": "クリップボードは空です",
"cancelSend": "送金をキャンセル",
"cancelSendConfirmTitle": "この送金をキャンセルしますか?",
"cancelSendConfirmBody": "受取人がまだトークンを引き換えていない場合のみ機能します。既に引き換え済みの場合、送金は完了として記録されます。",
"cancelSendSuccess": "送金をキャンセルしました。{amount} {unit}を回復しました",
"@cancelSendSuccess": {
"placeholders": {
"amount": {"type": "String"},
"unit": {"type": "String"}
}
},
"cancelSendAlreadyClaimed": "受取人は既にトークンを引き換えました",
"pendingOfflineSend": "保留中のオフライン送金",
"pendingOfflineSendsHeader": "未請求のオフライン送金",
"pendingSendDetailTitle": "保留中の送金",
"validToken": "有効なトークン",
"invalidToken": "無効または不正なトークン",
"amount": "金額:",
@@ -118,6 +133,7 @@
"confirm": "確認",
"cancel": "キャンセル",
"insufficientBalance": "残高不足",
"feeExceedsAmount": "手数料が送金額を超えています",
"tokenCreationError": "トークン作成エラー:{error}",
"@tokenCreationError": {
"placeholders": {
@@ -137,6 +153,20 @@
},
"keepTokenWarning": "受取人が請求するまでこのトークンを保管してください。紛失すると資金を失います。",
"tokenCopiedToClipboard": "トークンをクリップボードにコピーしました",
"copyAsEmoji": "絵文字としてコピー",
"emojiCopiedToClipboard": "トークンを絵文字としてコピーしました 🥜",
"peanutDecodeError": "絵文字トークンをデコードできませんでした。破損している可能性があります。",
"nfcWrite": "NFCタグに書き込む",
"nfcRead": "NFCタグを読み取る",
"nfcHoldNear": "デバイスをNFCタグに近づけてください...",
"nfcWriteSuccess": "トークンをNFCタグに書き込みました",
"nfcWriteError": "NFC書き込みエラー: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "NFC読み取りエラー: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFCが無効です。設定で有効にしてください。",
"nfcUnsupported": "このデバイスはNFCに対応していません",
"amountToDeposit": "入金額:",
"descriptionOptional": "説明(任意):",
@@ -213,6 +243,8 @@
"pendingStatus": "保留中",
"receivedStatus": "受取済み",
"sentStatus": "送金済み",
"receiving": "受取中",
"sending": "送金中",
"now": "たった今",
"agoMinutes": "{minutes}分前",
"@agoMinutes": {
@@ -296,7 +328,7 @@
}
},
"close": "閉じる",
"aboutDescription": "キューバのDNAを持つ世界のためのCashuウォレット。La Chispaの兄弟。",
"aboutDescription": "キューバのDNAを持つ世界のためのCashuウォレット。LaChispaの兄弟。",
"couldNotOpenLink": "リンクを開けませんでした",
"deleteWalletQuestion": "ウォレットを削除しますか?",
@@ -488,7 +520,22 @@
"scanLightningInvoiceHint": "Lightningインボイスをスキャン(lnbc...",
"addMintQuestion": "このMintを追加しますか?",
"cameraPermissionDenied": "カメラの許可が拒否されました",
"paymentRequestNotSupported": "支払いリクエストはまだサポートされていません",
"paymentRequestTitle": "支払いリクエスト",
"paymentRequestFrom": "リクエスト元",
"paymentRequestAmount": "リクエスト金額",
"paymentRequestDescription": "説明",
"paymentRequestMints": "対応ミント",
"paymentRequestAnyMint": "すべてのミント",
"paymentRequestPay": "支払う",
"paymentRequestPaying": "支払い中...",
"paymentRequestSuccess": "支払いが正常に送信されました",
"paymentRequestNoTransport": "このリクエストには配信方法が設定されていません",
"paymentRequestTransport": "トランスポート",
"paymentRequestMintNotAccepted": "アクティブなミントは対応ミントリストにありません",
"paymentRequestUnitMismatch": "単位が互換性がありません:リクエストには{unit}が必要です",
"paymentRequestInsufficientBalance": "残高不足",
"paymentRequestErrorParsing": "支払いリクエストの読み取りエラー",
"p2pkTitle": "P2PK鍵",
"p2pkSettingsDescription": "ロックされたecashを受け取る",
"p2pkExperimental": "P2PKは実験的機能です。注意してご使用ください。",
@@ -520,5 +567,37 @@
"p2pkErrorInvalidNsec": "無効なnsec",
"p2pkErrorKeyAlreadyExists": "この鍵は既に存在します",
"p2pkErrorKeyNotFound": "鍵が見つかりません",
"p2pkErrorCannotDeletePrimary": "プライマリ鍵は削除できません"
"p2pkErrorCannotDeletePrimary": "プライマリ鍵は削除できません",
"request": "リクエスト",
"requestPayment": "支払いをリクエスト",
"requestPaymentDescription": "統合支払いリクエストを生成",
"generateRequest": "リクエストを生成",
"generatingRequest": "生成中...",
"requestPaymentReceived": "支払いを受け取りました",
"requestDescriptionHint": "説明(任意)",
"universal": "ユニバーサル",
"copiedToClipboard": "クリップボードにコピーしました",
"swap": "交換",
"swapDescription": "SatsとUSDを変換",
"swapFrom": "送信元",
"swapTo": "送信先",
"swapAction": "交換",
"swapEstimatedFee": "推定手数料",
"swapUseAll": "全額使用",
"swapMinimum": "最小: {amount}",
"swapProcessing": "スワップ処理中...",
"swapSuccess": "スワップ完了",
"swapErrorInsufficient": "残高不足",
"swapErrorExpired": "見積もりの有効期限切れ",
"swapErrorGeneric": "スワップエラー: {error}",
"swapChartUnavailable": "価格を取得できません・タップで再試行",
"swapChartMinMax": "24h 安値: {minPrice} — 高値: {maxPrice}",
"privacyPolicy": "プライバシーポリシー",
"privacyTitle": "何も収集しません",
"privacyGoodbye": "さようなら",
"privacyKeepReading": "(読み続けたければどうぞ…)",
"privacyBody": "あなたが誰か知りません\nいくら持っているか知りません\n何をしているか知りません",
"privacyConclusion": "データを守る最善の方法は\nデータを持たないことです"
}
+84 -5
View File
@@ -63,8 +63,8 @@
"homeTitle": "홈",
"receive": "받기",
"send": "보내기",
"sendAction": "보내기",
"receiveAction": "받기",
"sendAction": "보내기",
"receiveAction": "받기",
"deposit": "입금",
"withdraw": "출금",
"lightning": "Lightning",
@@ -86,6 +86,21 @@
"receiveCashu": "Cashu 받기",
"pasteTheCashuToken": "Cashu 토큰을 붙여넣으세요:",
"pasteFromClipboard": "클립보드에서 붙여넣기",
"emptyClipboard": "클립보드가 비어 있음",
"cancelSend": "전송 취소",
"cancelSendConfirmTitle": "이 전송을 취소하시겠습니까?",
"cancelSendConfirmBody": "수신자가 아직 토큰을 받지 않은 경우에만 작동합니다. 이미 받았다면 전송이 완료로 표시됩니다.",
"cancelSendSuccess": "전송이 취소되었습니다. {amount} {unit}를 복구했습니다",
"@cancelSendSuccess": {
"placeholders": {
"amount": {"type": "String"},
"unit": {"type": "String"}
}
},
"cancelSendAlreadyClaimed": "수신자가 이미 토큰을 받았습니다",
"pendingOfflineSend": "대기 중인 오프라인 전송",
"pendingOfflineSendsHeader": "청구되지 않은 오프라인 전송",
"pendingSendDetailTitle": "대기 중인 전송",
"validToken": "유효한 토큰",
"invalidToken": "유효하지 않거나 손상된 토큰",
"amount": "금액:",
@@ -118,6 +133,7 @@
"confirm": "확인",
"cancel": "취소",
"insufficientBalance": "잔액 부족",
"feeExceedsAmount": "수수료가 송금액을 초과합니다",
"tokenCreationError": "토큰 생성 오류: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -137,6 +153,20 @@
},
"keepTokenWarning": "수신자가 청구할 때까지 이 토큰을 보관하세요. 분실하면 자금을 잃게 됩니다.",
"tokenCopiedToClipboard": "토큰이 클립보드에 복사되었습니다",
"copyAsEmoji": "이모지로 복사",
"emojiCopiedToClipboard": "토큰이 이모지로 복사되었습니다 🥜",
"peanutDecodeError": "이모지 토큰을 디코딩할 수 없습니다. 손상되었을 수 있습니다.",
"nfcWrite": "NFC 태그에 쓰기",
"nfcRead": "NFC 태그 읽기",
"nfcHoldNear": "기기를 NFC 태그에 가까이 대세요...",
"nfcWriteSuccess": "토큰이 NFC 태그에 기록되었습니다",
"nfcWriteError": "NFC 쓰기 오류: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "NFC 읽기 오류: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC가 비활성화되어 있습니다. 설정에서 활성화하세요.",
"nfcUnsupported": "이 기기는 NFC를 지원하지 않습니다",
"amountToDeposit": "입금할 금액:",
"descriptionOptional": "설명 (선택사항):",
@@ -213,6 +243,8 @@
"pendingStatus": "대기 중",
"receivedStatus": "받음",
"sentStatus": "보냄",
"receiving": "받는 중",
"sending": "보내는 중",
"now": "방금",
"agoMinutes": "{minutes}분 전",
"@agoMinutes": {
@@ -296,7 +328,7 @@
}
},
"close": "닫기",
"aboutDescription": "전 세계를 위한 쿠바 DNA를 가진 Cashu 지갑. La Chispa의 형제.",
"aboutDescription": "전 세계를 위한 쿠바 DNA를 가진 Cashu 지갑. LaChispa의 형제.",
"couldNotOpenLink": "링크를 열 수 없음",
"deleteWalletQuestion": "지갑을 삭제하시겠습니까?",
@@ -488,7 +520,22 @@
"scanLightningInvoiceHint": "Lightning 인보이스 스캔 (lnbc...)",
"addMintQuestion": "이 mint를 추가하시겠습니까?",
"cameraPermissionDenied": "카메라 권한이 거부되었습니다",
"paymentRequestNotSupported": "결제 요청은 아직 지원되지 않습니다",
"paymentRequestTitle": "결제 요청",
"paymentRequestFrom": "요청자",
"paymentRequestAmount": "요청 금액",
"paymentRequestDescription": "설명",
"paymentRequestMints": "허용된 민트",
"paymentRequestAnyMint": "모든 민트",
"paymentRequestPay": "결제",
"paymentRequestPaying": "결제 중...",
"paymentRequestSuccess": "결제가 성공적으로 전송되었습니다",
"paymentRequestNoTransport": "이 요청에는 전달 방법이 설정되지 않았습니다",
"paymentRequestTransport": "전송 방식",
"paymentRequestMintNotAccepted": "활성 민트가 허용된 민트 목록에 없습니다",
"paymentRequestUnitMismatch": "호환되지 않는 단위: 요청에 {unit}이(가) 필요합니다",
"paymentRequestInsufficientBalance": "잔액 부족",
"paymentRequestErrorParsing": "결제 요청을 읽는 중 오류 발생",
"p2pkTitle": "P2PK 키",
"p2pkSettingsDescription": "잠긴 ecash 받기",
"p2pkExperimental": "P2PK는 실험적 기능입니다. 주의하여 사용하세요.",
@@ -520,5 +567,37 @@
"p2pkErrorInvalidNsec": "유효하지 않은 nsec",
"p2pkErrorKeyAlreadyExists": "이 키는 이미 존재합니다",
"p2pkErrorKeyNotFound": "키를 찾을 수 없습니다",
"p2pkErrorCannotDeletePrimary": "기본 키는 삭제할 수 없습니다"
"p2pkErrorCannotDeletePrimary": "기본 키는 삭제할 수 없습니다",
"request": "요청",
"requestPayment": "결제 요청",
"requestPaymentDescription": "통합 결제 요청 생성",
"generateRequest": "요청 생성",
"generatingRequest": "생성 중...",
"requestPaymentReceived": "결제가 수신되었습니다",
"requestDescriptionHint": "설명 (선택사항)",
"universal": "유니버설",
"copiedToClipboard": "클립보드에 복사되었습니다",
"swap": "교환",
"swapDescription": "Sats와 USD 간 변환",
"swapFrom": "에서",
"swapTo": "으로",
"swapAction": "교환",
"swapEstimatedFee": "예상 수수료",
"swapUseAll": "전액 사용",
"swapMinimum": "최소: {amount}",
"swapProcessing": "교환 처리 중...",
"swapSuccess": "교환 완료",
"swapErrorInsufficient": "잔액 부족",
"swapErrorExpired": "견적이 만료되었습니다",
"swapErrorGeneric": "교환 오류: {error}",
"swapChartUnavailable": "가격 불러오기 실패 · 탭하여 재시도",
"swapChartMinMax": "24h 최저: {minPrice} — 최고: {maxPrice}",
"privacyPolicy": "개인정보 처리방침",
"privacyTitle": "우리는 아무것도 수집하지 않습니다",
"privacyGoodbye": "안녕",
"privacyKeepReading": "(계속 읽고 싶으면…)",
"privacyBody": "당신이 누구인지 모릅니다\n얼마를 가지고 있는지 모릅니다\n무엇을 하는지 모릅니다",
"privacyConclusion": "데이터를 보호하는 가장 좋은 방법은\n데이터를 갖지 않는 것입니다"
}
+454 -29
View File
@@ -71,7 +71,8 @@ import 'app_localizations_zh.dart';
/// be consistent with the languages listed in the L10n.supportedLocales
/// property.
abstract class L10n {
L10n(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
L10n(String locale)
: localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
@@ -91,12 +92,13 @@ abstract class L10n {
/// Additional delegates can be added by appending to this list in
/// MaterialApp. This list does not have to be used at all if a custom list
/// of delegates is preferred or required.
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates = <LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
static const List<LocalizationsDelegate<dynamic>> localizationsDelegates =
<LocalizationsDelegate<dynamic>>[
delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
];
/// A list of this localizations delegate's supported locales.
static const List<Locale> supportedLocales = <Locale>[
@@ -110,7 +112,7 @@ abstract class L10n {
Locale('pt'),
Locale('ru'),
Locale('sw'),
Locale('zh')
Locale('zh'),
];
/// No description provided for @appName.
@@ -386,13 +388,13 @@ abstract class L10n {
/// No description provided for @sendAction.
///
/// In es, this message translates to:
/// **'Enviar'**
/// **'Enviar'**
String get sendAction;
/// No description provided for @receiveAction.
///
/// In es, this message translates to:
/// **'Recibir'**
/// **'Recibir'**
String get receiveAction;
/// No description provided for @deposit.
@@ -503,6 +505,60 @@ abstract class L10n {
/// **'Pegar del portapapeles'**
String get pasteFromClipboard;
/// No description provided for @emptyClipboard.
///
/// In es, this message translates to:
/// **'Portapapeles vacío'**
String get emptyClipboard;
/// No description provided for @cancelSend.
///
/// In es, this message translates to:
/// **'Cancelar envío'**
String get cancelSend;
/// No description provided for @cancelSendConfirmTitle.
///
/// In es, this message translates to:
/// **'¿Cancelar envío?'**
String get cancelSendConfirmTitle;
/// No description provided for @cancelSendConfirmBody.
///
/// In es, this message translates to:
/// **'Solo funciona si el destinatario aún no reclamó el token. Si ya lo hizo, el envío se marcará como completado.'**
String get cancelSendConfirmBody;
/// No description provided for @cancelSendSuccess.
///
/// In es, this message translates to:
/// **'Envío cancelado. Recuperaste {amount} {unit}'**
String cancelSendSuccess(String amount, String unit);
/// No description provided for @cancelSendAlreadyClaimed.
///
/// In es, this message translates to:
/// **'El destinatario ya reclamó el token'**
String get cancelSendAlreadyClaimed;
/// No description provided for @pendingOfflineSend.
///
/// In es, this message translates to:
/// **'Envío offline pendiente'**
String get pendingOfflineSend;
/// No description provided for @pendingOfflineSendsHeader.
///
/// In es, this message translates to:
/// **'Envíos offline no reclamados'**
String get pendingOfflineSendsHeader;
/// No description provided for @pendingSendDetailTitle.
///
/// In es, this message translates to:
/// **'Envío pendiente'**
String get pendingSendDetailTitle;
/// No description provided for @validToken.
///
/// In es, this message translates to:
@@ -659,6 +715,12 @@ abstract class L10n {
/// **'Balance insuficiente'**
String get insufficientBalance;
/// No description provided for @feeExceedsAmount.
///
/// In es, this message translates to:
/// **'La comisión supera el monto a enviar'**
String get feeExceedsAmount;
/// No description provided for @tokenCreationError.
///
/// In es, this message translates to:
@@ -707,6 +769,72 @@ abstract class L10n {
/// **'Token copiado al portapapeles'**
String get tokenCopiedToClipboard;
/// No description provided for @copyAsEmoji.
///
/// In es, this message translates to:
/// **'Copiar como emoji'**
String get copyAsEmoji;
/// No description provided for @emojiCopiedToClipboard.
///
/// In es, this message translates to:
/// **'Token copiado como emoji 🥜'**
String get emojiCopiedToClipboard;
/// No description provided for @peanutDecodeError.
///
/// In es, this message translates to:
/// **'No se pudo decodificar el token emoji. Puede estar corrupto.'**
String get peanutDecodeError;
/// No description provided for @nfcWrite.
///
/// In es, this message translates to:
/// **'Escribir en tag NFC'**
String get nfcWrite;
/// No description provided for @nfcRead.
///
/// In es, this message translates to:
/// **'Leer tag NFC'**
String get nfcRead;
/// No description provided for @nfcHoldNear.
///
/// In es, this message translates to:
/// **'Acerca el dispositivo al tag NFC...'**
String get nfcHoldNear;
/// No description provided for @nfcWriteSuccess.
///
/// In es, this message translates to:
/// **'Token escrito en tag NFC'**
String get nfcWriteSuccess;
/// No description provided for @nfcWriteError.
///
/// In es, this message translates to:
/// **'Error NFC al escribir: {error}'**
String nfcWriteError(String error);
/// No description provided for @nfcReadError.
///
/// In es, this message translates to:
/// **'Error NFC al leer: {error}'**
String nfcReadError(String error);
/// No description provided for @nfcDisabled.
///
/// In es, this message translates to:
/// **'NFC está desactivado. Actívalo en Ajustes.'**
String get nfcDisabled;
/// No description provided for @nfcUnsupported.
///
/// In es, this message translates to:
/// **'Este dispositivo no soporta NFC'**
String get nfcUnsupported;
/// No description provided for @amountToDeposit.
///
/// In es, this message translates to:
@@ -1007,6 +1135,18 @@ abstract class L10n {
/// **'Enviado'**
String get sentStatus;
/// No description provided for @receiving.
///
/// In es, this message translates to:
/// **'Recibiendo'**
String get receiving;
/// No description provided for @sending.
///
/// In es, this message translates to:
/// **'Enviando'**
String get sending;
/// No description provided for @now.
///
/// In es, this message translates to:
@@ -1376,7 +1516,7 @@ abstract class L10n {
/// No description provided for @aboutDescription.
///
/// In es, this message translates to:
/// **'Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de La Chispa.'**
/// **'Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de LaChispa.'**
String get aboutDescription;
/// No description provided for @couldNotOpenLink.
@@ -1949,11 +2089,95 @@ abstract class L10n {
/// **'Permiso de cámara denegado'**
String get cameraPermissionDenied;
/// No description provided for @paymentRequestNotSupported.
/// No description provided for @paymentRequestTitle.
///
/// In es, this message translates to:
/// **'Los payment requests aún no están soportados'**
String get paymentRequestNotSupported;
/// **'Solicitud de pago'**
String get paymentRequestTitle;
/// No description provided for @paymentRequestFrom.
///
/// In es, this message translates to:
/// **'Solicitud de'**
String get paymentRequestFrom;
/// No description provided for @paymentRequestAmount.
///
/// In es, this message translates to:
/// **'Monto solicitado'**
String get paymentRequestAmount;
/// No description provided for @paymentRequestDescription.
///
/// In es, this message translates to:
/// **'Descripción'**
String get paymentRequestDescription;
/// No description provided for @paymentRequestMints.
///
/// In es, this message translates to:
/// **'Mints aceptados'**
String get paymentRequestMints;
/// No description provided for @paymentRequestAnyMint.
///
/// In es, this message translates to:
/// **'Cualquier mint'**
String get paymentRequestAnyMint;
/// No description provided for @paymentRequestPay.
///
/// In es, this message translates to:
/// **'Pagar'**
String get paymentRequestPay;
/// No description provided for @paymentRequestPaying.
///
/// In es, this message translates to:
/// **'Pagando...'**
String get paymentRequestPaying;
/// No description provided for @paymentRequestSuccess.
///
/// In es, this message translates to:
/// **'Pago enviado correctamente'**
String get paymentRequestSuccess;
/// No description provided for @paymentRequestNoTransport.
///
/// In es, this message translates to:
/// **'Esta solicitud no tiene método de entrega configurado'**
String get paymentRequestNoTransport;
/// No description provided for @paymentRequestTransport.
///
/// In es, this message translates to:
/// **'Transporte'**
String get paymentRequestTransport;
/// No description provided for @paymentRequestMintNotAccepted.
///
/// In es, this message translates to:
/// **'Tu mint activo no está en la lista de mints aceptados'**
String get paymentRequestMintNotAccepted;
/// No description provided for @paymentRequestUnitMismatch.
///
/// In es, this message translates to:
/// **'Unidad incompatible: la solicitud requiere {unit}'**
String paymentRequestUnitMismatch(String unit);
/// No description provided for @paymentRequestInsufficientBalance.
///
/// In es, this message translates to:
/// **'Balance insuficiente'**
String get paymentRequestInsufficientBalance;
/// No description provided for @paymentRequestErrorParsing.
///
/// In es, this message translates to:
/// **'Error al leer la solicitud de pago'**
String get paymentRequestErrorParsing;
/// No description provided for @p2pkTitle.
///
@@ -2146,6 +2370,186 @@ abstract class L10n {
/// In es, this message translates to:
/// **'No se puede eliminar la clave principal'**
String get p2pkErrorCannotDeletePrimary;
/// No description provided for @request.
///
/// In es, this message translates to:
/// **'Solicitar'**
String get request;
/// No description provided for @requestPayment.
///
/// In es, this message translates to:
/// **'Solicitar pago'**
String get requestPayment;
/// No description provided for @requestPaymentDescription.
///
/// In es, this message translates to:
/// **'Generar solicitud de pago unificada'**
String get requestPaymentDescription;
/// No description provided for @generateRequest.
///
/// In es, this message translates to:
/// **'Generar solicitud'**
String get generateRequest;
/// No description provided for @generatingRequest.
///
/// In es, this message translates to:
/// **'Generando solicitud...'**
String get generatingRequest;
/// No description provided for @requestPaymentReceived.
///
/// In es, this message translates to:
/// **'Pago recibido'**
String get requestPaymentReceived;
/// No description provided for @requestDescriptionHint.
///
/// In es, this message translates to:
/// **'Descripción (opcional)'**
String get requestDescriptionHint;
/// No description provided for @universal.
///
/// In es, this message translates to:
/// **'Universal'**
String get universal;
/// No description provided for @copiedToClipboard.
///
/// In es, this message translates to:
/// **'Copiado al portapapeles'**
String get copiedToClipboard;
/// No description provided for @swap.
///
/// In es, this message translates to:
/// **'Cambiar'**
String get swap;
/// No description provided for @swapDescription.
///
/// In es, this message translates to:
/// **'Convertir entre sats y USD'**
String get swapDescription;
/// No description provided for @swapFrom.
///
/// In es, this message translates to:
/// **'De'**
String get swapFrom;
/// No description provided for @swapTo.
///
/// In es, this message translates to:
/// **'A'**
String get swapTo;
/// No description provided for @swapAction.
///
/// In es, this message translates to:
/// **'Cambiar'**
String get swapAction;
/// No description provided for @swapEstimatedFee.
///
/// In es, this message translates to:
/// **'Fee estimado'**
String get swapEstimatedFee;
/// No description provided for @swapUseAll.
///
/// In es, this message translates to:
/// **'Usar todo'**
String get swapUseAll;
/// No description provided for @swapMinimum.
///
/// In es, this message translates to:
/// **'Mínimo: {amount}'**
String swapMinimum(String amount);
/// No description provided for @swapProcessing.
///
/// In es, this message translates to:
/// **'Procesando swap...'**
String get swapProcessing;
/// No description provided for @swapSuccess.
///
/// In es, this message translates to:
/// **'Swap completado'**
String get swapSuccess;
/// No description provided for @swapErrorInsufficient.
///
/// In es, this message translates to:
/// **'Saldo insuficiente'**
String get swapErrorInsufficient;
/// No description provided for @swapErrorExpired.
///
/// In es, this message translates to:
/// **'La cotización ha expirado'**
String get swapErrorExpired;
/// No description provided for @swapErrorGeneric.
///
/// In es, this message translates to:
/// **'Error en el swap: {error}'**
String swapErrorGeneric(String error);
/// No description provided for @swapChartUnavailable.
///
/// In es, this message translates to:
/// **'Precio no disponible · Toca para reintentar'**
String get swapChartUnavailable;
/// No description provided for @swapChartMinMax.
///
/// In es, this message translates to:
/// **'24h Mín: {minPrice} — Máx: {maxPrice}'**
String swapChartMinMax(String minPrice, String maxPrice);
/// No description provided for @privacyPolicy.
///
/// In es, this message translates to:
/// **'Política de privacidad'**
String get privacyPolicy;
/// No description provided for @privacyTitle.
///
/// In es, this message translates to:
/// **'NO RECOPILAMOS NADA'**
String get privacyTitle;
/// No description provided for @privacyGoodbye.
///
/// In es, this message translates to:
/// **'ADIÓS'**
String get privacyGoodbye;
/// No description provided for @privacyKeepReading.
///
/// In es, this message translates to:
/// **'(sigue leyendo si quieres…)'**
String get privacyKeepReading;
/// No description provided for @privacyBody.
///
/// In es, this message translates to:
/// **'No sabemos quién eres\nNo sabemos cuánto tienes\nNo sabemos qué haces'**
String get privacyBody;
/// No description provided for @privacyConclusion.
///
/// In es, this message translates to:
/// **'La mejor forma de proteger tus datos\nes no tenerlos'**
String get privacyConclusion;
}
class _L10nDelegate extends LocalizationsDelegate<L10n> {
@@ -2157,34 +2561,55 @@ class _L10nDelegate extends LocalizationsDelegate<L10n> {
}
@override
bool isSupported(Locale locale) => <String>['de', 'en', 'es', 'fr', 'it', 'ja', 'ko', 'pt', 'ru', 'sw', 'zh'].contains(locale.languageCode);
bool isSupported(Locale locale) => <String>[
'de',
'en',
'es',
'fr',
'it',
'ja',
'ko',
'pt',
'ru',
'sw',
'zh',
].contains(locale.languageCode);
@override
bool shouldReload(_L10nDelegate old) => false;
}
L10n lookupL10n(Locale locale) {
// Lookup logic when only language code is specified.
switch (locale.languageCode) {
case 'de': return L10nDe();
case 'en': return L10nEn();
case 'es': return L10nEs();
case 'fr': return L10nFr();
case 'it': return L10nIt();
case 'ja': return L10nJa();
case 'ko': return L10nKo();
case 'pt': return L10nPt();
case 'ru': return L10nRu();
case 'sw': return L10nSw();
case 'zh': return L10nZh();
case 'de':
return L10nDe();
case 'en':
return L10nEn();
case 'es':
return L10nEs();
case 'fr':
return L10nFr();
case 'it':
return L10nIt();
case 'ja':
return L10nJa();
case 'ko':
return L10nKo();
case 'pt':
return L10nPt();
case 'ru':
return L10nRu();
case 'sw':
return L10nSw();
case 'zh':
return L10nZh();
}
throw FlutterError(
'L10n.delegate failed to load unsupported locale "$locale". This is likely '
'an issue with the localizations generation tool. Please file an issue '
'on GitHub with a reproducible sample app and the gen-l10n configuration '
'that was used.'
'that was used.',
);
}
+297 -37
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nDe extends L10n {
String get generatingSeed => 'Sichere Generierung deiner Seed-Phrase';
@override
String get createWalletDescription => 'Eine 12-Wort Seed-Phrase wird generiert.\nBewahre sie an einem sicheren Ort auf.';
String get createWalletDescription =>
'Eine 12-Wort Seed-Phrase wird generiert.\nBewahre sie an einem sicheren Ort auf.';
@override
String get generateWallet => 'Wallet generieren';
@@ -69,10 +70,12 @@ class L10nDe extends L10n {
String get walletCreated => 'Wallet erstellt!';
@override
String get walletCreatedDescription => 'Deine Wallet ist bereit. Wir empfehlen, jetzt ein Backup deiner Seed-Phrase zu erstellen.';
String get walletCreatedDescription =>
'Deine Wallet ist bereit. Wir empfehlen, jetzt ein Backup deiner Seed-Phrase zu erstellen.';
@override
String get backupWarning => 'Ohne Backup verlierst du den Zugang zu deinen Mitteln, wenn du das Gerät verlierst.';
String get backupWarning =>
'Ohne Backup verlierst du den Zugang zu deinen Mitteln, wenn du das Gerät verlierst.';
@override
String get backupNow => 'Jetzt sichern';
@@ -87,13 +90,15 @@ class L10nDe extends L10n {
String get seedPhraseTitle => 'Deine Seed-Phrase';
@override
String get seedPhraseDescription => 'Speichere diese 12 Wörter in der richtigen Reihenfolge. Sie sind der einzige Weg, deine Wallet wiederherzustellen.';
String get seedPhraseDescription =>
'Speichere diese 12 Wörter in der richtigen Reihenfolge. Sie sind der einzige Weg, deine Wallet wiederherzustellen.';
@override
String get revealSeedPhrase => 'Seed-Phrase anzeigen';
@override
String get tapToReveal => 'Tippe auf den Button, um\ndeine Seed-Phrase anzuzeigen';
String get tapToReveal =>
'Tippe auf den Button, um\ndeine Seed-Phrase anzuzeigen';
@override
String get copyToClipboard => 'In Zwischenablage kopieren';
@@ -105,7 +110,8 @@ class L10nDe extends L10n {
String get neverShareSeed => 'Teile deine Seed-Phrase niemals mit anderen.';
@override
String get confirmBackup => 'Ich habe meine Seed-Phrase an einem sicheren Ort gespeichert';
String get confirmBackup =>
'Ich habe meine Seed-Phrase an einem sicheren Ort gespeichert';
@override
String get continue_ => 'Weiter';
@@ -117,7 +123,8 @@ class L10nDe extends L10n {
String get enterSeedPhrase => 'Gib deine Seed-Phrase ein';
@override
String get enterSeedDescription => 'Gib die 12 oder 24 Wörter durch Leerzeichen getrennt ein.';
String get enterSeedDescription =>
'Gib die 12 oder 24 Wörter durch Leerzeichen getrennt ein.';
@override
String get seedPlaceholder => 'wort1 wort2 wort3 ...';
@@ -131,7 +138,8 @@ class L10nDe extends L10n {
String get needWords => '(du brauchst 12 oder 24)';
@override
String get restoreScanningMint => 'Mint wird nach vorhandenen Token durchsucht...';
String get restoreScanningMint =>
'Mint wird nach vorhandenen Token durchsucht...';
@override
String restoreError(String error) {
@@ -148,10 +156,10 @@ class L10nDe extends L10n {
String get send => 'Senden';
@override
String get sendAction => 'Senden ↗';
String get sendAction => 'Senden';
@override
String get receiveAction => '↘ Empfangen';
String get receiveAction => 'Empfangen';
@override
String get deposit => 'Einzahlen';
@@ -207,6 +215,38 @@ class L10nDe extends L10n {
@override
String get pasteFromClipboard => 'Aus Zwischenablage einfügen';
@override
String get emptyClipboard => 'Zwischenablage leer';
@override
String get cancelSend => 'Senden abbrechen';
@override
String get cancelSendConfirmTitle => 'Diesen Versand abbrechen?';
@override
String get cancelSendConfirmBody =>
'Funktioniert nur, wenn der Empfänger das Token noch nicht eingelöst hat. Falls doch, wird der Versand als abgeschlossen markiert.';
@override
String cancelSendSuccess(String amount, String unit) {
return 'Versand abgebrochen. $amount $unit zurückerhalten';
}
@override
String get cancelSendAlreadyClaimed =>
'Der Empfänger hat das Token bereits eingelöst';
@override
String get pendingOfflineSend => 'Ausstehender Offline-Versand';
@override
String get pendingOfflineSendsHeader =>
'Nicht eingelöste Offline-Versendungen';
@override
String get pendingSendDetailTitle => 'Ausstehender Versand';
@override
String get validToken => 'Token gültig';
@@ -287,6 +327,9 @@ class L10nDe extends L10n {
@override
String get insufficientBalance => 'Unzureichendes Guthaben';
@override
String get feeExceedsAmount => 'Die Gebühr übersteigt den Sendebetrag';
@override
String tokenCreationError(String error) {
return 'Fehler beim Erstellen des Tokens: $error';
@@ -310,11 +353,51 @@ class L10nDe extends L10n {
}
@override
String get keepTokenWarning => 'Bewahre diesen Token auf, bis der Empfänger ihn einlöst. Wenn du ihn verlierst, verlierst du die Mittel.';
String get keepTokenWarning =>
'Bewahre diesen Token auf, bis der Empfänger ihn einlöst. Wenn du ihn verlierst, verlierst du die Mittel.';
@override
String get tokenCopiedToClipboard => 'Token in Zwischenablage kopiert';
@override
String get copyAsEmoji => 'Als Emoji kopieren';
@override
String get emojiCopiedToClipboard => 'Token als Emoji kopiert 🥜';
@override
String get peanutDecodeError =>
'Emoji-Token konnte nicht dekodiert werden. Möglicherweise beschädigt.';
@override
String get nfcWrite => 'Auf NFC-Tag schreiben';
@override
String get nfcRead => 'NFC-Tag lesen';
@override
String get nfcHoldNear => 'Gerät an NFC-Tag halten...';
@override
String get nfcWriteSuccess => 'Token auf NFC-Tag geschrieben';
@override
String nfcWriteError(String error) {
return 'NFC-Schreibfehler: $error';
}
@override
String nfcReadError(String error) {
return 'NFC-Lesefehler: $error';
}
@override
String get nfcDisabled =>
'NFC ist deaktiviert. Aktiviere es in den Einstellungen.';
@override
String get nfcUnsupported => 'Dieses Gerät unterstützt kein NFC';
@override
String get amountToDeposit => 'Einzuzahlender Betrag:';
@@ -450,7 +533,8 @@ class L10nDe extends L10n {
String get noPendingTransactions => 'Keine ausstehenden Transaktionen';
@override
String get allTransactionsCompleted => 'Alle deine Transaktionen sind abgeschlossen';
String get allTransactionsCompleted =>
'Alle deine Transaktionen sind abgeschlossen';
@override
String get noEcashTransactions => 'Keine Ecash Transaktionen';
@@ -462,7 +546,8 @@ class L10nDe extends L10n {
String get noLightningTransactions => 'Keine Lightning Transaktionen';
@override
String get depositOrWithdrawLightning => 'Zahle ein oder hebe ab via Lightning';
String get depositOrWithdrawLightning =>
'Zahle ein oder hebe ab via Lightning';
@override
String get pendingStatus => 'Ausstehend';
@@ -473,6 +558,12 @@ class L10nDe extends L10n {
@override
String get sentStatus => 'Gesendet';
@override
String get receiving => 'Wird empfangen';
@override
String get sending => 'Wird gesendet';
@override
String get now => 'Jetzt';
@@ -665,7 +756,8 @@ class L10nDe extends L10n {
String get close => 'Schließen';
@override
String get aboutDescription => 'Eine Cashu Wallet mit kubanischer DNA für die ganze Welt. Bruder von La Chispa.';
String get aboutDescription =>
'Eine Cashu Wallet mit kubanischer DNA für die ganze Welt. Bruder von LaChispa.';
@override
String get couldNotOpenLink => 'Link konnte nicht geöffnet werden';
@@ -677,7 +769,8 @@ class L10nDe extends L10n {
String get actionIrreversible => 'Diese Aktion ist unwiderruflich';
@override
String get deleteWalletWarning => 'Alle Daten werden gelöscht, einschließlich deiner Seed-Phrase und Tokens. Stelle sicher, dass du ein Backup hast.';
String get deleteWalletWarning =>
'Alle Daten werden gelöscht, einschließlich deiner Seed-Phrase und Tokens. Stelle sicher, dass du ein Backup hast.';
@override
String get typeDeleteToConfirm => 'Gib \"LÖSCHEN\" zur Bestätigung ein:';
@@ -694,19 +787,22 @@ class L10nDe extends L10n {
String get recoverTokensTitle => 'Tokens wiederherstellen';
@override
String get recoverTokensDescription => 'Mints scannen, um Tokens wiederherzustellen, die mit deiner Seed-Phrase verknüpft sind (NUT-13)';
String get recoverTokensDescription =>
'Mints scannen, um Tokens wiederherzustellen, die mit deiner Seed-Phrase verknüpft sind (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Meine aktuelle Seed-Phrase verwenden';
@override
String get scanWithSavedWords => 'Mints mit den gespeicherten 12 Wörtern scannen';
String get scanWithSavedWords =>
'Mints mit den gespeicherten 12 Wörtern scannen';
@override
String get useOtherSeedPhrase => 'Andere Seed-Phrase verwenden';
@override
String get recoverFromOtherWords => 'Tokens von anderen 12 Wörtern wiederherstellen';
String get recoverFromOtherWords =>
'Tokens von anderen 12 Wörtern wiederherstellen';
@override
String get mintsToScan => 'Zu scannende Mints:';
@@ -720,7 +816,8 @@ class L10nDe extends L10n {
String get specificMint => 'Ein bestimmter Mint';
@override
String get enterMnemonicWords => 'Gib die 12 Wörter durch Leerzeichen getrennt ein...';
String get enterMnemonicWords =>
'Gib die 12 Wörter durch Leerzeichen getrennt ein...';
@override
String get scanMints => 'Mints scannen';
@@ -740,7 +837,8 @@ class L10nDe extends L10n {
}
@override
String get scanCompleteNoTokens => 'Scan abgeschlossen. Keine neuen Tokens gefunden.';
String get scanCompleteNoTokens =>
'Scan abgeschlossen. Keine neuen Tokens gefunden.';
@override
String mintsWithError(int count) {
@@ -763,7 +861,8 @@ class L10nDe extends L10n {
}
@override
String get noTokensForMnemonic => 'Keine Tokens mit diesem Mnemonic verknüpft gefunden.';
String get noTokensForMnemonic =>
'Keine Tokens mit diesem Mnemonic verknüpft gefunden.';
@override
String get noConnectedMints => 'Keine verbundenen Mints';
@@ -843,7 +942,8 @@ class L10nDe extends L10n {
String get deleteMintConfirmTitle => 'Mint löschen';
@override
String get deleteMintConfirmMessage => 'Wenn du Guthaben auf diesem Mint hast, geht es verloren. Bist du sicher?';
String get deleteMintConfirmMessage =>
'Wenn du Guthaben auf diesem Mint hast, geht es verloren. Bist du sicher?';
@override
String get delete => 'Löschen';
@@ -896,19 +996,24 @@ class L10nDe extends L10n {
String get tokenSavedForLater => 'Token zum späteren Einlösen gespeichert';
@override
String get noConnectionTokenSaved => 'Keine Verbindung. Token zum späteren Einlösen gespeichert.';
String get noConnectionTokenSaved =>
'Keine Verbindung. Token zum späteren Einlösen gespeichert.';
@override
String get unknownMintOffline => 'Dieser Token stammt von einem unbekannten Mint. Verbinde dich mit dem Internet, um ihn hinzuzufügen und den Token einzulösen.';
String get unknownMintOffline =>
'Dieser Token stammt von einem unbekannten Mint. Verbinde dich mit dem Internet, um ihn hinzuzufügen und den Token einzulösen.';
@override
String get noConnectionTryLater => 'Keine Verbindung zum Mint. Versuche es später erneut.';
String get noConnectionTryLater =>
'Keine Verbindung zum Mint. Versuche es später erneut.';
@override
String get saveTokenError => 'Fehler beim Speichern des Tokens. Bitte erneut versuchen.';
String get saveTokenError =>
'Fehler beim Speichern des Tokens. Bitte erneut versuchen.';
@override
String get pendingTokenLimitReached => 'Limit für ausstehende Tokens erreicht (max 50)';
String get pendingTokenLimitReached =>
'Limit für ausstehende Tokens erreicht (max 50)';
@override
String get filterToReceive => 'Zu empfangen';
@@ -985,10 +1090,12 @@ class L10nDe extends L10n {
String get unrecognizedQrCode => 'Nicht erkannter QR-Code';
@override
String get scanCashuTokenHint => 'Scanne einen Cashu Token (cashuA... oder cashuB...)';
String get scanCashuTokenHint =>
'Scanne einen Cashu Token (cashuA... oder cashuB...)';
@override
String get scanLightningInvoiceHint => 'Scanne eine Lightning Rechnung (lnbc...)';
String get scanLightningInvoiceHint =>
'Scanne eine Lightning Rechnung (lnbc...)';
@override
String get addMintQuestion => 'Diesen Mint hinzufügen?';
@@ -997,7 +1104,54 @@ class L10nDe extends L10n {
String get cameraPermissionDenied => 'Kamera-Berechtigung verweigert';
@override
String get paymentRequestNotSupported => 'Zahlungsanfragen werden noch nicht unterstützt';
String get paymentRequestTitle => 'Zahlungsanfrage';
@override
String get paymentRequestFrom => 'Anfrage von';
@override
String get paymentRequestAmount => 'Angeforderter Betrag';
@override
String get paymentRequestDescription => 'Beschreibung';
@override
String get paymentRequestMints => 'Akzeptierte Mints';
@override
String get paymentRequestAnyMint => 'Jeder Mint';
@override
String get paymentRequestPay => 'Bezahlen';
@override
String get paymentRequestPaying => 'Bezahle...';
@override
String get paymentRequestSuccess => 'Zahlung erfolgreich gesendet';
@override
String get paymentRequestNoTransport =>
'Diese Anfrage hat keine konfigurierte Zustellmethode';
@override
String get paymentRequestTransport => 'Transport';
@override
String get paymentRequestMintNotAccepted =>
'Dein aktiver Mint ist nicht in der Liste der akzeptierten Mints';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Inkompatible Einheit: Anfrage erfordert $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Unzureichendes Guthaben';
@override
String get paymentRequestErrorParsing =>
'Fehler beim Lesen der Zahlungsanfrage';
@override
String get p2pkTitle => 'P2PK-Schlüssel';
@@ -1006,10 +1160,12 @@ class L10nDe extends L10n {
String get p2pkSettingsDescription => 'Gesperrtes ecash empfangen';
@override
String get p2pkExperimental => 'P2PK ist experimentell. Mit Vorsicht verwenden.';
String get p2pkExperimental =>
'P2PK ist experimentell. Mit Vorsicht verwenden.';
@override
String get p2pkPendingSendWarning => 'Du hast einen ausstehenden P2PK-Versand. Gehe zum Verlauf und aktualisiere, nachdem der Empfänger den Token eingelöst hat.';
String get p2pkPendingSendWarning =>
'Du hast einen ausstehenden P2PK-Versand. Gehe zum Verlauf und aktualisiere, nachdem der Empfänger den Token eingelöst hat.';
@override
String get p2pkExperimentalShort => 'Experimentell';
@@ -1066,7 +1222,8 @@ class L10nDe extends L10n {
String get p2pkLockedToOther => 'Für anderen Schlüssel gesperrt';
@override
String get p2pkCannotUnlock => 'Du hast nicht den Schlüssel, um diesen Token zu entsperren';
String get p2pkCannotUnlock =>
'Du hast nicht den Schlüssel, um diesen Token zu entsperren';
@override
String get p2pkEnterPrivateKey => 'Privaten Schlüssel eingeben (nsec)';
@@ -1075,13 +1232,15 @@ class L10nDe extends L10n {
String get p2pkDeleteTitle => 'Schlüssel löschen';
@override
String get p2pkDeleteConfirm => 'Diesen Schlüssel löschen? Du kannst keine Token mehr empfangen, die daran gesperrt sind.';
String get p2pkDeleteConfirm =>
'Diesen Schlüssel löschen? Du kannst keine Token mehr empfangen, die daran gesperrt sind.';
@override
String get p2pkRequiresConnection => 'P2PK erfordert Verbindung zum Mint';
@override
String get p2pkErrorMaxKeysReached => 'Maximale Anzahl importierter Schlüssel erreicht (10)';
String get p2pkErrorMaxKeysReached =>
'Maximale Anzahl importierter Schlüssel erreicht (10)';
@override
String get p2pkErrorInvalidNsec => 'Ungültiger nsec';
@@ -1093,5 +1252,106 @@ class L10nDe extends L10n {
String get p2pkErrorKeyNotFound => 'Schlüssel nicht gefunden';
@override
String get p2pkErrorCannotDeletePrimary => 'Primärschlüssel kann nicht gelöscht werden';
String get p2pkErrorCannotDeletePrimary =>
'Primärschlüssel kann nicht gelöscht werden';
@override
String get request => 'Anfordern';
@override
String get requestPayment => 'Zahlung anfordern';
@override
String get requestPaymentDescription =>
'Einheitliche Zahlungsanforderung erstellen';
@override
String get generateRequest => 'Anforderung erstellen';
@override
String get generatingRequest => 'Anforderung wird erstellt...';
@override
String get requestPaymentReceived => 'Zahlung erhalten';
@override
String get requestDescriptionHint => 'Beschreibung (optional)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'In die Zwischenablage kopiert';
@override
String get swap => 'Tauschen';
@override
String get swapDescription => 'Zwischen Sats und USD wechseln';
@override
String get swapFrom => 'Von';
@override
String get swapTo => 'Nach';
@override
String get swapAction => 'Tauschen';
@override
String get swapEstimatedFee => 'Geschätzte Gebühr';
@override
String get swapUseAll => 'Alles verwenden';
@override
String swapMinimum(String amount) {
return 'Minimum: $amount';
}
@override
String get swapProcessing => 'Swap wird verarbeitet...';
@override
String get swapSuccess => 'Swap abgeschlossen';
@override
String get swapErrorInsufficient => 'Unzureichendes Guthaben';
@override
String get swapErrorExpired => 'Angebot abgelaufen';
@override
String swapErrorGeneric(String error) {
return 'Swap-Fehler: $error';
}
@override
String get swapChartUnavailable =>
'Preis nicht verfügbar · Tippen zum Wiederholen';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Min: $minPrice — Max: $maxPrice';
}
@override
String get privacyPolicy => 'Datenschutz';
@override
String get privacyTitle => 'WIR SAMMELN NICHTS';
@override
String get privacyGoodbye => 'TSCHÜSS';
@override
String get privacyKeepReading => '(lies weiter, wenn du willst…)';
@override
String get privacyBody =>
'Wir wissen nicht, wer du bist\nWir wissen nicht, wie viel du hast\nWir wissen nicht, was du tust';
@override
String get privacyConclusion =>
'Der beste Weg, deine Daten zu schützen,\nist sie nicht zu haben';
}
+260 -21
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nEn extends L10n {
String get generatingSeed => 'Generating your seed phrase securely';
@override
String get createWalletDescription => 'A 12-word seed phrase will be generated.\nStore it in a safe place.';
String get createWalletDescription =>
'A 12-word seed phrase will be generated.\nStore it in a safe place.';
@override
String get generateWallet => 'Generate wallet';
@@ -69,10 +70,12 @@ class L10nEn extends L10n {
String get walletCreated => 'Wallet created!';
@override
String get walletCreatedDescription => 'Your wallet is ready. We recommend backing up your seed phrase now.';
String get walletCreatedDescription =>
'Your wallet is ready. We recommend backing up your seed phrase now.';
@override
String get backupWarning => 'Without backup, you will lose access to your funds if you lose the device.';
String get backupWarning =>
'Without backup, you will lose access to your funds if you lose the device.';
@override
String get backupNow => 'Backup now';
@@ -87,7 +90,8 @@ class L10nEn extends L10n {
String get seedPhraseTitle => 'Your seed phrase';
@override
String get seedPhraseDescription => 'Save these 12 words in order. They are the only way to recover your wallet.';
String get seedPhraseDescription =>
'Save these 12 words in order. They are the only way to recover your wallet.';
@override
String get revealSeedPhrase => 'Reveal seed phrase';
@@ -117,7 +121,8 @@ class L10nEn extends L10n {
String get enterSeedPhrase => 'Enter your seed phrase';
@override
String get enterSeedDescription => 'Type the 12 or 24 words separated by spaces.';
String get enterSeedDescription =>
'Type the 12 or 24 words separated by spaces.';
@override
String get seedPlaceholder => 'word1 word2 word3 ...';
@@ -148,10 +153,10 @@ class L10nEn extends L10n {
String get send => 'Send';
@override
String get sendAction => 'Send ↗';
String get sendAction => 'Send';
@override
String get receiveAction => '↘ Receive';
String get receiveAction => 'Receive';
@override
String get deposit => 'Deposit';
@@ -207,6 +212,37 @@ class L10nEn extends L10n {
@override
String get pasteFromClipboard => 'Paste from clipboard';
@override
String get emptyClipboard => 'Clipboard is empty';
@override
String get cancelSend => 'Cancel send';
@override
String get cancelSendConfirmTitle => 'Cancel this send?';
@override
String get cancelSendConfirmBody =>
'Only works if the recipient has not claimed the token yet. If they already did, the send will be marked as completed.';
@override
String cancelSendSuccess(String amount, String unit) {
return 'Send canceled. Recovered $amount $unit';
}
@override
String get cancelSendAlreadyClaimed =>
'The recipient already claimed the token';
@override
String get pendingOfflineSend => 'Pending offline send';
@override
String get pendingOfflineSendsHeader => 'Unclaimed offline sends';
@override
String get pendingSendDetailTitle => 'Pending send';
@override
String get validToken => 'Valid token';
@@ -287,6 +323,9 @@ class L10nEn extends L10n {
@override
String get insufficientBalance => 'Insufficient balance';
@override
String get feeExceedsAmount => 'Fee exceeds the amount to send';
@override
String tokenCreationError(String error) {
return 'Error creating token: $error';
@@ -310,11 +349,50 @@ class L10nEn extends L10n {
}
@override
String get keepTokenWarning => 'Keep this token until the recipient claims it. If you lose it, you will lose the funds.';
String get keepTokenWarning =>
'Keep this token until the recipient claims it. If you lose it, you will lose the funds.';
@override
String get tokenCopiedToClipboard => 'Token copied to clipboard';
@override
String get copyAsEmoji => 'Copy as emoji';
@override
String get emojiCopiedToClipboard => 'Token copied as emoji 🥜';
@override
String get peanutDecodeError =>
'Could not decode emoji token. It may be corrupted.';
@override
String get nfcWrite => 'Write to NFC tag';
@override
String get nfcRead => 'Read NFC tag';
@override
String get nfcHoldNear => 'Hold device near NFC tag...';
@override
String get nfcWriteSuccess => 'Token written to NFC tag';
@override
String nfcWriteError(String error) {
return 'NFC write error: $error';
}
@override
String nfcReadError(String error) {
return 'NFC read error: $error';
}
@override
String get nfcDisabled => 'NFC is disabled. Enable it in Settings.';
@override
String get nfcUnsupported => 'This device does not support NFC';
@override
String get amountToDeposit => 'Amount to deposit:';
@@ -473,6 +551,12 @@ class L10nEn extends L10n {
@override
String get sentStatus => 'Sent';
@override
String get receiving => 'Receiving';
@override
String get sending => 'Sending';
@override
String get now => 'Now';
@@ -665,7 +749,8 @@ class L10nEn extends L10n {
String get close => 'Close';
@override
String get aboutDescription => 'A Cashu wallet with Cuban DNA for the entire world. Brother of La Chispa.';
String get aboutDescription =>
'A Cashu wallet with Cuban DNA for the entire world. Brother of LaChispa.';
@override
String get couldNotOpenLink => 'Could not open link';
@@ -677,7 +762,8 @@ class L10nEn extends L10n {
String get actionIrreversible => 'This action is irreversible';
@override
String get deleteWalletWarning => 'All data will be deleted including your seed phrase and tokens. Make sure you have a backup.';
String get deleteWalletWarning =>
'All data will be deleted including your seed phrase and tokens. Make sure you have a backup.';
@override
String get typeDeleteToConfirm => 'Type \"DELETE\" to confirm:';
@@ -694,7 +780,8 @@ class L10nEn extends L10n {
String get recoverTokensTitle => 'Recover tokens';
@override
String get recoverTokensDescription => 'Scan mints to recover tokens associated with your seed phrase (NUT-13)';
String get recoverTokensDescription =>
'Scan mints to recover tokens associated with your seed phrase (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Use my current seed phrase';
@@ -763,7 +850,8 @@ class L10nEn extends L10n {
}
@override
String get noTokensForMnemonic => 'No tokens found associated with that mnemonic.';
String get noTokensForMnemonic =>
'No tokens found associated with that mnemonic.';
@override
String get noConnectedMints => 'No connected mints';
@@ -843,7 +931,8 @@ class L10nEn extends L10n {
String get deleteMintConfirmTitle => 'Delete mint';
@override
String get deleteMintConfirmMessage => 'If you have balance in this mint, it will be lost. Are you sure?';
String get deleteMintConfirmMessage =>
'If you have balance in this mint, it will be lost. Are you sure?';
@override
String get delete => 'Delete';
@@ -896,10 +985,12 @@ class L10nEn extends L10n {
String get tokenSavedForLater => 'Token saved to claim later';
@override
String get noConnectionTokenSaved => 'No connection. Token saved to claim later.';
String get noConnectionTokenSaved =>
'No connection. Token saved to claim later.';
@override
String get unknownMintOffline => 'This token is from an unknown mint. Connect to the internet to add it and claim the token.';
String get unknownMintOffline =>
'This token is from an unknown mint. Connect to the internet to add it and claim the token.';
@override
String get noConnectionTryLater => 'No connection to mint. Try again later.';
@@ -908,7 +999,8 @@ class L10nEn extends L10n {
String get saveTokenError => 'Error saving token. Please try again.';
@override
String get pendingTokenLimitReached => 'Pending tokens limit reached (max 50)';
String get pendingTokenLimitReached =>
'Pending tokens limit reached (max 50)';
@override
String get filterToReceive => 'To receive';
@@ -985,7 +1077,8 @@ class L10nEn extends L10n {
String get unrecognizedQrCode => 'Unrecognized QR code';
@override
String get scanCashuTokenHint => 'Scan a Cashu token (cashuA... or cashuB...)';
String get scanCashuTokenHint =>
'Scan a Cashu token (cashuA... or cashuB...)';
@override
String get scanLightningInvoiceHint => 'Scan a Lightning invoice (lnbc...)';
@@ -997,7 +1090,53 @@ class L10nEn extends L10n {
String get cameraPermissionDenied => 'Camera permission denied';
@override
String get paymentRequestNotSupported => 'Payment requests are not yet supported';
String get paymentRequestTitle => 'Payment Request';
@override
String get paymentRequestFrom => 'Request from';
@override
String get paymentRequestAmount => 'Requested amount';
@override
String get paymentRequestDescription => 'Description';
@override
String get paymentRequestMints => 'Accepted mints';
@override
String get paymentRequestAnyMint => 'Any mint';
@override
String get paymentRequestPay => 'Pay';
@override
String get paymentRequestPaying => 'Paying...';
@override
String get paymentRequestSuccess => 'Payment sent successfully';
@override
String get paymentRequestNoTransport =>
'This request has no delivery method configured';
@override
String get paymentRequestTransport => 'Transport';
@override
String get paymentRequestMintNotAccepted =>
'Your active mint is not in the list of accepted mints';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Incompatible unit: request requires $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Insufficient balance';
@override
String get paymentRequestErrorParsing => 'Error reading payment request';
@override
String get p2pkTitle => 'P2PK Keys';
@@ -1009,7 +1148,8 @@ class L10nEn extends L10n {
String get p2pkExperimental => 'P2PK is experimental. Use with caution.';
@override
String get p2pkPendingSendWarning => 'You have a pending P2PK send. Go to history and refresh after the recipient claims the token.';
String get p2pkPendingSendWarning =>
'You have a pending P2PK send. Go to history and refresh after the recipient claims the token.';
@override
String get p2pkExperimentalShort => 'Experimental';
@@ -1075,7 +1215,8 @@ class L10nEn extends L10n {
String get p2pkDeleteTitle => 'Delete key';
@override
String get p2pkDeleteConfirm => 'Delete this key? You won\'t be able to receive tokens locked to it.';
String get p2pkDeleteConfirm =>
'Delete this key? You won\'t be able to receive tokens locked to it.';
@override
String get p2pkRequiresConnection => 'P2PK requires connection to the mint';
@@ -1094,4 +1235,102 @@ class L10nEn extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => 'Cannot delete primary key';
@override
String get request => 'Request';
@override
String get requestPayment => 'Request Payment';
@override
String get requestPaymentDescription => 'Generate unified payment request';
@override
String get generateRequest => 'Generate Request';
@override
String get generatingRequest => 'Generating request...';
@override
String get requestPaymentReceived => 'Payment received';
@override
String get requestDescriptionHint => 'Description (optional)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'Copied to clipboard';
@override
String get swap => 'Swap';
@override
String get swapDescription => 'Convert between sats and USD';
@override
String get swapFrom => 'From';
@override
String get swapTo => 'To';
@override
String get swapAction => 'Swap';
@override
String get swapEstimatedFee => 'Estimated fee';
@override
String get swapUseAll => 'Use all';
@override
String swapMinimum(String amount) {
return 'Minimum: $amount';
}
@override
String get swapProcessing => 'Processing swap...';
@override
String get swapSuccess => 'Swap completed';
@override
String get swapErrorInsufficient => 'Insufficient balance';
@override
String get swapErrorExpired => 'Quote has expired';
@override
String swapErrorGeneric(String error) {
return 'Swap error: $error';
}
@override
String get swapChartUnavailable => 'Price unavailable · Tap to retry';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Min: $minPrice — Max: $maxPrice';
}
@override
String get privacyPolicy => 'Privacy policy';
@override
String get privacyTitle => 'WE COLLECT NOTHING';
@override
String get privacyGoodbye => 'BYE';
@override
String get privacyKeepReading => '(keep reading if you want…)';
@override
String get privacyBody =>
'We don\'t know who you are\nWe don\'t know how much you have\nWe don\'t know what you do';
@override
String get privacyConclusion =>
'The best way to protect your data\nis not to have it';
}
+276 -29
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nEs extends L10n {
String get generatingSeed => 'Generando tu frase semilla de forma segura';
@override
String get createWalletDescription => 'Se generará una frase semilla de 12 palabras.\nGuárdala en un lugar seguro.';
String get createWalletDescription =>
'Se generará una frase semilla de 12 palabras.\nGuárdala en un lugar seguro.';
@override
String get generateWallet => 'Generar wallet';
@@ -69,10 +70,12 @@ class L10nEs extends L10n {
String get walletCreated => '¡Wallet creada!';
@override
String get walletCreatedDescription => 'Tu wallet está lista. Te recomendamos hacer backup de tu frase semilla ahora.';
String get walletCreatedDescription =>
'Tu wallet está lista. Te recomendamos hacer backup de tu frase semilla ahora.';
@override
String get backupWarning => 'Sin backup, perderás acceso a tus fondos si pierdes el dispositivo.';
String get backupWarning =>
'Sin backup, perderás acceso a tus fondos si pierdes el dispositivo.';
@override
String get backupNow => 'Hacer backup ahora';
@@ -87,7 +90,8 @@ class L10nEs extends L10n {
String get seedPhraseTitle => 'Tu frase semilla';
@override
String get seedPhraseDescription => 'Guarda estas 12 palabras en orden. Son la única forma de recuperar tu wallet.';
String get seedPhraseDescription =>
'Guarda estas 12 palabras en orden. Son la única forma de recuperar tu wallet.';
@override
String get revealSeedPhrase => 'Revelar frase semilla';
@@ -117,7 +121,8 @@ class L10nEs extends L10n {
String get enterSeedPhrase => 'Ingresa tu frase semilla';
@override
String get enterSeedDescription => 'Escribe las 12 o 24 palabras separadas por espacios.';
String get enterSeedDescription =>
'Escribe las 12 o 24 palabras separadas por espacios.';
@override
String get seedPlaceholder => 'palabra1 palabra2 palabra3 ...';
@@ -148,10 +153,10 @@ class L10nEs extends L10n {
String get send => 'Enviar';
@override
String get sendAction => 'Enviar ↗';
String get sendAction => 'Enviar';
@override
String get receiveAction => '↘ Recibir';
String get receiveAction => 'Recibir';
@override
String get deposit => 'Depositar';
@@ -207,6 +212,36 @@ class L10nEs extends L10n {
@override
String get pasteFromClipboard => 'Pegar del portapapeles';
@override
String get emptyClipboard => 'Portapapeles vacío';
@override
String get cancelSend => 'Cancelar envío';
@override
String get cancelSendConfirmTitle => '¿Cancelar envío?';
@override
String get cancelSendConfirmBody =>
'Solo funciona si el destinatario aún no reclamó el token. Si ya lo hizo, el envío se marcará como completado.';
@override
String cancelSendSuccess(String amount, String unit) {
return 'Envío cancelado. Recuperaste $amount $unit';
}
@override
String get cancelSendAlreadyClaimed => 'El destinatario ya reclamó el token';
@override
String get pendingOfflineSend => 'Envío offline pendiente';
@override
String get pendingOfflineSendsHeader => 'Envíos offline no reclamados';
@override
String get pendingSendDetailTitle => 'Envío pendiente';
@override
String get validToken => 'Token válido';
@@ -287,6 +322,9 @@ class L10nEs extends L10n {
@override
String get insufficientBalance => 'Balance insuficiente';
@override
String get feeExceedsAmount => 'La comisión supera el monto a enviar';
@override
String tokenCreationError(String error) {
return 'Error al crear token: $error';
@@ -310,11 +348,50 @@ class L10nEs extends L10n {
}
@override
String get keepTokenWarning => 'Guarda este token hasta que el receptor lo reclame. Si lo pierdes, perderás los fondos.';
String get keepTokenWarning =>
'Guarda este token hasta que el receptor lo reclame. Si lo pierdes, perderás los fondos.';
@override
String get tokenCopiedToClipboard => 'Token copiado al portapapeles';
@override
String get copyAsEmoji => 'Copiar como emoji';
@override
String get emojiCopiedToClipboard => 'Token copiado como emoji 🥜';
@override
String get peanutDecodeError =>
'No se pudo decodificar el token emoji. Puede estar corrupto.';
@override
String get nfcWrite => 'Escribir en tag NFC';
@override
String get nfcRead => 'Leer tag NFC';
@override
String get nfcHoldNear => 'Acerca el dispositivo al tag NFC...';
@override
String get nfcWriteSuccess => 'Token escrito en tag NFC';
@override
String nfcWriteError(String error) {
return 'Error NFC al escribir: $error';
}
@override
String nfcReadError(String error) {
return 'Error NFC al leer: $error';
}
@override
String get nfcDisabled => 'NFC está desactivado. Actívalo en Ajustes.';
@override
String get nfcUnsupported => 'Este dispositivo no soporta NFC';
@override
String get amountToDeposit => 'Monto a depositar:';
@@ -450,7 +527,8 @@ class L10nEs extends L10n {
String get noPendingTransactions => 'Sin transacciones pendientes';
@override
String get allTransactionsCompleted => 'Todas tus transacciones están completadas';
String get allTransactionsCompleted =>
'Todas tus transacciones están completadas';
@override
String get noEcashTransactions => 'Sin transacciones Ecash';
@@ -473,6 +551,12 @@ class L10nEs extends L10n {
@override
String get sentStatus => 'Enviado';
@override
String get receiving => 'Recibiendo';
@override
String get sending => 'Enviando';
@override
String get now => 'Ahora';
@@ -665,7 +749,8 @@ class L10nEs extends L10n {
String get close => 'Cerrar';
@override
String get aboutDescription => 'Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de La Chispa.';
String get aboutDescription =>
'Un wallet de Cashu con ADN cubano para el mundo entero. Hermano de LaChispa.';
@override
String get couldNotOpenLink => 'No se pudo abrir el enlace';
@@ -677,7 +762,8 @@ class L10nEs extends L10n {
String get actionIrreversible => 'Esta acción es irreversible';
@override
String get deleteWalletWarning => 'Se eliminarán todos los datos incluyendo tu seed phrase y tokens. Asegúrate de tener un backup.';
String get deleteWalletWarning =>
'Se eliminarán todos los datos incluyendo tu seed phrase y tokens. Asegúrate de tener un backup.';
@override
String get typeDeleteToConfirm => 'Escribe \"BORRAR\" para confirmar:';
@@ -694,13 +780,15 @@ class L10nEs extends L10n {
String get recoverTokensTitle => 'Recuperar tokens';
@override
String get recoverTokensDescription => 'Escanea los mints para recuperar tokens asociados a tu seed phrase (NUT-13)';
String get recoverTokensDescription =>
'Escanea los mints para recuperar tokens asociados a tu seed phrase (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Usar mi seed phrase actual';
@override
String get scanWithSavedWords => 'Escanear mints con las 12 palabras guardadas';
String get scanWithSavedWords =>
'Escanear mints con las 12 palabras guardadas';
@override
String get useOtherSeedPhrase => 'Usar otra seed phrase';
@@ -720,7 +808,8 @@ class L10nEs extends L10n {
String get specificMint => 'Un mint específico';
@override
String get enterMnemonicWords => 'Ingresa las 12 palabras separadas por espacios...';
String get enterMnemonicWords =>
'Ingresa las 12 palabras separadas por espacios...';
@override
String get scanMints => 'Escanear mints';
@@ -740,7 +829,8 @@ class L10nEs extends L10n {
}
@override
String get scanCompleteNoTokens => 'Escaneo completado. No se encontraron tokens nuevos.';
String get scanCompleteNoTokens =>
'Escaneo completado. No se encontraron tokens nuevos.';
@override
String mintsWithError(int count) {
@@ -763,7 +853,8 @@ class L10nEs extends L10n {
}
@override
String get noTokensForMnemonic => 'No se encontraron tokens asociados a ese mnemonic.';
String get noTokensForMnemonic =>
'No se encontraron tokens asociados a ese mnemonic.';
@override
String get noConnectedMints => 'No hay mints conectados';
@@ -843,7 +934,8 @@ class L10nEs extends L10n {
String get deleteMintConfirmTitle => 'Eliminar mint';
@override
String get deleteMintConfirmMessage => 'Si tienes balance en este mint, se perderá. ¿Estás seguro?';
String get deleteMintConfirmMessage =>
'Si tienes balance en este mint, se perderá. ¿Estás seguro?';
@override
String get delete => 'Eliminar';
@@ -896,10 +988,12 @@ class L10nEs extends L10n {
String get tokenSavedForLater => 'Token guardado para reclamar después';
@override
String get noConnectionTokenSaved => 'Sin conexión. Token guardado para reclamar después.';
String get noConnectionTokenSaved =>
'Sin conexión. Token guardado para reclamar después.';
@override
String get unknownMintOffline => 'Este token es de un mint desconocido. Conéctate a internet para agregarlo y reclamar el token.';
String get unknownMintOffline =>
'Este token es de un mint desconocido. Conéctate a internet para agregarlo y reclamar el token.';
@override
String get noConnectionTryLater => 'Sin conexión al mint. Intenta más tarde.';
@@ -908,7 +1002,8 @@ class L10nEs extends L10n {
String get saveTokenError => 'Error al guardar el token. Intenta de nuevo.';
@override
String get pendingTokenLimitReached => 'Límite de tokens pendientes alcanzado (max 50)';
String get pendingTokenLimitReached =>
'Límite de tokens pendientes alcanzado (max 50)';
@override
String get filterToReceive => 'Para recibir';
@@ -985,10 +1080,12 @@ class L10nEs extends L10n {
String get unrecognizedQrCode => 'Código QR no reconocido';
@override
String get scanCashuTokenHint => 'Escanea un token Cashu (cashuA... o cashuB...)';
String get scanCashuTokenHint =>
'Escanea un token Cashu (cashuA... o cashuB...)';
@override
String get scanLightningInvoiceHint => 'Escanea un invoice Lightning (lnbc...)';
String get scanLightningInvoiceHint =>
'Escanea un invoice Lightning (lnbc...)';
@override
String get addMintQuestion => '¿Agregar este mint?';
@@ -997,7 +1094,53 @@ class L10nEs extends L10n {
String get cameraPermissionDenied => 'Permiso de cámara denegado';
@override
String get paymentRequestNotSupported => 'Los payment requests aún no están soportados';
String get paymentRequestTitle => 'Solicitud de pago';
@override
String get paymentRequestFrom => 'Solicitud de';
@override
String get paymentRequestAmount => 'Monto solicitado';
@override
String get paymentRequestDescription => 'Descripción';
@override
String get paymentRequestMints => 'Mints aceptados';
@override
String get paymentRequestAnyMint => 'Cualquier mint';
@override
String get paymentRequestPay => 'Pagar';
@override
String get paymentRequestPaying => 'Pagando...';
@override
String get paymentRequestSuccess => 'Pago enviado correctamente';
@override
String get paymentRequestNoTransport =>
'Esta solicitud no tiene método de entrega configurado';
@override
String get paymentRequestTransport => 'Transporte';
@override
String get paymentRequestMintNotAccepted =>
'Tu mint activo no está en la lista de mints aceptados';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Unidad incompatible: la solicitud requiere $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Balance insuficiente';
@override
String get paymentRequestErrorParsing => 'Error al leer la solicitud de pago';
@override
String get p2pkTitle => 'Claves P2PK';
@@ -1009,7 +1152,8 @@ class L10nEs extends L10n {
String get p2pkExperimental => 'P2PK es experimental. Úsala con precaución.';
@override
String get p2pkPendingSendWarning => 'Tienes un envío P2PK pendiente. Ve al historial y presiona actualizar después de que el destinatario reclame el token.';
String get p2pkPendingSendWarning =>
'Tienes un envío P2PK pendiente. Ve al historial y presiona actualizar después de que el destinatario reclame el token.';
@override
String get p2pkExperimentalShort => 'Experimental';
@@ -1066,7 +1210,8 @@ class L10nEs extends L10n {
String get p2pkLockedToOther => 'Bloqueado para otra clave';
@override
String get p2pkCannotUnlock => 'No tienes la clave para desbloquear este token';
String get p2pkCannotUnlock =>
'No tienes la clave para desbloquear este token';
@override
String get p2pkEnterPrivateKey => 'Ingresa la clave privada (nsec)';
@@ -1075,13 +1220,15 @@ class L10nEs extends L10n {
String get p2pkDeleteTitle => 'Eliminar clave';
@override
String get p2pkDeleteConfirm => '¿Eliminar esta clave? No podrás recibir tokens bloqueados a ella.';
String get p2pkDeleteConfirm =>
'¿Eliminar esta clave? No podrás recibir tokens bloqueados a ella.';
@override
String get p2pkRequiresConnection => 'P2PK requiere conexión al mint';
@override
String get p2pkErrorMaxKeysReached => 'Máximo de claves importadas alcanzado (10)';
String get p2pkErrorMaxKeysReached =>
'Máximo de claves importadas alcanzado (10)';
@override
String get p2pkErrorInvalidNsec => 'nsec inválido';
@@ -1093,5 +1240,105 @@ class L10nEs extends L10n {
String get p2pkErrorKeyNotFound => 'Clave no encontrada';
@override
String get p2pkErrorCannotDeletePrimary => 'No se puede eliminar la clave principal';
String get p2pkErrorCannotDeletePrimary =>
'No se puede eliminar la clave principal';
@override
String get request => 'Solicitar';
@override
String get requestPayment => 'Solicitar pago';
@override
String get requestPaymentDescription => 'Generar solicitud de pago unificada';
@override
String get generateRequest => 'Generar solicitud';
@override
String get generatingRequest => 'Generando solicitud...';
@override
String get requestPaymentReceived => 'Pago recibido';
@override
String get requestDescriptionHint => 'Descripción (opcional)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'Copiado al portapapeles';
@override
String get swap => 'Cambiar';
@override
String get swapDescription => 'Convertir entre sats y USD';
@override
String get swapFrom => 'De';
@override
String get swapTo => 'A';
@override
String get swapAction => 'Cambiar';
@override
String get swapEstimatedFee => 'Fee estimado';
@override
String get swapUseAll => 'Usar todo';
@override
String swapMinimum(String amount) {
return 'Mínimo: $amount';
}
@override
String get swapProcessing => 'Procesando swap...';
@override
String get swapSuccess => 'Swap completado';
@override
String get swapErrorInsufficient => 'Saldo insuficiente';
@override
String get swapErrorExpired => 'La cotización ha expirado';
@override
String swapErrorGeneric(String error) {
return 'Error en el swap: $error';
}
@override
String get swapChartUnavailable =>
'Precio no disponible · Toca para reintentar';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Mín: $minPrice — Máx: $maxPrice';
}
@override
String get privacyPolicy => 'Política de privacidad';
@override
String get privacyTitle => 'NO RECOPILAMOS NADA';
@override
String get privacyGoodbye => 'ADIÓS';
@override
String get privacyKeepReading => '(sigue leyendo si quieres…)';
@override
String get privacyBody =>
'No sabemos quién eres\nNo sabemos cuánto tienes\nNo sabemos qué haces';
@override
String get privacyConclusion =>
'La mejor forma de proteger tus datos\nes no tenerlos';
}
+306 -42
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -57,10 +57,12 @@ class L10nFr extends L10n {
String get creatingWallet => 'Création de votre portefeuille...';
@override
String get generatingSeed => 'Génération sécurisée de votre phrase de récupération';
String get generatingSeed =>
'Génération sécurisée de votre phrase de récupération';
@override
String get createWalletDescription => 'Une phrase de récupération de 12 mots sera générée.\nConservez-la dans un endroit sûr.';
String get createWalletDescription =>
'Une phrase de récupération de 12 mots sera générée.\nConservez-la dans un endroit sûr.';
@override
String get generateWallet => 'Générer le portefeuille';
@@ -69,10 +71,12 @@ class L10nFr extends L10n {
String get walletCreated => 'Portefeuille créé !';
@override
String get walletCreatedDescription => 'Votre portefeuille est prêt. Nous vous recommandons de sauvegarder votre phrase de récupération maintenant.';
String get walletCreatedDescription =>
'Votre portefeuille est prêt. Nous vous recommandons de sauvegarder votre phrase de récupération maintenant.';
@override
String get backupWarning => 'Sans sauvegarde, vous perdrez l\'accès à vos fonds si vous perdez l\'appareil.';
String get backupWarning =>
'Sans sauvegarde, vous perdrez l\'accès à vos fonds si vous perdez l\'appareil.';
@override
String get backupNow => 'Sauvegarder maintenant';
@@ -87,13 +91,15 @@ class L10nFr extends L10n {
String get seedPhraseTitle => 'Votre phrase de récupération';
@override
String get seedPhraseDescription => 'Conservez ces 12 mots dans l\'ordre. C\'est le seul moyen de récupérer votre portefeuille.';
String get seedPhraseDescription =>
'Conservez ces 12 mots dans l\'ordre. C\'est le seul moyen de récupérer votre portefeuille.';
@override
String get revealSeedPhrase => 'Révéler la phrase de récupération';
@override
String get tapToReveal => 'Appuyez sur le bouton pour révéler\nvotre phrase de récupération';
String get tapToReveal =>
'Appuyez sur le bouton pour révéler\nvotre phrase de récupération';
@override
String get copyToClipboard => 'Copier dans le presse-papiers';
@@ -102,10 +108,12 @@ class L10nFr extends L10n {
String get seedCopied => 'Phrase copiée dans le presse-papiers';
@override
String get neverShareSeed => 'Ne partagez jamais votre phrase de récupération avec personne.';
String get neverShareSeed =>
'Ne partagez jamais votre phrase de récupération avec personne.';
@override
String get confirmBackup => 'J\'ai sauvegardé ma phrase de récupération dans un endroit sûr';
String get confirmBackup =>
'J\'ai sauvegardé ma phrase de récupération dans un endroit sûr';
@override
String get continue_ => 'Continuer';
@@ -117,7 +125,8 @@ class L10nFr extends L10n {
String get enterSeedPhrase => 'Entrez votre phrase de récupération';
@override
String get enterSeedDescription => 'Tapez les 12 ou 24 mots séparés par des espaces.';
String get enterSeedDescription =>
'Tapez les 12 ou 24 mots séparés par des espaces.';
@override
String get seedPlaceholder => 'mot1 mot2 mot3 ...';
@@ -148,10 +157,10 @@ class L10nFr extends L10n {
String get send => 'Envoyer';
@override
String get sendAction => 'Envoyer ↗';
String get sendAction => 'Envoyer';
@override
String get receiveAction => '↘ Recevoir';
String get receiveAction => 'Recevoir';
@override
String get deposit => 'Déposer';
@@ -207,6 +216,37 @@ class L10nFr extends L10n {
@override
String get pasteFromClipboard => 'Coller depuis le presse-papiers';
@override
String get emptyClipboard => 'Presse-papiers vide';
@override
String get cancelSend => 'Annuler l\'envoi';
@override
String get cancelSendConfirmTitle => 'Annuler cet envoi ?';
@override
String get cancelSendConfirmBody =>
'Ne fonctionne que si le destinataire n\'a pas encore réclamé le token. S\'il l\'a déjà fait, l\'envoi sera marqué comme terminé.';
@override
String cancelSendSuccess(String amount, String unit) {
return 'Envoi annulé. Récupéré $amount $unit';
}
@override
String get cancelSendAlreadyClaimed =>
'Le destinataire a déjà réclamé le token';
@override
String get pendingOfflineSend => 'Envoi hors ligne en attente';
@override
String get pendingOfflineSendsHeader => 'Envois hors ligne non réclamés';
@override
String get pendingSendDetailTitle => 'Envoi en attente';
@override
String get validToken => 'Token valide';
@@ -287,6 +327,9 @@ class L10nFr extends L10n {
@override
String get insufficientBalance => 'Solde insuffisant';
@override
String get feeExceedsAmount => 'Les frais dépassent le montant à envoyer';
@override
String tokenCreationError(String error) {
return 'Erreur de création du token : $error';
@@ -310,11 +353,51 @@ class L10nFr extends L10n {
}
@override
String get keepTokenWarning => 'Conservez ce token jusqu\'à ce que le destinataire le réclame. Si vous le perdez, vous perdrez les fonds.';
String get keepTokenWarning =>
'Conservez ce token jusqu\'à ce que le destinataire le réclame. Si vous le perdez, vous perdrez les fonds.';
@override
String get tokenCopiedToClipboard => 'Token copié dans le presse-papiers';
@override
String get copyAsEmoji => 'Copier en emoji';
@override
String get emojiCopiedToClipboard => 'Token copié en emoji 🥜';
@override
String get peanutDecodeError =>
'Impossible de décoder le token emoji. Il est peut-être corrompu.';
@override
String get nfcWrite => 'Écrire sur tag NFC';
@override
String get nfcRead => 'Lire tag NFC';
@override
String get nfcHoldNear => 'Approchez l\'appareil du tag NFC...';
@override
String get nfcWriteSuccess => 'Token écrit sur le tag NFC';
@override
String nfcWriteError(String error) {
return 'Erreur NFC écriture : $error';
}
@override
String nfcReadError(String error) {
return 'Erreur NFC lecture : $error';
}
@override
String get nfcDisabled =>
'NFC est désactivé. Activez-le dans les Paramètres.';
@override
String get nfcUnsupported => 'Cet appareil ne prend pas en charge le NFC';
@override
String get amountToDeposit => 'Montant à déposer :';
@@ -364,7 +447,8 @@ class L10nFr extends L10n {
String get description => 'Description :';
@override
String get invoiceCopiedToClipboard => 'Facture copiée dans le presse-papiers';
String get invoiceCopiedToClipboard =>
'Facture copiée dans le presse-papiers';
@override
String deposited(String amount, String unit) {
@@ -450,7 +534,8 @@ class L10nFr extends L10n {
String get noPendingTransactions => 'Aucune transaction en attente';
@override
String get allTransactionsCompleted => 'Toutes vos transactions sont terminées';
String get allTransactionsCompleted =>
'Toutes vos transactions sont terminées';
@override
String get noEcashTransactions => 'Aucune transaction Ecash';
@@ -473,6 +558,12 @@ class L10nFr extends L10n {
@override
String get sentStatus => 'Envoyé';
@override
String get receiving => 'En réception';
@override
String get sending => 'En envoi';
@override
String get now => 'Maintenant';
@@ -567,7 +658,8 @@ class L10nFr extends L10n {
String get recoverTokens => 'Récupérer les tokens';
@override
String get scanMintsWithSeed => 'Scanner les mints avec la phrase de récupération';
String get scanMintsWithSeed =>
'Scanner les mints avec la phrase de récupération';
@override
String get appearanceSection => 'LANGUE';
@@ -665,7 +757,8 @@ class L10nFr extends L10n {
String get close => 'Fermer';
@override
String get aboutDescription => 'Un portefeuille Cashu avec ADN cubain pour le monde entier. Frère de La Chispa.';
String get aboutDescription =>
'Un portefeuille Cashu avec ADN cubain pour le monde entier. Frère de LaChispa.';
@override
String get couldNotOpenLink => 'Impossible d\'ouvrir le lien';
@@ -677,7 +770,8 @@ class L10nFr extends L10n {
String get actionIrreversible => 'Cette action est irréversible';
@override
String get deleteWalletWarning => 'Toutes les données seront supprimées, y compris votre phrase de récupération et vos tokens. Assurez-vous d\'avoir une sauvegarde.';
String get deleteWalletWarning =>
'Toutes les données seront supprimées, y compris votre phrase de récupération et vos tokens. Assurez-vous d\'avoir une sauvegarde.';
@override
String get typeDeleteToConfirm => 'Tapez \"SUPPRIMER\" pour confirmer :';
@@ -694,13 +788,16 @@ class L10nFr extends L10n {
String get recoverTokensTitle => 'Récupérer les tokens';
@override
String get recoverTokensDescription => 'Scanner les mints pour récupérer les tokens associés à votre phrase de récupération (NUT-13)';
String get recoverTokensDescription =>
'Scanner les mints pour récupérer les tokens associés à votre phrase de récupération (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Utiliser ma phrase de récupération actuelle';
String get useCurrentSeedPhrase =>
'Utiliser ma phrase de récupération actuelle';
@override
String get scanWithSavedWords => 'Scanner les mints avec les 12 mots sauvegardés';
String get scanWithSavedWords =>
'Scanner les mints avec les 12 mots sauvegardés';
@override
String get useOtherSeedPhrase => 'Utiliser une autre phrase de récupération';
@@ -720,7 +817,8 @@ class L10nFr extends L10n {
String get specificMint => 'Un mint spécifique';
@override
String get enterMnemonicWords => 'Entrez les 12 mots séparés par des espaces...';
String get enterMnemonicWords =>
'Entrez les 12 mots séparés par des espaces...';
@override
String get scanMints => 'Scanner les mints';
@@ -740,7 +838,8 @@ class L10nFr extends L10n {
}
@override
String get scanCompleteNoTokens => 'Scan terminé. Aucun nouveau token trouvé.';
String get scanCompleteNoTokens =>
'Scan terminé. Aucun nouveau token trouvé.';
@override
String mintsWithError(int count) {
@@ -763,7 +862,8 @@ class L10nFr extends L10n {
}
@override
String get noTokensForMnemonic => 'Aucun token trouvé associé à ce mnémonique.';
String get noTokensForMnemonic =>
'Aucun token trouvé associé à ce mnémonique.';
@override
String get noConnectedMints => 'Aucun mint connecté';
@@ -843,7 +943,8 @@ class L10nFr extends L10n {
String get deleteMintConfirmTitle => 'Supprimer le mint';
@override
String get deleteMintConfirmMessage => 'Si vous avez un solde sur ce mint, il sera perdu. Êtes-vous sûr ?';
String get deleteMintConfirmMessage =>
'Si vous avez un solde sur ce mint, il sera perdu. Êtes-vous sûr ?';
@override
String get delete => 'Supprimer';
@@ -896,19 +997,24 @@ class L10nFr extends L10n {
String get tokenSavedForLater => 'Token sauvegardé pour réclamer plus tard';
@override
String get noConnectionTokenSaved => 'Pas de connexion. Token sauvegardé pour réclamer plus tard.';
String get noConnectionTokenSaved =>
'Pas de connexion. Token sauvegardé pour réclamer plus tard.';
@override
String get unknownMintOffline => 'Ce token provient d\'un mint inconnu. Connectez-vous à Internet pour l\'ajouter et réclamer le token.';
String get unknownMintOffline =>
'Ce token provient d\'un mint inconnu. Connectez-vous à Internet pour l\'ajouter et réclamer le token.';
@override
String get noConnectionTryLater => 'Pas de connexion au mint. Réessayez plus tard.';
String get noConnectionTryLater =>
'Pas de connexion au mint. Réessayez plus tard.';
@override
String get saveTokenError => 'Erreur lors de la sauvegarde du token. Veuillez réessayer.';
String get saveTokenError =>
'Erreur lors de la sauvegarde du token. Veuillez réessayer.';
@override
String get pendingTokenLimitReached => 'Limite de tokens en attente atteinte (max 50)';
String get pendingTokenLimitReached =>
'Limite de tokens en attente atteinte (max 50)';
@override
String get filterToReceive => 'À recevoir';
@@ -917,7 +1023,8 @@ class L10nFr extends L10n {
String get noPendingTokens => 'Aucun token en attente';
@override
String get noPendingTokensHint => 'Sauvegardez des tokens pour les réclamer plus tard';
String get noPendingTokensHint =>
'Sauvegardez des tokens pour les réclamer plus tard';
@override
String get pendingBadge => 'EN ATTENTE';
@@ -976,19 +1083,23 @@ class L10nFr extends L10n {
String get pointCameraAtQr => 'Pointez la caméra vers le code QR';
@override
String get pointCameraAtCashuQr => 'Pointez la caméra vers le QR du token Cashu';
String get pointCameraAtCashuQr =>
'Pointez la caméra vers le QR du token Cashu';
@override
String get pointCameraAtInvoiceQr => 'Pointez la caméra vers le QR de la facture';
String get pointCameraAtInvoiceQr =>
'Pointez la caméra vers le QR de la facture';
@override
String get unrecognizedQrCode => 'Code QR non reconnu';
@override
String get scanCashuTokenHint => 'Scannez un token Cashu (cashuA... ou cashuB...)';
String get scanCashuTokenHint =>
'Scannez un token Cashu (cashuA... ou cashuB...)';
@override
String get scanLightningInvoiceHint => 'Scannez une facture Lightning (lnbc...)';
String get scanLightningInvoiceHint =>
'Scannez une facture Lightning (lnbc...)';
@override
String get addMintQuestion => 'Ajouter ce mint ?';
@@ -997,7 +1108,54 @@ class L10nFr extends L10n {
String get cameraPermissionDenied => 'Permission de la caméra refusée';
@override
String get paymentRequestNotSupported => 'Les demandes de paiement ne sont pas encore prises en charge';
String get paymentRequestTitle => 'Demande de paiement';
@override
String get paymentRequestFrom => 'Demande de';
@override
String get paymentRequestAmount => 'Montant demandé';
@override
String get paymentRequestDescription => 'Description';
@override
String get paymentRequestMints => 'Mints acceptés';
@override
String get paymentRequestAnyMint => 'N\'importe quel mint';
@override
String get paymentRequestPay => 'Payer';
@override
String get paymentRequestPaying => 'Paiement en cours...';
@override
String get paymentRequestSuccess => 'Paiement envoyé avec succès';
@override
String get paymentRequestNoTransport =>
'Cette demande n\'a pas de méthode de livraison configurée';
@override
String get paymentRequestTransport => 'Transport';
@override
String get paymentRequestMintNotAccepted =>
'Votre mint actif n\'est pas dans la liste des mints acceptés';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Unité incompatible : la demande nécessite $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Solde insuffisant';
@override
String get paymentRequestErrorParsing =>
'Erreur lors de la lecture de la demande de paiement';
@override
String get p2pkTitle => 'Clés P2PK';
@@ -1006,10 +1164,12 @@ class L10nFr extends L10n {
String get p2pkSettingsDescription => 'Recevoir ecash verrouillé';
@override
String get p2pkExperimental => 'P2PK est expérimental. Utiliser avec prudence.';
String get p2pkExperimental =>
'P2PK est expérimental. Utiliser avec prudence.';
@override
String get p2pkPendingSendWarning => 'Vous avez un envoi P2PK en attente. Allez dans l\'historique et actualisez après que le destinataire a réclamé le jeton.';
String get p2pkPendingSendWarning =>
'Vous avez un envoi P2PK en attente. Allez dans l\'historique et actualisez après que le destinataire a réclamé le jeton.';
@override
String get p2pkExperimentalShort => 'Expérimental';
@@ -1066,7 +1226,8 @@ class L10nFr extends L10n {
String get p2pkLockedToOther => 'Verrouillé pour une autre clé';
@override
String get p2pkCannotUnlock => 'Vous n\'avez pas la clé pour déverrouiller ce token';
String get p2pkCannotUnlock =>
'Vous n\'avez pas la clé pour déverrouiller ce token';
@override
String get p2pkEnterPrivateKey => 'Entrer la clé privée (nsec)';
@@ -1075,13 +1236,15 @@ class L10nFr extends L10n {
String get p2pkDeleteTitle => 'Supprimer la clé';
@override
String get p2pkDeleteConfirm => 'Supprimer cette clé ? Vous ne pourrez plus recevoir de tokens verrouillés dessus.';
String get p2pkDeleteConfirm =>
'Supprimer cette clé ? Vous ne pourrez plus recevoir de tokens verrouillés dessus.';
@override
String get p2pkRequiresConnection => 'P2PK nécessite une connexion au mint';
@override
String get p2pkErrorMaxKeysReached => 'Nombre maximum de clés importées atteint (10)';
String get p2pkErrorMaxKeysReached =>
'Nombre maximum de clés importées atteint (10)';
@override
String get p2pkErrorInvalidNsec => 'nsec invalide';
@@ -1093,5 +1256,106 @@ class L10nFr extends L10n {
String get p2pkErrorKeyNotFound => 'Clé non trouvée';
@override
String get p2pkErrorCannotDeletePrimary => 'Impossible de supprimer la clé principale';
String get p2pkErrorCannotDeletePrimary =>
'Impossible de supprimer la clé principale';
@override
String get request => 'Demander';
@override
String get requestPayment => 'Demander un paiement';
@override
String get requestPaymentDescription =>
'Générer une demande de paiement unifiée';
@override
String get generateRequest => 'Générer la demande';
@override
String get generatingRequest => 'Génération en cours...';
@override
String get requestPaymentReceived => 'Paiement reçu';
@override
String get requestDescriptionHint => 'Description (facultatif)';
@override
String get universal => 'Universel';
@override
String get copiedToClipboard => 'Copié dans le presse-papiers';
@override
String get swap => 'Échanger';
@override
String get swapDescription => 'Convertir entre sats et USD';
@override
String get swapFrom => 'De';
@override
String get swapTo => 'Vers';
@override
String get swapAction => 'Échanger';
@override
String get swapEstimatedFee => 'Frais estimés';
@override
String get swapUseAll => 'Tout utiliser';
@override
String swapMinimum(String amount) {
return 'Minimum : $amount';
}
@override
String get swapProcessing => 'Swap en cours...';
@override
String get swapSuccess => 'Swap terminé';
@override
String get swapErrorInsufficient => 'Solde insuffisant';
@override
String get swapErrorExpired => 'Le devis a expiré';
@override
String swapErrorGeneric(String error) {
return 'Erreur de swap : $error';
}
@override
String get swapChartUnavailable =>
'Prix indisponible · Appuyez pour réessayer';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Min : $minPrice — Max : $maxPrice';
}
@override
String get privacyPolicy => 'Politique de confidentialité';
@override
String get privacyTitle => 'NOUS NE COLLECTONS RIEN';
@override
String get privacyGoodbye => 'AU REVOIR';
@override
String get privacyKeepReading => '(continue à lire si tu veux…)';
@override
String get privacyBody =>
'Nous ne savons pas qui tu es\nNous ne savons pas combien tu as\nNous ne savons pas ce que tu fais';
@override
String get privacyConclusion =>
'La meilleure façon de protéger tes données,\nc\'est de ne pas les avoir';
}
+283 -31
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nIt extends L10n {
String get generatingSeed => 'Generazione sicura della tua frase seed';
@override
String get createWalletDescription => 'Verrà generata una frase seed di 12 parole.\nConservala in un luogo sicuro.';
String get createWalletDescription =>
'Verrà generata una frase seed di 12 parole.\nConservala in un luogo sicuro.';
@override
String get generateWallet => 'Genera portafoglio';
@@ -69,10 +70,12 @@ class L10nIt extends L10n {
String get walletCreated => 'Portafoglio creato!';
@override
String get walletCreatedDescription => 'Il tuo portafoglio è pronto. Ti consigliamo di fare il backup della frase seed ora.';
String get walletCreatedDescription =>
'Il tuo portafoglio è pronto. Ti consigliamo di fare il backup della frase seed ora.';
@override
String get backupWarning => 'Senza backup, perderai l\'accesso ai tuoi fondi se perdi il dispositivo.';
String get backupWarning =>
'Senza backup, perderai l\'accesso ai tuoi fondi se perdi il dispositivo.';
@override
String get backupNow => 'Backup adesso';
@@ -87,7 +90,8 @@ class L10nIt extends L10n {
String get seedPhraseTitle => 'La tua frase seed';
@override
String get seedPhraseDescription => 'Conserva queste 12 parole in ordine. Sono l\'unico modo per recuperare il tuo portafoglio.';
String get seedPhraseDescription =>
'Conserva queste 12 parole in ordine. Sono l\'unico modo per recuperare il tuo portafoglio.';
@override
String get revealSeedPhrase => 'Mostra frase seed';
@@ -102,7 +106,8 @@ class L10nIt extends L10n {
String get seedCopied => 'Frase copiata negli appunti';
@override
String get neverShareSeed => 'Non condividere mai la tua frase seed con nessuno.';
String get neverShareSeed =>
'Non condividere mai la tua frase seed con nessuno.';
@override
String get confirmBackup => 'Ho salvato la mia frase seed in un luogo sicuro';
@@ -117,7 +122,8 @@ class L10nIt extends L10n {
String get enterSeedPhrase => 'Inserisci la tua frase seed';
@override
String get enterSeedDescription => 'Scrivi le 12 o 24 parole separate da spazi.';
String get enterSeedDescription =>
'Scrivi le 12 o 24 parole separate da spazi.';
@override
String get seedPlaceholder => 'parola1 parola2 parola3 ...';
@@ -148,10 +154,10 @@ class L10nIt extends L10n {
String get send => 'Invia';
@override
String get sendAction => 'Invia ↗';
String get sendAction => 'Invia';
@override
String get receiveAction => '↘ Ricevi';
String get receiveAction => 'Ricevi';
@override
String get deposit => 'Deposita';
@@ -207,6 +213,37 @@ class L10nIt extends L10n {
@override
String get pasteFromClipboard => 'Incolla dagli appunti';
@override
String get emptyClipboard => 'Appunti vuoti';
@override
String get cancelSend => 'Annulla invio';
@override
String get cancelSendConfirmTitle => 'Annullare questo invio?';
@override
String get cancelSendConfirmBody =>
'Funziona solo se il destinatario non ha ancora riscattato il token. Se l\'ha già fatto, l\'invio sarà contrassegnato come completato.';
@override
String cancelSendSuccess(String amount, String unit) {
return 'Invio annullato. Recuperati $amount $unit';
}
@override
String get cancelSendAlreadyClaimed =>
'Il destinatario ha già riscattato il token';
@override
String get pendingOfflineSend => 'Invio offline in sospeso';
@override
String get pendingOfflineSendsHeader => 'Invii offline non riscattati';
@override
String get pendingSendDetailTitle => 'Invio in sospeso';
@override
String get validToken => 'Token valido';
@@ -287,6 +324,9 @@ class L10nIt extends L10n {
@override
String get insufficientBalance => 'Saldo insufficiente';
@override
String get feeExceedsAmount => 'La commissione supera l\'importo da inviare';
@override
String tokenCreationError(String error) {
return 'Errore creazione token: $error';
@@ -310,11 +350,50 @@ class L10nIt extends L10n {
}
@override
String get keepTokenWarning => 'Conserva questo token finché il destinatario non lo riscatta. Se lo perdi, perderai i fondi.';
String get keepTokenWarning =>
'Conserva questo token finché il destinatario non lo riscatta. Se lo perdi, perderai i fondi.';
@override
String get tokenCopiedToClipboard => 'Token copiato negli appunti';
@override
String get copyAsEmoji => 'Copia come emoji';
@override
String get emojiCopiedToClipboard => 'Token copiato come emoji 🥜';
@override
String get peanutDecodeError =>
'Impossibile decodificare il token emoji. Potrebbe essere corrotto.';
@override
String get nfcWrite => 'Scrivi su tag NFC';
@override
String get nfcRead => 'Leggi tag NFC';
@override
String get nfcHoldNear => 'Avvicina il dispositivo al tag NFC...';
@override
String get nfcWriteSuccess => 'Token scritto sul tag NFC';
@override
String nfcWriteError(String error) {
return 'Errore NFC scrittura: $error';
}
@override
String nfcReadError(String error) {
return 'Errore NFC lettura: $error';
}
@override
String get nfcDisabled => 'NFC è disattivato. Attivalo nelle Impostazioni.';
@override
String get nfcUnsupported => 'Questo dispositivo non supporta NFC';
@override
String get amountToDeposit => 'Importo da depositare:';
@@ -450,7 +529,8 @@ class L10nIt extends L10n {
String get noPendingTransactions => 'Nessuna transazione in attesa';
@override
String get allTransactionsCompleted => 'Tutte le tue transazioni sono completate';
String get allTransactionsCompleted =>
'Tutte le tue transazioni sono completate';
@override
String get noEcashTransactions => 'Nessuna transazione Ecash';
@@ -473,6 +553,12 @@ class L10nIt extends L10n {
@override
String get sentStatus => 'Inviato';
@override
String get receiving => 'In ricezione';
@override
String get sending => 'In invio';
@override
String get now => 'Adesso';
@@ -665,7 +751,8 @@ class L10nIt extends L10n {
String get close => 'Chiudi';
@override
String get aboutDescription => 'Un portafoglio Cashu con DNA cubano per il mondo intero. Fratello di La Chispa.';
String get aboutDescription =>
'Un portafoglio Cashu con DNA cubano per il mondo intero. Fratello di LaChispa.';
@override
String get couldNotOpenLink => 'Impossibile aprire il link';
@@ -677,7 +764,8 @@ class L10nIt extends L10n {
String get actionIrreversible => 'Questa azione è irreversibile';
@override
String get deleteWalletWarning => 'Tutti i dati verranno eliminati, inclusa la frase seed e i token. Assicurati di avere un backup.';
String get deleteWalletWarning =>
'Tutti i dati verranno eliminati, inclusa la frase seed e i token. Assicurati di avere un backup.';
@override
String get typeDeleteToConfirm => 'Scrivi \"ELIMINA\" per confermare:';
@@ -694,7 +782,8 @@ class L10nIt extends L10n {
String get recoverTokensTitle => 'Recupera token';
@override
String get recoverTokensDescription => 'Scansiona i mint per recuperare i token associati alla tua frase seed (NUT-13)';
String get recoverTokensDescription =>
'Scansiona i mint per recuperare i token associati alla tua frase seed (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Usa la mia frase seed attuale';
@@ -720,7 +809,8 @@ class L10nIt extends L10n {
String get specificMint => 'Un mint specifico';
@override
String get enterMnemonicWords => 'Inserisci le 12 parole separate da spazi...';
String get enterMnemonicWords =>
'Inserisci le 12 parole separate da spazi...';
@override
String get scanMints => 'Scansiona mint';
@@ -740,7 +830,8 @@ class L10nIt extends L10n {
}
@override
String get scanCompleteNoTokens => 'Scansione completata. Nessun nuovo token trovato.';
String get scanCompleteNoTokens =>
'Scansione completata. Nessun nuovo token trovato.';
@override
String mintsWithError(int count) {
@@ -763,7 +854,8 @@ class L10nIt extends L10n {
}
@override
String get noTokensForMnemonic => 'Nessun token trovato associato a questo mnemonic.';
String get noTokensForMnemonic =>
'Nessun token trovato associato a questo mnemonic.';
@override
String get noConnectedMints => 'Nessun mint connesso';
@@ -843,7 +935,8 @@ class L10nIt extends L10n {
String get deleteMintConfirmTitle => 'Elimina mint';
@override
String get deleteMintConfirmMessage => 'Se hai saldo su questo mint, verrà perso. Sei sicuro?';
String get deleteMintConfirmMessage =>
'Se hai saldo su questo mint, verrà perso. Sei sicuro?';
@override
String get delete => 'Elimina';
@@ -896,19 +989,23 @@ class L10nIt extends L10n {
String get tokenSavedForLater => 'Token salvato per riscattarlo dopo';
@override
String get noConnectionTokenSaved => 'Nessuna connessione. Token salvato per riscattarlo dopo.';
String get noConnectionTokenSaved =>
'Nessuna connessione. Token salvato per riscattarlo dopo.';
@override
String get unknownMintOffline => 'Questo token proviene da un mint sconosciuto. Connettiti a internet per aggiungerlo e riscattare il token.';
String get unknownMintOffline =>
'Questo token proviene da un mint sconosciuto. Connettiti a internet per aggiungerlo e riscattare il token.';
@override
String get noConnectionTryLater => 'Nessuna connessione al mint. Riprova più tardi.';
String get noConnectionTryLater =>
'Nessuna connessione al mint. Riprova più tardi.';
@override
String get saveTokenError => 'Errore nel salvare il token. Riprova.';
@override
String get pendingTokenLimitReached => 'Limite token in attesa raggiunto (max 50)';
String get pendingTokenLimitReached =>
'Limite token in attesa raggiunto (max 50)';
@override
String get filterToReceive => 'Da ricevere';
@@ -976,19 +1073,23 @@ class L10nIt extends L10n {
String get pointCameraAtQr => 'Punta la fotocamera sul codice QR';
@override
String get pointCameraAtCashuQr => 'Punta la fotocamera sul QR del token Cashu';
String get pointCameraAtCashuQr =>
'Punta la fotocamera sul QR del token Cashu';
@override
String get pointCameraAtInvoiceQr => 'Punta la fotocamera sul QR della fattura';
String get pointCameraAtInvoiceQr =>
'Punta la fotocamera sul QR della fattura';
@override
String get unrecognizedQrCode => 'Codice QR non riconosciuto';
@override
String get scanCashuTokenHint => 'Scansiona un token Cashu (cashuA... o cashuB...)';
String get scanCashuTokenHint =>
'Scansiona un token Cashu (cashuA... o cashuB...)';
@override
String get scanLightningInvoiceHint => 'Scansiona una fattura Lightning (lnbc...)';
String get scanLightningInvoiceHint =>
'Scansiona una fattura Lightning (lnbc...)';
@override
String get addMintQuestion => 'Aggiungere questo mint?';
@@ -997,7 +1098,54 @@ class L10nIt extends L10n {
String get cameraPermissionDenied => 'Permesso fotocamera negato';
@override
String get paymentRequestNotSupported => 'Le richieste di pagamento non sono ancora supportate';
String get paymentRequestTitle => 'Richiesta di pagamento';
@override
String get paymentRequestFrom => 'Richiesta da';
@override
String get paymentRequestAmount => 'Importo richiesto';
@override
String get paymentRequestDescription => 'Descrizione';
@override
String get paymentRequestMints => 'Mint accettati';
@override
String get paymentRequestAnyMint => 'Qualsiasi mint';
@override
String get paymentRequestPay => 'Paga';
@override
String get paymentRequestPaying => 'Pagamento in corso...';
@override
String get paymentRequestSuccess => 'Pagamento inviato con successo';
@override
String get paymentRequestNoTransport =>
'Questa richiesta non ha un metodo di consegna configurato';
@override
String get paymentRequestTransport => 'Trasporto';
@override
String get paymentRequestMintNotAccepted =>
'Il tuo mint attivo non è nella lista dei mint accettati';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Unità incompatibile: la richiesta richiede $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Saldo insufficiente';
@override
String get paymentRequestErrorParsing =>
'Errore nella lettura della richiesta di pagamento';
@override
String get p2pkTitle => 'Chiavi P2PK';
@@ -1009,7 +1157,8 @@ class L10nIt extends L10n {
String get p2pkExperimental => 'P2PK è sperimentale. Usare con cautela.';
@override
String get p2pkPendingSendWarning => 'Hai un invio P2PK in sospeso. Vai alla cronologia e aggiorna dopo che il destinatario ha riscattato il token.';
String get p2pkPendingSendWarning =>
'Hai un invio P2PK in sospeso. Vai alla cronologia e aggiorna dopo che il destinatario ha riscattato il token.';
@override
String get p2pkExperimentalShort => 'Sperimentale';
@@ -1075,13 +1224,15 @@ class L10nIt extends L10n {
String get p2pkDeleteTitle => 'Elimina chiave';
@override
String get p2pkDeleteConfirm => 'Eliminare questa chiave? Non potrai ricevere token bloccati ad essa.';
String get p2pkDeleteConfirm =>
'Eliminare questa chiave? Non potrai ricevere token bloccati ad essa.';
@override
String get p2pkRequiresConnection => 'P2PK richiede connessione al mint';
@override
String get p2pkErrorMaxKeysReached => 'Numero massimo di chiavi importate raggiunto (10)';
String get p2pkErrorMaxKeysReached =>
'Numero massimo di chiavi importate raggiunto (10)';
@override
String get p2pkErrorInvalidNsec => 'nsec non valido';
@@ -1093,5 +1244,106 @@ class L10nIt extends L10n {
String get p2pkErrorKeyNotFound => 'Chiave non trovata';
@override
String get p2pkErrorCannotDeletePrimary => 'Impossibile eliminare la chiave principale';
String get p2pkErrorCannotDeletePrimary =>
'Impossibile eliminare la chiave principale';
@override
String get request => 'Richiedi';
@override
String get requestPayment => 'Richiedi pagamento';
@override
String get requestPaymentDescription =>
'Genera richiesta di pagamento unificata';
@override
String get generateRequest => 'Genera richiesta';
@override
String get generatingRequest => 'Generazione in corso...';
@override
String get requestPaymentReceived => 'Pagamento ricevuto';
@override
String get requestDescriptionHint => 'Descrizione (opzionale)';
@override
String get universal => 'Universale';
@override
String get copiedToClipboard => 'Copiato negli appunti';
@override
String get swap => 'Scambia';
@override
String get swapDescription => 'Converti tra sats e USD';
@override
String get swapFrom => 'Da';
@override
String get swapTo => 'A';
@override
String get swapAction => 'Scambia';
@override
String get swapEstimatedFee => 'Commissione stimata';
@override
String get swapUseAll => 'Usa tutto';
@override
String swapMinimum(String amount) {
return 'Minimo: $amount';
}
@override
String get swapProcessing => 'Swap in corso...';
@override
String get swapSuccess => 'Swap completato';
@override
String get swapErrorInsufficient => 'Saldo insufficiente';
@override
String get swapErrorExpired => 'Il preventivo è scaduto';
@override
String swapErrorGeneric(String error) {
return 'Errore swap: $error';
}
@override
String get swapChartUnavailable =>
'Prezzo non disponibile · Tocca per riprovare';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Min: $minPrice — Max: $maxPrice';
}
@override
String get privacyPolicy => 'Informativa sulla privacy';
@override
String get privacyTitle => 'NON RACCOGLIAMO NULLA';
@override
String get privacyGoodbye => 'CIAO';
@override
String get privacyKeepReading => '(continua a leggere se vuoi…)';
@override
String get privacyBody =>
'Non sappiamo chi sei\nNon sappiamo quanto hai\nNon sappiamo cosa fai';
@override
String get privacyConclusion =>
'Il modo migliore per proteggere i tuoi dati\nè non averli';
}
+231 -10
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -69,7 +69,8 @@ class L10nJa extends L10n {
String get walletCreated => 'ウォレット作成完了!';
@override
String get walletCreatedDescription => 'ウォレットの準備ができました。今すぐシードフレーズをバックアップすることをお勧めします。';
String get walletCreatedDescription =>
'ウォレットの準備ができました。今すぐシードフレーズをバックアップすることをお勧めします。';
@override
String get backupWarning => 'バックアップがないと、デバイスを紛失した場合に資金にアクセスできなくなります。';
@@ -148,10 +149,10 @@ class L10nJa extends L10n {
String get send => '送金';
@override
String get sendAction => '送金';
String get sendAction => '送金';
@override
String get receiveAction => '受取';
String get receiveAction => '受取';
@override
String get deposit => '入金';
@@ -207,6 +208,36 @@ class L10nJa extends L10n {
@override
String get pasteFromClipboard => 'クリップボードから貼り付け';
@override
String get emptyClipboard => 'クリップボードは空です';
@override
String get cancelSend => '送金をキャンセル';
@override
String get cancelSendConfirmTitle => 'この送金をキャンセルしますか?';
@override
String get cancelSendConfirmBody =>
'受取人がまだトークンを引き換えていない場合のみ機能します。既に引き換え済みの場合、送金は完了として記録されます。';
@override
String cancelSendSuccess(String amount, String unit) {
return '送金をキャンセルしました。$amount $unitを回復しました';
}
@override
String get cancelSendAlreadyClaimed => '受取人は既にトークンを引き換えました';
@override
String get pendingOfflineSend => '保留中のオフライン送金';
@override
String get pendingOfflineSendsHeader => '未請求のオフライン送金';
@override
String get pendingSendDetailTitle => '保留中の送金';
@override
String get validToken => '有効なトークン';
@@ -287,6 +318,9 @@ class L10nJa extends L10n {
@override
String get insufficientBalance => '残高不足';
@override
String get feeExceedsAmount => '手数料が送金額を超えています';
@override
String tokenCreationError(String error) {
return 'トークン作成エラー:$error';
@@ -315,6 +349,43 @@ class L10nJa extends L10n {
@override
String get tokenCopiedToClipboard => 'トークンをクリップボードにコピーしました';
@override
String get copyAsEmoji => '絵文字としてコピー';
@override
String get emojiCopiedToClipboard => 'トークンを絵文字としてコピーしました 🥜';
@override
String get peanutDecodeError => '絵文字トークンをデコードできませんでした。破損している可能性があります。';
@override
String get nfcWrite => 'NFCタグに書き込む';
@override
String get nfcRead => 'NFCタグを読み取る';
@override
String get nfcHoldNear => 'デバイスをNFCタグに近づけてください...';
@override
String get nfcWriteSuccess => 'トークンをNFCタグに書き込みました';
@override
String nfcWriteError(String error) {
return 'NFC書き込みエラー: $error';
}
@override
String nfcReadError(String error) {
return 'NFC読み取りエラー: $error';
}
@override
String get nfcDisabled => 'NFCが無効です。設定で有効にしてください。';
@override
String get nfcUnsupported => 'このデバイスはNFCに対応していません';
@override
String get amountToDeposit => '入金額:';
@@ -473,6 +544,12 @@ class L10nJa extends L10n {
@override
String get sentStatus => '送金済み';
@override
String get receiving => '受取中';
@override
String get sending => '送金中';
@override
String get now => 'たった今';
@@ -665,7 +742,7 @@ class L10nJa extends L10n {
String get close => '閉じる';
@override
String get aboutDescription => 'キューバのDNAを持つ世界のためのCashuウォレット。La Chispaの兄弟。';
String get aboutDescription => 'キューバのDNAを持つ世界のためのCashuウォレット。LaChispaの兄弟。';
@override
String get couldNotOpenLink => 'リンクを開けませんでした';
@@ -677,7 +754,8 @@ class L10nJa extends L10n {
String get actionIrreversible => 'この操作は取り消せません';
@override
String get deleteWalletWarning => 'シードフレーズとトークンを含むすべてのデータが削除されます。バックアップがあることを確認してください。';
String get deleteWalletWarning =>
'シードフレーズとトークンを含むすべてのデータが削除されます。バックアップがあることを確認してください。';
@override
String get typeDeleteToConfirm => '確認のため「DELETE」と入力:';
@@ -694,7 +772,8 @@ class L10nJa extends L10n {
String get recoverTokensTitle => 'トークンを復元';
@override
String get recoverTokensDescription => 'シードフレーズに関連付けられたトークンを復元するためにMintをスキャン(NUT-13)';
String get recoverTokensDescription =>
'シードフレーズに関連付けられたトークンを復元するためにMintをスキャン(NUT-13)';
@override
String get useCurrentSeedPhrase => '現在のシードフレーズを使用';
@@ -899,7 +978,8 @@ class L10nJa extends L10n {
String get noConnectionTokenSaved => '接続なし。トークンを保存しました。後で請求できます。';
@override
String get unknownMintOffline => 'このトークンは不明なMintからのものです。インターネットに接続してMintを追加し、トークンを請求してください。';
String get unknownMintOffline =>
'このトークンは不明なMintからのものです。インターネットに接続してMintを追加し、トークンを請求してください。';
@override
String get noConnectionTryLater => 'Mintに接続できません。後でもう一度お試しください。';
@@ -997,7 +1077,51 @@ class L10nJa extends L10n {
String get cameraPermissionDenied => 'カメラの許可が拒否されました';
@override
String get paymentRequestNotSupported => '支払いリクエストはまだサポートされていません';
String get paymentRequestTitle => '支払いリクエスト';
@override
String get paymentRequestFrom => 'リクエスト元';
@override
String get paymentRequestAmount => 'リクエスト金額';
@override
String get paymentRequestDescription => '説明';
@override
String get paymentRequestMints => '対応ミント';
@override
String get paymentRequestAnyMint => 'すべてのミント';
@override
String get paymentRequestPay => '支払う';
@override
String get paymentRequestPaying => '支払い中...';
@override
String get paymentRequestSuccess => '支払いが正常に送信されました';
@override
String get paymentRequestNoTransport => 'このリクエストには配信方法が設定されていません';
@override
String get paymentRequestTransport => 'トランスポート';
@override
String get paymentRequestMintNotAccepted => 'アクティブなミントは対応ミントリストにありません';
@override
String paymentRequestUnitMismatch(String unit) {
return '単位が互換性がありません:リクエストには$unitが必要です';
}
@override
String get paymentRequestInsufficientBalance => '残高不足';
@override
String get paymentRequestErrorParsing => '支払いリクエストの読み取りエラー';
@override
String get p2pkTitle => 'P2PK鍵';
@@ -1009,7 +1133,8 @@ class L10nJa extends L10n {
String get p2pkExperimental => 'P2PKは実験的機能です。注意してご使用ください。';
@override
String get p2pkPendingSendWarning => '保留中のP2PK送信があります。受取人がトークンを受け取った後、履歴で更新してください。';
String get p2pkPendingSendWarning =>
'保留中のP2PK送信があります。受取人がトークンを受け取った後、履歴で更新してください。';
@override
String get p2pkExperimentalShort => '実験的';
@@ -1094,4 +1219,100 @@ class L10nJa extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => 'プライマリ鍵は削除できません';
@override
String get request => 'リクエスト';
@override
String get requestPayment => '支払いをリクエスト';
@override
String get requestPaymentDescription => '統合支払いリクエストを生成';
@override
String get generateRequest => 'リクエストを生成';
@override
String get generatingRequest => '生成中...';
@override
String get requestPaymentReceived => '支払いを受け取りました';
@override
String get requestDescriptionHint => '説明(任意)';
@override
String get universal => 'ユニバーサル';
@override
String get copiedToClipboard => 'クリップボードにコピーしました';
@override
String get swap => '交換';
@override
String get swapDescription => 'SatsとUSDを変換';
@override
String get swapFrom => '送信元';
@override
String get swapTo => '送信先';
@override
String get swapAction => '交換';
@override
String get swapEstimatedFee => '推定手数料';
@override
String get swapUseAll => '全額使用';
@override
String swapMinimum(String amount) {
return '最小: $amount';
}
@override
String get swapProcessing => 'スワップ処理中...';
@override
String get swapSuccess => 'スワップ完了';
@override
String get swapErrorInsufficient => '残高不足';
@override
String get swapErrorExpired => '見積もりの有効期限切れ';
@override
String swapErrorGeneric(String error) {
return 'スワップエラー: $error';
}
@override
String get swapChartUnavailable => '価格を取得できません・タップで再試行';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h 安値: $minPrice — 高値: $maxPrice';
}
@override
String get privacyPolicy => 'プライバシーポリシー';
@override
String get privacyTitle => '何も収集しません';
@override
String get privacyGoodbye => 'さようなら';
@override
String get privacyKeepReading => '(読み続けたければどうぞ…)';
@override
String get privacyBody => 'あなたが誰か知りません\nいくら持っているか知りません\n何をしているか知りません';
@override
String get privacyConclusion => 'データを守る最善の方法は\nデータを持たないことです';
}
+235 -12
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nKo extends L10n {
String get generatingSeed => '시드 문구를 안전하게 생성 중';
@override
String get createWalletDescription => '12개의 단어로 된 시드 문구가 생성됩니다.\n안전한 곳에 보관하세요.';
String get createWalletDescription =>
'12개의 단어로 된 시드 문구가 생성됩니다.\n안전한 곳에 보관하세요.';
@override
String get generateWallet => '지갑 생성';
@@ -69,7 +70,8 @@ class L10nKo extends L10n {
String get walletCreated => '지갑이 생성되었습니다!';
@override
String get walletCreatedDescription => '지갑이 준비되었습니다. 지금 시드 문구를 백업하는 것을 권장합니다.';
String get walletCreatedDescription =>
'지갑이 준비되었습니다. 지금 시드 문구를 백업하는 것을 권장합니다.';
@override
String get backupWarning => '백업 없이는 기기를 분실하면 자금에 접근할 수 없습니다.';
@@ -87,7 +89,8 @@ class L10nKo extends L10n {
String get seedPhraseTitle => '시드 문구';
@override
String get seedPhraseDescription => '이 12개의 단어를 순서대로 저장하세요. 지갑을 복구하는 유일한 방법입니다.';
String get seedPhraseDescription =>
'이 12개의 단어를 순서대로 저장하세요. 지갑을 복구하는 유일한 방법입니다.';
@override
String get revealSeedPhrase => '시드 문구 보기';
@@ -148,10 +151,10 @@ class L10nKo extends L10n {
String get send => '보내기';
@override
String get sendAction => '보내기';
String get sendAction => '보내기';
@override
String get receiveAction => '받기';
String get receiveAction => '받기';
@override
String get deposit => '입금';
@@ -207,6 +210,36 @@ class L10nKo extends L10n {
@override
String get pasteFromClipboard => '클립보드에서 붙여넣기';
@override
String get emptyClipboard => '클립보드가 비어 있음';
@override
String get cancelSend => '전송 취소';
@override
String get cancelSendConfirmTitle => '이 전송을 취소하시겠습니까?';
@override
String get cancelSendConfirmBody =>
'수신자가 아직 토큰을 받지 않은 경우에만 작동합니다. 이미 받았다면 전송이 완료로 표시됩니다.';
@override
String cancelSendSuccess(String amount, String unit) {
return '전송이 취소되었습니다. $amount $unit를 복구했습니다';
}
@override
String get cancelSendAlreadyClaimed => '수신자가 이미 토큰을 받았습니다';
@override
String get pendingOfflineSend => '대기 중인 오프라인 전송';
@override
String get pendingOfflineSendsHeader => '청구되지 않은 오프라인 전송';
@override
String get pendingSendDetailTitle => '대기 중인 전송';
@override
String get validToken => '유효한 토큰';
@@ -287,6 +320,9 @@ class L10nKo extends L10n {
@override
String get insufficientBalance => '잔액 부족';
@override
String get feeExceedsAmount => '수수료가 송금액을 초과합니다';
@override
String tokenCreationError(String error) {
return '토큰 생성 오류: $error';
@@ -315,6 +351,43 @@ class L10nKo extends L10n {
@override
String get tokenCopiedToClipboard => '토큰이 클립보드에 복사되었습니다';
@override
String get copyAsEmoji => '이모지로 복사';
@override
String get emojiCopiedToClipboard => '토큰이 이모지로 복사되었습니다 🥜';
@override
String get peanutDecodeError => '이모지 토큰을 디코딩할 수 없습니다. 손상되었을 수 있습니다.';
@override
String get nfcWrite => 'NFC 태그에 쓰기';
@override
String get nfcRead => 'NFC 태그 읽기';
@override
String get nfcHoldNear => '기기를 NFC 태그에 가까이 대세요...';
@override
String get nfcWriteSuccess => '토큰이 NFC 태그에 기록되었습니다';
@override
String nfcWriteError(String error) {
return 'NFC 쓰기 오류: $error';
}
@override
String nfcReadError(String error) {
return 'NFC 읽기 오류: $error';
}
@override
String get nfcDisabled => 'NFC가 비활성화되어 있습니다. 설정에서 활성화하세요.';
@override
String get nfcUnsupported => '이 기기는 NFC를 지원하지 않습니다';
@override
String get amountToDeposit => '입금할 금액:';
@@ -473,6 +546,12 @@ class L10nKo extends L10n {
@override
String get sentStatus => '보냄';
@override
String get receiving => '받는 중';
@override
String get sending => '보내는 중';
@override
String get now => '방금';
@@ -665,7 +744,7 @@ class L10nKo extends L10n {
String get close => '닫기';
@override
String get aboutDescription => '전 세계를 위한 쿠바 DNA를 가진 Cashu 지갑. La Chispa의 형제.';
String get aboutDescription => '전 세계를 위한 쿠바 DNA를 가진 Cashu 지갑. LaChispa의 형제.';
@override
String get couldNotOpenLink => '링크를 열 수 없음';
@@ -677,7 +756,8 @@ class L10nKo extends L10n {
String get actionIrreversible => '이 작업은 되돌릴 수 없습니다';
@override
String get deleteWalletWarning => '시드 문구와 토큰을 포함한 모든 데이터가 삭제됩니다. 백업이 있는지 확인하세요.';
String get deleteWalletWarning =>
'시드 문구와 토큰을 포함한 모든 데이터가 삭제됩니다. 백업이 있는지 확인하세요.';
@override
String get typeDeleteToConfirm => '확인하려면 \"삭제\"를 입력하세요:';
@@ -694,7 +774,8 @@ class L10nKo extends L10n {
String get recoverTokensTitle => '토큰 복구';
@override
String get recoverTokensDescription => '시드 문구와 연결된 토큰을 복구하기 위해 mint 스캔 (NUT-13)';
String get recoverTokensDescription =>
'시드 문구와 연결된 토큰을 복구하기 위해 mint 스캔 (NUT-13)';
@override
String get useCurrentSeedPhrase => '현재 시드 문구 사용';
@@ -899,7 +980,8 @@ class L10nKo extends L10n {
String get noConnectionTokenSaved => '연결 없음. 나중에 청구하기 위해 토큰이 저장되었습니다.';
@override
String get unknownMintOffline => '이 토큰은 알 수 없는 mint의 것입니다. 인터넷에 연결하여 추가하고 토큰을 청구하세요.';
String get unknownMintOffline =>
'이 토큰은 알 수 없는 mint의 것입니다. 인터넷에 연결하여 추가하고 토큰을 청구하세요.';
@override
String get noConnectionTryLater => 'Mint에 연결할 수 없음. 나중에 다시 시도하세요.';
@@ -997,7 +1079,51 @@ class L10nKo extends L10n {
String get cameraPermissionDenied => '카메라 권한이 거부되었습니다';
@override
String get paymentRequestNotSupported => '결제 요청은 아직 지원되지 않습니다';
String get paymentRequestTitle => '결제 요청';
@override
String get paymentRequestFrom => '요청자';
@override
String get paymentRequestAmount => '요청 금액';
@override
String get paymentRequestDescription => '설명';
@override
String get paymentRequestMints => '허용된 민트';
@override
String get paymentRequestAnyMint => '모든 민트';
@override
String get paymentRequestPay => '결제';
@override
String get paymentRequestPaying => '결제 중...';
@override
String get paymentRequestSuccess => '결제가 성공적으로 전송되었습니다';
@override
String get paymentRequestNoTransport => '이 요청에는 전달 방법이 설정되지 않았습니다';
@override
String get paymentRequestTransport => '전송 방식';
@override
String get paymentRequestMintNotAccepted => '활성 민트가 허용된 민트 목록에 없습니다';
@override
String paymentRequestUnitMismatch(String unit) {
return '호환되지 않는 단위: 요청에 $unit이(가) 필요합니다';
}
@override
String get paymentRequestInsufficientBalance => '잔액 부족';
@override
String get paymentRequestErrorParsing => '결제 요청을 읽는 중 오류 발생';
@override
String get p2pkTitle => 'P2PK 키';
@@ -1009,7 +1135,8 @@ class L10nKo extends L10n {
String get p2pkExperimental => 'P2PK는 실험적 기능입니다. 주의하여 사용하세요.';
@override
String get p2pkPendingSendWarning => '대기 중인 P2PK 전송이 있습니다. 수신자가 토큰을 수령한 후 기록에서 새로고침하세요.';
String get p2pkPendingSendWarning =>
'대기 중인 P2PK 전송이 있습니다. 수신자가 토큰을 수령한 후 기록에서 새로고침하세요.';
@override
String get p2pkExperimentalShort => '실험적';
@@ -1094,4 +1221,100 @@ class L10nKo extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => '기본 키는 삭제할 수 없습니다';
@override
String get request => '요청';
@override
String get requestPayment => '결제 요청';
@override
String get requestPaymentDescription => '통합 결제 요청 생성';
@override
String get generateRequest => '요청 생성';
@override
String get generatingRequest => '생성 중...';
@override
String get requestPaymentReceived => '결제가 수신되었습니다';
@override
String get requestDescriptionHint => '설명 (선택사항)';
@override
String get universal => '유니버설';
@override
String get copiedToClipboard => '클립보드에 복사되었습니다';
@override
String get swap => '교환';
@override
String get swapDescription => 'Sats와 USD 간 변환';
@override
String get swapFrom => '에서';
@override
String get swapTo => '으로';
@override
String get swapAction => '교환';
@override
String get swapEstimatedFee => '예상 수수료';
@override
String get swapUseAll => '전액 사용';
@override
String swapMinimum(String amount) {
return '최소: $amount';
}
@override
String get swapProcessing => '교환 처리 중...';
@override
String get swapSuccess => '교환 완료';
@override
String get swapErrorInsufficient => '잔액 부족';
@override
String get swapErrorExpired => '견적이 만료되었습니다';
@override
String swapErrorGeneric(String error) {
return '교환 오류: $error';
}
@override
String get swapChartUnavailable => '가격 불러오기 실패 · 탭하여 재시도';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h 최저: $minPrice — 최고: $maxPrice';
}
@override
String get privacyPolicy => '개인정보 처리방침';
@override
String get privacyTitle => '우리는 아무것도 수집하지 않습니다';
@override
String get privacyGoodbye => '안녕';
@override
String get privacyKeepReading => '(계속 읽고 싶으면…)';
@override
String get privacyBody => '당신이 누구인지 모릅니다\n얼마를 가지고 있는지 모릅니다\n무엇을 하는지 모릅니다';
@override
String get privacyConclusion => '데이터를 보호하는 가장 좋은 방법은\n데이터를 갖지 않는 것입니다';
}
+282 -31
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nPt extends L10n {
String get generatingSeed => 'Gerando sua frase semente de forma segura';
@override
String get createWalletDescription => 'Uma frase semente de 12 palavras será gerada.\nGuarde-a em um lugar seguro.';
String get createWalletDescription =>
'Uma frase semente de 12 palavras será gerada.\nGuarde-a em um lugar seguro.';
@override
String get generateWallet => 'Gerar wallet';
@@ -69,10 +70,12 @@ class L10nPt extends L10n {
String get walletCreated => 'Wallet criada!';
@override
String get walletCreatedDescription => 'Sua wallet está pronta. Recomendamos fazer backup da sua frase semente agora.';
String get walletCreatedDescription =>
'Sua wallet está pronta. Recomendamos fazer backup da sua frase semente agora.';
@override
String get backupWarning => 'Sem backup, você perderá acesso aos seus fundos se perder o dispositivo.';
String get backupWarning =>
'Sem backup, você perderá acesso aos seus fundos se perder o dispositivo.';
@override
String get backupNow => 'Fazer backup agora';
@@ -87,7 +90,8 @@ class L10nPt extends L10n {
String get seedPhraseTitle => 'Sua frase semente';
@override
String get seedPhraseDescription => 'Guarde estas 12 palavras em ordem. Elas são a única forma de recuperar sua wallet.';
String get seedPhraseDescription =>
'Guarde estas 12 palavras em ordem. Elas são a única forma de recuperar sua wallet.';
@override
String get revealSeedPhrase => 'Revelar frase semente';
@@ -102,7 +106,8 @@ class L10nPt extends L10n {
String get seedCopied => 'Frase copiada para área de transferência';
@override
String get neverShareSeed => 'Nunca compartilhe sua frase semente com ninguém.';
String get neverShareSeed =>
'Nunca compartilhe sua frase semente com ninguém.';
@override
String get confirmBackup => 'Guardei minha frase semente em um lugar seguro';
@@ -117,7 +122,8 @@ class L10nPt extends L10n {
String get enterSeedPhrase => 'Digite sua frase semente';
@override
String get enterSeedDescription => 'Digite as 12 ou 24 palavras separadas por espaços.';
String get enterSeedDescription =>
'Digite as 12 ou 24 palavras separadas por espaços.';
@override
String get seedPlaceholder => 'palavra1 palavra2 palavra3 ...';
@@ -148,10 +154,10 @@ class L10nPt extends L10n {
String get send => 'Enviar';
@override
String get sendAction => 'Enviar ↗';
String get sendAction => 'Enviar';
@override
String get receiveAction => '↘ Receber';
String get receiveAction => 'Receber';
@override
String get deposit => 'Depositar';
@@ -207,6 +213,36 @@ class L10nPt extends L10n {
@override
String get pasteFromClipboard => 'Colar da área de transferência';
@override
String get emptyClipboard => 'Área de transferência vazia';
@override
String get cancelSend => 'Cancelar envio';
@override
String get cancelSendConfirmTitle => 'Cancelar este envio?';
@override
String get cancelSendConfirmBody =>
'Só funciona se o destinatário ainda não resgatou o token. Se já resgatou, o envio será marcado como concluído.';
@override
String cancelSendSuccess(String amount, String unit) {
return 'Envio cancelado. Recuperou $amount $unit';
}
@override
String get cancelSendAlreadyClaimed => 'O destinatário já resgatou o token';
@override
String get pendingOfflineSend => 'Envio offline pendente';
@override
String get pendingOfflineSendsHeader => 'Envios offline não resgatados';
@override
String get pendingSendDetailTitle => 'Envio pendente';
@override
String get validToken => 'Token válido';
@@ -287,6 +323,9 @@ class L10nPt extends L10n {
@override
String get insufficientBalance => 'Saldo insuficiente';
@override
String get feeExceedsAmount => 'A taxa excede o valor a enviar';
@override
String tokenCreationError(String error) {
return 'Erro ao criar token: $error';
@@ -310,10 +349,50 @@ class L10nPt extends L10n {
}
@override
String get keepTokenWarning => 'Guarde este token até que o destinatário o resgate. Se você perdê-lo, perderá os fundos.';
String get keepTokenWarning =>
'Guarde este token até que o destinatário o resgate. Se você perdê-lo, perderá os fundos.';
@override
String get tokenCopiedToClipboard => 'Token copiado para área de transferência';
String get tokenCopiedToClipboard =>
'Token copiado para área de transferência';
@override
String get copyAsEmoji => 'Copiar como emoji';
@override
String get emojiCopiedToClipboard => 'Token copiado como emoji 🥜';
@override
String get peanutDecodeError =>
'Não foi possível decodificar o token emoji. Pode estar corrompido.';
@override
String get nfcWrite => 'Escrever em tag NFC';
@override
String get nfcRead => 'Ler tag NFC';
@override
String get nfcHoldNear => 'Aproxime o dispositivo da tag NFC...';
@override
String get nfcWriteSuccess => 'Token escrito na tag NFC';
@override
String nfcWriteError(String error) {
return 'Erro NFC ao escrever: $error';
}
@override
String nfcReadError(String error) {
return 'Erro NFC ao ler: $error';
}
@override
String get nfcDisabled => 'NFC está desativado. Ative nas Configurações.';
@override
String get nfcUnsupported => 'Este dispositivo não suporta NFC';
@override
String get amountToDeposit => 'Valor a depositar:';
@@ -364,7 +443,8 @@ class L10nPt extends L10n {
String get description => 'Descrição:';
@override
String get invoiceCopiedToClipboard => 'Invoice copiado para área de transferência';
String get invoiceCopiedToClipboard =>
'Invoice copiado para área de transferência';
@override
String deposited(String amount, String unit) {
@@ -450,7 +530,8 @@ class L10nPt extends L10n {
String get noPendingTransactions => 'Sem transações pendentes';
@override
String get allTransactionsCompleted => 'Todas as suas transações estão completas';
String get allTransactionsCompleted =>
'Todas as suas transações estão completas';
@override
String get noEcashTransactions => 'Sem transações Ecash';
@@ -473,6 +554,12 @@ class L10nPt extends L10n {
@override
String get sentStatus => 'Enviado';
@override
String get receiving => 'Recebendo';
@override
String get sending => 'Enviando';
@override
String get now => 'Agora';
@@ -665,7 +752,8 @@ class L10nPt extends L10n {
String get close => 'Fechar';
@override
String get aboutDescription => 'Uma wallet Cashu com DNA cubano para o mundo inteiro. Irmã de La Chispa.';
String get aboutDescription =>
'Uma wallet Cashu com DNA cubano para o mundo inteiro. Irmã de LaChispa.';
@override
String get couldNotOpenLink => 'Não foi possível abrir o link';
@@ -677,7 +765,8 @@ class L10nPt extends L10n {
String get actionIrreversible => 'Esta ação é irreversível';
@override
String get deleteWalletWarning => 'Todos os dados serão excluídos, incluindo sua frase semente e tokens. Certifique-se de ter um backup.';
String get deleteWalletWarning =>
'Todos os dados serão excluídos, incluindo sua frase semente e tokens. Certifique-se de ter um backup.';
@override
String get typeDeleteToConfirm => 'Digite \"APAGAR\" para confirmar:';
@@ -694,7 +783,8 @@ class L10nPt extends L10n {
String get recoverTokensTitle => 'Recuperar tokens';
@override
String get recoverTokensDescription => 'Escanear mints para recuperar tokens associados à sua frase semente (NUT-13)';
String get recoverTokensDescription =>
'Escanear mints para recuperar tokens associados à sua frase semente (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Usar minha frase semente atual';
@@ -720,7 +810,8 @@ class L10nPt extends L10n {
String get specificMint => 'Um mint específico';
@override
String get enterMnemonicWords => 'Digite as 12 palavras separadas por espaços...';
String get enterMnemonicWords =>
'Digite as 12 palavras separadas por espaços...';
@override
String get scanMints => 'Escanear mints';
@@ -740,7 +831,8 @@ class L10nPt extends L10n {
}
@override
String get scanCompleteNoTokens => 'Escaneamento completo. Nenhum token novo encontrado.';
String get scanCompleteNoTokens =>
'Escaneamento completo. Nenhum token novo encontrado.';
@override
String mintsWithError(int count) {
@@ -763,7 +855,8 @@ class L10nPt extends L10n {
}
@override
String get noTokensForMnemonic => 'Nenhum token encontrado associado a esse mnemônico.';
String get noTokensForMnemonic =>
'Nenhum token encontrado associado a esse mnemônico.';
@override
String get noConnectedMints => 'Nenhum mint conectado';
@@ -843,7 +936,8 @@ class L10nPt extends L10n {
String get deleteMintConfirmTitle => 'Excluir mint';
@override
String get deleteMintConfirmMessage => 'Se você tiver saldo neste mint, ele será perdido. Tem certeza?';
String get deleteMintConfirmMessage =>
'Se você tiver saldo neste mint, ele será perdido. Tem certeza?';
@override
String get delete => 'Excluir';
@@ -896,10 +990,12 @@ class L10nPt extends L10n {
String get tokenSavedForLater => 'Token salvo para resgatar depois';
@override
String get noConnectionTokenSaved => 'Sem conexão. Token salvo para resgatar depois.';
String get noConnectionTokenSaved =>
'Sem conexão. Token salvo para resgatar depois.';
@override
String get unknownMintOffline => 'Este token é de um mint desconhecido. Conecte-se à internet para adicioná-lo e resgatar o token.';
String get unknownMintOffline =>
'Este token é de um mint desconhecido. Conecte-se à internet para adicioná-lo e resgatar o token.';
@override
String get noConnectionTryLater => 'Sem conexão ao mint. Tente mais tarde.';
@@ -908,7 +1004,8 @@ class L10nPt extends L10n {
String get saveTokenError => 'Erro ao salvar o token. Tente novamente.';
@override
String get pendingTokenLimitReached => 'Limite de tokens pendentes atingido (máx 50)';
String get pendingTokenLimitReached =>
'Limite de tokens pendentes atingido (máx 50)';
@override
String get filterToReceive => 'Para receber';
@@ -985,10 +1082,12 @@ class L10nPt extends L10n {
String get unrecognizedQrCode => 'Código QR não reconhecido';
@override
String get scanCashuTokenHint => 'Escaneie um token Cashu (cashuA... ou cashuB...)';
String get scanCashuTokenHint =>
'Escaneie um token Cashu (cashuA... ou cashuB...)';
@override
String get scanLightningInvoiceHint => 'Escaneie um invoice Lightning (lnbc...)';
String get scanLightningInvoiceHint =>
'Escaneie um invoice Lightning (lnbc...)';
@override
String get addMintQuestion => 'Adicionar este mint?';
@@ -997,7 +1096,54 @@ class L10nPt extends L10n {
String get cameraPermissionDenied => 'Permissão de câmera negada';
@override
String get paymentRequestNotSupported => 'Solicitações de pagamento ainda não são suportadas';
String get paymentRequestTitle => 'Solicitação de pagamento';
@override
String get paymentRequestFrom => 'Solicitação de';
@override
String get paymentRequestAmount => 'Valor solicitado';
@override
String get paymentRequestDescription => 'Descrição';
@override
String get paymentRequestMints => 'Mints aceitos';
@override
String get paymentRequestAnyMint => 'Qualquer mint';
@override
String get paymentRequestPay => 'Pagar';
@override
String get paymentRequestPaying => 'Pagando...';
@override
String get paymentRequestSuccess => 'Pagamento enviado com sucesso';
@override
String get paymentRequestNoTransport =>
'Esta solicitação não tem método de entrega configurado';
@override
String get paymentRequestTransport => 'Transporte';
@override
String get paymentRequestMintNotAccepted =>
'Seu mint ativo não está na lista de mints aceitos';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Unidade incompatível: a solicitação requer $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Saldo insuficiente';
@override
String get paymentRequestErrorParsing =>
'Erro ao ler a solicitação de pagamento';
@override
String get p2pkTitle => 'Chaves P2PK';
@@ -1009,7 +1155,8 @@ class L10nPt extends L10n {
String get p2pkExperimental => 'P2PK é experimental. Use com cautela.';
@override
String get p2pkPendingSendWarning => 'Você tem um envio P2PK pendente. Vá ao histórico e atualize após o destinatário resgatar o token.';
String get p2pkPendingSendWarning =>
'Você tem um envio P2PK pendente. Vá ao histórico e atualize após o destinatário resgatar o token.';
@override
String get p2pkExperimentalShort => 'Experimental';
@@ -1066,7 +1213,8 @@ class L10nPt extends L10n {
String get p2pkLockedToOther => 'Bloqueado para outra chave';
@override
String get p2pkCannotUnlock => 'Você não tem a chave para desbloquear este token';
String get p2pkCannotUnlock =>
'Você não tem a chave para desbloquear este token';
@override
String get p2pkEnterPrivateKey => 'Digite a chave privada (nsec)';
@@ -1075,13 +1223,15 @@ class L10nPt extends L10n {
String get p2pkDeleteTitle => 'Excluir chave';
@override
String get p2pkDeleteConfirm => 'Excluir esta chave? Você não poderá receber tokens bloqueados para ela.';
String get p2pkDeleteConfirm =>
'Excluir esta chave? Você não poderá receber tokens bloqueados para ela.';
@override
String get p2pkRequiresConnection => 'P2PK requer conexão com o mint';
@override
String get p2pkErrorMaxKeysReached => 'Número máximo de chaves importadas atingido (10)';
String get p2pkErrorMaxKeysReached =>
'Número máximo de chaves importadas atingido (10)';
@override
String get p2pkErrorInvalidNsec => 'nsec inválido';
@@ -1093,5 +1243,106 @@ class L10nPt extends L10n {
String get p2pkErrorKeyNotFound => 'Chave não encontrada';
@override
String get p2pkErrorCannotDeletePrimary => 'Não é possível excluir a chave principal';
String get p2pkErrorCannotDeletePrimary =>
'Não é possível excluir a chave principal';
@override
String get request => 'Solicitar';
@override
String get requestPayment => 'Solicitar pagamento';
@override
String get requestPaymentDescription =>
'Gerar solicitação de pagamento unificada';
@override
String get generateRequest => 'Gerar solicitação';
@override
String get generatingRequest => 'Gerando solicitação...';
@override
String get requestPaymentReceived => 'Pagamento recebido';
@override
String get requestDescriptionHint => 'Descrição (opcional)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'Copiado para a área de transferência';
@override
String get swap => 'Trocar';
@override
String get swapDescription => 'Converter entre sats e USD';
@override
String get swapFrom => 'De';
@override
String get swapTo => 'Para';
@override
String get swapAction => 'Trocar';
@override
String get swapEstimatedFee => 'Taxa estimada';
@override
String get swapUseAll => 'Usar tudo';
@override
String swapMinimum(String amount) {
return 'Mínimo: $amount';
}
@override
String get swapProcessing => 'Processando swap...';
@override
String get swapSuccess => 'Swap concluído';
@override
String get swapErrorInsufficient => 'Saldo insuficiente';
@override
String get swapErrorExpired => 'A cotação expirou';
@override
String swapErrorGeneric(String error) {
return 'Erro no swap: $error';
}
@override
String get swapChartUnavailable =>
'Preço indisponível · Toque para tentar novamente';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Mín: $minPrice — Máx: $maxPrice';
}
@override
String get privacyPolicy => 'Política de privacidade';
@override
String get privacyTitle => 'NÃO RECOLHEMOS NADA';
@override
String get privacyGoodbye => 'ADEUS';
@override
String get privacyKeepReading => '(continue lendo se quiser…)';
@override
String get privacyBody =>
'Não sabemos quem você é\nNão sabemos quanto você tem\nNão sabemos o que você faz';
@override
String get privacyConclusion =>
'A melhor forma de proteger seus dados\né não tê-los';
}
+274 -28
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -33,7 +33,8 @@ class L10nRu extends L10n {
String get loadingMessage6 => 'Go full Calle...';
@override
String get loadingMessage7 => 'Cashu + Bitchat = Конфиденциальность + Свобода';
String get loadingMessage7 =>
'Cashu + Bitchat = Конфиденциальность + Свобода';
@override
String get aboutTagline => 'Конфиденциальность без границ.';
@@ -60,7 +61,8 @@ class L10nRu extends L10n {
String get generatingSeed => 'Безопасная генерация вашей сид-фразы';
@override
String get createWalletDescription => 'Будет сгенерирована сид-фраза из 12 слов.\nСохраните её в безопасном месте.';
String get createWalletDescription =>
'Будет сгенерирована сид-фраза из 12 слов.\nСохраните её в безопасном месте.';
@override
String get generateWallet => 'Создать кошелёк';
@@ -69,10 +71,12 @@ class L10nRu extends L10n {
String get walletCreated => 'Кошелёк создан!';
@override
String get walletCreatedDescription => 'Ваш кошелёк готов. Рекомендуем сделать резервную копию сид-фразы сейчас.';
String get walletCreatedDescription =>
'Ваш кошелёк готов. Рекомендуем сделать резервную копию сид-фразы сейчас.';
@override
String get backupWarning => 'Без резервной копии вы потеряете доступ к средствам при потере устройства.';
String get backupWarning =>
'Без резервной копии вы потеряете доступ к средствам при потере устройства.';
@override
String get backupNow => 'Сделать резервную копию';
@@ -87,7 +91,8 @@ class L10nRu extends L10n {
String get seedPhraseTitle => 'Ваша сид-фраза';
@override
String get seedPhraseDescription => 'Сохраните эти 12 слов по порядку. Это единственный способ восстановить кошелёк.';
String get seedPhraseDescription =>
'Сохраните эти 12 слов по порядку. Это единственный способ восстановить кошелёк.';
@override
String get revealSeedPhrase => 'Показать сид-фразу';
@@ -148,10 +153,10 @@ class L10nRu extends L10n {
String get send => 'Отправить';
@override
String get sendAction => 'Отправить';
String get sendAction => 'Отправить';
@override
String get receiveAction => 'Получить';
String get receiveAction => 'Получить';
@override
String get deposit => 'Пополнить';
@@ -207,6 +212,36 @@ class L10nRu extends L10n {
@override
String get pasteFromClipboard => 'Вставить из буфера обмена';
@override
String get emptyClipboard => 'Буфер обмена пуст';
@override
String get cancelSend => 'Отменить отправку';
@override
String get cancelSendConfirmTitle => 'Отменить эту отправку?';
@override
String get cancelSendConfirmBody =>
'Работает только если получатель еще не получил токен. Если уже получил, отправка будет помечена как завершенная.';
@override
String cancelSendSuccess(String amount, String unit) {
return 'Отправка отменена. Восстановлено $amount $unit';
}
@override
String get cancelSendAlreadyClaimed => 'Получатель уже получил токен';
@override
String get pendingOfflineSend => 'Ожидающая оффлайн-отправка';
@override
String get pendingOfflineSendsHeader => 'Неполученные оффлайн-отправки';
@override
String get pendingSendDetailTitle => 'Ожидающая отправка';
@override
String get validToken => 'Токен действителен';
@@ -287,6 +322,9 @@ class L10nRu extends L10n {
@override
String get insufficientBalance => 'Недостаточный баланс';
@override
String get feeExceedsAmount => 'Комиссия превышает сумму отправки';
@override
String tokenCreationError(String error) {
return 'Ошибка создания токена: $error';
@@ -310,11 +348,50 @@ class L10nRu extends L10n {
}
@override
String get keepTokenWarning => 'Сохраните этот токен, пока получатель не заберёт его. Если потеряете — потеряете средства.';
String get keepTokenWarning =>
'Сохраните этот токен, пока получатель не заберёт его. Если потеряете — потеряете средства.';
@override
String get tokenCopiedToClipboard => 'Токен скопирован в буфер обмена';
@override
String get copyAsEmoji => 'Скопировать как эмодзи';
@override
String get emojiCopiedToClipboard => 'Токен скопирован как эмодзи 🥜';
@override
String get peanutDecodeError =>
'Не удалось декодировать эмодзи-токен. Возможно, он повреждён.';
@override
String get nfcWrite => 'Записать на NFC-метку';
@override
String get nfcRead => 'Читать NFC-метку';
@override
String get nfcHoldNear => 'Поднесите устройство к NFC-метке...';
@override
String get nfcWriteSuccess => 'Токен записан на NFC-метку';
@override
String nfcWriteError(String error) {
return 'Ошибка записи NFC: $error';
}
@override
String nfcReadError(String error) {
return 'Ошибка чтения NFC: $error';
}
@override
String get nfcDisabled => 'NFC отключён. Включите в Настройках.';
@override
String get nfcUnsupported => 'Это устройство не поддерживает NFC';
@override
String get amountToDeposit => 'Сумма для пополнения:';
@@ -384,7 +461,8 @@ class L10nRu extends L10n {
String get invalidInvoice => 'Недействительный счёт';
@override
String get invalidInvoiceMalformed => 'Недействительный или повреждённый счёт';
String get invalidInvoiceMalformed =>
'Недействительный или повреждённый счёт';
@override
String get feeReserved => 'Зарезервированная комиссия:';
@@ -462,7 +540,8 @@ class L10nRu extends L10n {
String get noLightningTransactions => 'Нет Lightning транзакций';
@override
String get depositOrWithdrawLightning => 'Пополните или выведите через Lightning';
String get depositOrWithdrawLightning =>
'Пополните или выведите через Lightning';
@override
String get pendingStatus => 'Ожидание';
@@ -473,6 +552,12 @@ class L10nRu extends L10n {
@override
String get sentStatus => 'Отправлено';
@override
String get receiving => 'Получение';
@override
String get sending => 'Отправка';
@override
String get now => 'Сейчас';
@@ -665,7 +750,8 @@ class L10nRu extends L10n {
String get close => 'Закрыть';
@override
String get aboutDescription => 'Cashu кошелёк с кубинской ДНК для всего мира. Брат La Chispa.';
String get aboutDescription =>
'Cashu кошелёк с кубинской ДНК для всего мира. Брат LaChispa.';
@override
String get couldNotOpenLink => 'Не удалось открыть ссылку';
@@ -677,7 +763,8 @@ class L10nRu extends L10n {
String get actionIrreversible => 'Это действие необратимо';
@override
String get deleteWalletWarning => 'Все данные будут удалены, включая сид-фразу и токены. Убедитесь, что у вас есть резервная копия.';
String get deleteWalletWarning =>
'Все данные будут удалены, включая сид-фразу и токены. Убедитесь, что у вас есть резервная копия.';
@override
String get typeDeleteToConfirm => 'Введите \"УДАЛИТЬ\" для подтверждения:';
@@ -694,7 +781,8 @@ class L10nRu extends L10n {
String get recoverTokensTitle => 'Восстановить токены';
@override
String get recoverTokensDescription => 'Сканировать mint для восстановления токенов, связанных с вашей сид-фразой (NUT-13)';
String get recoverTokensDescription =>
'Сканировать mint для восстановления токенов, связанных с вашей сид-фразой (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Использовать текущую сид-фразу';
@@ -729,7 +817,8 @@ class L10nRu extends L10n {
String get selectMintToScan => 'Выберите mint для сканирования';
@override
String get mnemonicMustHaveWords => 'Мнемоника должна содержать 12 или 24 слова';
String get mnemonicMustHaveWords =>
'Мнемоника должна содержать 12 или 24 слова';
@override
String get noConnectedMintsToScan => 'Нет подключённых mint для сканирования';
@@ -740,7 +829,8 @@ class L10nRu extends L10n {
}
@override
String get scanCompleteNoTokens => 'Сканирование завершено. Новых токенов не найдено.';
String get scanCompleteNoTokens =>
'Сканирование завершено. Новых токенов не найдено.';
@override
String mintsWithError(int count) {
@@ -763,7 +853,8 @@ class L10nRu extends L10n {
}
@override
String get noTokensForMnemonic => 'Токены, связанные с этой мнемоникой, не найдены.';
String get noTokensForMnemonic =>
'Токены, связанные с этой мнемоникой, не найдены.';
@override
String get noConnectedMints => 'Нет подключённых mint';
@@ -843,7 +934,8 @@ class L10nRu extends L10n {
String get deleteMintConfirmTitle => 'Удалить mint';
@override
String get deleteMintConfirmMessage => 'Если у вас есть баланс на этом mint, он будет потерян. Вы уверены?';
String get deleteMintConfirmMessage =>
'Если у вас есть баланс на этом mint, он будет потерян. Вы уверены?';
@override
String get delete => 'Удалить';
@@ -896,10 +988,12 @@ class L10nRu extends L10n {
String get tokenSavedForLater => 'Токен сохранён для получения позже';
@override
String get noConnectionTokenSaved => 'Нет соединения. Токен сохранён для получения позже.';
String get noConnectionTokenSaved =>
'Нет соединения. Токен сохранён для получения позже.';
@override
String get unknownMintOffline => 'Этот токен от неизвестного mint. Подключитесь к интернету, чтобы добавить его и получить токен.';
String get unknownMintOffline =>
'Этот токен от неизвестного mint. Подключитесь к интернету, чтобы добавить его и получить токен.';
@override
String get noConnectionTryLater => 'Нет соединения с mint. Попробуйте позже.';
@@ -908,7 +1002,8 @@ class L10nRu extends L10n {
String get saveTokenError => 'Ошибка сохранения токена. Попробуйте снова.';
@override
String get pendingTokenLimitReached => 'Достигнут лимит ожидающих токенов (макс 50)';
String get pendingTokenLimitReached =>
'Достигнут лимит ожидающих токенов (макс 50)';
@override
String get filterToReceive => 'К получению';
@@ -985,7 +1080,8 @@ class L10nRu extends L10n {
String get unrecognizedQrCode => 'Нераспознанный QR-код';
@override
String get scanCashuTokenHint => 'Сканируйте Cashu токен (cashuA... или cashuB...)';
String get scanCashuTokenHint =>
'Сканируйте Cashu токен (cashuA... или cashuB...)';
@override
String get scanLightningInvoiceHint => 'Сканируйте Lightning счёт (lnbc...)';
@@ -997,7 +1093,53 @@ class L10nRu extends L10n {
String get cameraPermissionDenied => 'Доступ к камере запрещён';
@override
String get paymentRequestNotSupported => 'Запросы на оплату пока не поддерживаются';
String get paymentRequestTitle => 'Запрос на оплату';
@override
String get paymentRequestFrom => 'Запрос от';
@override
String get paymentRequestAmount => 'Запрошенная сумма';
@override
String get paymentRequestDescription => 'Описание';
@override
String get paymentRequestMints => 'Принимаемые минты';
@override
String get paymentRequestAnyMint => 'Любой минт';
@override
String get paymentRequestPay => 'Оплатить';
@override
String get paymentRequestPaying => 'Оплата...';
@override
String get paymentRequestSuccess => 'Платёж успешно отправлен';
@override
String get paymentRequestNoTransport =>
'В этом запросе не настроен метод доставки';
@override
String get paymentRequestTransport => 'Транспорт';
@override
String get paymentRequestMintNotAccepted =>
'Ваш активный минт не в списке принимаемых';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Несовместимая единица: запрос требует $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Недостаточный баланс';
@override
String get paymentRequestErrorParsing => 'Ошибка чтения запроса на оплату';
@override
String get p2pkTitle => 'Ключи P2PK';
@@ -1006,10 +1148,12 @@ class L10nRu extends L10n {
String get p2pkSettingsDescription => 'Получить заблокированный ecash';
@override
String get p2pkExperimental => 'P2PK экспериментальный. Используйте с осторожностью.';
String get p2pkExperimental =>
'P2PK экспериментальный. Используйте с осторожностью.';
@override
String get p2pkPendingSendWarning => 'У вас есть ожидающая отправка P2PK. Перейдите в историю и обновите после того, как получатель заберёт токен.';
String get p2pkPendingSendWarning =>
'У вас есть ожидающая отправка P2PK. Перейдите в историю и обновите после того, как получатель заберёт токен.';
@override
String get p2pkExperimentalShort => 'Экспериментальный';
@@ -1066,7 +1210,8 @@ class L10nRu extends L10n {
String get p2pkLockedToOther => 'Заблокировано для другого ключа';
@override
String get p2pkCannotUnlock => 'У вас нет ключа для разблокировки этого токена';
String get p2pkCannotUnlock =>
'У вас нет ключа для разблокировки этого токена';
@override
String get p2pkEnterPrivateKey => 'Введите приватный ключ (nsec)';
@@ -1075,13 +1220,15 @@ class L10nRu extends L10n {
String get p2pkDeleteTitle => 'Удалить ключ';
@override
String get p2pkDeleteConfirm => 'Удалить этот ключ? Вы не сможете получать токены заблокированные на него.';
String get p2pkDeleteConfirm =>
'Удалить этот ключ? Вы не сможете получать токены заблокированные на него.';
@override
String get p2pkRequiresConnection => 'P2PK требует подключения к mint';
@override
String get p2pkErrorMaxKeysReached => 'Достигнуто максимальное количество импортированных ключей (10)';
String get p2pkErrorMaxKeysReached =>
'Достигнуто максимальное количество импортированных ключей (10)';
@override
String get p2pkErrorInvalidNsec => 'Недействительный nsec';
@@ -1094,4 +1241,103 @@ class L10nRu extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => 'Невозможно удалить основной ключ';
@override
String get request => 'Запросить';
@override
String get requestPayment => 'Запросить платёж';
@override
String get requestPaymentDescription =>
'Создать унифицированный запрос на оплату';
@override
String get generateRequest => 'Создать запрос';
@override
String get generatingRequest => 'Создание запроса...';
@override
String get requestPaymentReceived => 'Платёж получен';
@override
String get requestDescriptionHint => 'Описание (необязательно)';
@override
String get universal => 'Универсальный';
@override
String get copiedToClipboard => 'Скопировано в буфер обмена';
@override
String get swap => 'Обменять';
@override
String get swapDescription => 'Конвертировать между sats и USD';
@override
String get swapFrom => 'Из';
@override
String get swapTo => 'В';
@override
String get swapAction => 'Обменять';
@override
String get swapEstimatedFee => 'Ориентировочная комиссия';
@override
String get swapUseAll => 'Использовать всё';
@override
String swapMinimum(String amount) {
return 'Минимум: $amount';
}
@override
String get swapProcessing => 'Обработка обмена...';
@override
String get swapSuccess => 'Обмен завершён';
@override
String get swapErrorInsufficient => 'Недостаточный баланс';
@override
String get swapErrorExpired => 'Котировка истекла';
@override
String swapErrorGeneric(String error) {
return 'Ошибка обмена: $error';
}
@override
String get swapChartUnavailable => 'Цена недоступна · Нажмите для повтора';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Мин: $minPrice — Макс: $maxPrice';
}
@override
String get privacyPolicy => 'Политика конфиденциальности';
@override
String get privacyTitle => 'МЫ НЕ СОБИРАЕМ НИЧЕГО';
@override
String get privacyGoodbye => 'ПОКА';
@override
String get privacyKeepReading => '(читай дальше, если хочешь…)';
@override
String get privacyBody =>
'Мы не знаем, кто ты\nМы не знаем, сколько у тебя\nМы не знаем, что ты делаешь';
@override
String get privacyConclusion =>
'Лучший способ защитить твои данные —\nне иметь их';
}
+276 -29
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -60,7 +60,8 @@ class L10nSw extends L10n {
String get generatingSeed => 'Kutengeneza maneno yako ya mbegu kwa usalama';
@override
String get createWalletDescription => 'Maneno 12 ya mbegu yatatengenezwa.\nYahifadhi mahali salama.';
String get createWalletDescription =>
'Maneno 12 ya mbegu yatatengenezwa.\nYahifadhi mahali salama.';
@override
String get generateWallet => 'Tengeneza pochi';
@@ -69,10 +70,12 @@ class L10nSw extends L10n {
String get walletCreated => 'Pochi imeundwa!';
@override
String get walletCreatedDescription => 'Pochi yako iko tayari. Tunakushauri uhifadhi nakala ya maneno yako ya mbegu sasa.';
String get walletCreatedDescription =>
'Pochi yako iko tayari. Tunakushauri uhifadhi nakala ya maneno yako ya mbegu sasa.';
@override
String get backupWarning => 'Bila nakala, utapoteza uwezo wa kufikia fedha zako ukipoteza kifaa.';
String get backupWarning =>
'Bila nakala, utapoteza uwezo wa kufikia fedha zako ukipoteza kifaa.';
@override
String get backupNow => 'Hifadhi nakala sasa';
@@ -87,7 +90,8 @@ class L10nSw extends L10n {
String get seedPhraseTitle => 'Maneno yako ya mbegu';
@override
String get seedPhraseDescription => 'Hifadhi maneno haya 12 kwa mpangilio. Ndiyo njia pekee ya kurejesha pochi yako.';
String get seedPhraseDescription =>
'Hifadhi maneno haya 12 kwa mpangilio. Ndiyo njia pekee ya kurejesha pochi yako.';
@override
String get revealSeedPhrase => 'Onyesha maneno ya mbegu';
@@ -117,7 +121,8 @@ class L10nSw extends L10n {
String get enterSeedPhrase => 'Ingiza maneno yako ya mbegu';
@override
String get enterSeedDescription => 'Andika maneno 12 au 24 yakitengwa na nafasi.';
String get enterSeedDescription =>
'Andika maneno 12 au 24 yakitengwa na nafasi.';
@override
String get seedPlaceholder => 'neno1 neno2 neno3 ...';
@@ -148,10 +153,10 @@ class L10nSw extends L10n {
String get send => 'Tuma';
@override
String get sendAction => 'Tuma ↗';
String get sendAction => 'Tuma';
@override
String get receiveAction => '↘ Pokea';
String get receiveAction => 'Pokea';
@override
String get deposit => 'Weka';
@@ -207,6 +212,36 @@ class L10nSw extends L10n {
@override
String get pasteFromClipboard => 'Bandika kutoka ubao';
@override
String get emptyClipboard => 'Ubao wa kunakili ni tupu';
@override
String get cancelSend => 'Ghairi kutuma';
@override
String get cancelSendConfirmTitle => 'Ghairi kutuma huku?';
@override
String get cancelSendConfirmBody =>
'Hufanya kazi tu ikiwa mpokeaji hajatumia tokeni. Ikiwa tayari alitumia, kutuma kutaonyeshwa kama kumekamilika.';
@override
String cancelSendSuccess(String amount, String unit) {
return 'Kutuma kumeghairiwa. Umerudisha $amount $unit';
}
@override
String get cancelSendAlreadyClaimed => 'Mpokeaji tayari alitumia tokeni';
@override
String get pendingOfflineSend => 'Kutuma nje ya mtandao kunasubiri';
@override
String get pendingOfflineSendsHeader => 'Kutuma nje ya mtandao bila kudaiwa';
@override
String get pendingSendDetailTitle => 'Kutuma kunakusubiri';
@override
String get validToken => 'Tokeni halali';
@@ -273,7 +308,8 @@ class L10nSw extends L10n {
String get noActiveMint => 'Hakuna mint inayofanya kazi';
@override
String get offlineModeMessage => 'Hakuna muunganisho. Kutumia hali ya nje ya mtandao...';
String get offlineModeMessage =>
'Hakuna muunganisho. Kutumia hali ya nje ya mtandao...';
@override
String get confirmSend => 'Thibitisha kutuma';
@@ -287,6 +323,9 @@ class L10nSw extends L10n {
@override
String get insufficientBalance => 'Salio halitoshi';
@override
String get feeExceedsAmount => 'Ada inazidi kiasi cha kutuma';
@override
String tokenCreationError(String error) {
return 'Hitilafu ya kuunda tokeni: $error';
@@ -310,11 +349,50 @@ class L10nSw extends L10n {
}
@override
String get keepTokenWarning => 'Hifadhi tokeni hii hadi mpokeaji aidai. Ukiipoteza, utapoteza fedha.';
String get keepTokenWarning =>
'Hifadhi tokeni hii hadi mpokeaji aidai. Ukiipoteza, utapoteza fedha.';
@override
String get tokenCopiedToClipboard => 'Tokeni imenakiliwa kwenye ubao';
@override
String get copyAsEmoji => 'Nakili kama emoji';
@override
String get emojiCopiedToClipboard => 'Tokeni imenakiliwa kama emoji 🥜';
@override
String get peanutDecodeError =>
'Imeshindwa kusimbua tokeni ya emoji. Inaweza kuwa imeharibika.';
@override
String get nfcWrite => 'Andika kwenye NFC tag';
@override
String get nfcRead => 'Soma NFC tag';
@override
String get nfcHoldNear => 'Karibia kifaa kwenye NFC tag...';
@override
String get nfcWriteSuccess => 'Tokeni imeandikwa kwenye NFC tag';
@override
String nfcWriteError(String error) {
return 'Hitilafu ya NFC kuandika: $error';
}
@override
String nfcReadError(String error) {
return 'Hitilafu ya NFC kusoma: $error';
}
@override
String get nfcDisabled => 'NFC imezimwa. Iwashe katika Mipangilio.';
@override
String get nfcUnsupported => 'Kifaa hiki hakitumii NFC';
@override
String get amountToDeposit => 'Kiasi cha kuweka:';
@@ -473,6 +551,12 @@ class L10nSw extends L10n {
@override
String get sentStatus => 'Imetumwa';
@override
String get receiving => 'Inapokea';
@override
String get sending => 'Inatumwa';
@override
String get now => 'Sasa';
@@ -665,7 +749,8 @@ class L10nSw extends L10n {
String get close => 'Funga';
@override
String get aboutDescription => 'Pochi ya Cashu yenye DNA ya Cuba kwa ulimwengu wote. Ndugu wa La Chispa.';
String get aboutDescription =>
'Pochi ya Cashu yenye DNA ya Cuba kwa ulimwengu wote. Ndugu wa LaChispa.';
@override
String get couldNotOpenLink => 'Haikuweza kufungua kiungo';
@@ -677,7 +762,8 @@ class L10nSw extends L10n {
String get actionIrreversible => 'Kitendo hiki hakiwezi kutenduliwa';
@override
String get deleteWalletWarning => 'Data zote zitafutwa ikiwa ni pamoja na maneno yako ya mbegu na tokeni. Hakikisha una nakala.';
String get deleteWalletWarning =>
'Data zote zitafutwa ikiwa ni pamoja na maneno yako ya mbegu na tokeni. Hakikisha una nakala.';
@override
String get typeDeleteToConfirm => 'Andika \"FUTA\" kuthibitisha:';
@@ -694,13 +780,15 @@ class L10nSw extends L10n {
String get recoverTokensTitle => 'Rejesha tokeni';
@override
String get recoverTokensDescription => 'Changanua mint kurejesha tokeni zinazohusiana na maneno yako ya mbegu (NUT-13)';
String get recoverTokensDescription =>
'Changanua mint kurejesha tokeni zinazohusiana na maneno yako ya mbegu (NUT-13)';
@override
String get useCurrentSeedPhrase => 'Tumia maneno yangu ya mbegu ya sasa';
@override
String get scanWithSavedWords => 'Changanua mint na maneno 12 yaliyohifadhiwa';
String get scanWithSavedWords =>
'Changanua mint na maneno 12 yaliyohifadhiwa';
@override
String get useOtherSeedPhrase => 'Tumia maneno mengine ya mbegu';
@@ -732,7 +820,8 @@ class L10nSw extends L10n {
String get mnemonicMustHaveWords => 'Mnemonic lazima iwe na maneno 12 au 24';
@override
String get noConnectedMintsToScan => 'Hakuna mint zilizounganishwa za kuchanganua';
String get noConnectedMintsToScan =>
'Hakuna mint zilizounganishwa za kuchanganua';
@override
String recoveredTokens(String tokens, int mints) {
@@ -740,7 +829,8 @@ class L10nSw extends L10n {
}
@override
String get scanCompleteNoTokens => 'Uchanganuzi umekamilika. Hakuna tokeni mpya zilizopatikana.';
String get scanCompleteNoTokens =>
'Uchanganuzi umekamilika. Hakuna tokeni mpya zilizopatikana.';
@override
String mintsWithError(int count) {
@@ -763,7 +853,8 @@ class L10nSw extends L10n {
}
@override
String get noTokensForMnemonic => 'Hakuna tokeni zilizopatikana zinazohusiana na mnemonic hiyo.';
String get noTokensForMnemonic =>
'Hakuna tokeni zilizopatikana zinazohusiana na mnemonic hiyo.';
@override
String get noConnectedMints => 'Hakuna mint zilizounganishwa';
@@ -843,7 +934,8 @@ class L10nSw extends L10n {
String get deleteMintConfirmTitle => 'Futa mint';
@override
String get deleteMintConfirmMessage => 'Ukiwa na salio katika mint hii, itapotea. Una uhakika?';
String get deleteMintConfirmMessage =>
'Ukiwa na salio katika mint hii, itapotea. Una uhakika?';
@override
String get delete => 'Futa';
@@ -896,19 +988,23 @@ class L10nSw extends L10n {
String get tokenSavedForLater => 'Tokeni imehifadhiwa kudai baadaye';
@override
String get noConnectionTokenSaved => 'Hakuna muunganisho. Tokeni imehifadhiwa kudai baadaye.';
String get noConnectionTokenSaved =>
'Hakuna muunganisho. Tokeni imehifadhiwa kudai baadaye.';
@override
String get unknownMintOffline => 'Tokeni hii ni kutoka mint isiyojulikana. Unganisha na mtandao kuongeza na kudai tokeni.';
String get unknownMintOffline =>
'Tokeni hii ni kutoka mint isiyojulikana. Unganisha na mtandao kuongeza na kudai tokeni.';
@override
String get noConnectionTryLater => 'Hakuna muunganisho na mint. Jaribu baadaye.';
String get noConnectionTryLater =>
'Hakuna muunganisho na mint. Jaribu baadaye.';
@override
String get saveTokenError => 'Hitilafu ya kuhifadhi tokeni. Jaribu tena.';
@override
String get pendingTokenLimitReached => 'Kikomo cha tokeni zinazosubiri kimefikiwa (upeo 50)';
String get pendingTokenLimitReached =>
'Kikomo cha tokeni zinazosubiri kimefikiwa (upeo 50)';
@override
String get filterToReceive => 'Za kupokea';
@@ -976,7 +1072,8 @@ class L10nSw extends L10n {
String get pointCameraAtQr => 'Elekeza kamera kwenye msimbo wa QR';
@override
String get pointCameraAtCashuQr => 'Elekeza kamera kwenye QR ya tokeni ya Cashu';
String get pointCameraAtCashuQr =>
'Elekeza kamera kwenye QR ya tokeni ya Cashu';
@override
String get pointCameraAtInvoiceQr => 'Elekeza kamera kwenye QR ya ankara';
@@ -985,10 +1082,12 @@ class L10nSw extends L10n {
String get unrecognizedQrCode => 'Msimbo wa QR haujatambuliwa';
@override
String get scanCashuTokenHint => 'Changanua tokeni ya Cashu (cashuA... au cashuB...)';
String get scanCashuTokenHint =>
'Changanua tokeni ya Cashu (cashuA... au cashuB...)';
@override
String get scanLightningInvoiceHint => 'Changanua ankara ya Lightning (lnbc...)';
String get scanLightningInvoiceHint =>
'Changanua ankara ya Lightning (lnbc...)';
@override
String get addMintQuestion => 'Ongeza mint hii?';
@@ -997,7 +1096,54 @@ class L10nSw extends L10n {
String get cameraPermissionDenied => 'Ruhusa ya kamera imekataliwa';
@override
String get paymentRequestNotSupported => 'Maombi ya malipo bado hayatumiki';
String get paymentRequestTitle => 'Ombi la malipo';
@override
String get paymentRequestFrom => 'Ombi kutoka';
@override
String get paymentRequestAmount => 'Kiasi kilichoombwa';
@override
String get paymentRequestDescription => 'Maelezo';
@override
String get paymentRequestMints => 'Mint zinazokubaliwa';
@override
String get paymentRequestAnyMint => 'Mint yoyote';
@override
String get paymentRequestPay => 'Lipa';
@override
String get paymentRequestPaying => 'Inalipa...';
@override
String get paymentRequestSuccess => 'Malipo yametumwa kwa mafanikio';
@override
String get paymentRequestNoTransport =>
'Ombi hili halina njia ya uwasilishaji iliyosanidiwa';
@override
String get paymentRequestTransport => 'Njia ya usafirishaji';
@override
String get paymentRequestMintNotAccepted =>
'Mint yako hai haiko kwenye orodha ya mint zinazokubaliwa';
@override
String paymentRequestUnitMismatch(String unit) {
return 'Kitengo kisichooana: ombi linahitaji $unit';
}
@override
String get paymentRequestInsufficientBalance => 'Salio haitoshi';
@override
String get paymentRequestErrorParsing =>
'Hitilafu wakati wa kusoma ombi la malipo';
@override
String get p2pkTitle => 'Funguo za P2PK';
@@ -1009,7 +1155,8 @@ class L10nSw extends L10n {
String get p2pkExperimental => 'P2PK ni ya majaribio. Tumia kwa uangalifu.';
@override
String get p2pkPendingSendWarning => 'Una usafirishaji wa P2PK unaosubiri. Nenda kwenye historia na usasishe baada ya mpokeaji kudai tokeni.';
String get p2pkPendingSendWarning =>
'Una usafirishaji wa P2PK unaosubiri. Nenda kwenye historia na usasishe baada ya mpokeaji kudai tokeni.';
@override
String get p2pkExperimentalShort => 'Majaribio';
@@ -1075,13 +1222,15 @@ class L10nSw extends L10n {
String get p2pkDeleteTitle => 'Futa ufunguo';
@override
String get p2pkDeleteConfirm => 'Futa ufunguo huu? Hutaweza kupokea tokeni zilizofungwa kwake.';
String get p2pkDeleteConfirm =>
'Futa ufunguo huu? Hutaweza kupokea tokeni zilizofungwa kwake.';
@override
String get p2pkRequiresConnection => 'P2PK inahitaji muunganisho kwa mint';
@override
String get p2pkErrorMaxKeysReached => 'Idadi ya juu ya funguo zilizoingizwa imefikiwa (10)';
String get p2pkErrorMaxKeysReached =>
'Idadi ya juu ya funguo zilizoingizwa imefikiwa (10)';
@override
String get p2pkErrorInvalidNsec => 'nsec batili';
@@ -1094,4 +1243,102 @@ class L10nSw extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => 'Haiwezekani kufuta ufunguo mkuu';
@override
String get request => 'Omba';
@override
String get requestPayment => 'Omba malipo';
@override
String get requestPaymentDescription => 'Tengeneza ombi la malipo la pamoja';
@override
String get generateRequest => 'Tengeneza ombi';
@override
String get generatingRequest => 'Inatengeneza ombi...';
@override
String get requestPaymentReceived => 'Malipo limepokelewa';
@override
String get requestDescriptionHint => 'Maelezo (si lazima)';
@override
String get universal => 'Universal';
@override
String get copiedToClipboard => 'Imenakiliwa kwenye ubao wa kunakili';
@override
String get swap => 'Badilisha';
@override
String get swapDescription => 'Badilisha kati ya sats na USD';
@override
String get swapFrom => 'Kutoka';
@override
String get swapTo => 'Kwenda';
@override
String get swapAction => 'Badilisha';
@override
String get swapEstimatedFee => 'Ada inayokadiriwa';
@override
String get swapUseAll => 'Tumia yote';
@override
String swapMinimum(String amount) {
return 'Kiwango cha chini: $amount';
}
@override
String get swapProcessing => 'Inashughulikia ubadilishaji...';
@override
String get swapSuccess => 'Ubadilishaji umekamilika';
@override
String get swapErrorInsufficient => 'Salio haitoshi';
@override
String get swapErrorExpired => 'Bei imeisha muda';
@override
String swapErrorGeneric(String error) {
return 'Kosa la ubadilishaji: $error';
}
@override
String get swapChartUnavailable => 'Bei haipatikani · Gusa kurudia';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Chini: $minPrice — Juu: $maxPrice';
}
@override
String get privacyPolicy => 'Sera ya faragha';
@override
String get privacyTitle => 'HATUKUSANYI CHOCHOTE';
@override
String get privacyGoodbye => 'KWAHERI';
@override
String get privacyKeepReading => '(endelea kusoma ukitaka…)';
@override
String get privacyBody =>
'Hatujui wewe ni nani\nHatujui una kiasi gani\nHatujui unafanya nini';
@override
String get privacyConclusion =>
'Njia bora ya kulinda data yako\nni kutokuwa nayo';
}
+220 -5
View File
@@ -1,5 +1,5 @@
// ignore: unused_import
import 'package:intl/intl.dart' as intl;
import 'app_localizations.dart';
// ignore_for_file: type=lint
@@ -148,10 +148,10 @@ class L10nZh extends L10n {
String get send => '付款';
@override
String get sendAction => '付款';
String get sendAction => '付款';
@override
String get receiveAction => '收款';
String get receiveAction => '收款';
@override
String get deposit => '存入';
@@ -207,6 +207,35 @@ class L10nZh extends L10n {
@override
String get pasteFromClipboard => '从剪贴板粘贴';
@override
String get emptyClipboard => '剪贴板为空';
@override
String get cancelSend => '取消发送';
@override
String get cancelSendConfirmTitle => '取消此次发送?';
@override
String get cancelSendConfirmBody => '仅当接收方尚未领取代币时有效。如已领取,发送将标记为已完成。';
@override
String cancelSendSuccess(String amount, String unit) {
return '发送已取消。已恢复 $amount $unit';
}
@override
String get cancelSendAlreadyClaimed => '接收方已领取代币';
@override
String get pendingOfflineSend => '待处理的离线发送';
@override
String get pendingOfflineSendsHeader => '未领取的离线发送';
@override
String get pendingSendDetailTitle => '待处理发送';
@override
String get validToken => '有效代币';
@@ -287,6 +316,9 @@ class L10nZh extends L10n {
@override
String get insufficientBalance => '余额不足';
@override
String get feeExceedsAmount => '手续费超过发送金额';
@override
String tokenCreationError(String error) {
return '创建代币错误:$error';
@@ -315,6 +347,43 @@ class L10nZh extends L10n {
@override
String get tokenCopiedToClipboard => '代币已复制到剪贴板';
@override
String get copyAsEmoji => '复制为表情符号';
@override
String get emojiCopiedToClipboard => '代币已复制为表情符号 🥜';
@override
String get peanutDecodeError => '无法解码表情符号代币。可能已损坏。';
@override
String get nfcWrite => '写入NFC标签';
@override
String get nfcRead => '读取NFC标签';
@override
String get nfcHoldNear => '将设备靠近NFC标签...';
@override
String get nfcWriteSuccess => '代币已写入NFC标签';
@override
String nfcWriteError(String error) {
return 'NFC写入错误:$error';
}
@override
String nfcReadError(String error) {
return 'NFC读取错误:$error';
}
@override
String get nfcDisabled => 'NFC已禁用,请在设置中启用。';
@override
String get nfcUnsupported => '此设备不支持NFC';
@override
String get amountToDeposit => '存入金额:';
@@ -473,6 +542,12 @@ class L10nZh extends L10n {
@override
String get sentStatus => '已发送';
@override
String get receiving => '接收中';
@override
String get sending => '发送中';
@override
String get now => '刚刚';
@@ -665,7 +740,7 @@ class L10nZh extends L10n {
String get close => '关闭';
@override
String get aboutDescription => '具有古巴基因的 Cashu 钱包,面向全世界。La Chispa 的兄弟项目。';
String get aboutDescription => '具有古巴基因的 Cashu 钱包,面向全世界。LaChispa 的兄弟项目。';
@override
String get couldNotOpenLink => '无法打开链接';
@@ -997,7 +1072,51 @@ class L10nZh extends L10n {
String get cameraPermissionDenied => '相机权限被拒绝';
@override
String get paymentRequestNotSupported => '付款请求尚不支持';
String get paymentRequestTitle => '付款请求';
@override
String get paymentRequestFrom => '来自';
@override
String get paymentRequestAmount => '请求金额';
@override
String get paymentRequestDescription => '描述';
@override
String get paymentRequestMints => '接受的铸造厂';
@override
String get paymentRequestAnyMint => '任何铸造厂';
@override
String get paymentRequestPay => '支付';
@override
String get paymentRequestPaying => '支付中...';
@override
String get paymentRequestSuccess => '付款发送成功';
@override
String get paymentRequestNoTransport => '此请求未配置交付方式';
@override
String get paymentRequestTransport => '传输方式';
@override
String get paymentRequestMintNotAccepted => '您的活跃铸造厂不在接受的铸造厂列表中';
@override
String paymentRequestUnitMismatch(String unit) {
return '单位不兼容:请求需要 $unit';
}
@override
String get paymentRequestInsufficientBalance => '余额不足';
@override
String get paymentRequestErrorParsing => '读取付款请求时出错';
@override
String get p2pkTitle => 'P2PK密钥';
@@ -1094,4 +1213,100 @@ class L10nZh extends L10n {
@override
String get p2pkErrorCannotDeletePrimary => '无法删除主密钥';
@override
String get request => '请求';
@override
String get requestPayment => '请求付款';
@override
String get requestPaymentDescription => '生成统一付款请求';
@override
String get generateRequest => '生成请求';
@override
String get generatingRequest => '正在生成请求...';
@override
String get requestPaymentReceived => '已收到付款';
@override
String get requestDescriptionHint => '描述(可选)';
@override
String get universal => '通用';
@override
String get copiedToClipboard => '已复制到剪贴板';
@override
String get swap => '兑换';
@override
String get swapDescription => '在 Sats 和 USD 之间转换';
@override
String get swapFrom => '';
@override
String get swapTo => '';
@override
String get swapAction => '兑换';
@override
String get swapEstimatedFee => '预估手续费';
@override
String get swapUseAll => '全部使用';
@override
String swapMinimum(String amount) {
return '最低: $amount';
}
@override
String get swapProcessing => '兑换处理中...';
@override
String get swapSuccess => '兑换完成';
@override
String get swapErrorInsufficient => '余额不足';
@override
String get swapErrorExpired => '报价已过期';
@override
String swapErrorGeneric(String error) {
return '兑换错误: $error';
}
@override
String get swapChartUnavailable => '价格不可用 · 点击重试';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h 最低: $minPrice — 最高: $maxPrice';
}
@override
String get privacyPolicy => '隐私政策';
@override
String get privacyTitle => '我们什么都不收集';
@override
String get privacyGoodbye => '再见';
@override
String get privacyKeepReading => '(想继续看就看吧…)';
@override
String get privacyBody => '我们不知道你是谁\n我们不知道你有多少\n我们不知道你在做什么';
@override
String get privacyConclusion => '保护数据的最好方式\n就是不拥有它';
}
+84 -5
View File
@@ -63,8 +63,8 @@
"homeTitle": "Início",
"receive": "Receber",
"send": "Enviar",
"sendAction": "Enviar",
"receiveAction": "Receber",
"sendAction": "Enviar",
"receiveAction": "Receber",
"deposit": "Depositar",
"withdraw": "Sacar",
"lightning": "Lightning",
@@ -86,6 +86,21 @@
"receiveCashu": "Receber Cashu",
"pasteTheCashuToken": "Cole o token Cashu:",
"pasteFromClipboard": "Colar da área de transferência",
"emptyClipboard": "Área de transferência vazia",
"cancelSend": "Cancelar envio",
"cancelSendConfirmTitle": "Cancelar este envio?",
"cancelSendConfirmBody": "Só funciona se o destinatário ainda não resgatou o token. Se já resgatou, o envio será marcado como concluído.",
"cancelSendSuccess": "Envio cancelado. Recuperou {amount} {unit}",
"@cancelSendSuccess": {
"placeholders": {
"amount": {"type": "String"},
"unit": {"type": "String"}
}
},
"cancelSendAlreadyClaimed": "O destinatário já resgatou o token",
"pendingOfflineSend": "Envio offline pendente",
"pendingOfflineSendsHeader": "Envios offline não resgatados",
"pendingSendDetailTitle": "Envio pendente",
"validToken": "Token válido",
"invalidToken": "Token inválido ou malformado",
"amount": "Valor:",
@@ -118,6 +133,7 @@
"confirm": "Confirmar",
"cancel": "Cancelar",
"insufficientBalance": "Saldo insuficiente",
"feeExceedsAmount": "A taxa excede o valor a enviar",
"tokenCreationError": "Erro ao criar token: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -137,6 +153,20 @@
},
"keepTokenWarning": "Guarde este token até que o destinatário o resgate. Se você perdê-lo, perderá os fundos.",
"tokenCopiedToClipboard": "Token copiado para área de transferência",
"copyAsEmoji": "Copiar como emoji",
"emojiCopiedToClipboard": "Token copiado como emoji 🥜",
"peanutDecodeError": "Não foi possível decodificar o token emoji. Pode estar corrompido.",
"nfcWrite": "Escrever em tag NFC",
"nfcRead": "Ler tag NFC",
"nfcHoldNear": "Aproxime o dispositivo da tag NFC...",
"nfcWriteSuccess": "Token escrito na tag NFC",
"nfcWriteError": "Erro NFC ao escrever: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "Erro NFC ao ler: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC está desativado. Ative nas Configurações.",
"nfcUnsupported": "Este dispositivo não suporta NFC",
"amountToDeposit": "Valor a depositar:",
"descriptionOptional": "Descrição (opcional):",
@@ -213,6 +243,8 @@
"pendingStatus": "Pendente",
"receivedStatus": "Recebido",
"sentStatus": "Enviado",
"receiving": "Recebendo",
"sending": "Enviando",
"now": "Agora",
"agoMinutes": "Há {minutes} min",
"@agoMinutes": {
@@ -296,7 +328,7 @@
}
},
"close": "Fechar",
"aboutDescription": "Uma wallet Cashu com DNA cubano para o mundo inteiro. Irmã de La Chispa.",
"aboutDescription": "Uma wallet Cashu com DNA cubano para o mundo inteiro. Irmã de LaChispa.",
"couldNotOpenLink": "Não foi possível abrir o link",
"deleteWalletQuestion": "Apagar wallet?",
@@ -488,7 +520,22 @@
"scanLightningInvoiceHint": "Escaneie um invoice Lightning (lnbc...)",
"addMintQuestion": "Adicionar este mint?",
"cameraPermissionDenied": "Permissão de câmera negada",
"paymentRequestNotSupported": "Solicitações de pagamento ainda não são suportadas",
"paymentRequestTitle": "Solicitação de pagamento",
"paymentRequestFrom": "Solicitação de",
"paymentRequestAmount": "Valor solicitado",
"paymentRequestDescription": "Descrição",
"paymentRequestMints": "Mints aceitos",
"paymentRequestAnyMint": "Qualquer mint",
"paymentRequestPay": "Pagar",
"paymentRequestPaying": "Pagando...",
"paymentRequestSuccess": "Pagamento enviado com sucesso",
"paymentRequestNoTransport": "Esta solicitação não tem método de entrega configurado",
"paymentRequestTransport": "Transporte",
"paymentRequestMintNotAccepted": "Seu mint ativo não está na lista de mints aceitos",
"paymentRequestUnitMismatch": "Unidade incompatível: a solicitação requer {unit}",
"paymentRequestInsufficientBalance": "Saldo insuficiente",
"paymentRequestErrorParsing": "Erro ao ler a solicitação de pagamento",
"p2pkTitle": "Chaves P2PK",
"p2pkSettingsDescription": "Receber ecash bloqueado",
"p2pkExperimental": "P2PK é experimental. Use com cautela.",
@@ -520,5 +567,37 @@
"p2pkErrorInvalidNsec": "nsec inválido",
"p2pkErrorKeyAlreadyExists": "Esta chave já existe",
"p2pkErrorKeyNotFound": "Chave não encontrada",
"p2pkErrorCannotDeletePrimary": "Não é possível excluir a chave principal"
"p2pkErrorCannotDeletePrimary": "Não é possível excluir a chave principal",
"request": "Solicitar",
"requestPayment": "Solicitar pagamento",
"requestPaymentDescription": "Gerar solicitação de pagamento unificada",
"generateRequest": "Gerar solicitação",
"generatingRequest": "Gerando solicitação...",
"requestPaymentReceived": "Pagamento recebido",
"requestDescriptionHint": "Descrição (opcional)",
"universal": "Universal",
"copiedToClipboard": "Copiado para a área de transferência",
"swap": "Trocar",
"swapDescription": "Converter entre sats e USD",
"swapFrom": "De",
"swapTo": "Para",
"swapAction": "Trocar",
"swapEstimatedFee": "Taxa estimada",
"swapUseAll": "Usar tudo",
"swapMinimum": "Mínimo: {amount}",
"swapProcessing": "Processando swap...",
"swapSuccess": "Swap concluído",
"swapErrorInsufficient": "Saldo insuficiente",
"swapErrorExpired": "A cotação expirou",
"swapErrorGeneric": "Erro no swap: {error}",
"swapChartUnavailable": "Preço indisponível · Toque para tentar novamente",
"swapChartMinMax": "24h Mín: {minPrice} — Máx: {maxPrice}",
"privacyPolicy": "Política de privacidade",
"privacyTitle": "NÃO RECOLHEMOS NADA",
"privacyGoodbye": "ADEUS",
"privacyKeepReading": "(continue lendo se quiser…)",
"privacyBody": "Não sabemos quem você é\nNão sabemos quanto você tem\nNão sabemos o que você faz",
"privacyConclusion": "A melhor forma de proteger seus dados\né não tê-los"
}
+84 -5
View File
@@ -63,8 +63,8 @@
"homeTitle": "Главная",
"receive": "Получить",
"send": "Отправить",
"sendAction": "Отправить",
"receiveAction": "Получить",
"sendAction": "Отправить",
"receiveAction": "Получить",
"deposit": "Пополнить",
"withdraw": "Вывести",
"lightning": "Lightning",
@@ -86,6 +86,21 @@
"receiveCashu": "Получить Cashu",
"pasteTheCashuToken": "Вставьте Cashu токен:",
"pasteFromClipboard": "Вставить из буфера обмена",
"emptyClipboard": "Буфер обмена пуст",
"cancelSend": "Отменить отправку",
"cancelSendConfirmTitle": "Отменить эту отправку?",
"cancelSendConfirmBody": "Работает только если получатель еще не получил токен. Если уже получил, отправка будет помечена как завершенная.",
"cancelSendSuccess": "Отправка отменена. Восстановлено {amount} {unit}",
"@cancelSendSuccess": {
"placeholders": {
"amount": {"type": "String"},
"unit": {"type": "String"}
}
},
"cancelSendAlreadyClaimed": "Получатель уже получил токен",
"pendingOfflineSend": "Ожидающая оффлайн-отправка",
"pendingOfflineSendsHeader": "Неполученные оффлайн-отправки",
"pendingSendDetailTitle": "Ожидающая отправка",
"validToken": "Токен действителен",
"invalidToken": "Недействительный или повреждённый токен",
"amount": "Сумма:",
@@ -118,6 +133,7 @@
"confirm": "Подтвердить",
"cancel": "Отмена",
"insufficientBalance": "Недостаточный баланс",
"feeExceedsAmount": "Комиссия превышает сумму отправки",
"tokenCreationError": "Ошибка создания токена: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -137,6 +153,20 @@
},
"keepTokenWarning": "Сохраните этот токен, пока получатель не заберёт его. Если потеряете — потеряете средства.",
"tokenCopiedToClipboard": "Токен скопирован в буфер обмена",
"copyAsEmoji": "Скопировать как эмодзи",
"emojiCopiedToClipboard": "Токен скопирован как эмодзи 🥜",
"peanutDecodeError": "Не удалось декодировать эмодзи-токен. Возможно, он повреждён.",
"nfcWrite": "Записать на NFC-метку",
"nfcRead": "Читать NFC-метку",
"nfcHoldNear": "Поднесите устройство к NFC-метке...",
"nfcWriteSuccess": "Токен записан на NFC-метку",
"nfcWriteError": "Ошибка записи NFC: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "Ошибка чтения NFC: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC отключён. Включите в Настройках.",
"nfcUnsupported": "Это устройство не поддерживает NFC",
"amountToDeposit": "Сумма для пополнения:",
"descriptionOptional": "Описание (необязательно):",
@@ -213,6 +243,8 @@
"pendingStatus": "Ожидание",
"receivedStatus": "Получено",
"sentStatus": "Отправлено",
"receiving": "Получение",
"sending": "Отправка",
"now": "Сейчас",
"agoMinutes": "{minutes} мин назад",
"@agoMinutes": {
@@ -296,7 +328,7 @@
}
},
"close": "Закрыть",
"aboutDescription": "Cashu кошелёк с кубинской ДНК для всего мира. Брат La Chispa.",
"aboutDescription": "Cashu кошелёк с кубинской ДНК для всего мира. Брат LaChispa.",
"couldNotOpenLink": "Не удалось открыть ссылку",
"deleteWalletQuestion": "Удалить кошелёк?",
@@ -488,7 +520,22 @@
"scanLightningInvoiceHint": "Сканируйте Lightning счёт (lnbc...)",
"addMintQuestion": "Добавить этот mint?",
"cameraPermissionDenied": "Доступ к камере запрещён",
"paymentRequestNotSupported": "Запросы на оплату пока не поддерживаются",
"paymentRequestTitle": "Запрос на оплату",
"paymentRequestFrom": "Запрос от",
"paymentRequestAmount": "Запрошенная сумма",
"paymentRequestDescription": "Описание",
"paymentRequestMints": "Принимаемые минты",
"paymentRequestAnyMint": "Любой минт",
"paymentRequestPay": "Оплатить",
"paymentRequestPaying": "Оплата...",
"paymentRequestSuccess": "Платёж успешно отправлен",
"paymentRequestNoTransport": "В этом запросе не настроен метод доставки",
"paymentRequestTransport": "Транспорт",
"paymentRequestMintNotAccepted": "Ваш активный минт не в списке принимаемых",
"paymentRequestUnitMismatch": "Несовместимая единица: запрос требует {unit}",
"paymentRequestInsufficientBalance": "Недостаточный баланс",
"paymentRequestErrorParsing": "Ошибка чтения запроса на оплату",
"p2pkTitle": "Ключи P2PK",
"p2pkSettingsDescription": "Получить заблокированный ecash",
"p2pkExperimental": "P2PK экспериментальный. Используйте с осторожностью.",
@@ -520,5 +567,37 @@
"p2pkErrorInvalidNsec": "Недействительный nsec",
"p2pkErrorKeyAlreadyExists": "Этот ключ уже существует",
"p2pkErrorKeyNotFound": "Ключ не найден",
"p2pkErrorCannotDeletePrimary": "Невозможно удалить основной ключ"
"p2pkErrorCannotDeletePrimary": "Невозможно удалить основной ключ",
"request": "Запросить",
"requestPayment": "Запросить платёж",
"requestPaymentDescription": "Создать унифицированный запрос на оплату",
"generateRequest": "Создать запрос",
"generatingRequest": "Создание запроса...",
"requestPaymentReceived": "Платёж получен",
"requestDescriptionHint": "Описание (необязательно)",
"universal": "Универсальный",
"copiedToClipboard": "Скопировано в буфер обмена",
"swap": "Обменять",
"swapDescription": "Конвертировать между sats и USD",
"swapFrom": "Из",
"swapTo": "В",
"swapAction": "Обменять",
"swapEstimatedFee": "Ориентировочная комиссия",
"swapUseAll": "Использовать всё",
"swapMinimum": "Минимум: {amount}",
"swapProcessing": "Обработка обмена...",
"swapSuccess": "Обмен завершён",
"swapErrorInsufficient": "Недостаточный баланс",
"swapErrorExpired": "Котировка истекла",
"swapErrorGeneric": "Ошибка обмена: {error}",
"swapChartUnavailable": "Цена недоступна · Нажмите для повтора",
"swapChartMinMax": "24h Мин: {minPrice} — Макс: {maxPrice}",
"privacyPolicy": "Политика конфиденциальности",
"privacyTitle": "МЫ НЕ СОБИРАЕМ НИЧЕГО",
"privacyGoodbye": "ПОКА",
"privacyKeepReading": "(читай дальше, если хочешь…)",
"privacyBody": "Мы не знаем, кто ты\nМы не знаем, сколько у тебя\nМы не знаем, что ты делаешь",
"privacyConclusion": "Лучший способ защитить твои данные —\nне иметь их"
}
+84 -5
View File
@@ -63,8 +63,8 @@
"homeTitle": "Nyumbani",
"receive": "Pokea",
"send": "Tuma",
"sendAction": "Tuma",
"receiveAction": "Pokea",
"sendAction": "Tuma",
"receiveAction": "Pokea",
"deposit": "Weka",
"withdraw": "Toa",
"lightning": "Lightning",
@@ -86,6 +86,21 @@
"receiveCashu": "Pokea Cashu",
"pasteTheCashuToken": "Bandika tokeni ya Cashu:",
"pasteFromClipboard": "Bandika kutoka ubao",
"emptyClipboard": "Ubao wa kunakili ni tupu",
"cancelSend": "Ghairi kutuma",
"cancelSendConfirmTitle": "Ghairi kutuma huku?",
"cancelSendConfirmBody": "Hufanya kazi tu ikiwa mpokeaji hajatumia tokeni. Ikiwa tayari alitumia, kutuma kutaonyeshwa kama kumekamilika.",
"cancelSendSuccess": "Kutuma kumeghairiwa. Umerudisha {amount} {unit}",
"@cancelSendSuccess": {
"placeholders": {
"amount": {"type": "String"},
"unit": {"type": "String"}
}
},
"cancelSendAlreadyClaimed": "Mpokeaji tayari alitumia tokeni",
"pendingOfflineSend": "Kutuma nje ya mtandao kunasubiri",
"pendingOfflineSendsHeader": "Kutuma nje ya mtandao bila kudaiwa",
"pendingSendDetailTitle": "Kutuma kunakusubiri",
"validToken": "Tokeni halali",
"invalidToken": "Tokeni batili au imeharibika",
"amount": "Kiasi:",
@@ -118,6 +133,7 @@
"confirm": "Thibitisha",
"cancel": "Ghairi",
"insufficientBalance": "Salio halitoshi",
"feeExceedsAmount": "Ada inazidi kiasi cha kutuma",
"tokenCreationError": "Hitilafu ya kuunda tokeni: {error}",
"@tokenCreationError": {
"placeholders": {
@@ -137,6 +153,20 @@
},
"keepTokenWarning": "Hifadhi tokeni hii hadi mpokeaji aidai. Ukiipoteza, utapoteza fedha.",
"tokenCopiedToClipboard": "Tokeni imenakiliwa kwenye ubao",
"copyAsEmoji": "Nakili kama emoji",
"emojiCopiedToClipboard": "Tokeni imenakiliwa kama emoji 🥜",
"peanutDecodeError": "Imeshindwa kusimbua tokeni ya emoji. Inaweza kuwa imeharibika.",
"nfcWrite": "Andika kwenye NFC tag",
"nfcRead": "Soma NFC tag",
"nfcHoldNear": "Karibia kifaa kwenye NFC tag...",
"nfcWriteSuccess": "Tokeni imeandikwa kwenye NFC tag",
"nfcWriteError": "Hitilafu ya NFC kuandika: {error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "Hitilafu ya NFC kusoma: {error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC imezimwa. Iwashe katika Mipangilio.",
"nfcUnsupported": "Kifaa hiki hakitumii NFC",
"amountToDeposit": "Kiasi cha kuweka:",
"descriptionOptional": "Maelezo (si lazima):",
@@ -213,6 +243,8 @@
"pendingStatus": "Inasubiri",
"receivedStatus": "Imepokelewa",
"sentStatus": "Imetumwa",
"receiving": "Inapokea",
"sending": "Inatumwa",
"now": "Sasa",
"agoMinutes": "Dakika {minutes} zilizopita",
"@agoMinutes": {
@@ -296,7 +328,7 @@
}
},
"close": "Funga",
"aboutDescription": "Pochi ya Cashu yenye DNA ya Cuba kwa ulimwengu wote. Ndugu wa La Chispa.",
"aboutDescription": "Pochi ya Cashu yenye DNA ya Cuba kwa ulimwengu wote. Ndugu wa LaChispa.",
"couldNotOpenLink": "Haikuweza kufungua kiungo",
"deleteWalletQuestion": "Futa pochi?",
@@ -488,7 +520,22 @@
"scanLightningInvoiceHint": "Changanua ankara ya Lightning (lnbc...)",
"addMintQuestion": "Ongeza mint hii?",
"cameraPermissionDenied": "Ruhusa ya kamera imekataliwa",
"paymentRequestNotSupported": "Maombi ya malipo bado hayatumiki",
"paymentRequestTitle": "Ombi la malipo",
"paymentRequestFrom": "Ombi kutoka",
"paymentRequestAmount": "Kiasi kilichoombwa",
"paymentRequestDescription": "Maelezo",
"paymentRequestMints": "Mint zinazokubaliwa",
"paymentRequestAnyMint": "Mint yoyote",
"paymentRequestPay": "Lipa",
"paymentRequestPaying": "Inalipa...",
"paymentRequestSuccess": "Malipo yametumwa kwa mafanikio",
"paymentRequestNoTransport": "Ombi hili halina njia ya uwasilishaji iliyosanidiwa",
"paymentRequestTransport": "Njia ya usafirishaji",
"paymentRequestMintNotAccepted": "Mint yako hai haiko kwenye orodha ya mint zinazokubaliwa",
"paymentRequestUnitMismatch": "Kitengo kisichooana: ombi linahitaji {unit}",
"paymentRequestInsufficientBalance": "Salio haitoshi",
"paymentRequestErrorParsing": "Hitilafu wakati wa kusoma ombi la malipo",
"p2pkTitle": "Funguo za P2PK",
"p2pkSettingsDescription": "Pokea ecash iliyofungwa",
"p2pkExperimental": "P2PK ni ya majaribio. Tumia kwa uangalifu.",
@@ -520,5 +567,37 @@
"p2pkErrorInvalidNsec": "nsec batili",
"p2pkErrorKeyAlreadyExists": "Ufunguo huu tayari upo",
"p2pkErrorKeyNotFound": "Ufunguo haujapatikana",
"p2pkErrorCannotDeletePrimary": "Haiwezekani kufuta ufunguo mkuu"
"p2pkErrorCannotDeletePrimary": "Haiwezekani kufuta ufunguo mkuu",
"request": "Omba",
"requestPayment": "Omba malipo",
"requestPaymentDescription": "Tengeneza ombi la malipo la pamoja",
"generateRequest": "Tengeneza ombi",
"generatingRequest": "Inatengeneza ombi...",
"requestPaymentReceived": "Malipo limepokelewa",
"requestDescriptionHint": "Maelezo (si lazima)",
"universal": "Universal",
"copiedToClipboard": "Imenakiliwa kwenye ubao wa kunakili",
"swap": "Badilisha",
"swapDescription": "Badilisha kati ya sats na USD",
"swapFrom": "Kutoka",
"swapTo": "Kwenda",
"swapAction": "Badilisha",
"swapEstimatedFee": "Ada inayokadiriwa",
"swapUseAll": "Tumia yote",
"swapMinimum": "Kiwango cha chini: {amount}",
"swapProcessing": "Inashughulikia ubadilishaji...",
"swapSuccess": "Ubadilishaji umekamilika",
"swapErrorInsufficient": "Salio haitoshi",
"swapErrorExpired": "Bei imeisha muda",
"swapErrorGeneric": "Kosa la ubadilishaji: {error}",
"swapChartUnavailable": "Bei haipatikani · Gusa kurudia",
"swapChartMinMax": "24h Chini: {minPrice} — Juu: {maxPrice}",
"privacyPolicy": "Sera ya faragha",
"privacyTitle": "HATUKUSANYI CHOCHOTE",
"privacyGoodbye": "KWAHERI",
"privacyKeepReading": "(endelea kusoma ukitaka…)",
"privacyBody": "Hatujui wewe ni nani\nHatujui una kiasi gani\nHatujui unafanya nini",
"privacyConclusion": "Njia bora ya kulinda data yako\nni kutokuwa nayo"
}
+84 -5
View File
@@ -63,8 +63,8 @@
"homeTitle": "首页",
"receive": "收款",
"send": "付款",
"sendAction": "付款",
"receiveAction": "收款",
"sendAction": "付款",
"receiveAction": "收款",
"deposit": "存入",
"withdraw": "提取",
"lightning": "闪电网络",
@@ -86,6 +86,21 @@
"receiveCashu": "接收 Cashu",
"pasteTheCashuToken": "粘贴 Cashu 代币:",
"pasteFromClipboard": "从剪贴板粘贴",
"emptyClipboard": "剪贴板为空",
"cancelSend": "取消发送",
"cancelSendConfirmTitle": "取消此次发送?",
"cancelSendConfirmBody": "仅当接收方尚未领取代币时有效。如已领取,发送将标记为已完成。",
"cancelSendSuccess": "发送已取消。已恢复 {amount} {unit}",
"@cancelSendSuccess": {
"placeholders": {
"amount": {"type": "String"},
"unit": {"type": "String"}
}
},
"cancelSendAlreadyClaimed": "接收方已领取代币",
"pendingOfflineSend": "待处理的离线发送",
"pendingOfflineSendsHeader": "未领取的离线发送",
"pendingSendDetailTitle": "待处理发送",
"validToken": "有效代币",
"invalidToken": "无效或格式错误的代币",
"amount": "金额:",
@@ -118,6 +133,7 @@
"confirm": "确认",
"cancel": "取消",
"insufficientBalance": "余额不足",
"feeExceedsAmount": "手续费超过发送金额",
"tokenCreationError": "创建代币错误:{error}",
"@tokenCreationError": {
"placeholders": {
@@ -137,6 +153,20 @@
},
"keepTokenWarning": "请保留此代币直到收款方领取。如果丢失,资金将无法找回。",
"tokenCopiedToClipboard": "代币已复制到剪贴板",
"copyAsEmoji": "复制为表情符号",
"emojiCopiedToClipboard": "代币已复制为表情符号 🥜",
"peanutDecodeError": "无法解码表情符号代币。可能已损坏。",
"nfcWrite": "写入NFC标签",
"nfcRead": "读取NFC标签",
"nfcHoldNear": "将设备靠近NFC标签...",
"nfcWriteSuccess": "代币已写入NFC标签",
"nfcWriteError": "NFC写入错误:{error}",
"@nfcWriteError": { "placeholders": { "error": { "type": "String" } } },
"nfcReadError": "NFC读取错误:{error}",
"@nfcReadError": { "placeholders": { "error": { "type": "String" } } },
"nfcDisabled": "NFC已禁用,请在设置中启用。",
"nfcUnsupported": "此设备不支持NFC",
"amountToDeposit": "存入金额:",
"descriptionOptional": "描述(可选):",
@@ -213,6 +243,8 @@
"pendingStatus": "待处理",
"receivedStatus": "已收到",
"sentStatus": "已发送",
"receiving": "接收中",
"sending": "发送中",
"now": "刚刚",
"agoMinutes": "{minutes} 分钟前",
"@agoMinutes": {
@@ -296,7 +328,7 @@
}
},
"close": "关闭",
"aboutDescription": "具有古巴基因的 Cashu 钱包,面向全世界。La Chispa 的兄弟项目。",
"aboutDescription": "具有古巴基因的 Cashu 钱包,面向全世界。LaChispa 的兄弟项目。",
"couldNotOpenLink": "无法打开链接",
"deleteWalletQuestion": "删除钱包?",
@@ -488,7 +520,22 @@
"scanLightningInvoiceHint": "扫描闪电发票(lnbc...",
"addMintQuestion": "添加此铸造厂?",
"cameraPermissionDenied": "相机权限被拒绝",
"paymentRequestNotSupported": "付款请求尚不支持",
"paymentRequestTitle": "付款请求",
"paymentRequestFrom": "来自",
"paymentRequestAmount": "请求金额",
"paymentRequestDescription": "描述",
"paymentRequestMints": "接受的铸造厂",
"paymentRequestAnyMint": "任何铸造厂",
"paymentRequestPay": "支付",
"paymentRequestPaying": "支付中...",
"paymentRequestSuccess": "付款发送成功",
"paymentRequestNoTransport": "此请求未配置交付方式",
"paymentRequestTransport": "传输方式",
"paymentRequestMintNotAccepted": "您的活跃铸造厂不在接受的铸造厂列表中",
"paymentRequestUnitMismatch": "单位不兼容:请求需要 {unit}",
"paymentRequestInsufficientBalance": "余额不足",
"paymentRequestErrorParsing": "读取付款请求时出错",
"p2pkTitle": "P2PK密钥",
"p2pkSettingsDescription": "接收锁定的ecash",
"p2pkExperimental": "P2PK是实验性功能。请谨慎使用。",
@@ -520,5 +567,37 @@
"p2pkErrorInvalidNsec": "无效的nsec",
"p2pkErrorKeyAlreadyExists": "此密钥已存在",
"p2pkErrorKeyNotFound": "未找到密钥",
"p2pkErrorCannotDeletePrimary": "无法删除主密钥"
"p2pkErrorCannotDeletePrimary": "无法删除主密钥",
"request": "请求",
"requestPayment": "请求付款",
"requestPaymentDescription": "生成统一付款请求",
"generateRequest": "生成请求",
"generatingRequest": "正在生成请求...",
"requestPaymentReceived": "已收到付款",
"requestDescriptionHint": "描述(可选)",
"universal": "通用",
"copiedToClipboard": "已复制到剪贴板",
"swap": "兑换",
"swapDescription": "在 Sats 和 USD 之间转换",
"swapFrom": "从",
"swapTo": "到",
"swapAction": "兑换",
"swapEstimatedFee": "预估手续费",
"swapUseAll": "全部使用",
"swapMinimum": "最低: {amount}",
"swapProcessing": "兑换处理中...",
"swapSuccess": "兑换完成",
"swapErrorInsufficient": "余额不足",
"swapErrorExpired": "报价已过期",
"swapErrorGeneric": "兑换错误: {error}",
"swapChartUnavailable": "价格不可用 · 点击重试",
"swapChartMinMax": "24h 最低: {minPrice} — 最高: {maxPrice}",
"privacyPolicy": "隐私政策",
"privacyTitle": "我们什么都不收集",
"privacyGoodbye": "再见",
"privacyKeepReading": "(想继续看就看吧…)",
"privacyBody": "我们不知道你是谁\n我们不知道你有多少\n我们不知道你在做什么",
"privacyConclusion": "保护数据的最好方式\n就是不拥有它"
}
+3 -3
View File
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:provider/provider.dart';
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
import 'src/rust/frb_generated.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import 'core/theme/app_theme.dart';
import 'providers/wallet_provider.dart';
@@ -15,8 +15,8 @@ import 'screens/1_splash/splash_screen.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Inicializar CDK Flutter (Cashu SDK)
await CdkFlutter.init();
// Inicializar bridge Rust interno (Cashu SDK)
await RustLib.init();
// Configurar orientación y estilo de barra de estado
SystemChrome.setPreferredOrientations([
+1 -1
View File
@@ -11,7 +11,7 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:bip32/bip32.dart' as bip32;
import 'package:cdk_flutter/cdk_flutter.dart' as cdk;
import '../src/rust/api/keys.dart' as cdk;
import 'package:uuid/uuid.dart';
import '../models/p2pk_key.dart';
File diff suppressed because it is too large Load Diff
+105 -31
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:elcaju/l10n/app_localizations.dart';
@@ -8,6 +9,7 @@ import '../../widgets/scanner/qr_scanner_widget.dart';
import '../../providers/wallet_provider.dart';
import '../4_receive/receive_screen.dart';
import '../7_melt/melt_screen.dart';
import '../11_payment_request/payment_request_screen.dart';
/// Pantalla de escaneo QR con soporte para diferentes modos
class ScanScreen extends StatefulWidget {
@@ -58,38 +60,81 @@ class _ScanScreenState extends State<ScanScreen> {
children: [
// Scanner
QrScannerWidget(
onDetect: _onCodeDetected,
showFlashControl: true,
showCameraSwitch: false,
),
// Instrucciones en la parte inferior
Positioned(
bottom: 100,
left: 24,
right: 24,
child: Text(
_getInstructionForMode(l10n),
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white.withValues(alpha: 0.8),
),
textAlign: TextAlign.center,
onDetect: _onCodeDetected,
showFlashControl: true,
showCameraSwitch: false,
),
),
// Indicador de procesamiento
if (_isProcessing)
Container(
color: Colors.black54,
child: const Center(
child: CircularProgressIndicator(
color: AppColors.primaryAction,
// Instrucciones en la parte inferior
Positioned(
bottom: 170,
left: 24,
right: 24,
child: Text(
_getInstructionForMode(l10n),
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white.withValues(alpha: 0.8),
),
textAlign: TextAlign.center,
),
),
// Botón pegar del portapapeles (encima del flash)
Positioned(
bottom: 100,
left: 24,
right: 24,
child: Center(
child: Material(
color: Colors.white.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(24),
child: InkWell(
onTap: _isProcessing ? null : _pasteFromClipboard,
borderRadius: BorderRadius.circular(24),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
LucideIcons.clipboard,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
Text(
l10n.pasteFromClipboard,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
),
),
),
),
),
],
// Indicador de procesamiento
if (_isProcessing)
Container(
color: Colors.black54,
child: const Center(
child: CircularProgressIndicator(
color: AppColors.primaryAction,
),
),
),
],
),
),
);
@@ -117,6 +162,24 @@ class _ScanScreenState extends State<ScanScreen> {
}
}
Future<void> _pasteFromClipboard() async {
final l10n = L10n.of(context)!;
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
final text = clipboardData?.text?.trim();
if (!mounted) return;
if (text == null || text.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.emptyClipboard),
backgroundColor: AppColors.warning,
duration: const Duration(seconds: 2),
),
);
return;
}
_onCodeDetected(text);
}
void _onCodeDetected(String rawData) async {
if (_isProcessing) return;
@@ -195,8 +258,17 @@ class _ScanScreenState extends State<ScanScreen> {
break;
case IncomingDataType.paymentRequest:
// TODO: Implementar manejo de payment requests (post-MVP)
_showError(l10n.paymentRequestNotSupported);
if (mounted) {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => PaymentRequestScreen(
encodedRequest: data.raw,
),
),
);
}
break;
case IncomingDataType.unknown:
@@ -221,8 +293,10 @@ class _ScanScreenState extends State<ScanScreen> {
}
void _handleInvoiceOnlyMode(ParsedData data) {
if (data.type == IncomingDataType.lightningInvoice) {
// Retornar el invoice vía callback
if (data.type == IncomingDataType.lightningInvoice ||
(data.type == IncomingDataType.paymentRequest &&
data.invoiceBolt11 != null)) {
// Return the invoice — works for pure BOLT11 and BIP-321 URIs with lightning=
Navigator.pop(context, data.invoiceBolt11 ?? data.raw);
widget.onDataScanned?.call(data.invoiceBolt11 ?? data.raw);
} else {
@@ -0,0 +1,338 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
import '../../src/rust/api/payment_request.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../providers/wallet_provider.dart';
/// Pantalla de confirmación para pagar un Payment Request (NUT-18/26)
class PaymentRequestScreen extends StatefulWidget {
/// Raw encoded payment request (creqA, CREQB1, or bitcoin:?creq=)
final String encodedRequest;
const PaymentRequestScreen({super.key, required this.encodedRequest});
@override
State<PaymentRequestScreen> createState() => _PaymentRequestScreenState();
}
class _PaymentRequestScreenState extends State<PaymentRequestScreen> {
PaymentRequestInfo? _info;
String? _parseError;
bool _isPaying = false;
String? _payError;
@override
void initState() {
super.initState();
_parseRequest();
}
void _parseRequest() {
try {
final info = PaymentRequestInfo.parse(encoded: widget.encodedRequest);
setState(() => _info = info);
} catch (e) {
setState(() => _parseError = e.toString());
}
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
title: Text(
l10n.paymentRequestTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
),
),
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft),
onPressed: () => Navigator.pop(context),
),
),
body: _parseError != null
? _buildError(l10n)
: _info == null
? const Center(child: CircularProgressIndicator())
: _buildContent(l10n),
),
);
}
Widget _buildError(L10n l10n) {
return Center(
child: Padding(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: GlassCard(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(LucideIcons.alertCircle, color: Colors.redAccent, size: 48),
const SizedBox(height: 16),
Text(
l10n.paymentRequestErrorParsing,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
_parseError!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 13,
color: Colors.white.withValues(alpha: 0.6),
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
Widget _buildContent(L10n l10n) {
final info = _info!;
final walletProvider = context.watch<WalletProvider>();
final activeUnit = walletProvider.activeUnit;
final requestUnit = info.unit ?? 'sat';
// Validaciones
final unitMismatch = info.unit != null && info.unit != activeUnit;
String stripSlash(String url) =>
url.endsWith('/') ? url.substring(0, url.length - 1) : url;
final normalizedMints = info.mints.map(stripSlash).toSet();
final activeMint = walletProvider.activeMintUrl;
final mintNotAccepted = normalizedMints.isNotEmpty &&
(activeMint == null || !normalizedMints.contains(stripSlash(activeMint)));
final hasTransport = info.transports.isNotEmpty;
return SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Monto
if (info.amount != null)
_buildAmountCard(info.amount!, requestUnit, l10n),
const SizedBox(height: 16),
// Detalles
GlassCard(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Descripción
if (info.description != null && info.description!.isNotEmpty) ...[
_buildDetailRow(
LucideIcons.fileText,
l10n.paymentRequestDescription,
info.description!,
),
const SizedBox(height: 12),
],
// Mints aceptados
_buildDetailRow(
LucideIcons.server,
l10n.paymentRequestMints,
info.mints.isEmpty
? l10n.paymentRequestAnyMint
: info.mints.map((m) => Uri.parse(m).host).join(', '),
),
// Transporte (solo mostrar si hay transporte configurado)
if (info.transports.isNotEmpty) ...[
const SizedBox(height: 12),
_buildDetailRow(
LucideIcons.send,
l10n.paymentRequestTransport,
info.transports
.map((t) => t.transportType == 'nostr' ? 'Nostr (NIP-17)' : 'HTTP POST')
.join(', '),
),
],
],
),
),
const SizedBox(height: 16),
// Warnings
if (unitMismatch)
_buildWarning(
LucideIcons.alertTriangle,
l10n.paymentRequestUnitMismatch(requestUnit),
),
if (mintNotAccepted)
_buildWarning(
LucideIcons.alertTriangle,
l10n.paymentRequestMintNotAccepted,
),
if (!hasTransport)
_buildWarning(
LucideIcons.info,
l10n.paymentRequestNoTransport,
),
if (_payError != null) ...[
const SizedBox(height: 12),
_buildWarning(LucideIcons.alertCircle, _payError!),
],
const SizedBox(height: 32),
// Botón pagar
PrimaryButton(
text: _isPaying ? l10n.paymentRequestPaying : l10n.paymentRequestPay,
icon: LucideIcons.zap,
isLoading: _isPaying,
onPressed: (!hasTransport || _isPaying)
? null
: _pay,
),
],
),
);
}
Widget _buildAmountCard(BigInt amount, String unit, L10n l10n) {
return GlassCard(
child: Column(
children: [
Text(
l10n.paymentRequestAmount,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white.withValues(alpha: 0.7),
),
),
const SizedBox(height: 8),
Text(
UnitFormatter.formatBalanceWithUnit(amount, unit),
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 36,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
],
),
);
}
Widget _buildDetailRow(IconData icon, String label, String value) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 18, color: AppColors.primaryAction),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white.withValues(alpha: 0.6),
),
),
const SizedBox(height: 2),
Text(
value,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
),
],
),
),
],
);
}
Widget _buildWarning(IconData icon, String text) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: GlassCard(
child: Row(
children: [
Icon(icon, size: 20, color: Colors.amber),
const SizedBox(width: 10),
Expanded(
child: Text(
text,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 13,
color: Colors.amber,
),
),
),
],
),
),
);
}
Future<void> _pay() async {
setState(() {
_isPaying = true;
_payError = null;
});
try {
final walletProvider = context.read<WalletProvider>();
await walletProvider.payPaymentRequest(
widget.encodedRequest,
customAmount: _info!.amount,
);
if (!mounted) return;
// Éxito — mostrar snackbar y volver
final l10n = L10n.of(context)!;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.paymentRequestSuccess),
backgroundColor: Colors.green.shade700,
),
);
Navigator.pop(context, true);
} catch (e) {
if (!mounted) return;
setState(() {
_isPaying = false;
_payError = e.toString();
});
}
}
}
+908
View File
@@ -0,0 +1,908 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:share_plus/share_plus.dart';
import '../../src/rust/api/wallet.dart';
import '../../src/rust/api/payment_request.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// import '../../core/utils/bip321_builder.dart';
import '../../core/services/nfc_service.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/common/numpad_widget.dart';
import '../../providers/wallet_provider.dart';
enum RequestStatus { input, generating, waiting, received, error }
enum QrMode { universal, cashu, lightning }
/// Pantalla para solicitar pagos via Payment Request unificado.
/// Genera creqB (Cashu/Nostr) + Lightning invoice en paralelo.
class RequestScreen extends StatefulWidget {
const RequestScreen({super.key});
@override
State<RequestScreen> createState() => _RequestScreenState();
}
class _RequestScreenState extends State<RequestScreen> {
final TextEditingController _descriptionController = TextEditingController();
// Input state
String _amountValue = '';
late String _activeUnit;
// Request state
RequestStatus _status = RequestStatus.input;
String? _errorMessage;
// Payment data
String? _creqB;
String? _bolt11;
// TODO: re-enable QrMode.cashu / universal once CDK fixes NIP-17 sender pubkey bug
// See: https://github.com/cashubtc/cdk/issues/1807
// QrMode _activeMode = QrMode.lightning;
bool _paymentHandled = false;
// Listeners
StreamSubscription<NostrPaymentEvent>? _nostrSubscription;
StreamSubscription<MintQuote>? _mintSubscription;
// NFC
NfcState _nfcState = NfcState.unsupported;
bool _nfcEmulating = false;
// Success
BigInt _receivedAmount = BigInt.zero;
late final WalletProvider _walletProvider;
@override
void initState() {
super.initState();
_walletProvider = context.read<WalletProvider>();
_activeUnit = _walletProvider.activeUnit;
_checkNfc();
}
@override
void dispose() {
_descriptionController.dispose();
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// No-op while Nostr is disabled (_nostrSubscription is always null, _creqB is always null)
_nostrSubscription?.cancel();
_mintSubscription?.cancel();
if (_nfcEmulating) NfcService.stopEmulating();
if (!_paymentHandled && _creqB != null) {
_walletProvider.removePendingNostrRequest();
}
super.dispose();
}
Future<void> _checkNfc() async {
final state = await NfcService.checkState();
if (mounted) setState(() => _nfcState = state);
}
String get _unitLabel => UnitFormatter.getUnitLabel(_activeUnit);
BigInt get _amount => UnitFormatter.parseRawDigits(_amountValue, _activeUnit);
bool get _isValidAmount => _amount > BigInt.zero;
// ─── Build ───
@override
Widget build(BuildContext context) {
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
L10n.of(context)!.requestPayment,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
actions: [
if (_status == RequestStatus.waiting)
IconButton(
icon: const Icon(LucideIcons.share2, color: Colors.white),
onPressed: _shareRequest,
),
],
),
body: SafeArea(child: _buildBody()),
),
);
}
Widget _buildBody() {
switch (_status) {
case RequestStatus.input:
return _buildInputView();
case RequestStatus.generating:
return _buildGeneratingView();
case RequestStatus.waiting:
return _buildWaitingView();
case RequestStatus.received:
return _buildSuccessView();
case RequestStatus.error:
return _buildErrorView();
}
}
// ─── Input View ───
Widget _buildInputView() {
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildAmountSection(),
const SizedBox(height: AppDimensions.paddingLarge),
_buildDescriptionSection(),
const SizedBox(height: AppDimensions.paddingMedium),
if (_errorMessage != null) _buildErrorMessage(),
],
),
),
),
Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: PrimaryButton(
text: L10n.of(context)!.generateRequest,
onPressed: _isValidAmount ? _generateRequest : null,
),
),
],
);
}
Widget _buildAmountSection() {
final displayAmount =
UnitFormatter.formatRawDigitsForDisplay(_amountValue, _activeUnit);
return Column(
children: [
Text(
displayAmount,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 48,
fontWeight: FontWeight.bold,
color: _isValidAmount || _amountValue.isEmpty
? Colors.white
: AppColors.error,
),
),
const SizedBox(height: 4),
Text(
_unitLabel,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 18,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
NumpadWidget(
value: _amountValue,
onChanged: (v) => setState(() {
_errorMessage = null;
_amountValue = v;
}),
),
],
);
}
Widget _buildDescriptionSection() {
final l10n = L10n.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.descriptionOptional,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingSmall),
child: TextField(
controller: _descriptionController,
maxLines: 2,
maxLength: 100,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white,
),
decoration: InputDecoration(
hintText: l10n.requestDescriptionHint,
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white.withValues(alpha: 0.3),
),
border: InputBorder.none,
counterStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
),
),
],
);
}
Widget _buildErrorMessage() {
return Container(
margin: const EdgeInsets.only(bottom: AppDimensions.paddingMedium),
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: AppColors.error.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
Icon(LucideIcons.alertCircle, color: AppColors.error, size: 20),
const SizedBox(width: 8),
Expanded(
child: Text(
_errorMessage!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.error,
),
),
),
],
),
);
}
// ─── Generating View ───
Widget _buildGeneratingView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(color: AppColors.primaryAction),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
L10n.of(context)!.generatingRequest,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary,
),
),
],
),
);
}
// ─── Waiting View (QR + sub-toggle + actions) ───
Widget _buildWaitingView() {
final l10n = L10n.of(context)!;
final formattedAmount = UnitFormatter.formatBalance(_amount, _activeUnit);
return Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Column(
children: [
// Amount header
Text(
'$formattedAmount $_unitLabel',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
// QR Code with logo overlay
Center(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
),
child: Stack(
alignment: Alignment.center,
children: [
QrImageView(
data: _getActiveQrContent(),
version: QrVersions.auto,
size: 260,
backgroundColor: Colors.white,
// TODO: use QrErrorCorrectLevel.M for universal mode once CDK fixes NIP-17 (cashubtc/cdk#1807)
errorCorrectionLevel: QrErrorCorrectLevel.H,
),
// TODO: re-enable Cashu logo overlay once CDK fixes NIP-17 (cashubtc/cdk#1807)
// if (_activeMode == QrMode.cashu)
// Container(
// width: 48, height: 48,
// decoration: BoxDecoration(
// color: Colors.white,
// borderRadius: BorderRadius.circular(10),
// border: Border.all(color: Colors.white, width: 4),
// ),
// child: ClipRRect(
// borderRadius: BorderRadius.circular(6),
// child: Image.asset('assets/img/cashu.png', fit: BoxFit.contain),
// ),
// ),
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: AppColors.bitcoinOrange,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: Colors.white, width: 4),
),
child: const Icon(
LucideIcons.zap,
color: Colors.white,
size: 28,
),
),
],
),
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Sub-toggle: Universal / Cashu / Lightning
_buildModeToggle(),
const SizedBox(height: AppDimensions.paddingMedium),
// Action buttons: Copy, NFC, Share
_buildActionButtons(),
const SizedBox(height: AppDimensions.paddingMedium),
// Status indicator
_buildStatusIndicator(),
],
),
),
),
// Cancel button
Padding(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
l10n.cancel,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
),
],
);
}
Widget _buildModeToggle() {
// TODO: re-enable Universal/Cashu tabs once CDK fixes NIP-17 (cashubtc/cdk#1807)
// Hidden while only Lightning is available (single button toggle is a no-op)
return const SizedBox.shrink(
// child: Container(
// padding: const EdgeInsets.all(4),
// decoration: BoxDecoration(
// color: Colors.white.withValues(alpha: 0.1),
// borderRadius: BorderRadius.circular(12),
// ),
// child: Row(
// children: [
// _buildToggleButton(label: L10n.of(context)!.universal, mode: QrMode.universal, enabled: _bolt11 != null),
// _buildToggleButton(label: 'Cashu', mode: QrMode.cashu, enabled: true),
// _buildToggleButton(label: 'Lightning', mode: QrMode.lightning, enabled: _bolt11 != null),
// ],
// ),
// ),
);
}
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// Widget _buildToggleButton({
// required String label,
// required QrMode mode,
// required bool enabled,
// }) {
// final isActive = _activeMode == mode;
// return Expanded(
// child: GestureDetector(
// onTap: enabled
// ? () {
// setState(() => _activeMode = mode);
// _updateNfcPayload();
// }
// : null,
// child: Container(
// padding: const EdgeInsets.symmetric(vertical: 10),
// decoration: BoxDecoration(
// gradient: isActive
// ? const LinearGradient(colors: AppColors.buttonGradient)
// : null,
// borderRadius: BorderRadius.circular(10),
// ),
// child: Center(
// child: Text(
// label,
// style: TextStyle(
// fontFamily: 'Inter',
// fontSize: 13,
// fontWeight: isActive ? FontWeight.w600 : FontWeight.w400,
// color: enabled
// ? (isActive ? Colors.white : AppColors.textSecondary)
// : Colors.white.withValues(alpha: 0.3),
// ),
// ),
// ),
// ),
// ),
// );
// }
Widget _buildActionButtons() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildActionButton(
icon: LucideIcons.copy,
label: L10n.of(context)!.copy,
onTap: _copyRequest,
),
if (_nfcState == NfcState.enabled) ...[
const SizedBox(width: 16),
_buildActionButton(
icon: _nfcEmulating ? LucideIcons.wifiOff : LucideIcons.wifi,
label: 'NFC',
onTap: _toggleNfc,
active: _nfcEmulating,
),
],
const SizedBox(width: 16),
_buildActionButton(
icon: LucideIcons.share2,
label: L10n.of(context)!.share,
onTap: _shareRequest,
),
],
);
}
Widget _buildActionButton({
required IconData icon,
required String label,
required VoidCallback onTap,
bool active = false,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: active
? AppColors.primaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: active
? Border.all(color: AppColors.primaryAction.withValues(alpha: 0.5))
: null,
),
child: Row(
children: [
Icon(icon, color: active ? AppColors.primaryAction : AppColors.textSecondary, size: 18),
const SizedBox(width: 6),
Text(
label,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: active ? AppColors.primaryAction : AppColors.textSecondary,
),
),
],
),
),
);
}
Widget _buildStatusIndicator() {
return GlassCard(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.textSecondary),
),
),
const SizedBox(width: 12),
Text(
L10n.of(context)!.waitingForPayment,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textSecondary,
),
),
],
),
);
}
// ─── Success View ───
Widget _buildSuccessView() {
final l10n = L10n.of(context)!;
final formattedAmount =
'+${UnitFormatter.formatBalance(_receivedAmount, _activeUnit)}';
final unitLabel = UnitFormatter.getUnitLabel(_activeUnit);
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.checkCircle2,
color: AppColors.success,
size: 40,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
'$formattedAmount $unitLabel',
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
Text(
l10n.requestPaymentReceived,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary,
),
),
const SizedBox(height: AppDimensions.paddingLarge * 2),
PrimaryButton(
text: l10n.backToHome,
onPressed: () =>
Navigator.popUntil(context, (route) => route.isFirst),
),
],
),
);
}
// ─── Error View ───
Widget _buildErrorView() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: const Icon(
LucideIcons.alertCircle,
color: AppColors.error,
size: 40,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
Text(
L10n.of(context)!.error,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
_errorMessage ?? L10n.of(context)!.unknownError,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: AppDimensions.paddingLarge),
PrimaryButton(
text: L10n.of(context)!.back,
onPressed: () => setState(() {
_status = RequestStatus.input;
_errorMessage = null;
}),
),
],
),
);
}
// ─── Logic ───
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// static const List<String> _defaultNostrRelays = [
// 'wss://relay.damus.io',
// 'wss://relay.primal.net',
// 'wss://nos.lol',
// ];
Future<void> _generateRequest() async {
final walletProvider = context.read<WalletProvider>();
final wallet = walletProvider.activeWallet;
// Clean up previous request state — await to ensure Rust locks are released
await _nostrSubscription?.cancel();
_nostrSubscription = null;
await _mintSubscription?.cancel();
_mintSubscription = null;
if (_nfcEmulating) {
NfcService.stopEmulating();
_nfcEmulating = false;
}
_paymentHandled = false;
_creqB = null;
_bolt11 = null;
// TODO: re-enable Cashu/Nostr payment request once CDK fixes NIP-17 sender pubkey bug
// See: https://github.com/cashubtc/cdk/issues/1807
// _activeMode = QrMode.lightning;
setState(() => _status = RequestStatus.generating);
if (wallet == null) {
setState(() {
_status = RequestStatus.error;
_errorMessage = 'No active wallet';
});
return;
}
try {
final description = _descriptionController.text.isNotEmpty
? _descriptionController.text
: null;
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// // 1. Create payment request (creqB + Nostr keys)
// final request = await wallet.createPaymentRequest(
// params: CreateRequestParams(
// amount: _amount,
// unit: _activeUnit,
// description: description,
// nostrRelays: _defaultNostrRelays,
// ),
// );
//
// _creqB = request.creqB;
//
// // Persist handle for recovery if app is killed
// await walletProvider.savePendingNostrRequest(
// request.listenerHandle.toPersisted(),
// );
//
// // 2. Start Nostr listener
// _nostrSubscription = wallet
// .waitForNostrPayment(handle: request.listenerHandle)
// .listen(_onNostrEvent);
// Lightning invoice generation via wallet provider
// (walletProvider.mintTokens handles pending invoice persistence and metadata)
final mintStream = await walletProvider.mintTokens(
_amount,
description,
);
if (!mounted) return;
_mintSubscription = mintStream.listen(_onMintEvent);
} catch (e) {
if (!mounted) return;
setState(() {
_status = RequestStatus.error;
_errorMessage = e.toString();
});
}
}
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// void _onNostrEvent(NostrPaymentEvent event) {
// if (!mounted || _paymentHandled) return;
// if (event.state == NostrPaymentState.received) {
// _paymentHandled = true;
// _mintSubscription?.cancel();
// _onPaymentSuccess(event.amount ?? _amount);
// }
// }
void _onMintEvent(MintQuote quote) {
if (!mounted) return;
switch (quote.state) {
case MintQuoteState.unpaid:
if (_bolt11 == null) {
setState(() {
_bolt11 = quote.request;
// TODO: switch to QrMode.universal once CDK fixes NIP-17 (cashubtc/cdk#1807)
// _activeMode = QrMode.lightning;
_status = RequestStatus.waiting;
});
_updateNfcPayload();
}
break;
case MintQuoteState.issued:
if (!_paymentHandled) {
_paymentHandled = true;
_nostrSubscription?.cancel();
_onPaymentSuccess(quote.amount ?? _amount);
}
break;
case MintQuoteState.error:
// No Nostr fallback while CDK NIP-17 bug is open (cashubtc/cdk#1807)
if (!_paymentHandled) {
if (_nfcEmulating) {
NfcService.stopEmulating();
_nfcEmulating = false;
}
setState(() {
_status = RequestStatus.error;
_errorMessage = quote.error ?? L10n.of(context)!.unknownError;
});
}
break;
default:
break;
}
}
Future<void> _onPaymentSuccess(BigInt amount) async {
if (_nfcEmulating) NfcService.stopEmulating();
setState(() {
_status = RequestStatus.received;
_receivedAmount = amount;
});
// Confetti se dispara globalmente desde WalletProvider._saveMintMetadata
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// Only remove pending Nostr request if this screen created one.
// Disabled while Nostr payment requests are disabled to avoid
// deleting a recovery record from a previous session.
// await walletProvider.removePendingNostrRequest();
}
// ─── QR Content ───
String _getActiveQrContent() {
// TODO: re-enable once CDK fixes NIP-17 (cashubtc/cdk#1807)
// case QrMode.universal:
// return buildUnifiedUri(creqB: _creqB!, bolt11: _bolt11);
// case QrMode.cashu:
// return _creqB!.toUpperCase();
return _bolt11?.toUpperCase() ?? '';
}
// ─── Actions ───
Future<void> _copyRequest() async {
final content = _getActiveQrContent();
await Clipboard.setData(ClipboardData(text: content));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.copiedToClipboard),
backgroundColor: AppColors.success,
duration: const Duration(seconds: 2),
),
);
}
}
void _toggleNfc() {
if (_nfcEmulating) {
NfcService.stopEmulating();
setState(() => _nfcEmulating = false);
} else {
NfcService.startEmulating(_getActiveQrContent());
setState(() => _nfcEmulating = true);
}
}
void _updateNfcPayload() {
if (_nfcEmulating) {
NfcService.startEmulating(_getActiveQrContent());
}
}
void _shareRequest() async {
final content = _getActiveQrContent();
final formattedAmount =
UnitFormatter.formatBalance(_amount, _activeUnit);
final unitLabel = UnitFormatter.getUnitLabel(_activeUnit);
await SharePlus.instance.share(
ShareParams(
text: '$formattedAmount $unitLabel\n\n$content',
subject: 'Payment Request - $formattedAmount $unitLabel',
),
);
}
}
File diff suppressed because it is too large Load Diff
+6 -1
View File
@@ -166,7 +166,12 @@ class _SplashScreenState extends State<SplashScreen>
/// Se ejecuta sin bloquear la navegación.
Future<void> _checkPendingTransactions(WalletProvider wallet) async {
try {
await wallet.checkPendingTransactions();
// CDK reconcilia online; reconcilePendingSends reconcilia los envíos
// offline (storage propio).
await Future.wait([
wallet.checkPendingTransactions(),
wallet.reconcilePendingSends(),
]);
debugPrint('Verificación de proofs completada');
} catch (e) {
debugPrint('Error verificando proofs: $e');
@@ -64,7 +64,7 @@ class _RestoreWalletScreenState extends State<RestoreWalletScreen> {
final p2pkProvider = context.read<P2PKProvider>();
// Inicializar wallet primero (valida el mnemonic internamente).
// Si es inválido, cdk_flutter lanzará una excepción antes de persistir.
// Si es inválido, el bridge Rust lanzará una excepción antes de persistir.
await walletProvider.initialize(mnemonic);
// Solo guardar mnemonic si initialize() pasó sin error
+84 -86
View File
@@ -9,11 +9,13 @@ import '../../core/utils/incoming_data_parser.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/animated_action_button.dart';
import '../../widgets/common/bottom_sheet_container.dart';
import '../../providers/wallet_provider.dart';
import '../../providers/settings_provider.dart';
import '../../providers/p2pk_provider.dart';
import '../4_receive/receive_screen.dart';
import '../5_send/send_screen.dart';
import '../6_mint/mint_screen.dart';
import '../12_request/request_screen.dart';
import '../7_melt/melt_screen.dart';
import '../8_settings/settings_screen.dart';
import '../8_settings/mints_screen.dart';
@@ -51,7 +53,10 @@ class _HomeScreenState extends State<HomeScreen> {
if (!walletProvider.hasPendingTokens) return;
try {
final result = await walletProvider.checkPendingTokens();
final p2pkProvider = context.read<P2PKProvider>();
final result = await walletProvider.checkPendingTokens(
p2pkKeyResolver: p2pkProvider.getPrivateKeyForToken,
);
final claimed = (result['claimed'] as int?) ?? 0;
final totalClaimed = result['totalClaimed'] as BigInt? ?? BigInt.zero;
final unit = (result['unit'] as String?) ?? walletProvider.activeUnit;
@@ -352,6 +357,9 @@ class _HomeScreenState extends State<HomeScreen> {
Expanded(
child: AnimatedActionButton(
label: l10n.sendAction,
icon: LucideIcons.arrowUpRight,
showIcon: true,
iconTrailing: true,
type: ButtonType.criticalAction,
onTap: _showSendOptions,
),
@@ -364,6 +372,8 @@ class _HomeScreenState extends State<HomeScreen> {
Expanded(
child: AnimatedActionButton(
label: l10n.receiveAction,
icon: LucideIcons.arrowDownRight,
showIcon: true,
type: ButtonType.primaryAction,
onTap: _showReceiveOptions,
),
@@ -433,14 +443,14 @@ class _HomeScreenState extends State<HomeScreen> {
},
),
_MethodOption(
icon: LucideIcons.zap,
label: l10n.lightning,
description: l10n.generateInvoiceToDeposit,
icon: LucideIcons.bellRing,
label: l10n.request,
description: l10n.requestPaymentDescription,
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const MintScreen()),
MaterialPageRoute(builder: (context) => const RequestScreen()),
);
},
),
@@ -569,29 +579,11 @@ class _MethodSelectorModal extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
return BottomSheetContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
const BottomSheetHandle(),
// Título
Text(
@@ -614,9 +606,6 @@ class _MethodSelectorModal extends StatelessWidget {
child: _MethodOptionTile(option: option),
),
),
// Espacio para la barra de navegación del sistema
SizedBox(height: MediaQuery.of(context).padding.bottom + AppDimensions.paddingSmall),
],
),
);
@@ -631,67 +620,76 @@ class _MethodOptionTile extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: option.onTap,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
return Semantics(
button: true,
label: option.label,
hint: option.description,
child: Material(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(16),
child: InkWell(
onTap: option.onTap,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Row(
children: [
// Icono
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: AppColors.buttonGradient,
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Row(
children: [
// Icono
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: AppColors.buttonGradient,
),
borderRadius: BorderRadius.circular(12),
),
child: Icon(option.icon, color: Colors.white, size: 24),
),
borderRadius: BorderRadius.circular(12),
),
child: Icon(option.icon, color: Colors.white, size: 24),
),
const SizedBox(width: AppDimensions.paddingMedium),
// Texto
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
option.label,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
const SizedBox(width: AppDimensions.paddingMedium),
// Texto
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
option.label,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
const SizedBox(height: 2),
Text(
option.description,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
],
),
const SizedBox(height: 2),
Text(
option.description,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
),
],
),
),
// Flecha
Icon(
LucideIcons.chevronRight,
color: Colors.white.withValues(alpha: 0.5),
size: 24,
),
],
),
// Flecha
Icon(
LucideIcons.chevronRight,
color: Colors.white.withValues(alpha: 0.5),
size: 24,
),
],
),
),
),
);
+215 -7
View File
@@ -8,12 +8,15 @@ import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
import '../../core/utils/incoming_data_parser.dart' hide TokenInfo;
import '../../core/utils/nostr_utils.dart';
import '../../core/utils/peanut_codec.dart';
import '../../core/services/nfc_service.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../providers/wallet_provider.dart';
import '../../providers/p2pk_provider.dart';
import '../10_scanner/scan_screen.dart';
import '../11_payment_request/payment_request_screen.dart';
/// Pantalla para recibir tokens Cashu
class ReceiveScreen extends StatefulWidget {
@@ -46,9 +49,14 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
bool _showManualKey = false; // Toggle para mostrar/ocultar nsec
String? _manualKeyError;
// NFC
NfcState _nfcState = NfcState.unsupported;
bool _nfcReading = false;
@override
void initState() {
super.initState();
_checkNfc();
// Pre-cargar token inicial si existe
if (widget.initialToken != null && widget.initialToken!.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -60,11 +68,19 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
@override
void dispose() {
if (_nfcReading) NfcService.stopRead();
_tokenController.dispose();
_manualKeyController.dispose();
super.dispose();
}
Future<void> _checkNfc() async {
final state = await NfcService.checkState();
if (mounted) {
setState(() => _nfcState = state);
}
}
@override
Widget build(BuildContext context) {
return GradientBackground(
@@ -245,6 +261,9 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
// Botón pegar (expandido)
Expanded(child: _buildPasteButton()),
const SizedBox(width: 12),
// Botón NFC (siempre visible)
_buildNfcButton(),
const SizedBox(width: 12),
// Botón escanear QR
_buildScanButton(),
],
@@ -291,6 +310,134 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
);
}
Widget _buildNfcButton() {
final isNfcEnabled = _nfcState == NfcState.enabled;
final isNfcUnsupported = _nfcState == NfcState.unsupported;
return Tooltip(
message: isNfcUnsupported
? L10n.of(context)!.nfcUnsupported
: isNfcEnabled
? L10n.of(context)!.nfcRead
: L10n.of(context)!.nfcDisabled,
child: GestureDetector(
onTap: _toggleNfcRead,
child: Opacity(
opacity: isNfcUnsupported ? 0.3 : 1.0,
child: Container(
padding: const EdgeInsets.all(AppDimensions.paddingSmall + 4),
decoration: BoxDecoration(
color: _nfcReading
? AppColors.primaryAction.withValues(alpha: 0.3)
: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _nfcReading
? AppColors.primaryAction
: Colors.white.withValues(alpha: 0.2),
width: 1,
),
),
child: Icon(
LucideIcons.nfc,
color: _nfcReading
? AppColors.primaryAction
: isNfcEnabled
? AppColors.textSecondary
: AppColors.textSecondary.withValues(alpha: 0.5),
size: 24,
),
),
),
),
);
}
void _toggleNfcRead() async {
// Cancel active session first (before state checks, in case NFC was
// disabled while a session was running)
if (_nfcReading) {
NfcService.stopRead();
setState(() => _nfcReading = false);
return;
}
// Re-check state in case user toggled NFC in settings
final currentState = await NfcService.checkState();
if (mounted) setState(() => _nfcState = currentState);
if (currentState == NfcState.unsupported) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcUnsupported),
backgroundColor: AppColors.textSecondary,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
return;
}
if (currentState == NfcState.disabled) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcDisabled),
backgroundColor: AppColors.warning,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
return;
}
setState(() => _nfcReading = true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcHoldNear),
backgroundColor: AppColors.primaryAction,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
try {
await NfcService.startRead(
onTokenRead: (token) {
if (!mounted) return;
setState(() => _nfcReading = false);
_tokenController.text = token;
_onTokenChanged(token);
},
onError: (error) {
if (!mounted) return;
setState(() => _nfcReading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcReadError(error)),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
);
},
);
} catch (e) {
if (!mounted) return;
setState(() => _nfcReading = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcReadError(e.toString())),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
);
}
}
Widget _buildScanButton() {
return GestureDetector(
onTap: _openScanner,
@@ -779,9 +926,29 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
Future<void> _pasteFromClipboard() async {
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
if (!mounted) return;
if (clipboardData?.text != null) {
_tokenController.text = clipboardData!.text!.trim();
_onTokenChanged(clipboardData.text!.trim());
var text = clipboardData!.text!.trim();
// Auto-decode peanut emoji format
if (PeanutCodec.isPeanut(text)) {
final decoded = PeanutCodec.decode(text);
if (decoded != null) {
text = decoded;
} else {
// Malformed peanut: clear field and show specific error
_tokenController.clear();
setState(() {
_isValidToken = false;
_tokenInfo = null;
_errorMessage = L10n.of(context)!.peanutDecodeError;
});
return;
}
}
_tokenController.text = text;
_onTokenChanged(text);
}
}
@@ -789,6 +956,28 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
final walletProvider = context.read<WalletProvider>();
final p2pkProvider = context.read<P2PKProvider>();
// Auto-decode peanut emoji format
var tokenValue = value.trim();
if (PeanutCodec.isPeanut(tokenValue)) {
final decoded = PeanutCodec.decode(tokenValue);
if (decoded != null) {
tokenValue = decoded;
// Update the text field with the decoded token
_tokenController.text = tokenValue;
_tokenController.selection = TextSelection.collapsed(
offset: tokenValue.length,
);
} else {
// Malformed peanut: show specific error
setState(() {
_isValidToken = false;
_tokenInfo = null;
_errorMessage = L10n.of(context)!.peanutDecodeError;
});
return;
}
}
setState(() {
_errorMessage = null;
_manualKeyError = null;
@@ -799,24 +988,43 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
_lockedToPubkeyHex = null;
_matchingKeyLabel = null;
if (value.isEmpty) {
if (tokenValue.isEmpty) {
_isValidToken = false;
_tokenInfo = null;
return;
}
// Detectar payment request y redirigir
final parsed = IncomingDataParser.parse(tokenValue);
if (parsed.type == IncomingDataType.paymentRequest) {
_isValidToken = false;
_tokenInfo = null;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => PaymentRequestScreen(
encodedRequest: tokenValue,
),
),
);
});
return;
}
// Parsear token real con cdk-flutter
final tokenInfo = walletProvider.parseToken(value.trim());
final tokenInfo = walletProvider.parseToken(tokenValue);
if (tokenInfo != null) {
_isValidToken = true;
_tokenInfo = tokenInfo;
// Detectar P2PK
_isP2PKLocked = p2pkProvider.isTokenLocked(value.trim());
_isP2PKLocked = p2pkProvider.isTokenLocked(tokenValue);
if (_isP2PKLocked) {
_lockedToPubkeyHex = p2pkProvider.extractLockedPubkey(value.trim());
_isLockedToUs = p2pkProvider.isTokenLockedToUs(value.trim());
_lockedToPubkeyHex = p2pkProvider.extractLockedPubkey(tokenValue);
_isLockedToUs = p2pkProvider.isTokenLockedToUs(tokenValue);
// Si es nuestra, buscar el label de la clave
if (_isLockedToUs && _lockedToPubkeyHex != null) {
+68 -8
View File
@@ -1,14 +1,19 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:provider/provider.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../providers/wallet_provider.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/models/proof.dart';
import '../../core/services/proof_service.dart';
import '../../src/rust/api/token.dart' as cdk;
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/proof/proof_selector.dart';
import '../../core/utils/keyset_debug.dart';
import 'share_token_screen.dart';
/// Pantalla para enviar tokens de forma offline seleccionando proofs manualmente.
@@ -30,6 +35,13 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
final TextEditingController _memoController = TextEditingController();
final ProofService _proofService = ProofService();
// Capturado en initState para poder persistir el PendingSend incluso si
// el widget se desmonta durante el await de markProofsPendingSpent.
// Usar `context.read` después del await es inseguro (widget puede no
// existir), y gatear con `mounted` perdería el record — los proofs
// quedan marcados PENDING_SPENT pero sin handle para reclamar.
late final WalletProvider _walletProvider;
List<LocalProof> _availableProofs = [];
Set<String> _selectedIds = {};
bool _isLoading = true;
@@ -39,13 +51,14 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
@override
void initState() {
super.initState();
_walletProvider = context.read<WalletProvider>();
_loadProofs();
}
@override
void dispose() {
_memoController.dispose();
_proofService.close();
if (!_isCreating) _proofService.close();
super.dispose();
}
@@ -321,32 +334,69 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
}
Future<void> _createOfflineToken() async {
final selectedProofs = _selectedProofs;
final selectedTotal = _proofService.calculateTotal(selectedProofs);
// Validar monto mínimo viable para el receptor
// Leer ppk del keyset local, fallback a 100 (estándar)
var ppk = await KeysetDebug.getInputFeePpk(widget.mintUrl, widget.unit);
if (ppk < 0) ppk = 100; // -1 = error leyendo DB, asumir fee estándar
final minReceiveFee = ppk > 0 ? BigInt.from((ppk + 999) ~/ 1000) : BigInt.zero;
if (selectedTotal <= minReceiveFee) {
if (mounted) {
setState(() {
_errorMessage = L10n.of(context)!.feeExceedsAmount;
});
}
return;
}
var proofsMarkedPending = false;
setState(() {
_isCreating = true;
});
try {
final selectedProofs = _selectedProofs;
// Marcar proofs como PENDING_SPENT
await _proofService.markProofsPendingSpent(selectedProofs);
proofsMarkedPending = true;
// Crear token V3
// Crear token via CDK (produce V4/cashuB válido)
final memo = _memoController.text.isNotEmpty ? _memoController.text : null;
final token = _proofService.createTokenV3(
final proofsJson = jsonEncode(
selectedProofs.map((p) => p.toTokenProof()).toList(),
);
final cdkToken = cdk.createOfflineToken(
mintUrl: widget.mintUrl,
proofs: selectedProofs,
proofsJson: proofsJson,
memo: memo,
unit: widget.unit,
);
final token = cdkToken.encoded;
// Rastrear el envío offline como pendiente para poder cancelarlo
// desde el historial. Los offline sends no crean CDK Transaction.
// Usamos el provider capturado en initState (no context.read) para
// que el record se persista aunque el widget se haya desmontado
// mientras esperábamos markProofsPendingSpent.
await _walletProvider.addPendingSend(
encoded: token,
amount: selectedTotal,
mintUrl: widget.mintUrl,
unit: widget.unit,
proofYs: selectedProofs.map((p) => p.yHex).toList(),
memo: memo,
);
if (mounted) {
_isCreating = false;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (context) => ShareTokenScreen(
token: token,
amount: _selectedTotal,
amount: selectedTotal,
unit: widget.unit,
memo: memo,
),
@@ -354,7 +404,17 @@ class _OfflineSendScreenState extends State<OfflineSendScreen> {
);
}
} catch (e) {
if (!mounted) return;
if (proofsMarkedPending) {
try {
await _proofService.markProofsUnspent(selectedProofs);
} catch (rollbackErr) {
debugPrint('Failed to rollback proofs to UNSPENT: $rollbackErr');
}
}
if (!mounted) {
_proofService.close();
return;
}
final errorMessage = L10n.of(context)!.creatingTokenError(e.toString());
setState(() {
_errorMessage = errorMessage;
+134 -48
View File
@@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
@@ -7,12 +8,16 @@ import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
import '../../core/utils/keyset_debug.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/common/secondary_button.dart';
import '../../widgets/common/bottom_sheet_container.dart';
import '../../widgets/common/numpad_widget.dart';
import '../../providers/wallet_provider.dart';
import '../../core/utils/nostr_utils.dart';
import '../../widgets/scanner/qr_scanner_widget.dart';
import 'share_token_screen.dart';
import 'offline_send_screen.dart';
@@ -350,10 +355,21 @@ class _SendScreenState extends State<SendScreen> {
color: Colors.white.withValues(alpha: 0.3),
),
border: InputBorder.none,
suffixIcon: IconButton(
icon: const Icon(LucideIcons.clipboard, size: 20),
color: AppColors.textSecondary,
onPressed: _pastePubkey,
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(LucideIcons.clipboard, size: 20),
color: AppColors.textSecondary,
onPressed: _pastePubkey,
),
IconButton(
icon: const Icon(LucideIcons.scanLine, size: 20),
color: AppColors.textSecondary,
tooltip: l10n.scanQrCode,
onPressed: _scanPubkey,
),
],
),
errorText: _pubkeyError,
errorStyle: TextStyle(
@@ -393,6 +409,63 @@ class _SendScreenState extends State<SendScreen> {
}
}
Future<void> _scanPubkey() async {
final result = await Navigator.push<String>(
context,
MaterialPageRoute(
builder: (context) => Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.x, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
L10n.of(context)!.scanQrCode,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
centerTitle: true,
),
body: QrScannerWidget(
onDetect: (data) {
final trimmed = data.trim();
if (NostrUtils.isValidP2PKPubkey(trimmed)) {
Navigator.pop(context, trimmed);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.p2pkInvalidPubkey),
duration: const Duration(seconds: 2),
),
);
}
},
onError: (error) {
debugPrint('[P2PK Scan] $error');
Navigator.pop(context);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.cameraPermissionDenied),
backgroundColor: AppColors.error,
),
);
},
),
),
),
);
if (!mounted || result == null) return;
_pubkeyController.text = result;
_validatePubkey(result);
}
Widget _buildErrorMessage() {
return Container(
margin: const EdgeInsets.only(bottom: AppDimensions.paddingMedium),
@@ -503,15 +576,37 @@ class _SendScreenState extends State<SendScreen> {
});
return;
}
// Offline: verificar monto mínimo con ppk del keyset local (fallback 100)
var ppk = await KeysetDebug.getInputFeePpk(mintUrl, _activeUnit);
if (ppk < 0) ppk = 100;
final minReceiveFee = ppk > 0 ? BigInt.from((ppk + 999) ~/ 1000) : BigInt.zero;
if (!mounted) return;
if (_amount <= minReceiveFee) {
setState(() {
_errorMessage = L10n.of(context)!.feeExceedsAmount;
});
return;
}
// Offline: ir directo a selección de monedas
_goToOfflineModeWithMessage();
return;
}
// Online: verificar monto mínimo con ppk real del mint
final minReceiveFee = await _getMinReceiveFee(mintUrl);
if (!mounted) return;
if (_amount <= minReceiveFee) {
setState(() {
_errorMessage = L10n.of(context)!.feeExceedsAmount;
});
return;
}
// Online: mostrar modal de confirmación normal
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => _ConfirmationModal(
amount: _amount,
unit: _activeUnit,
@@ -525,6 +620,32 @@ class _SendScreenState extends State<SendScreen> {
);
}
/// Calcula el fee mínimo que el receptor pagará al reclamar el token.
/// Consulta /v1/keysets del mint para obtener el ppk real.
/// Fallback: ppk=100 (estándar) → fee mínimo = 1 sat.
Future<BigInt> _getMinReceiveFee(String mintUrl) async {
try {
final response = await http.get(
Uri.parse('$mintUrl/v1/keysets'),
).timeout(const Duration(seconds: 3));
if (response.statusCode == 200) {
final data = json.decode(response.body) as Map<String, dynamic>;
final keysets = data['keysets'] as List<dynamic>? ?? [];
for (final ks in keysets) {
if (ks['active'] == true && ks['unit'] == _activeUnit) {
final ppk = (ks['input_fee_ppk'] as num?)?.toInt() ?? 0;
if (ppk > 0) {
return BigInt.from((ppk + 999) ~/ 1000); // ceil(ppk / 1000)
}
return BigInt.zero;
}
}
}
} catch (_) {}
// Fallback: ppk=100 estándar
return BigInt.one;
}
/// Verifica conectividad haciendo petición HTTP real al mint.
Future<bool> _checkConnectivity(String mintUrl) async {
try {
@@ -678,29 +799,12 @@ class _ConfirmationModal extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
final l10n = L10n.of(context)!;
return BottomSheetContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
const BottomSheetHandle(),
// Icono
Container(
@@ -720,7 +824,7 @@ class _ConfirmationModal extends StatelessWidget {
// Titulo
Text(
L10n.of(context)!.confirmSend,
l10n.confirmSend,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 20,
@@ -761,40 +865,22 @@ class _ConfirmationModal extends StatelessWidget {
Row(
children: [
Expanded(
child: GestureDetector(
onTap: onCancel,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
L10n.of(context)!.cancel,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
child: SecondaryButton(
text: l10n.cancel,
onPressed: onCancel,
height: 52,
),
),
const SizedBox(width: AppDimensions.paddingMedium),
Expanded(
child: PrimaryButton(
text: L10n.of(context)!.confirm,
text: l10n.confirm,
onPressed: onConfirm,
height: 52,
),
),
],
),
const SizedBox(height: AppDimensions.paddingSmall),
],
),
);
+173 -4
View File
@@ -4,11 +4,13 @@ import 'package:flutter/services.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:share_plus/share_plus.dart';
import 'package:cdk_flutter/cdk_flutter.dart' as cdk;
import '../../src/rust/api/token.dart' as cdk;
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
import '../../core/utils/peanut_codec.dart';
import '../../core/services/nfc_service.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
@@ -44,21 +46,39 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
int _currentInterval = _intervalMedium;
String _speedLabel = 'M';
// NFC
NfcState _nfcState = NfcState.unsupported;
bool _nfcWriting = false;
@override
void initState() {
super.initState();
_encodeTokenToUR();
_checkNfc();
}
@override
void dispose() {
_animationTimer?.cancel();
if (_nfcWriting) NfcService.stopEmulating();
super.dispose();
}
Future<void> _checkNfc() async {
final state = await NfcService.checkState();
if (mounted) {
setState(() => _nfcState = state);
}
}
void _encodeTokenToUR() {
// Parsear el token string a objeto Token de cdk-flutter
final token = cdk.Token.parse(encoded: widget.token);
// Construir Token directamente sin Token.parse para evitar validación
// estricta de keyset IDs. encodeQrToken solo usa token.encoded.
final token = cdk.Token(
encoded: widget.token,
amount: widget.amount,
mintUrl: '',
);
// Codificar a fragmentos UR usando cdk-flutter
// maxFragmentLength por defecto es 150 bytes
@@ -155,9 +175,14 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
const SizedBox(height: AppDimensions.paddingMedium),
// Botones copiar y compartir
// Botones principales: copiar y compartir
_buildActionButtons(context),
const SizedBox(height: AppDimensions.paddingSmall),
// Botones secundarios: bean (peanut) y NFC
_buildSecondaryActions(context),
const SizedBox(height: AppDimensions.paddingLarge),
// Advertencia
@@ -450,6 +475,75 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
);
}
Widget _buildSecondaryActions(BuildContext context) {
final isNfcEnabled = _nfcState == NfcState.enabled;
final isNfcUnsupported = _nfcState == NfcState.unsupported;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Copiar como emoji (peanut encoding)
Tooltip(
message: L10n.of(context)!.copyAsEmoji,
child: GestureDetector(
onTap: () => _copyPeanut(context),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.07),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Icon(LucideIcons.bean, color: AppColors.primaryAction, size: 20),
),
),
),
const SizedBox(width: 12),
// NFC - siempre visible con 3 estados
Tooltip(
message: isNfcUnsupported
? L10n.of(context)!.nfcUnsupported
: isNfcEnabled
? L10n.of(context)!.nfcWrite
: L10n.of(context)!.nfcDisabled,
child: GestureDetector(
onTap: () => _writeNfc(context),
child: Opacity(
opacity: isNfcUnsupported ? 0.3 : 1.0,
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _nfcWriting
? AppColors.primaryAction.withValues(alpha: 0.3)
: Colors.white.withValues(alpha: 0.07),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _nfcWriting
? AppColors.primaryAction
: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Icon(
LucideIcons.nfc,
color: _nfcWriting
? AppColors.primaryAction
: isNfcEnabled
? AppColors.textSecondary
: AppColors.textSecondary.withValues(alpha: 0.5),
size: 20,
),
),
),
),
),
],
);
}
Widget _buildWarning() {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
@@ -492,6 +586,19 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
);
}
void _copyPeanut(BuildContext context) {
final peanut = PeanutCodec.encode(widget.token);
Clipboard.setData(ClipboardData(text: peanut));
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.emojiCopiedToClipboard),
backgroundColor: AppColors.success,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
}
void _shareToken(BuildContext context) async {
final memo = widget.memo != null && widget.memo!.isNotEmpty
? '\n"${widget.memo}"'
@@ -508,6 +615,68 @@ class _ShareTokenScreenState extends State<ShareTokenScreen> {
);
}
void _writeNfc(BuildContext context) async {
// Toggle off: stop emulating
if (_nfcWriting) {
await NfcService.stopEmulating();
setState(() => _nfcWriting = false);
return;
}
// Re-check state in case user toggled NFC in settings
final currentState = await NfcService.checkState();
if (mounted) setState(() => _nfcState = currentState);
if (currentState == NfcState.unsupported) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcUnsupported),
backgroundColor: AppColors.textSecondary,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
return;
}
if (currentState == NfcState.disabled) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcDisabled),
backgroundColor: AppColors.warning,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
return;
}
try {
await NfcService.startEmulating(widget.token);
setState(() => _nfcWriting = true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcHoldNear),
backgroundColor: AppColors.primaryAction,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
);
} catch (e) {
if (!mounted) return;
setState(() => _nfcWriting = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.nfcWriteError(e.toString())),
backgroundColor: AppColors.error,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8)),
),
);
}
}
void _goToHome(BuildContext context) {
Navigator.of(context).popUntil((route) => route.isFirst);
}
+3 -4
View File
@@ -4,7 +4,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
import '../../src/rust/api/wallet.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
@@ -80,10 +80,9 @@ class _InvoiceScreenState extends State<InvoiceScreen> {
_onMintCompleted();
break;
default:
// Cualquier otro estado se trata como error
case MintQuoteState.error:
_status = MintStatus.error;
_errorMessage = L10n.of(context)!.unknownState;
_errorMessage = quote.error ?? L10n.of(context)!.unknownError;
}
});
},
+1
View File
@@ -249,6 +249,7 @@ class _MintScreenState extends State<MintScreen> {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (context) => _ConfirmationModal(
amount: _amount,
unit: _activeUnit,
+23 -45
View File
@@ -9,6 +9,8 @@ import '../../core/services/lnurl_service.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/common/secondary_button.dart';
import '../../widgets/common/bottom_sheet_container.dart';
import '../../widgets/common/numpad_widget.dart';
import '../../providers/wallet_provider.dart';
import '../../providers/price_provider.dart';
@@ -404,6 +406,8 @@ class _AmountScreenState extends State<AmountScreen> {
Future<void> _processPayment() async {
if (!_canPay) return;
final walletProvider = context.read<WalletProvider>();
setState(() {
_isProcessing = true;
_errorMessage = null;
@@ -431,14 +435,22 @@ class _AmountScreenState extends State<AmountScreen> {
if (!mounted) return;
// 3. Obtener quote del mint (en la unidad del mint)
final walletProvider = context.read<WalletProvider>();
final quote = await walletProvider.getMeltQuote(invoiceResult.invoice);
if (!mounted) return;
final total = quote.amount + quote.feeReserve;
// 4. Verificar balance suficiente
// 4a. Verificar que el fee no supere el monto (operación inviable)
if (quote.amount <= quote.feeReserve) {
setState(() {
_isProcessing = false;
_errorMessage = L10n.of(context)!.feeExceedsAmount;
});
return;
}
// 4b. Verificar balance suficiente
if (total > _availableBalance) {
setState(() {
_isProcessing = false;
@@ -506,32 +518,16 @@ class _AmountScreenState extends State<AmountScreen> {
Future<bool> _showConfirmation(BigInt amount, BigInt fee, BigInt total) async {
final unitLabel = UnitFormatter.getUnitLabel(_activeUnit);
final l10n = L10n.of(context)!;
final result = await showModalBottomSheet<bool>(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
),
),
builder: (context) => BottomSheetContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
const BottomSheetHandle(),
// Icono
Container(
@@ -551,7 +547,7 @@ class _AmountScreenState extends State<AmountScreen> {
// Título
Text(
L10n.of(context)!.confirmPayment,
l10n.confirmPayment,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 20,
@@ -596,40 +592,22 @@ class _AmountScreenState extends State<AmountScreen> {
Row(
children: [
Expanded(
child: GestureDetector(
onTap: () => Navigator.pop(context, false),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
L10n.of(context)!.cancel,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
child: SecondaryButton(
text: l10n.cancel,
onPressed: () => Navigator.pop(context, false),
height: 52,
),
),
const SizedBox(width: AppDimensions.paddingMedium),
Expanded(
child: PrimaryButton(
text: L10n.of(context)!.pay,
text: l10n.pay,
onPressed: () => Navigator.pop(context, true),
height: 52,
),
),
],
),
const SizedBox(height: AppDimensions.paddingSmall),
],
),
),
+27 -47
View File
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:cdk_flutter/cdk_flutter.dart' hide WalletProvider;
import '../../src/rust/api/wallet.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
@@ -12,6 +12,8 @@ import '../../core/services/lnurl_service.dart';
import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../../widgets/common/primary_button.dart';
import '../../widgets/common/secondary_button.dart';
import '../../widgets/common/bottom_sheet_container.dart';
import '../../providers/wallet_provider.dart';
import '../10_scanner/scan_screen.dart';
import 'amount_screen.dart';
@@ -594,15 +596,28 @@ class _MeltScreenState extends State<MeltScreen> {
}
});
// Extract lightning invoice from BIP-321 URI if present
var inputValue = value;
if (value.trim().toLowerCase().startsWith('bitcoin:')) {
final parsed = IncomingDataParser.parse(value.trim());
if (parsed.invoiceBolt11 != null) {
inputValue = parsed.invoiceBolt11!;
_invoiceController.text = inputValue;
_invoiceController.selection = TextSelection.fromPosition(
TextPosition(offset: inputValue.length),
);
}
}
// Detectar tipo de input (sin mostrar error, solo detectar)
final inputType = LnurlService.detectType(value);
final inputType = LnurlService.detectType(inputValue);
setState(() => _inputType = inputType);
// Solo procesar automáticamente invoices BOLT11
// LNURL y Lightning Address requieren botón explícito
if (inputType == LnInputType.bolt11Invoice) {
// BOLT11 invoices son 200+ chars; evitar llamar API con input parcial
final cleaned = LnurlService.cleanInput(value);
final cleaned = LnurlService.cleanInput(inputValue);
if (cleaned.length > 50) {
_getQuote(cleaned);
}
@@ -845,29 +860,12 @@ class _ConfirmationModal extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppDimensions.paddingMedium),
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
final l10n = L10n.of(context)!;
return BottomSheetContainer(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Handle
Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
const BottomSheetHandle(),
// Icono
Container(
@@ -887,7 +885,7 @@ class _ConfirmationModal extends StatelessWidget {
// Título
Text(
L10n.of(context)!.confirmPayment,
l10n.confirmPayment,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 20,
@@ -932,40 +930,22 @@ class _ConfirmationModal extends StatelessWidget {
Row(
children: [
Expanded(
child: GestureDetector(
onTap: onCancel,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: Text(
L10n.of(context)!.cancel,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
child: SecondaryButton(
text: l10n.cancel,
onPressed: onCancel,
height: 52,
),
),
const SizedBox(width: AppDimensions.paddingMedium),
Expanded(
child: PrimaryButton(
text: L10n.of(context)!.pay,
text: l10n.pay,
onPressed: onConfirm,
height: 52,
),
),
],
),
const SizedBox(height: AppDimensions.paddingSmall),
],
),
);
+144
View File
@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
/// Muestra el diálogo "Acerca de" de ElCaju
void showElCajuAboutDialog(BuildContext context, String appVersion) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppColors.deepVoidPurple,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Logo
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.asset(
'assets/img/elcajucubano.png',
fit: BoxFit.cover,
),
),
),
const SizedBox(height: 16),
const Text(
'El Caju',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 4),
Text(
'v$appVersion',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 16),
Text(
L10n.of(context)!.aboutTagline,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
_buildAboutDescription(context),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
L10n.of(context)!.close,
style: const TextStyle(
fontFamily: 'Inter',
color: AppColors.textSecondary,
),
),
),
],
),
);
}
Widget _buildAboutDescription(BuildContext context) {
final description = L10n.of(context)!.aboutDescription;
const keyword = 'LaChispa';
final index = description.indexOf(keyword);
if (index == -1) {
return Text(
description,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
);
}
final before = description.substring(0, index);
final after = description.substring(index + keyword.length);
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
children: [
TextSpan(text: before),
WidgetSpan(
alignment: PlaceholderAlignment.baseline,
baseline: TextBaseline.alphabetic,
child: GestureDetector(
onTap: () => _openLaChispa(context),
child: Text(
keyword,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.secondaryAction,
decoration: TextDecoration.underline,
decorationColor: AppColors.secondaryAction,
),
),
),
),
TextSpan(text: after),
],
),
);
}
Future<void> _openLaChispa(BuildContext context) async {
final url = Uri.parse('https://app.lachispa.me');
try {
await launchUrl(url, mode: LaunchMode.externalApplication);
} catch (_) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.couldNotOpenLink),
backgroundColor: AppColors.error,
),
);
}
}
}
@@ -0,0 +1,240 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/bottom_sheet_container.dart';
import '../../widgets/common/secondary_button.dart';
/// Modal para confirmar borrado de wallet
class DeleteWalletModal extends StatefulWidget {
final VoidCallback onConfirm;
final VoidCallback onCancel;
const DeleteWalletModal({
super.key,
required this.onConfirm,
required this.onCancel,
});
@override
State<DeleteWalletModal> createState() => _DeleteWalletModalState();
}
class _DeleteWalletModalState extends State<DeleteWalletModal> {
final TextEditingController _controller = TextEditingController();
bool _canDelete = false;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
final confirmWord = l10n.deleteConfirmWord;
return BottomSheetContainer(
respectKeyboard: true,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const BottomSheetHandle(),
// Icono advertencia
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: Icon(
LucideIcons.alertTriangle,
color: AppColors.error,
size: 32,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Título
Text(
l10n.deleteWalletQuestion,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Advertencia
Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Row(
children: [
Icon(
LucideIcons.alertCircle,
color: AppColors.error,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
l10n.actionIrreversible,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.error,
),
),
),
],
),
const SizedBox(height: 8),
Text(
l10n.deleteWalletWarning,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.error.withValues(alpha: 0.8),
),
),
],
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Input de confirmación
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.typeDeleteToConfirm,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 8),
TextField(
controller: _controller,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white,
),
decoration: InputDecoration(
hintText: confirmWord,
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: Colors.white.withValues(alpha: 0.3),
),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
),
onChanged: (value) {
setState(() {
_canDelete = value == confirmWord;
});
},
),
],
),
),
const SizedBox(height: AppDimensions.paddingLarge),
// Botones
Row(
children: [
Expanded(
child: SecondaryButton(
text: l10n.cancel,
onPressed: widget.onCancel,
height: 52,
),
),
const SizedBox(width: AppDimensions.paddingMedium),
Expanded(
child: _DestructiveButton(
text: l10n.deleteWallet,
enabled: _canDelete,
onPressed: widget.onConfirm,
),
),
],
),
],
),
);
}
}
/// Botón destructivo con semantics accesibles.
/// Separado de los botones estándar porque la acción es irreversible.
class _DestructiveButton extends StatelessWidget {
final String text;
final bool enabled;
final VoidCallback onPressed;
const _DestructiveButton({
required this.text,
required this.enabled,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Semantics(
button: true,
enabled: enabled,
label: text,
child: SizedBox(
height: 52,
child: Material(
color: enabled
? AppColors.error
: AppColors.error.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(16),
child: InkWell(
onTap: enabled ? onPressed : null,
borderRadius: BorderRadius.circular(16),
child: Center(
child: Text(
text,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: enabled
? Colors.white
: Colors.white.withValues(alpha: 0.5),
),
),
),
),
),
),
);
}
}
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:cdk_flutter/cdk_flutter.dart' show MintInfo, ContactInfo;
import '../../src/rust/api/mint_info.dart' show MintInfo, ContactInfo;
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/utils/formatters.dart';
+28 -27
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:provider/provider.dart';
import 'package:cdk_flutter/cdk_flutter.dart' show MintInfo;
import '../../src/rust/api/mint_info.dart' show MintInfo;
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
@@ -275,52 +275,53 @@ class _MintsScreenState extends State<MintsScreen> {
);
}
/// Badges de balance compactos (estilo cashu.me)
/// Badges de balance compactos (estilo cashu.me).
/// Muestra todas las unidades soportadas por el mint: las de saldo > 0
/// con estilo activo y las de saldo 0 atenuadas, para reflejar las
/// capacidades del mint aunque aún no se hayan usado.
Widget _buildBalanceBadges(List<String> units, Map<String, BigInt> balances) {
final nonZeroBalances = balances.entries
.where((e) => e.value > BigInt.zero)
.toList();
final entries = balances.isNotEmpty
? balances.entries.toList()
: units.map((u) => MapEntry(u, BigInt.zero)).toList();
if (nonZeroBalances.isEmpty) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'0 ${units.isNotEmpty ? units.first : "sat"}',
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: Colors.white.withValues(alpha: 0.6),
),
),
);
if (entries.isEmpty) {
return const SizedBox.shrink();
}
entries.sort((a, b) {
final aZero = a.value == BigInt.zero;
final bZero = b.value == BigInt.zero;
if (aZero == bZero) return 0;
return aZero ? 1 : -1;
});
return Wrap(
spacing: 6,
runSpacing: 4,
children: nonZeroBalances.map((entry) {
children: entries.map((entry) {
final unit = entry.key;
final balance = entry.value;
final isZero = balance == BigInt.zero;
final formatted = UnitFormatter.formatBalance(balance, unit);
final label = UnitFormatter.getUnitLabel(unit);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: AppColors.primaryAction.withValues(alpha: 0.15),
color: isZero
? Colors.white.withValues(alpha: 0.08)
: AppColors.primaryAction.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'$formatted $label',
style: const TextStyle(
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.primaryAction,
fontWeight: isZero ? FontWeight.w400 : FontWeight.w500,
color: isZero
? Colors.white.withValues(alpha: 0.5)
: AppColors.primaryAction,
),
),
);
@@ -623,7 +624,7 @@ class _AddMintDialogState extends State<_AddMintDialog> {
});
try {
// Intentar agregar el mint (cdk_flutter validará la conexión)
// Intentar agregar el mint (el bridge Rust validará la conexión)
await widget.walletProvider.addMint(url);
if (mounted) {
+194
View File
@@ -0,0 +1,194 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import '../../core/constants/colors.dart';
/// Diálogo para ingresar PIN de 4 dígitos
class PinDialog extends StatefulWidget {
final String title;
final String subtitle;
const PinDialog({
super.key,
required this.title,
required this.subtitle,
});
@override
State<PinDialog> createState() => _PinDialogState();
}
class _PinDialogState extends State<PinDialog> {
final List<String> _pin = [];
@override
Widget build(BuildContext context) {
return AlertDialog(
backgroundColor: AppColors.deepVoidPurple,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
title: Text(
widget.title,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
widget.subtitle,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Indicadores de PIN
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(4, (index) {
final filled = index < _pin.length;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
width: 16,
height: 16,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: filled
? AppColors.primaryAction
: Colors.white.withValues(alpha: 0.2),
border: Border.all(
color: filled
? AppColors.primaryAction
: Colors.white.withValues(alpha: 0.3),
width: 2,
),
),
);
}),
),
const SizedBox(height: 24),
// Teclado numérico
_buildNumPad(),
],
),
);
}
Widget _buildNumPad() {
return Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: ['1', '2', '3'].map((n) => _buildNumKey(n)).toList(),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: ['4', '5', '6'].map((n) => _buildNumKey(n)).toList(),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: ['7', '8', '9'].map((n) => _buildNumKey(n)).toList(),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Cancelar
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(30),
),
child: Icon(
LucideIcons.x,
color: AppColors.textSecondary,
size: 24,
),
),
),
_buildNumKey('0'),
// Borrar
GestureDetector(
onTap: _deleteDigit,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(30),
),
child: Icon(
LucideIcons.delete,
color: AppColors.textSecondary,
size: 24,
),
),
),
],
),
],
);
}
Widget _buildNumKey(String number) {
return GestureDetector(
onTap: () => _addDigit(number),
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(30),
),
child: Center(
child: Text(
number,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
);
}
void _addDigit(String digit) {
if (_pin.length < 4) {
setState(() {
_pin.add(digit);
});
if (_pin.length == 4) {
// PIN completo, cerrar con resultado
Future.delayed(const Duration(milliseconds: 200), () {
if (mounted) {
Navigator.pop(context, _pin.join());
}
});
}
}
}
void _deleteDigit() {
if (_pin.isNotEmpty) {
setState(() {
_pin.removeLast();
});
}
}
}
+131
View File
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
class PrivacyScreen extends StatelessWidget {
const PrivacyScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
final conclusionLines = l10n.privacyConclusion
.split('\n')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
l10n.privacyPolicy,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.privacyTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
l10n.privacyGoodbye,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppColors.primaryAction,
),
),
const SizedBox(height: 40),
Text(
l10n.privacyKeepReading,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 13,
fontStyle: FontStyle.italic,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
const SizedBox(height: 28),
...l10n.privacyBody.split('\n')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.map(
(line) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
line,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary.withValues(alpha: 0.85),
),
textAlign: TextAlign.center,
),
),
),
const SizedBox(height: 28),
...conclusionLines.asMap().entries.map((entry) {
final isLast = entry.key == conclusionLines.length - 1;
return Padding(
padding: EdgeInsets.only(bottom: isLast ? 0 : 8),
child: Text(
entry.value,
style: isLast
? const TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.white,
)
: TextStyle(
fontFamily: 'Inter',
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
);
}),
],
),
),
),
),
),
),
),
);
}
}
@@ -0,0 +1,704 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:provider/provider.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../core/utils/formatters.dart';
import '../../providers/wallet_provider.dart';
import '../../widgets/common/bottom_sheet_container.dart';
/// Modal para recuperar tokens usando NUT-13
class RecoverTokensModal extends StatefulWidget {
const RecoverTokensModal({super.key});
@override
State<RecoverTokensModal> createState() => _RecoverTokensModalState();
}
class _RecoverTokensModalState extends State<RecoverTokensModal> {
final TextEditingController _mnemonicController = TextEditingController();
bool _useCurrentMnemonic = true;
bool _scanAllMints = true;
String? _selectedMintUrl;
List<String> _availableMints = [];
bool _isLoading = false;
bool _isLoadingMints = true;
String? _result;
bool _isSuccess = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _loadMints());
}
void _loadMints() {
final walletProvider = context.read<WalletProvider>();
setState(() {
_availableMints = walletProvider.mintUrls;
_isLoadingMints = false;
if (_availableMints.isNotEmpty) {
_selectedMintUrl = _availableMints.first;
}
});
}
@override
void dispose() {
_mnemonicController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return BottomSheetContainer(
respectKeyboard: true,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const BottomSheetHandle(),
// Icono
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.primaryAction.withValues(alpha: 0.2),
shape: BoxShape.circle,
),
child: Icon(
LucideIcons.searchCode,
color: AppColors.primaryAction,
size: 32,
),
),
const SizedBox(height: AppDimensions.paddingMedium),
// Título
Text(
L10n.of(context)!.recoverTokensTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: AppDimensions.paddingSmall),
// Descripción
Text(
L10n.of(context)!.recoverTokensDescription,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppDimensions.paddingLarge),
// Opciones de mnemonic
_buildMnemonicOptions(),
const SizedBox(height: AppDimensions.paddingMedium),
// Selector de mint (solo si usa mnemonic actual)
if (_useCurrentMnemonic) _buildMintSelector(),
// Input para mnemonic personalizado
if (!_useCurrentMnemonic) _buildMnemonicInput(),
// Resultado
if (_result != null) _buildResult(),
const SizedBox(height: AppDimensions.paddingMedium),
// Botón de acción
_buildActionButton(),
const SizedBox(height: AppDimensions.paddingSmall),
],
),
),
);
}
Widget _buildMnemonicOptions() {
final l10n = L10n.of(context)!;
return Column(
children: [
// Opción: Usar mnemonic actual
GestureDetector(
onTap: () => setState(() => _useCurrentMnemonic = true),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _useCurrentMnemonic
? AppColors.primaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _useCurrentMnemonic
? AppColors.primaryAction.withValues(alpha: 0.5)
: Colors.transparent,
),
),
child: Row(
children: [
Icon(
_useCurrentMnemonic
? LucideIcons.checkCircle
: LucideIcons.circle,
color: _useCurrentMnemonic
? AppColors.primaryAction
: AppColors.textSecondary,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.useCurrentSeedPhrase,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
Text(
l10n.scanWithSavedWords,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
),
),
],
),
),
),
const SizedBox(height: 8),
// Opción: Usar otro mnemonic
GestureDetector(
onTap: () => setState(() => _useCurrentMnemonic = false),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: !_useCurrentMnemonic
? AppColors.primaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: !_useCurrentMnemonic
? AppColors.primaryAction.withValues(alpha: 0.5)
: Colors.transparent,
),
),
child: Row(
children: [
Icon(
!_useCurrentMnemonic
? LucideIcons.checkCircle
: LucideIcons.circle,
color: !_useCurrentMnemonic
? AppColors.primaryAction
: AppColors.textSecondary,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.useOtherSeedPhrase,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
Text(
l10n.recoverFromOtherWords,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
),
),
],
),
),
),
],
);
}
Widget _buildMintSelector() {
final l10n = L10n.of(context)!;
if (_isLoadingMints) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: AppColors.primaryAction,
strokeWidth: 2,
),
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
l10n.mintsToScan,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
),
const SizedBox(height: 8),
// Opción: Escanear todos
GestureDetector(
onTap: () => setState(() => _scanAllMints = true),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: _scanAllMints
? AppColors.primaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: _scanAllMints
? AppColors.primaryAction.withValues(alpha: 0.5)
: Colors.transparent,
),
),
child: Row(
children: [
Icon(
_scanAllMints ? LucideIcons.checkCircle : LucideIcons.circle,
color: _scanAllMints
? AppColors.primaryAction
: AppColors.textSecondary,
size: 18,
),
const SizedBox(width: 10),
Text(
l10n.allMints(_availableMints.length),
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
fontWeight: _scanAllMints ? FontWeight.w500 : FontWeight.normal,
),
),
],
),
),
),
const SizedBox(height: 8),
// Opción: Mint específico
GestureDetector(
onTap: () => setState(() => _scanAllMints = false),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: !_scanAllMints
? AppColors.primaryAction.withValues(alpha: 0.2)
: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: !_scanAllMints
? AppColors.primaryAction.withValues(alpha: 0.5)
: Colors.transparent,
),
),
child: Row(
children: [
Icon(
!_scanAllMints ? LucideIcons.checkCircle : LucideIcons.circle,
color: !_scanAllMints
? AppColors.primaryAction
: AppColors.textSecondary,
size: 18,
),
const SizedBox(width: 10),
Text(
l10n.specificMint,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
),
],
),
),
),
// Dropdown de mints (solo si selecciona específico)
if (!_scanAllMints && _availableMints.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
),
),
child: DropdownButton<String>(
value: _selectedMintUrl,
isExpanded: true,
dropdownColor: AppColors.deepVoidPurple,
underline: const SizedBox(),
icon: Icon(
LucideIcons.chevronDown,
color: AppColors.textSecondary,
size: 20,
),
items: _availableMints.map((url) {
final host = Uri.parse(url).host;
return DropdownMenuItem(
value: url,
child: Text(
host,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
),
);
}).toList(),
onChanged: (value) {
if (value != null) {
setState(() => _selectedMintUrl = value);
}
},
),
),
),
],
);
}
Widget _buildMnemonicInput() {
return Padding(
padding: const EdgeInsets.only(top: AppDimensions.paddingMedium),
child: TextField(
controller: _mnemonicController,
autocorrect: false,
enableSuggestions: false,
textCapitalization: TextCapitalization.none,
maxLines: 3,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white,
),
decoration: InputDecoration(
hintText: L10n.of(context)!.enterMnemonicWords,
hintStyle: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: Colors.white.withValues(alpha: 0.3),
),
filled: true,
fillColor: Colors.white.withValues(alpha: 0.05),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.all(16),
),
),
);
}
Widget _buildResult() {
return Container(
margin: const EdgeInsets.only(top: AppDimensions.paddingMedium),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: _isSuccess
? AppColors.success.withValues(alpha: 0.1)
: AppColors.error.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _isSuccess
? AppColors.success.withValues(alpha: 0.3)
: AppColors.error.withValues(alpha: 0.3),
),
),
child: Row(
children: [
Icon(
_isSuccess ? LucideIcons.checkCircle : LucideIcons.alertCircle,
color: _isSuccess ? AppColors.success : AppColors.error,
size: 20,
),
const SizedBox(width: 12),
Expanded(
child: Text(
_result!,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 14,
color: _isSuccess ? AppColors.success : AppColors.error,
),
),
),
],
),
);
}
Widget _buildActionButton() {
return SizedBox(
width: double.infinity,
child: GestureDetector(
onTap: _isLoading ? null : _startRecover,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 16),
decoration: BoxDecoration(
gradient: _isLoading
? null
: const LinearGradient(
colors: [AppColors.primaryAction, Color(0xFFFF9100)],
),
color: _isLoading ? Colors.grey : null,
borderRadius: BorderRadius.circular(16),
),
child: Center(
child: _isLoading
? const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: Text(
L10n.of(context)!.scanMints,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
),
),
);
}
Future<void> _startRecover() async {
if (!mounted) return;
final l10n = L10n.of(context)!;
setState(() {
_isLoading = true;
_result = null;
});
try {
final walletProvider = context.read<WalletProvider>();
if (_useCurrentMnemonic) {
// Usar mnemonic actual
if (_scanAllMints) {
// Escanear todos los mints (retorna Map<String, Map<String, BigInt>>)
final results = await walletProvider.restoreAllMints();
int mintsRecovered = 0;
int mintsWithError = 0;
final recoveredDetails = <String>[];
for (final mintEntry in results.entries) {
final unitBalances = mintEntry.value;
bool hasError = false;
bool hasRecovered = false;
for (final unitEntry in unitBalances.entries) {
final unit = unitEntry.key;
final balance = unitEntry.value;
if (balance < BigInt.zero) {
hasError = true;
} else if (balance > BigInt.zero) {
final formatted = UnitFormatter.formatBalance(balance, unit);
final label = UnitFormatter.getUnitLabel(unit);
recoveredDetails.add('$formatted $label');
hasRecovered = true;
}
}
if (hasError) mintsWithError++;
if (hasRecovered) mintsRecovered++;
}
// Recuperar proofs pendientes (sends no reclamados, melts fallidos)
final reclaimMap = await walletProvider.reclaimPendingProofs();
if (!mounted) return;
// Agregar cada unidad reclamada por separado
if (reclaimMap.isNotEmpty) {
for (final entry in reclaimMap.entries) {
final unit = entry.key;
final reclaim = entry.value;
final formatted = UnitFormatter.formatBalance(reclaim.amount, unit);
final label = UnitFormatter.getUnitLabel(unit);
recoveredDetails.add('$formatted $label (${reclaim.count} proofs)');
}
mintsRecovered++;
}
setState(() {
_isSuccess = true;
if (recoveredDetails.isNotEmpty) {
_result = l10n.recoveredTokens(recoveredDetails.join(", "), mintsRecovered);
} else {
_result = l10n.scanCompleteNoTokens;
}
if (mintsWithError > 0) {
_result = '$_result ${l10n.mintsWithError(mintsWithError)}';
}
});
} else {
// Escanear mint específico (retorna Map<String, BigInt>)
if (_selectedMintUrl == null) {
if (!mounted) return;
setState(() {
_isSuccess = false;
_result = l10n.selectMintToScan;
});
return;
}
final unitBalances = await walletProvider.restoreFromMint(_selectedMintUrl!);
final mintHost = UnitFormatter.getMintDisplayName(_selectedMintUrl!);
final recoveredDetails = <String>[];
for (final entry in unitBalances.entries) {
final unit = entry.key;
final balance = entry.value;
if (balance > BigInt.zero) {
final formatted = UnitFormatter.formatBalance(balance, unit);
final label = UnitFormatter.getUnitLabel(unit);
recoveredDetails.add('$formatted $label');
}
}
// Recuperar proofs pendientes de este mint
final reclaimMap = await walletProvider.reclaimPendingProofs(mintUrl: _selectedMintUrl);
if (!mounted) return;
// Agregar cada unidad reclamada por separado
for (final entry in reclaimMap.entries) {
final unit = entry.key;
final reclaim = entry.value;
final formatted = UnitFormatter.formatBalance(reclaim.amount, unit);
final label = UnitFormatter.getUnitLabel(unit);
recoveredDetails.add('$formatted $label (${reclaim.count} proofs)');
}
setState(() {
_isSuccess = true;
if (recoveredDetails.isNotEmpty) {
_result = l10n.recoveredFromMint(recoveredDetails.join(", "), mintHost);
} else {
_result = l10n.noTokensFoundInMint(mintHost);
}
});
}
} else {
// Usar otro mnemonic
final mnemonic = _mnemonicController.text.trim().toLowerCase();
final words = mnemonic.split(RegExp(r'\s+'));
if (words.length != 12 && words.length != 24) {
if (!mounted) return;
setState(() {
_isSuccess = false;
_result = l10n.mnemonicMustHaveWords;
});
return;
}
// Obtener lista de mints actuales para escanear
final mintUrls = walletProvider.mintUrls;
if (mintUrls.isEmpty) {
if (!mounted) return;
setState(() {
_isSuccess = false;
_result = l10n.noConnectedMintsToScan;
});
return;
}
final recovered = await walletProvider.restoreWithMnemonic(
mnemonic,
mintUrls,
);
if (!mounted) return;
setState(() {
_isSuccess = true;
if (recovered > BigInt.zero) {
// Usamos la unidad activa como aproximación para el formato
final activeUnit = walletProvider.activeUnit;
final formatted = UnitFormatter.formatBalance(recovered, activeUnit);
final label = UnitFormatter.getUnitLabel(activeUnit);
_result = l10n.recoveredAndTransferred(formatted, label);
} else {
_result = l10n.noTokensForMnemonic;
}
});
}
} catch (e) {
if (!mounted) return;
setState(() {
_isSuccess = false;
_result = '${l10n.error}: $e';
});
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+31
View File
@@ -0,0 +1,31 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'package:freezed_annotation/freezed_annotation.dart' hide protected;
part 'error.freezed.dart';
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
@freezed
sealed class Error with _$Error implements FrbException {
const Error._();
/// Error del protocolo Cashu (cdk)
const factory Error.cdk(String field0) = Error_Cdk;
/// Error de base de datos (SQLite)
const factory Error.database(String field0) = Error_Database;
/// Input inválido del usuario
const factory Error.invalidInput() = Error_InvalidInput;
/// Error de red (HTTP, conexión)
const factory Error.network(String field0) = Error_Network;
/// Error de UR (QR multiframe)
const factory Error.ur(String field0) = Error_Ur;
}
+863
View File
@@ -0,0 +1,863 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'error.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$Error {
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String field0) cdk,
required TResult Function(String field0) database,
required TResult Function() invalidInput,
required TResult Function(String field0) network,
required TResult Function(String field0) ur,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String field0)? cdk,
TResult? Function(String field0)? database,
TResult? Function()? invalidInput,
TResult? Function(String field0)? network,
TResult? Function(String field0)? ur,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String field0)? cdk,
TResult Function(String field0)? database,
TResult Function()? invalidInput,
TResult Function(String field0)? network,
TResult Function(String field0)? ur,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Error_Cdk value) cdk,
required TResult Function(Error_Database value) database,
required TResult Function(Error_InvalidInput value) invalidInput,
required TResult Function(Error_Network value) network,
required TResult Function(Error_Ur value) ur,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(Error_Cdk value)? cdk,
TResult? Function(Error_Database value)? database,
TResult? Function(Error_InvalidInput value)? invalidInput,
TResult? Function(Error_Network value)? network,
TResult? Function(Error_Ur value)? ur,
}) => throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Error_Cdk value)? cdk,
TResult Function(Error_Database value)? database,
TResult Function(Error_InvalidInput value)? invalidInput,
TResult Function(Error_Network value)? network,
TResult Function(Error_Ur value)? ur,
required TResult orElse(),
}) => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ErrorCopyWith<$Res> {
factory $ErrorCopyWith(Error value, $Res Function(Error) then) =
_$ErrorCopyWithImpl<$Res, Error>;
}
/// @nodoc
class _$ErrorCopyWithImpl<$Res, $Val extends Error>
implements $ErrorCopyWith<$Res> {
_$ErrorCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
abstract class _$$Error_CdkImplCopyWith<$Res> {
factory _$$Error_CdkImplCopyWith(
_$Error_CdkImpl value,
$Res Function(_$Error_CdkImpl) then,
) = __$$Error_CdkImplCopyWithImpl<$Res>;
@useResult
$Res call({String field0});
}
/// @nodoc
class __$$Error_CdkImplCopyWithImpl<$Res>
extends _$ErrorCopyWithImpl<$Res, _$Error_CdkImpl>
implements _$$Error_CdkImplCopyWith<$Res> {
__$$Error_CdkImplCopyWithImpl(
_$Error_CdkImpl _value,
$Res Function(_$Error_CdkImpl) _then,
) : super(_value, _then);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? field0 = null}) {
return _then(
_$Error_CdkImpl(
null == field0
? _value.field0
: field0 // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$Error_CdkImpl extends Error_Cdk {
const _$Error_CdkImpl(this.field0) : super._();
@override
final String field0;
@override
String toString() {
return 'Error.cdk(field0: $field0)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$Error_CdkImpl &&
(identical(other.field0, field0) || other.field0 == field0));
}
@override
int get hashCode => Object.hash(runtimeType, field0);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$Error_CdkImplCopyWith<_$Error_CdkImpl> get copyWith =>
__$$Error_CdkImplCopyWithImpl<_$Error_CdkImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String field0) cdk,
required TResult Function(String field0) database,
required TResult Function() invalidInput,
required TResult Function(String field0) network,
required TResult Function(String field0) ur,
}) {
return cdk(field0);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String field0)? cdk,
TResult? Function(String field0)? database,
TResult? Function()? invalidInput,
TResult? Function(String field0)? network,
TResult? Function(String field0)? ur,
}) {
return cdk?.call(field0);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String field0)? cdk,
TResult Function(String field0)? database,
TResult Function()? invalidInput,
TResult Function(String field0)? network,
TResult Function(String field0)? ur,
required TResult orElse(),
}) {
if (cdk != null) {
return cdk(field0);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Error_Cdk value) cdk,
required TResult Function(Error_Database value) database,
required TResult Function(Error_InvalidInput value) invalidInput,
required TResult Function(Error_Network value) network,
required TResult Function(Error_Ur value) ur,
}) {
return cdk(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(Error_Cdk value)? cdk,
TResult? Function(Error_Database value)? database,
TResult? Function(Error_InvalidInput value)? invalidInput,
TResult? Function(Error_Network value)? network,
TResult? Function(Error_Ur value)? ur,
}) {
return cdk?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Error_Cdk value)? cdk,
TResult Function(Error_Database value)? database,
TResult Function(Error_InvalidInput value)? invalidInput,
TResult Function(Error_Network value)? network,
TResult Function(Error_Ur value)? ur,
required TResult orElse(),
}) {
if (cdk != null) {
return cdk(this);
}
return orElse();
}
}
abstract class Error_Cdk extends Error {
const factory Error_Cdk(final String field0) = _$Error_CdkImpl;
const Error_Cdk._() : super._();
String get field0;
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$Error_CdkImplCopyWith<_$Error_CdkImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$Error_DatabaseImplCopyWith<$Res> {
factory _$$Error_DatabaseImplCopyWith(
_$Error_DatabaseImpl value,
$Res Function(_$Error_DatabaseImpl) then,
) = __$$Error_DatabaseImplCopyWithImpl<$Res>;
@useResult
$Res call({String field0});
}
/// @nodoc
class __$$Error_DatabaseImplCopyWithImpl<$Res>
extends _$ErrorCopyWithImpl<$Res, _$Error_DatabaseImpl>
implements _$$Error_DatabaseImplCopyWith<$Res> {
__$$Error_DatabaseImplCopyWithImpl(
_$Error_DatabaseImpl _value,
$Res Function(_$Error_DatabaseImpl) _then,
) : super(_value, _then);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? field0 = null}) {
return _then(
_$Error_DatabaseImpl(
null == field0
? _value.field0
: field0 // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$Error_DatabaseImpl extends Error_Database {
const _$Error_DatabaseImpl(this.field0) : super._();
@override
final String field0;
@override
String toString() {
return 'Error.database(field0: $field0)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$Error_DatabaseImpl &&
(identical(other.field0, field0) || other.field0 == field0));
}
@override
int get hashCode => Object.hash(runtimeType, field0);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$Error_DatabaseImplCopyWith<_$Error_DatabaseImpl> get copyWith =>
__$$Error_DatabaseImplCopyWithImpl<_$Error_DatabaseImpl>(
this,
_$identity,
);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String field0) cdk,
required TResult Function(String field0) database,
required TResult Function() invalidInput,
required TResult Function(String field0) network,
required TResult Function(String field0) ur,
}) {
return database(field0);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String field0)? cdk,
TResult? Function(String field0)? database,
TResult? Function()? invalidInput,
TResult? Function(String field0)? network,
TResult? Function(String field0)? ur,
}) {
return database?.call(field0);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String field0)? cdk,
TResult Function(String field0)? database,
TResult Function()? invalidInput,
TResult Function(String field0)? network,
TResult Function(String field0)? ur,
required TResult orElse(),
}) {
if (database != null) {
return database(field0);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Error_Cdk value) cdk,
required TResult Function(Error_Database value) database,
required TResult Function(Error_InvalidInput value) invalidInput,
required TResult Function(Error_Network value) network,
required TResult Function(Error_Ur value) ur,
}) {
return database(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(Error_Cdk value)? cdk,
TResult? Function(Error_Database value)? database,
TResult? Function(Error_InvalidInput value)? invalidInput,
TResult? Function(Error_Network value)? network,
TResult? Function(Error_Ur value)? ur,
}) {
return database?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Error_Cdk value)? cdk,
TResult Function(Error_Database value)? database,
TResult Function(Error_InvalidInput value)? invalidInput,
TResult Function(Error_Network value)? network,
TResult Function(Error_Ur value)? ur,
required TResult orElse(),
}) {
if (database != null) {
return database(this);
}
return orElse();
}
}
abstract class Error_Database extends Error {
const factory Error_Database(final String field0) = _$Error_DatabaseImpl;
const Error_Database._() : super._();
String get field0;
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$Error_DatabaseImplCopyWith<_$Error_DatabaseImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$Error_InvalidInputImplCopyWith<$Res> {
factory _$$Error_InvalidInputImplCopyWith(
_$Error_InvalidInputImpl value,
$Res Function(_$Error_InvalidInputImpl) then,
) = __$$Error_InvalidInputImplCopyWithImpl<$Res>;
}
/// @nodoc
class __$$Error_InvalidInputImplCopyWithImpl<$Res>
extends _$ErrorCopyWithImpl<$Res, _$Error_InvalidInputImpl>
implements _$$Error_InvalidInputImplCopyWith<$Res> {
__$$Error_InvalidInputImplCopyWithImpl(
_$Error_InvalidInputImpl _value,
$Res Function(_$Error_InvalidInputImpl) _then,
) : super(_value, _then);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
}
/// @nodoc
class _$Error_InvalidInputImpl extends Error_InvalidInput {
const _$Error_InvalidInputImpl() : super._();
@override
String toString() {
return 'Error.invalidInput()';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType && other is _$Error_InvalidInputImpl);
}
@override
int get hashCode => runtimeType.hashCode;
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String field0) cdk,
required TResult Function(String field0) database,
required TResult Function() invalidInput,
required TResult Function(String field0) network,
required TResult Function(String field0) ur,
}) {
return invalidInput();
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String field0)? cdk,
TResult? Function(String field0)? database,
TResult? Function()? invalidInput,
TResult? Function(String field0)? network,
TResult? Function(String field0)? ur,
}) {
return invalidInput?.call();
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String field0)? cdk,
TResult Function(String field0)? database,
TResult Function()? invalidInput,
TResult Function(String field0)? network,
TResult Function(String field0)? ur,
required TResult orElse(),
}) {
if (invalidInput != null) {
return invalidInput();
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Error_Cdk value) cdk,
required TResult Function(Error_Database value) database,
required TResult Function(Error_InvalidInput value) invalidInput,
required TResult Function(Error_Network value) network,
required TResult Function(Error_Ur value) ur,
}) {
return invalidInput(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(Error_Cdk value)? cdk,
TResult? Function(Error_Database value)? database,
TResult? Function(Error_InvalidInput value)? invalidInput,
TResult? Function(Error_Network value)? network,
TResult? Function(Error_Ur value)? ur,
}) {
return invalidInput?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Error_Cdk value)? cdk,
TResult Function(Error_Database value)? database,
TResult Function(Error_InvalidInput value)? invalidInput,
TResult Function(Error_Network value)? network,
TResult Function(Error_Ur value)? ur,
required TResult orElse(),
}) {
if (invalidInput != null) {
return invalidInput(this);
}
return orElse();
}
}
abstract class Error_InvalidInput extends Error {
const factory Error_InvalidInput() = _$Error_InvalidInputImpl;
const Error_InvalidInput._() : super._();
}
/// @nodoc
abstract class _$$Error_NetworkImplCopyWith<$Res> {
factory _$$Error_NetworkImplCopyWith(
_$Error_NetworkImpl value,
$Res Function(_$Error_NetworkImpl) then,
) = __$$Error_NetworkImplCopyWithImpl<$Res>;
@useResult
$Res call({String field0});
}
/// @nodoc
class __$$Error_NetworkImplCopyWithImpl<$Res>
extends _$ErrorCopyWithImpl<$Res, _$Error_NetworkImpl>
implements _$$Error_NetworkImplCopyWith<$Res> {
__$$Error_NetworkImplCopyWithImpl(
_$Error_NetworkImpl _value,
$Res Function(_$Error_NetworkImpl) _then,
) : super(_value, _then);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? field0 = null}) {
return _then(
_$Error_NetworkImpl(
null == field0
? _value.field0
: field0 // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$Error_NetworkImpl extends Error_Network {
const _$Error_NetworkImpl(this.field0) : super._();
@override
final String field0;
@override
String toString() {
return 'Error.network(field0: $field0)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$Error_NetworkImpl &&
(identical(other.field0, field0) || other.field0 == field0));
}
@override
int get hashCode => Object.hash(runtimeType, field0);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$Error_NetworkImplCopyWith<_$Error_NetworkImpl> get copyWith =>
__$$Error_NetworkImplCopyWithImpl<_$Error_NetworkImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String field0) cdk,
required TResult Function(String field0) database,
required TResult Function() invalidInput,
required TResult Function(String field0) network,
required TResult Function(String field0) ur,
}) {
return network(field0);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String field0)? cdk,
TResult? Function(String field0)? database,
TResult? Function()? invalidInput,
TResult? Function(String field0)? network,
TResult? Function(String field0)? ur,
}) {
return network?.call(field0);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String field0)? cdk,
TResult Function(String field0)? database,
TResult Function()? invalidInput,
TResult Function(String field0)? network,
TResult Function(String field0)? ur,
required TResult orElse(),
}) {
if (network != null) {
return network(field0);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Error_Cdk value) cdk,
required TResult Function(Error_Database value) database,
required TResult Function(Error_InvalidInput value) invalidInput,
required TResult Function(Error_Network value) network,
required TResult Function(Error_Ur value) ur,
}) {
return network(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(Error_Cdk value)? cdk,
TResult? Function(Error_Database value)? database,
TResult? Function(Error_InvalidInput value)? invalidInput,
TResult? Function(Error_Network value)? network,
TResult? Function(Error_Ur value)? ur,
}) {
return network?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Error_Cdk value)? cdk,
TResult Function(Error_Database value)? database,
TResult Function(Error_InvalidInput value)? invalidInput,
TResult Function(Error_Network value)? network,
TResult Function(Error_Ur value)? ur,
required TResult orElse(),
}) {
if (network != null) {
return network(this);
}
return orElse();
}
}
abstract class Error_Network extends Error {
const factory Error_Network(final String field0) = _$Error_NetworkImpl;
const Error_Network._() : super._();
String get field0;
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$Error_NetworkImplCopyWith<_$Error_NetworkImpl> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class _$$Error_UrImplCopyWith<$Res> {
factory _$$Error_UrImplCopyWith(
_$Error_UrImpl value,
$Res Function(_$Error_UrImpl) then,
) = __$$Error_UrImplCopyWithImpl<$Res>;
@useResult
$Res call({String field0});
}
/// @nodoc
class __$$Error_UrImplCopyWithImpl<$Res>
extends _$ErrorCopyWithImpl<$Res, _$Error_UrImpl>
implements _$$Error_UrImplCopyWith<$Res> {
__$$Error_UrImplCopyWithImpl(
_$Error_UrImpl _value,
$Res Function(_$Error_UrImpl) _then,
) : super(_value, _then);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({Object? field0 = null}) {
return _then(
_$Error_UrImpl(
null == field0
? _value.field0
: field0 // ignore: cast_nullable_to_non_nullable
as String,
),
);
}
}
/// @nodoc
class _$Error_UrImpl extends Error_Ur {
const _$Error_UrImpl(this.field0) : super._();
@override
final String field0;
@override
String toString() {
return 'Error.ur(field0: $field0)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$Error_UrImpl &&
(identical(other.field0, field0) || other.field0 == field0));
}
@override
int get hashCode => Object.hash(runtimeType, field0);
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$Error_UrImplCopyWith<_$Error_UrImpl> get copyWith =>
__$$Error_UrImplCopyWithImpl<_$Error_UrImpl>(this, _$identity);
@override
@optionalTypeArgs
TResult when<TResult extends Object?>({
required TResult Function(String field0) cdk,
required TResult Function(String field0) database,
required TResult Function() invalidInput,
required TResult Function(String field0) network,
required TResult Function(String field0) ur,
}) {
return ur(field0);
}
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult? Function(String field0)? cdk,
TResult? Function(String field0)? database,
TResult? Function()? invalidInput,
TResult? Function(String field0)? network,
TResult? Function(String field0)? ur,
}) {
return ur?.call(field0);
}
@override
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({
TResult Function(String field0)? cdk,
TResult Function(String field0)? database,
TResult Function()? invalidInput,
TResult Function(String field0)? network,
TResult Function(String field0)? ur,
required TResult orElse(),
}) {
if (ur != null) {
return ur(field0);
}
return orElse();
}
@override
@optionalTypeArgs
TResult map<TResult extends Object?>({
required TResult Function(Error_Cdk value) cdk,
required TResult Function(Error_Database value) database,
required TResult Function(Error_InvalidInput value) invalidInput,
required TResult Function(Error_Network value) network,
required TResult Function(Error_Ur value) ur,
}) {
return ur(this);
}
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult? Function(Error_Cdk value)? cdk,
TResult? Function(Error_Database value)? database,
TResult? Function(Error_InvalidInput value)? invalidInput,
TResult? Function(Error_Network value)? network,
TResult? Function(Error_Ur value)? ur,
}) {
return ur?.call(this);
}
@override
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({
TResult Function(Error_Cdk value)? cdk,
TResult Function(Error_Database value)? database,
TResult Function(Error_InvalidInput value)? invalidInput,
TResult Function(Error_Network value)? network,
TResult Function(Error_Ur value)? ur,
required TResult orElse(),
}) {
if (ur != null) {
return ur(this);
}
return orElse();
}
}
abstract class Error_Ur extends Error {
const factory Error_Ur(final String field0) = _$Error_UrImpl;
const Error_Ur._() : super._();
String get field0;
/// Create a copy of Error
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
_$$Error_UrImplCopyWith<_$Error_UrImpl> get copyWith =>
throw _privateConstructorUsedError;
}
+20
View File
@@ -0,0 +1,20 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'error.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
/// Generate a new 12-word BIP39 mnemonic
String generateMnemonic() =>
RustLib.instance.api.crateApiKeysGenerateMnemonic();
/// Convert a mnemonic to a 64-byte seed
Uint8List mnemonicToSeed({required String mnemonic}) =>
RustLib.instance.api.crateApiKeysMnemonicToSeed(mnemonic: mnemonic);
/// Derive the public key (hex) from a secret key (hex)
String getPubKey({required String secret}) =>
RustLib.instance.api.crateApiKeysGetPubKey(secret: secret);
+309
View File
@@ -0,0 +1,309 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'error.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`, `from`
/// Fetch full mint info from a mint URL
Future<MintInfo> getMintInfo({required String mintUrl}) =>
RustLib.instance.api.crateApiMintInfoGetMintInfo(mintUrl: mintUrl);
/// Ping a mint to check if it's reachable (replaces Dart HTTP call)
Future<bool> pingMint({required String mintUrl}) =>
RustLib.instance.api.crateApiMintInfoPingMint(mintUrl: mintUrl);
/// Fetch keysets from a mint (replaces Dart HTTP call)
Future<List<KeysetInfo>> fetchKeysets({required String mintUrl}) =>
RustLib.instance.api.crateApiMintInfoFetchKeysets(mintUrl: mintUrl);
class ContactInfo {
final String method;
final String info;
const ContactInfo({required this.method, required this.info});
@override
int get hashCode => method.hashCode ^ info.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ContactInfo &&
runtimeType == other.runtimeType &&
method == other.method &&
info == other.info;
}
/// Keyset info returned by fetch_keysets
class KeysetInfo {
final String id;
final String unit;
final bool active;
const KeysetInfo({
required this.id,
required this.unit,
required this.active,
});
@override
int get hashCode => id.hashCode ^ unit.hashCode ^ active.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is KeysetInfo &&
runtimeType == other.runtimeType &&
id == other.id &&
unit == other.unit &&
active == other.active;
}
class MeltMethodSettings {
final String method;
final String unit;
final BigInt? minAmount;
final BigInt? maxAmount;
const MeltMethodSettings({
required this.method,
required this.unit,
this.minAmount,
this.maxAmount,
});
@override
int get hashCode =>
method.hashCode ^ unit.hashCode ^ minAmount.hashCode ^ maxAmount.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MeltMethodSettings &&
runtimeType == other.runtimeType &&
method == other.method &&
unit == other.unit &&
minAmount == other.minAmount &&
maxAmount == other.maxAmount;
}
class MintInfo {
final String? name;
final String? pubkey;
final MintVersion? version;
final String? description;
final String? descriptionLong;
final List<ContactInfo>? contact;
final Nuts nuts;
final String? iconUrl;
final List<String>? urls;
final String? motd;
final BigInt? time;
final String? tosUrl;
const MintInfo({
this.name,
this.pubkey,
this.version,
this.description,
this.descriptionLong,
this.contact,
required this.nuts,
this.iconUrl,
this.urls,
this.motd,
this.time,
this.tosUrl,
});
@override
int get hashCode =>
name.hashCode ^
pubkey.hashCode ^
version.hashCode ^
description.hashCode ^
descriptionLong.hashCode ^
contact.hashCode ^
nuts.hashCode ^
iconUrl.hashCode ^
urls.hashCode ^
motd.hashCode ^
time.hashCode ^
tosUrl.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MintInfo &&
runtimeType == other.runtimeType &&
name == other.name &&
pubkey == other.pubkey &&
version == other.version &&
description == other.description &&
descriptionLong == other.descriptionLong &&
contact == other.contact &&
nuts == other.nuts &&
iconUrl == other.iconUrl &&
urls == other.urls &&
motd == other.motd &&
time == other.time &&
tosUrl == other.tosUrl;
}
class MintMethodSettings {
final String method;
final String unit;
final BigInt? minAmount;
final BigInt? maxAmount;
const MintMethodSettings({
required this.method,
required this.unit,
this.minAmount,
this.maxAmount,
});
@override
int get hashCode =>
method.hashCode ^ unit.hashCode ^ minAmount.hashCode ^ maxAmount.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MintMethodSettings &&
runtimeType == other.runtimeType &&
method == other.method &&
unit == other.unit &&
minAmount == other.minAmount &&
maxAmount == other.maxAmount;
}
class MintVersion {
final String name;
final String version;
const MintVersion({required this.name, required this.version});
@override
int get hashCode => name.hashCode ^ version.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MintVersion &&
runtimeType == other.runtimeType &&
name == other.name &&
version == other.version;
}
class Nut04Settings {
final List<MintMethodSettings> methods;
final bool disabled;
const Nut04Settings({required this.methods, required this.disabled});
@override
int get hashCode => methods.hashCode ^ disabled.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Nut04Settings &&
runtimeType == other.runtimeType &&
methods == other.methods &&
disabled == other.disabled;
}
class Nut05Settings {
final List<MeltMethodSettings> methods;
final bool disabled;
const Nut05Settings({required this.methods, required this.disabled});
@override
int get hashCode => methods.hashCode ^ disabled.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Nut05Settings &&
runtimeType == other.runtimeType &&
methods == other.methods &&
disabled == other.disabled;
}
class Nuts {
final Nut04Settings nut04;
final Nut05Settings nut05;
final SupportedSettings nut07;
final SupportedSettings nut08;
final SupportedSettings nut09;
final SupportedSettings nut10;
final SupportedSettings nut11;
final SupportedSettings nut12;
final SupportedSettings nut14;
final SupportedSettings nut20;
const Nuts({
required this.nut04,
required this.nut05,
required this.nut07,
required this.nut08,
required this.nut09,
required this.nut10,
required this.nut11,
required this.nut12,
required this.nut14,
required this.nut20,
});
@override
int get hashCode =>
nut04.hashCode ^
nut05.hashCode ^
nut07.hashCode ^
nut08.hashCode ^
nut09.hashCode ^
nut10.hashCode ^
nut11.hashCode ^
nut12.hashCode ^
nut14.hashCode ^
nut20.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Nuts &&
runtimeType == other.runtimeType &&
nut04 == other.nut04 &&
nut05 == other.nut05 &&
nut07 == other.nut07 &&
nut08 == other.nut08 &&
nut09 == other.nut09 &&
nut10 == other.nut10 &&
nut11 == other.nut11 &&
nut12 == other.nut12 &&
nut14 == other.nut14 &&
nut20 == other.nut20;
}
class SupportedSettings {
final bool supported;
const SupportedSettings({required this.supported});
@override
int get hashCode => supported.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SupportedSettings &&
runtimeType == other.runtimeType &&
supported == other.supported;
}
+251
View File
@@ -0,0 +1,251 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'error.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `extract_creq_from_uri`, `parse_payment_request_inner`, `percent_decode`, `wait_for_nostr_payment_inner`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<CreatedPaymentRequest>>
abstract class CreatedPaymentRequest implements RustOpaqueInterface {
String get creqA;
String get creqB;
NostrListenerHandle get listenerHandle;
set creqA(String creqA);
set creqB(String creqB);
set listenerHandle(NostrListenerHandle listenerHandle);
}
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<NostrListenerHandle>>
abstract class NostrListenerHandle implements RustOpaqueInterface {
/// Reconstruct a handle from persisted data (e.g. after app restart).
static NostrListenerHandle fromPersisted({
required PersistedRequestData data,
}) => RustLib.instance.api
.crateApiPaymentRequestNostrListenerHandleFromPersisted(data: data);
/// Export handle data for persistence (e.g. SharedPreferences).
/// The secret key is exposed as hex — the caller is responsible
/// for storing it securely.
PersistedRequestData toPersisted();
}
/// Parameters for creating a payment request.
class CreateRequestParams {
/// Amount to request (in smallest unit, e.g. sats)
final BigInt? amount;
/// Currency unit ("sat", "usd", etc.)
final String unit;
/// Human-readable description
final String? description;
/// Nostr relay URLs for the transport
final List<String> nostrRelays;
const CreateRequestParams({
this.amount,
required this.unit,
this.description,
required this.nostrRelays,
});
@override
int get hashCode =>
amount.hashCode ^
unit.hashCode ^
description.hashCode ^
nostrRelays.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is CreateRequestParams &&
runtimeType == other.runtimeType &&
amount == other.amount &&
unit == other.unit &&
description == other.description &&
nostrRelays == other.nostrRelays;
}
/// Event emitted by the Nostr payment listener.
class NostrPaymentEvent {
final NostrPaymentState state;
final BigInt? amount;
final String? error;
const NostrPaymentEvent({required this.state, this.amount, this.error});
@override
int get hashCode => state.hashCode ^ amount.hashCode ^ error.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is NostrPaymentEvent &&
runtimeType == other.runtimeType &&
state == other.state &&
amount == other.amount &&
error == other.error;
}
/// State of a Nostr payment listener.
enum NostrPaymentState {
/// Connected to relays, waiting for payment
waiting,
/// Payment received and tokens claimed
received,
/// Error occurred
error,
}
/// Parsed payment request info exposed to Flutter.
/// CDK's FromStr auto-detects creqA (CBOR) vs creqB (Bech32m).
class PaymentRequestInfo {
/// Raw encoded string (creqA... or CREQB1...)
final String raw;
/// Payment id
final String? paymentId;
/// Requested amount (in base unit)
final BigInt? amount;
/// Currency unit ("sat", "usd", etc.)
final String? unit;
/// Whether this is a single-use request
final bool? singleUse;
/// Accepted mints (empty = any mint)
final List<String> mints;
/// Human-readable description
final String? description;
/// Available transports
final List<TransportInfo> transports;
/// Whether NUT-10 spending conditions are required
final bool hasNut10;
const PaymentRequestInfo({
required this.raw,
this.paymentId,
this.amount,
this.unit,
this.singleUse,
required this.mints,
this.description,
required this.transports,
required this.hasNut10,
});
/// Parse a payment request string (creqA, CREQB1, or bitcoin:?creq=).
/// Supports NUT-18 (CBOR+base64) and NUT-26 (Bech32m) automatically.
static PaymentRequestInfo parse({required String encoded}) => RustLib
.instance
.api
.crateApiPaymentRequestPaymentRequestInfoParse(encoded: encoded);
@override
int get hashCode =>
raw.hashCode ^
paymentId.hashCode ^
amount.hashCode ^
unit.hashCode ^
singleUse.hashCode ^
mints.hashCode ^
description.hashCode ^
transports.hashCode ^
hasNut10.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PaymentRequestInfo &&
runtimeType == other.runtimeType &&
raw == other.raw &&
paymentId == other.paymentId &&
amount == other.amount &&
unit == other.unit &&
singleUse == other.singleUse &&
mints == other.mints &&
description == other.description &&
transports == other.transports &&
hasNut10 == other.hasNut10;
}
/// Serializable data for persisting a NostrListenerHandle across app restarts.
class PersistedRequestData {
final String secretHex;
final String pubkeyHex;
final List<String> relays;
final BigInt? amount;
final String unit;
final String mintUrl;
const PersistedRequestData({
required this.secretHex,
required this.pubkeyHex,
required this.relays,
this.amount,
required this.unit,
required this.mintUrl,
});
@override
int get hashCode =>
secretHex.hashCode ^
pubkeyHex.hashCode ^
relays.hashCode ^
amount.hashCode ^
unit.hashCode ^
mintUrl.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PersistedRequestData &&
runtimeType == other.runtimeType &&
secretHex == other.secretHex &&
pubkeyHex == other.pubkeyHex &&
relays == other.relays &&
amount == other.amount &&
unit == other.unit &&
mintUrl == other.mintUrl;
}
/// Transport info exposed to Flutter
class TransportInfo {
/// "nostr" or "post"
final String transportType;
/// Target (nprofile or URL)
final String target;
const TransportInfo({required this.transportType, required this.target});
@override
int get hashCode => transportType.hashCode ^ target.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is TransportInfo &&
runtimeType == other.runtimeType &&
transportType == other.transportType &&
target == other.target;
}
+77
View File
@@ -0,0 +1,77 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'error.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These functions are ignored because they are not marked as `pub`: `proofs`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `fmt`, `from_str`, `try_from`, `try_into`, `try_into`
/// Create a token from raw proof data without network access.
/// Uses CDK's CdkToken::new to produce a valid V4 (cashuB) token.
/// `proofs_json` is a JSON array of proofs: [{"id":"...","amount":1,"secret":"...","C":"..."}, ...]
Token createOfflineToken({
required String mintUrl,
required String proofsJson,
String? memo,
String? unit,
}) => RustLib.instance.api.crateApiTokenCreateOfflineToken(
mintUrl: mintUrl,
proofsJson: proofsJson,
memo: memo,
unit: unit,
);
List<String> encodeQrToken({required Token token, BigInt? maxFragmentLength}) =>
RustLib.instance.api.crateApiTokenEncodeQrToken(
token: token,
maxFragmentLength: maxFragmentLength,
);
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<TokenDecoder>>
abstract class TokenDecoder implements RustOpaqueInterface {
bool isComplete();
factory TokenDecoder() => RustLib.instance.api.crateApiTokenTokenDecoderNew();
void receive({required String input});
Token? value();
}
class Token {
final String encoded;
final Uint8List? raw;
final BigInt amount;
final String mintUrl;
const Token({
required this.encoded,
this.raw,
required this.amount,
required this.mintUrl,
});
static Token fromRawBytes({required List<int> raw}) =>
RustLib.instance.api.crateApiTokenTokenFromRawBytes(raw: raw);
static Token parse({required String encoded}) =>
RustLib.instance.api.crateApiTokenTokenParse(encoded: encoded);
@override
int get hashCode =>
encoded.hashCode ^ raw.hashCode ^ amount.hashCode ^ mintUrl.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Token &&
runtimeType == other.runtimeType &&
encoded == other.encoded &&
raw == other.raw &&
amount == other.amount &&
mintUrl == other.mintUrl;
}
+470
View File
@@ -0,0 +1,470 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'error.dart';
import 'mint_info.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'payment_request.dart';
import 'token.dart';
// These functions are ignored because they are not marked as `pub`: `empty`, `from_states`, `into_result`, `mint_url`, `unit`, `update_balance_streams`
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `StateBuckets`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `assert_receiver_is_total_eq`, `clone`, `clone`, `clone`, `clone`, `cmp`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `from`, `into`, `partial_cmp`, `try_into`, `try_into`
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PreparedMelt>>
abstract class PreparedMelt implements RustOpaqueInterface {
BigInt get amount;
BigInt get feeReserve;
BigInt get inputFee;
BigInt get swapFee;
set amount(BigInt amount);
set feeReserve(BigInt feeReserve);
set inputFee(BigInt inputFee);
set swapFee(BigInt swapFee);
}
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PreparedSend>>
abstract class PreparedSend implements RustOpaqueInterface {
BigInt get amount;
BigInt get fee;
BigInt get sendFee;
BigInt get swapFee;
set amount(BigInt amount);
set fee(BigInt fee);
set sendFee(BigInt sendFee);
set swapFee(BigInt swapFee);
}
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Wallet>>
abstract class Wallet implements RustOpaqueInterface {
String get mintUrl;
String get unit;
set mintUrl(String mintUrl);
set unit(String unit);
Future<BigInt> balance();
Future<void> cancelMelt({required PreparedMelt melt});
Future<void> cancelSend({required PreparedSend send});
Future<void> checkAllMintQuotes();
/// Ask the mint for the current state of a single quote by id and
/// return the refreshed MintQuote. Local store is updated as a side
/// effect inside CDK.
Future<MintQuote> checkMintQuoteStatus({required String quoteId});
Future<void> checkPendingTransactions();
/// Observe-only counterpart of `reclaim_proofs_by_ys`. Queries the mint
/// for the state of the target Ys but **does NOT** revert Unspent proofs
/// to the local Unspent state.
///
/// Use this for periodic reconciliation where we want to auto-settle
/// sends the receiver already claimed, without accidentally cancelling
/// sends the receiver has not claimed yet (which `reclaim_proofs_by_ys`
/// would do by calling `unreserve_proofs`).
///
/// Return semantics:
/// - `count` / `amount` always 0 (nothing is recovered).
/// - `pending_count` / `spent_count` reflect the mint's report.
/// - `spent_count` also includes target Ys that are no longer present
/// in local PendingSpent (already reconciled or externally cleaned).
/// The caller can infer "all resolved as spent" from
/// `spent_count == target_ys.len() && pending_count == 0`.
Future<ReclaimResult> checkProofsByYs({required List<String> ys});
Future<BigInt> confirmMelt({required PreparedMelt melt});
/// Create a NUT-18 payment request with Nostr transport.
///
/// Builds a PaymentRequest with the wallet's mint URL and unit,
/// generates ephemeral Nostr keys, and returns both creqA and creqB
/// encodings plus the keys needed for the Nostr listener.
Future<CreatedPaymentRequest> createPaymentRequest({
required CreateRequestParams params,
});
Future<void> finalizePendingMelts();
Future<MintInfo?> getMint();
/// Return pending (unissued) mint quotes known to the local store,
/// filtered to this wallet's mint/unit.
Future<List<MintQuote>> getUnissuedMintQuotes();
Future<bool> isTokenSpent({required Token token});
Future<List<Transaction>> listTransactions({TransactionDirection? direction});
Future<MeltQuote> meltQuote({required String request});
Stream<MintQuote> mint({required BigInt amount, String? description});
/// Mint proofs for a paid quote and return an Issued MintQuote with
/// `transaction_id` and `token` populated — same shape the `mint()`
/// stream emits on Issued. Use this to recover metadata for quotes
/// that were paid while the app was killed.
Future<MintQuote> mintByQuoteId({required String quoteId});
factory Wallet({
required String mintUrl,
required String unit,
required String mnemonic,
BigInt? targetProofCount,
required WalletDatabase db,
}) => RustLib.instance.api.crateApiWalletWalletNew(
mintUrl: mintUrl,
unit: unit,
mnemonic: mnemonic,
targetProofCount: targetProofCount,
db: db,
);
/// Pay a NUT-18 payment request.
/// Uses CDK's pay_request() which handles:
/// - NUT-10 spending conditions
/// - Transport selection (Nostr preferred, HTTP POST fallback)
/// - Token preparation and delivery
Future<void> payPaymentRequest({
required String encoded,
BigInt? customAmount,
});
Future<PreparedMelt> prepareMelt({required MeltQuote quote});
Future<PreparedSend> prepareSend({required BigInt amount, SendOptions? opts});
Future<BigInt> receive({required Token token, ReceiveOptions? opts});
/// Check pending-spent proofs with the mint and revert unspent ones.
/// Returns per-state buckets (unspent reverted, still pending, spent).
Future<ReclaimResult> reclaimPendingProofs();
/// Reclaim proofs belonging to a specific send identified by its Y values.
/// Scoped variant of `reclaim_pending_proofs`: only proofs whose Y matches
/// the input list are checked with the mint and reverted if Unspent.
///
/// Empty returns:
/// - Input `ys` empty or all unparseable → `ReclaimResult::empty()`.
/// - None of the target Ys are in local pending anymore (a previous
/// reconciliation already processed them) → `spent_count` is set to
/// the number of targets, signalling to callers that the send is
/// definitively finished and any local record can be removed.
Future<ReclaimResult> reclaimProofsByYs({required List<String> ys});
Future<void> recoverIncompleteSagas();
/// Recalcula el balance desde la DB y lo empuja al stream.
/// Llamar después de manipulaciones directas al DB (ej: offline send
/// marking proofs as PendingSpent fuera de la API de CDK).
Future<void> refreshBalance();
Future<void> restore();
Future<SendResult> send({
required PreparedSend send,
String? memo,
bool? includeMemo,
});
Stream<BigInt> streamBalance();
/// Wait for an incoming Nostr payment (NIP-17 gift-wrap).
///
/// Takes the opaque NostrListenerHandle returned by create_payment_request().
/// The secret key never leaves Rust memory.
Stream<NostrPaymentEvent> waitForNostrPayment({
required NostrListenerHandle handle,
});
}
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<WalletDatabase>>
abstract class WalletDatabase implements RustOpaqueInterface {
String get path;
set path(String path);
static Future<WalletDatabase> newInstance({required String path}) =>
RustLib.instance.api.crateApiWalletWalletDatabaseNewInstance(path: path);
Future<void> removeMint({required String mintUrl});
}
class MeltQuote {
final String id;
final String request;
final BigInt amount;
final BigInt feeReserve;
final BigInt expiry;
const MeltQuote({
required this.id,
required this.request,
required this.amount,
required this.feeReserve,
required this.expiry,
});
@override
int get hashCode =>
id.hashCode ^
request.hashCode ^
amount.hashCode ^
feeReserve.hashCode ^
expiry.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MeltQuote &&
runtimeType == other.runtimeType &&
id == other.id &&
request == other.request &&
amount == other.amount &&
feeReserve == other.feeReserve &&
expiry == other.expiry;
}
class MintQuote {
final String id;
final String request;
final BigInt? amount;
final BigInt? expiry;
final MintQuoteState state;
final Token? token;
final String? error;
/// Deterministic transaction ID (set when proofs are available, typically on Issued)
final String? transactionId;
const MintQuote({
required this.id,
required this.request,
this.amount,
this.expiry,
required this.state,
this.token,
this.error,
this.transactionId,
});
@override
int get hashCode =>
id.hashCode ^
request.hashCode ^
amount.hashCode ^
expiry.hashCode ^
state.hashCode ^
token.hashCode ^
error.hashCode ^
transactionId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is MintQuote &&
runtimeType == other.runtimeType &&
id == other.id &&
request == other.request &&
amount == other.amount &&
expiry == other.expiry &&
state == other.state &&
token == other.token &&
error == other.error &&
transactionId == other.transactionId;
}
enum MintQuoteState { unpaid, paid, issued, error }
class ReceiveOptions {
final List<String>? signingKeys;
final List<String>? preimages;
const ReceiveOptions({this.signingKeys, this.preimages});
static Future<ReceiveOptions> default_() =>
RustLib.instance.api.crateApiWalletReceiveOptionsDefault();
@override
int get hashCode => signingKeys.hashCode ^ preimages.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ReceiveOptions &&
runtimeType == other.runtimeType &&
signingKeys == other.signingKeys &&
preimages == other.preimages;
}
class ReclaimResult {
/// Proofs reverted from PendingSpent to Unspent. Also the number of
/// recovered proofs reflected in `amount`.
final BigInt count;
/// Total value of the recovered proofs.
final BigInt amount;
/// Proofs the mint reports as still Pending (receiver mid-swap). The
/// caller should keep the record for retry — a later reconciliation
/// will resolve them.
final BigInt pendingCount;
/// Proofs the mint reports as Spent (receiver already claimed). When
/// `count == 0 && pending_count == 0`, the send is definitively
/// finished and the caller can delete its local record.
final BigInt spentCount;
const ReclaimResult({
required this.count,
required this.amount,
required this.pendingCount,
required this.spentCount,
});
@override
int get hashCode =>
count.hashCode ^
amount.hashCode ^
pendingCount.hashCode ^
spentCount.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is ReclaimResult &&
runtimeType == other.runtimeType &&
count == other.count &&
amount == other.amount &&
pendingCount == other.pendingCount &&
spentCount == other.spentCount;
}
class SendOptions {
final String? pubkey;
final bool? includeFee;
const SendOptions({this.pubkey, this.includeFee});
static Future<SendOptions> default_() =>
RustLib.instance.api.crateApiWalletSendOptionsDefault();
@override
int get hashCode => pubkey.hashCode ^ includeFee.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SendOptions &&
runtimeType == other.runtimeType &&
pubkey == other.pubkey &&
includeFee == other.includeFee;
}
/// Result of a confirmed send, carrying both the ecash token and the
/// deterministic transaction ID so Dart can save metadata without a racy
/// listTransactions lookup.
class SendResult {
final Token token;
/// Deterministic transaction ID (None if computation failed)
final String? transactionId;
const SendResult({required this.token, this.transactionId});
@override
int get hashCode => token.hashCode ^ transactionId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is SendResult &&
runtimeType == other.runtimeType &&
token == other.token &&
transactionId == other.transactionId;
}
class Transaction {
final String id;
final String mintUrl;
final TransactionDirection direction;
final BigInt amount;
final BigInt fee;
final String unit;
final List<String> ys;
final BigInt timestamp;
final String? memo;
final Map<String, String> metadata;
final TransactionStatus status;
const Transaction({
required this.id,
required this.mintUrl,
required this.direction,
required this.amount,
required this.fee,
required this.unit,
required this.ys,
required this.timestamp,
this.memo,
required this.metadata,
required this.status,
});
@override
int get hashCode =>
id.hashCode ^
mintUrl.hashCode ^
direction.hashCode ^
amount.hashCode ^
fee.hashCode ^
unit.hashCode ^
ys.hashCode ^
timestamp.hashCode ^
memo.hashCode ^
metadata.hashCode ^
status.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Transaction &&
runtimeType == other.runtimeType &&
id == other.id &&
mintUrl == other.mintUrl &&
direction == other.direction &&
amount == other.amount &&
fee == other.fee &&
unit == other.unit &&
ys == other.ys &&
timestamp == other.timestamp &&
memo == other.memo &&
metadata == other.metadata &&
status == other.status;
}
enum TransactionDirection { incoming, outgoing }
enum TransactionStatus { pending, settled }
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+15 -2
View File
@@ -58,6 +58,9 @@ class AnimatedActionButton extends StatefulWidget {
/// Si mostrar el icono (default: false)
final bool showIcon;
/// Si el icono va después del texto (default: false = antes del texto)
final bool iconTrailing;
/// Ancho del botón (default: expandir al padre)
final double? width;
@@ -73,6 +76,7 @@ class AnimatedActionButton extends StatefulWidget {
this.backgroundColor,
this.icon,
this.showIcon = false,
this.iconTrailing = false,
this.width,
this.height,
});
@@ -300,8 +304,8 @@ class _AnimatedActionButtonState extends State<AnimatedActionButton>
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Icono opcional
if (widget.showIcon && widget.icon != null) ...[
// Icono leading (antes del texto)
if (widget.showIcon && widget.icon != null && !widget.iconTrailing) ...[
Icon(
widget.icon,
color: Colors.white,
@@ -319,6 +323,15 @@ class _AnimatedActionButtonState extends State<AnimatedActionButton>
color: Colors.white,
),
),
// Icono trailing (después del texto)
if (widget.showIcon && widget.icon != null && widget.iconTrailing) ...[
const SizedBox(width: AppDimensions.paddingSmall),
Icon(
widget.icon,
color: Colors.white,
size: widget.type == ButtonType.navigation ? 28 : 20,
),
],
],
),
),
@@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
/// Frame común para modales tipo bottom sheet.
///
/// Aplica SafeArea(top: false) para que los botones internos no queden
/// tapados por la barra de navegación del sistema (gestos o 3 botones).
///
/// Usá [respectKeyboard]: true cuando el modal contenga TextField, para
/// combinar el inset de la barra con el del teclado (viewInsets.bottom).
class BottomSheetContainer extends StatelessWidget {
final Widget child;
final EdgeInsetsGeometry padding;
final bool respectKeyboard;
const BottomSheetContainer({
super.key,
required this.child,
this.padding = const EdgeInsets.all(AppDimensions.paddingMedium),
this.respectKeyboard = false,
});
@override
Widget build(BuildContext context) {
final mq = MediaQuery.of(context);
// viewPadding.bottom es el inset crudo de la nav bar del sistema
// (no se consume aunque un SafeArea padre ya lo haya absorbido).
final navBarInset = mq.viewPadding.bottom;
// Combinar el padding base con el extra del navBar abajo.
final resolvedPadding = padding.resolve(Directionality.of(context));
final effectivePadding = EdgeInsets.only(
left: resolvedPadding.left,
top: resolvedPadding.top,
right: resolvedPadding.right,
bottom: resolvedPadding.bottom + navBarInset,
);
final frame = Container(
decoration: BoxDecoration(
color: AppColors.deepVoidPurple,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppDimensions.radiusXLarge),
),
border: Border.all(
color: Colors.white.withValues(alpha: 0.1),
width: 1,
),
),
child: Padding(padding: effectivePadding, child: child),
);
if (respectKeyboard) {
return Padding(
padding: EdgeInsets.only(bottom: mq.viewInsets.bottom),
child: frame,
);
}
return frame;
}
}
/// Handle visual en la parte superior del modal (la barrita).
class BottomSheetHandle extends StatelessWidget {
const BottomSheetHandle({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
);
}
}
+2 -2
View File
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import 'package:cdk_flutter/cdk_flutter.dart';
import '../../src/rust/api/token.dart';
import '../../core/constants/colors.dart';
import '../../core/utils/incoming_data_parser.dart';
@@ -36,7 +36,7 @@ class QrScannerWidget extends StatefulWidget {
class _QrScannerWidgetState extends State<QrScannerWidget> {
late MobileScannerController _controller;
// Estado para QR animados (UR multipartes) usando TokenDecoder de cdk-flutter
// Estado para QR animados (UR multipartes) TokenDecoder (Rust bridge)
TokenDecoder? _urDecoder;
bool _isCapturingUr = false;
final Set<String> _urFragmentsSeen = {}; // Trackear fragmentos únicos
+1 -1
View File
@@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
set(BINARY_NAME "elcaju")
# The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.elcaju")
set(APPLICATION_ID "me.elcaju")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake.
+1 -1
View File
@@ -8,7 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
cdk_flutter
rust_builder
)
set(PLUGIN_BUNDLED_LIBRARIES)
@@ -7,6 +7,7 @@ import Foundation
import flutter_secure_storage_macos
import mobile_scanner
import package_info_plus
import path_provider_foundation
import share_plus
import shared_preferences_foundation
@@ -16,6 +17,7 @@ import url_launcher_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
MobileScannerPlugin.register(with: registry.registrar(forPlugin: "MobileScannerPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
+3 -3
View File
@@ -385,7 +385,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/elcaju.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/elcaju";
@@ -399,7 +399,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/elcaju.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/elcaju";
@@ -413,7 +413,7 @@
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju.RunnerTests;
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/elcaju.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/elcaju";
+2 -2
View File
@@ -8,7 +8,7 @@
PRODUCT_NAME = elcaju
// The application's bundle identifier
PRODUCT_BUNDLE_IDENTIFIER = com.example.elcaju
PRODUCT_BUNDLE_IDENTIFIER = me.elcaju
// The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved.
PRODUCT_COPYRIGHT = Copyright © 2026 ElCaju. All rights reserved.
+322 -44
View File
@@ -1,6 +1,22 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f
url: "https://pub.dev"
source: hosted
version: "85.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d"
url: "https://pub.dev"
source: hosted
version: "7.7.1"
ansicolor:
dependency: transitive
description:
@@ -65,6 +81,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.2"
build:
dependency: transitive
description:
name: build
sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_cli_annotations:
dependency: transitive
description:
@@ -73,6 +97,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
build_config:
dependency: transitive
description:
name: build_config
sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
build_daemon:
dependency: transitive
description:
name: build_daemon
sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957
url: "https://pub.dev"
source: hosted
version: "4.1.1"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_runner:
dependency: "direct dev"
description:
name: build_runner
sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
build_runner_core:
dependency: transitive
description:
name: build_runner_core
sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
url: "https://pub.dev"
source: hosted
version: "9.1.2"
built_collection:
dependency: transitive
description:
name: built_collection
sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af"
url: "https://pub.dev"
source: hosted
version: "8.12.5"
cbor:
dependency: "direct main"
description:
@@ -81,23 +161,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.3.5"
cdk_flutter:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: "7fb8580530d963c9ff09f23e55a1e56bccd8676b"
url: "https://github.com/cashubtc/cdk_flutter.git"
source: git
version: "0.1.0"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
url: "https://pub.dev"
source: hosted
version: "1.3.0"
version: "1.4.1"
checked_yaml:
dependency: transitive
description:
@@ -118,18 +189,26 @@ packages:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.1"
version: "1.1.2"
code_builder:
dependency: transitive
description:
name: code_builder
sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d"
url: "https://pub.dev"
source: hosted
version: "4.11.1"
collection:
dependency: transitive
description:
name: collection
sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.0"
version: "1.19.1"
convert:
dependency: transitive
description:
@@ -170,14 +249,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
version: "1.3.3"
ffi:
dependency: transitive
description:
@@ -237,7 +324,7 @@ packages:
source: hosted
version: "2.4.4"
flutter_rust_bridge:
dependency: transitive
dependency: "direct main"
description:
name: flutter_rust_bridge
sha256: "37ef40bc6f863652e865f0b2563ea07f0d3c58d8efad803cc01933a4b2ee067e"
@@ -302,14 +389,38 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
freezed:
dependency: "direct dev"
description:
name: freezed
sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c"
url: "https://pub.dev"
source: hosted
version: "2.5.8"
freezed_annotation:
dependency: transitive
dependency: "direct main"
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.4"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
google_fonts:
dependency: "direct main"
description:
@@ -318,6 +429,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.3.0"
graphs:
dependency: transitive
description:
name: graphs
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
hex:
dependency: transitive
description:
@@ -342,6 +461,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.6.0"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
@@ -370,10 +497,18 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
url: "https://pub.dev"
source: hosted
version: "0.19.0"
version: "0.20.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
js:
dependency: transitive
description:
@@ -394,26 +529,26 @@ packages:
dependency: transitive
description:
name: leak_tracker
sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06"
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "10.0.7"
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379"
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.8"
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
version: "3.0.2"
lints:
dependency: transitive
description:
@@ -422,6 +557,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.1"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
lucide_icons:
dependency: "direct main"
description:
@@ -434,26 +577,26 @@ packages:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
version: "0.12.19"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
url: "https://pub.dev"
source: hosted
version: "0.11.1"
version: "0.13.0"
meta:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
version: "1.17.0"
mime:
dependency: transitive
description:
@@ -470,6 +613,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.2.3"
ndef_record:
dependency: "direct main"
description:
name: ndef_record
sha256: "210ffb12284961cab9e44b99462143316d9a20cd992581170706069ef77d74a6"
url: "https://pub.dev"
source: hosted
version: "1.4.2"
nested:
dependency: transitive
description:
@@ -478,14 +629,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
nfc_manager:
dependency: "direct main"
description:
name: nfc_manager
sha256: "24c78b0e5702da53e7f8794d073624c0bee7cd99924f257cbd11f5d1c5866879"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
package_info_plus:
dependency: "direct main"
description:
name: package_info_plus
sha256: f69da0d3189a4b4ceaeb1a3defb0f329b3b352517f52bed4290f83d4f06bc08d
url: "https://pub.dev"
source: hosted
version: "9.0.0"
package_info_plus_platform_interface:
dependency: transitive
description:
name: package_info_plus_platform_interface
sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086"
url: "https://pub.dev"
source: hosted
version: "3.2.1"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
version: "1.9.1"
path_provider:
dependency: "direct main"
description:
@@ -566,6 +749,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
posix:
dependency: transitive
description:
@@ -582,6 +773,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
qr:
dependency: transitive
description:
@@ -598,6 +805,13 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rust_builder:
dependency: "direct main"
description:
path: rust_builder
relative: true
source: path
version: "0.1.0"
share_plus:
dependency: "direct main"
description:
@@ -670,11 +884,35 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_gen:
dependency: transitive
description:
name: source_gen
sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b"
url: "https://pub.dev"
source: hosted
version: "2.0.0"
source_span:
dependency: transitive
description:
@@ -743,18 +981,26 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377"
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.0"
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
@@ -783,10 +1029,18 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c"
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
url: "https://pub.dev"
source: hosted
version: "0.7.3"
version: "0.7.10"
timing:
dependency: transitive
description:
name: timing
sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
typed_data:
dependency: transitive
description:
@@ -879,10 +1133,10 @@ packages:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version: "2.2.0"
vm_service:
dependency: transitive
description:
@@ -891,6 +1145,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "14.3.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: transitive
description:
@@ -899,6 +1161,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
win32:
dependency: transitive
description:
@@ -932,5 +1210,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.6.0 <4.0.0"
dart: ">=3.11.0 <4.0.0"
flutter: ">=3.27.0"
+14 -6
View File
@@ -1,10 +1,10 @@
name: elcaju
description: "ElCaju - Tu wallet de ecash privado"
publish_to: 'none'
version: 0.0.1+1
version: 0.3.0+4
environment:
sdk: ^3.6.0
sdk: ^3.11.0
dependencies:
flutter:
@@ -15,10 +15,9 @@ dependencies:
cupertino_icons: ^1.0.8
# CDK Flutter (Cashu backend)
cdk_flutter:
git:
url: https://github.com/cashubtc/cdk_flutter.git
# Internal Rust bridge (Cashu backend via cdk)
rust_builder:
path: rust_builder
# State Management
provider: ^6.1.2
@@ -56,8 +55,15 @@ dependencies:
# UUID generator for pending tokens
uuid: ^4.3.3
# NFC for token sharing
nfc_manager: ^4.1.1
ndef_record: ^1.4.2
# BIP32 for NIP-06 key derivation (P2PK)
bip32: ^2.0.0
package_info_plus: ^9.0.0
flutter_rust_bridge: 2.11.1
freezed_annotation: ^2.4.4
dev_dependencies:
flutter_test:
@@ -65,6 +71,8 @@ dev_dependencies:
flutter_lints: ^5.0.0
flutter_launcher_icons: ^0.14.3
flutter_native_splash: ^2.4.4
freezed: ^2.5.8
build_runner: ^2.5.4
# App Icon configuration
flutter_launcher_icons:
+4237
View File
File diff suppressed because it is too large Load Diff
+65
View File
@@ -0,0 +1,65 @@
[package]
name = "elcaju_core"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib", "staticlib"]
[dependencies]
# Cashu — acceso directo, sin intermediarios
cdk = { version = "0.16.0", default-features = false, features = ["wallet", "nostr"] }
cdk-common = { version = "0.16.0", default-features = false, features = ["nostr"] }
cdk-sqlite = { version = "0.16.0", default-features = false, features = ["wallet"] }
# Bridge Flutter <-> Rust
flutter_rust_bridge = { version = "=2.11.1", default-features = false, features = [
"anyhow",
"dart-opaque",
"portable-atomic",
"rust-async",
"thread-pool",
"wasm-start",
] }
# QR multiframe (bc-ur)
bc-ur = { version = "0.12.0", default-features = false }
# Mnemonic
bip39 = { version = "2.1", default-features = false, features = ["std"] }
# Entropy
getrandom = { version = "0.3", default-features = false, features = ["std"] }
# Nostr keys (same version as CDK 0.16.0 — CDK does not re-export Keys)
nostr-sdk = { version = "0.44.1", default-features = false, features = ["nip04", "nip44", "nip59"] }
# Async runtime
tokio = { version = "1", default-features = false }
# HTTP (para ping_mint, fetch_keysets)
reqwest = { version = "0.12", default-features = false, features = [
"deflate",
"gzip",
"json",
"rustls-tls",
"rustls-tls-native-roots",
] }
# Serialización
serde_json = { version = "1.0", default-features = false, features = ["std"] }
# UUID (for PreparedSend operation_id)
uuid = { version = "1", default-features = false }
# Logging
log = { version = "0.4", default-features = false }
[target.'cfg(target_os = "android")'.dependencies]
android_logger = "0.15"
[target.'cfg(any(target_os = "ios", target_os = "macos"))'.dependencies]
oslog = "0.2.0"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
+100
View File
@@ -0,0 +1,100 @@
use std::fmt;
/// Errores unificados de elcaju_core
#[derive(Debug)]
pub enum Error {
/// Error del protocolo Cashu (cdk)
Cdk(String),
/// Error de base de datos (SQLite)
Database(String),
/// Input inválido del usuario
InvalidInput,
/// Error de red (HTTP, conexión)
Network(String),
/// Error de UR (QR multiframe)
Ur(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Cdk(msg) => write!(f, "Cashu error: {msg}"),
Error::Database(msg) => write!(f, "Database error: {msg}"),
Error::InvalidInput => write!(f, "Invalid input"),
Error::Network(msg) => write!(f, "Network error: {msg}"),
Error::Ur(msg) => write!(f, "UR error: {msg}"),
}
}
}
impl std::error::Error for Error {}
// --- cdk errors ---
impl From<cdk::error::Error> for Error {
fn from(e: cdk::error::Error) -> Self {
Error::Cdk(e.to_string())
}
}
impl From<cdk::amount::Error> for Error {
fn from(e: cdk::amount::Error) -> Self {
Error::Cdk(e.to_string())
}
}
impl From<cdk::cdk_database::Error> for Error {
fn from(e: cdk::cdk_database::Error) -> Self {
Error::Database(e.to_string())
}
}
impl From<cdk::mint_url::Error> for Error {
fn from(e: cdk::mint_url::Error) -> Self {
Error::Network(e.to_string())
}
}
impl From<cdk::nuts::nut00::Error> for Error {
fn from(e: cdk::nuts::nut00::Error) -> Self {
Error::Cdk(e.to_string())
}
}
impl From<cdk::nuts::nut01::Error> for Error {
fn from(e: cdk::nuts::nut01::Error) -> Self {
Error::Cdk(e.to_string())
}
}
impl From<cdk::nuts::nut11::Error> for Error {
fn from(e: cdk::nuts::nut11::Error) -> Self {
Error::Cdk(e.to_string())
}
}
// --- external errors ---
impl From<bc_ur::Error> for Error {
fn from(e: bc_ur::Error) -> Self {
Error::Ur(e.to_string())
}
}
impl From<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Self {
Error::Network(e.to_string())
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::Cdk(e.to_string())
}
}
impl From<std::string::FromUtf8Error> for Error {
fn from(e: std::string::FromUtf8Error) -> Self {
Error::Cdk(e.to_string())
}
}
+31
View File
@@ -0,0 +1,31 @@
use std::str::FromStr;
use bip39::Mnemonic;
use cdk_common::SecretKey;
use flutter_rust_bridge::frb;
use super::error::Error;
/// Generate a new 12-word BIP39 mnemonic
#[frb(sync)]
pub fn generate_mnemonic() -> Result<String, Error> {
let mut entropy = [0u8; 16]; // 128 bits → 12 words
getrandom::fill(&mut entropy).map_err(|_| Error::InvalidInput)?;
let mnemonic = Mnemonic::from_entropy(&entropy).map_err(|_| Error::InvalidInput)?;
Ok(mnemonic.to_string())
}
/// Convert a mnemonic to a 64-byte seed
#[frb(sync)]
pub fn mnemonic_to_seed(mnemonic: String) -> Result<Vec<u8>, Error> {
let mnemonic = Mnemonic::parse(&mnemonic).map_err(|_| Error::InvalidInput)?;
Ok(mnemonic.to_seed("").to_vec())
}
/// Derive the public key (hex) from a secret key (hex)
#[frb(sync)]
pub fn get_pub_key(secret: String) -> Result<String, Error> {
let secret = SecretKey::from_str(&secret)?;
let pub_key = secret.public_key();
Ok(pub_key.to_string())
}
+243
View File
@@ -0,0 +1,243 @@
use std::str::FromStr;
use cdk::{
nuts::{nut04, nut05, nut06},
wallet::{HttpClient, MintConnector},
};
use cdk_common::{
mint_url::MintUrl, ContactInfo as CdkContactInfo, MintInfo as CdkMintInfo,
MintVersion as CdkMintVersion, Nuts as CdkNuts,
};
use super::error::Error;
// === MintInfo structs ===
pub struct MintInfo {
pub name: Option<String>,
pub pubkey: Option<String>,
pub version: Option<MintVersion>,
pub description: Option<String>,
pub description_long: Option<String>,
pub contact: Option<Vec<ContactInfo>>,
pub nuts: Nuts,
pub icon_url: Option<String>,
pub urls: Option<Vec<String>>,
pub motd: Option<String>,
pub time: Option<u64>,
pub tos_url: Option<String>,
}
impl From<CdkMintInfo> for MintInfo {
fn from(value: CdkMintInfo) -> Self {
Self {
name: value.name,
pubkey: value.pubkey.map(|p| p.to_string()),
version: value.version.map(|v| v.into()),
description: value.description,
description_long: value.description_long,
contact: value
.contact
.map(|c| c.into_iter().map(|c| c.into()).collect()),
nuts: value.nuts.into(),
icon_url: value.icon_url,
urls: value.urls,
motd: value.motd,
time: value.time,
tos_url: value.tos_url,
}
}
}
pub struct MintVersion {
pub name: String,
pub version: String,
}
impl From<CdkMintVersion> for MintVersion {
fn from(value: CdkMintVersion) -> Self {
Self {
name: value.name,
version: value.version,
}
}
}
pub struct ContactInfo {
pub method: String,
pub info: String,
}
impl From<CdkContactInfo> for ContactInfo {
fn from(c: CdkContactInfo) -> Self {
Self {
method: c.method,
info: c.info,
}
}
}
// === NUT settings ===
pub struct Nuts {
pub nut04: Nut04Settings,
pub nut05: Nut05Settings,
pub nut07: SupportedSettings,
pub nut08: SupportedSettings,
pub nut09: SupportedSettings,
pub nut10: SupportedSettings,
pub nut11: SupportedSettings,
pub nut12: SupportedSettings,
pub nut14: SupportedSettings,
pub nut20: SupportedSettings,
}
impl From<CdkNuts> for Nuts {
fn from(value: CdkNuts) -> Self {
Self {
nut04: value.nut04.into(),
nut05: value.nut05.into(),
nut07: value.nut07.into(),
nut08: value.nut08.into(),
nut09: value.nut09.into(),
nut10: value.nut10.into(),
nut11: value.nut11.into(),
nut12: value.nut12.into(),
nut14: value.nut14.into(),
nut20: value.nut20.into(),
}
}
}
pub struct SupportedSettings {
pub supported: bool,
}
impl From<nut06::SupportedSettings> for SupportedSettings {
fn from(value: nut06::SupportedSettings) -> Self {
Self {
supported: value.supported,
}
}
}
pub struct Nut04Settings {
pub methods: Vec<MintMethodSettings>,
pub disabled: bool,
}
impl From<nut04::Settings> for Nut04Settings {
fn from(value: nut04::Settings) -> Self {
Self {
methods: value.methods.into_iter().map(|m| m.into()).collect(),
disabled: value.disabled,
}
}
}
pub struct MintMethodSettings {
pub method: String,
pub unit: String,
pub min_amount: Option<u64>,
pub max_amount: Option<u64>,
}
impl From<nut04::MintMethodSettings> for MintMethodSettings {
fn from(value: nut04::MintMethodSettings) -> Self {
Self {
method: value.method.to_string(),
unit: value.unit.to_string(),
min_amount: value.min_amount.map(|v| v.into()),
max_amount: value.max_amount.map(|v| v.into()),
}
}
}
pub struct Nut05Settings {
pub methods: Vec<MeltMethodSettings>,
pub disabled: bool,
}
impl From<nut05::Settings> for Nut05Settings {
fn from(value: nut05::Settings) -> Self {
Self {
methods: value.methods.into_iter().map(|m| m.into()).collect(),
disabled: value.disabled,
}
}
}
pub struct MeltMethodSettings {
pub method: String,
pub unit: String,
pub min_amount: Option<u64>,
pub max_amount: Option<u64>,
}
impl From<nut05::MeltMethodSettings> for MeltMethodSettings {
fn from(value: nut05::MeltMethodSettings) -> Self {
Self {
method: value.method.to_string(),
unit: value.unit.to_string(),
min_amount: value.min_amount.map(|v| v.into()),
max_amount: value.max_amount.map(|v| v.into()),
}
}
}
// === API functions ===
/// Fetch full mint info from a mint URL
pub async fn get_mint_info(mint_url: &str) -> Result<MintInfo, Error> {
let mint_url = MintUrl::from_str(mint_url)?;
let client = HttpClient::new(mint_url, None);
Ok(client.get_mint_info().await?.into())
}
/// Ping a mint to check if it's reachable (replaces Dart HTTP call)
pub async fn ping_mint(mint_url: &str) -> Result<bool, Error> {
let url = format!("{}/v1/info", mint_url.trim_end_matches('/'));
let resp = reqwest::Client::new()
.get(&url)
.timeout(std::time::Duration::from_secs(3))
.send()
.await;
match resp {
Ok(r) => Ok(r.status().is_success()),
Err(_) => Ok(false),
}
}
/// Keyset info returned by fetch_keysets
pub struct KeysetInfo {
pub id: String,
pub unit: String,
pub active: bool,
}
/// Fetch keysets from a mint (replaces Dart HTTP call)
pub async fn fetch_keysets(mint_url: &str) -> Result<Vec<KeysetInfo>, Error> {
let url = format!("{}/v1/keysets", mint_url.trim_end_matches('/'));
let resp = reqwest::Client::new()
.get(&url)
.timeout(std::time::Duration::from_secs(5))
.send()
.await?
.text()
.await?;
let data: serde_json::Value = serde_json::from_str(&resp)?;
let keysets = data["keysets"]
.as_array()
.ok_or(Error::InvalidInput)?
.iter()
.filter_map(|ks| {
Some(KeysetInfo {
id: ks["id"].as_str()?.to_string(),
unit: ks["unit"].as_str()?.to_string(),
active: ks["active"].as_bool().unwrap_or(false),
})
})
.collect();
Ok(keysets)
}
+6
View File
@@ -0,0 +1,6 @@
pub mod error;
pub mod keys;
pub mod mint_info;
pub mod payment_request;
pub mod token;
pub mod wallet;

Some files were not shown because too many files have changed in this diff Show More