add wallet send view

This commit is contained in:
hzrd149 2025-03-13 15:53:58 +00:00
parent 4ed1a0b528
commit 5982ce2a4e
23 changed files with 686 additions and 263 deletions

View File

@ -30,7 +30,7 @@
"@chakra-ui/theme-tools": "^2.2.6",
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/language": "^6.10.8",
"@codemirror/language": "^6.11.0",
"@codemirror/view": "^6.36.4",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
@ -130,7 +130,7 @@
"workbox-core": "7.0.0",
"workbox-precaching": "7.0.0",
"workbox-routing": "7.0.0",
"yet-another-react-lightbox": "^3.21.7"
"yet-another-react-lightbox": "^3.21.8"
},
"devDependencies": {
"@capacitor-community/http": "^1.4.1",
@ -173,7 +173,8 @@
},
"resolutions": {
"@types/react": "^18.2.22",
"@types/react-dom": "^18.2.7"
"@types/react-dom": "^18.2.7",
"three-render-objects": "1.39.0"
},
"funding": {
"type": "lightning",

253
pnpm-lock.yaml generated
View File

@ -7,6 +7,7 @@ settings:
overrides:
'@types/react': ^18.2.22
'@types/react-dom': ^18.2.7
three-render-objects: 1.39.0
importers:
@ -46,8 +47,8 @@ importers:
specifier: ^6.0.1
version: 6.0.1
'@codemirror/language':
specifier: ^6.10.8
version: 6.10.8
specifier: ^6.11.0
version: 6.11.0
'@codemirror/view':
specifier: ^6.36.4
version: 6.36.4
@ -92,10 +93,10 @@ importers:
version: 1.3.1
'@uiw/codemirror-theme-github':
specifier: ^4.23.10
version: 4.23.10(@codemirror/language@6.10.8)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)
version: 4.23.10(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)
'@uiw/react-codemirror':
specifier: ^4.23.10
version: 4.23.10(@babel/runtime@7.26.10)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.4)(codemirror@6.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
version: 4.23.10(@babel/runtime@7.26.10)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.4)(codemirror@6.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
'@webscopeio/react-textarea-autocomplete':
specifier: ^4.9.2
version: 4.9.2(prop-types@15.8.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
@ -104,34 +105,34 @@ importers:
version: 0.7.2
applesauce-accounts:
specifier: next
version: 0.0.0-next-20250312201602(typescript@5.8.2)
version: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-actions:
specifier: next
version: 0.0.0-next-20250312201602(typescript@5.8.2)
version: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-content:
specifier: next
version: 0.0.0-next-20250312201602(typescript@5.8.2)
version: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-core:
specifier: next
version: 0.0.0-next-20250312201602(typescript@5.8.2)
version: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-factory:
specifier: next
version: 0.0.0-next-20250312201602(typescript@5.8.2)
version: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-loaders:
specifier: next
version: 0.0.0-next-20250312201602(typescript@5.8.2)
version: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-react:
specifier: next
version: 0.0.0-next-20250312201602(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2)
version: 0.0.0-next-20250313155042(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2)
applesauce-relay:
specifier: next
version: 0.0.0-next-20250312201602(typescript@5.8.2)
version: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-signers:
specifier: next
version: 0.0.0-next-20250312201602(typescript@5.8.2)
version: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-wallet:
specifier: next
version: 0.0.0-next-20250312201602(typescript@5.8.2)
version: 0.0.0-next-20250313155042(typescript@5.8.2)
bech32:
specifier: ^2.0.0
version: 2.0.0
@ -155,7 +156,7 @@ importers:
version: 6.0.1
codemirror-json-schema:
specifier: ^0.7.9
version: 0.7.9(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3)
version: 0.7.9(@codemirror/language@6.11.0)(@codemirror/lint@6.8.4)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3)
dayjs:
specifier: ^1.11.13
version: 1.11.13
@ -346,8 +347,8 @@ importers:
specifier: 7.0.0
version: 7.0.0
yet-another-react-lightbox:
specifier: ^3.21.7
version: 3.21.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
specifier: ^3.21.8
version: 3.21.8(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
devDependencies:
'@capacitor-community/http':
specifier: ^1.4.1
@ -477,6 +478,9 @@ packages:
peerDependencies:
ajv: '>=8'
'@apocentre/alias-sampling@0.5.3':
resolution: {integrity: sha512-7UDWIIF9hIeJqfKXkNIzkVandlwLf1FWTSdrb9iXvOP8oF544JRXQjCbiTmCv2c9n44n/FIWtehhBfNuAx2CZA==}
'@babel/code-frame@7.26.2':
resolution: {integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==}
engines: {node: '>=6.9.0'}
@ -1241,8 +1245,8 @@ packages:
'@codemirror/lang-yaml@6.1.2':
resolution: {integrity: sha512-dxrfG8w5Ce/QbT7YID7mWZFKhdhsaTNOYjOkSIMt1qmC4VQnXSDSYVHHHn8k6kJUfIhtLo8t1JJgltlxWdsITw==}
'@codemirror/language@6.10.8':
resolution: {integrity: sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==}
'@codemirror/language@6.11.0':
resolution: {integrity: sha512-A7+f++LodNNc1wGgoRDTt78cOwWm9KVezApgjOMp1W4hM0898nsqBXwF+sbePE7ZRcjN7Sa1Z5m2oN27XkmEjQ==}
'@codemirror/lint@6.8.4':
resolution: {integrity: sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==}
@ -1473,6 +1477,9 @@ packages:
cpu: [x64]
os: [win32]
'@gandlaf21/bc-ur@1.1.12':
resolution: {integrity: sha512-AQfbZJ1o1AdK9/W9VcTyMkwp6iZWDWQQV2SGep2ygJUkTNaafSjdWLUgpc6Uo/VLlGYaS9A28gCh+GVtAdwTpA==}
'@getalby/bitcoin-connect-react@3.7.0':
resolution: {integrity: sha512-wO8RhUlxJ4ub6vl8x8BScUaG4Z/tnLcDvJd9V4V7AOlrmrItMJfViZmc14c/WVU/RREeE3MSY2GZ0wYoH2TzxA==}
peerDependencies:
@ -2198,35 +2205,35 @@ packages:
engines: {node: '>=8.0.0'}
hasBin: true
applesauce-accounts@0.0.0-next-20250312201602:
resolution: {integrity: sha512-WikbLZdD7BRx03I51CMSiTu6TH41xGHpHSBqHzGC+7MldDcRG7aGzlmmTTuxrkvAbbMrYxabVgiIoVUzW3hkHg==}
applesauce-accounts@0.0.0-next-20250313155042:
resolution: {integrity: sha512-2bWwif44iIi/3ZQJwgTmMT9lIi+8m43W18FGc3esgnuXdux0gb36Nyk3xMD+4HIvxa1E/KJnl2MeNnQf5/rBFA==}
applesauce-actions@0.0.0-next-20250312201602:
resolution: {integrity: sha512-MiAxNKn2X+VAii34TigZkWyjV56nfp5mN6CW9fP7Acm/YGMlhnFVNZvZyMZ3I7riLlV+s3bALy5Ag3Iqin1gZQ==}
applesauce-actions@0.0.0-next-20250313155042:
resolution: {integrity: sha512-F/yQ1su5njzvmC09SbzyCJpgRC/t7URt3ZogEXFas9r1k3rllodXhC5ZM3b+QUU1XQUQQcX1ZBHBK6oTzF20Qw==}
applesauce-content@0.0.0-next-20250312201602:
resolution: {integrity: sha512-t4ElFBTE3/kaeZo5DvgIq8xsDYKZGLcVtJWhWjwi27iM9IouRUiN9wc1YuGsPrAK3K3WaIVENq9wcdErzfr6Ug==}
applesauce-content@0.0.0-next-20250313155042:
resolution: {integrity: sha512-JMvpH9a7s5dTFvk5ey4t/wd1K0YRId3IHGa8hMKEoa7ZRm3PHmCDKhK8KFgjAKF2nd6erIIOkCyfDmEO+7p+vQ==}
applesauce-core@0.0.0-next-20250312201602:
resolution: {integrity: sha512-IK0y6eFZNY14Y/wnquaX+zZRUqCuwSH6ufULPffvImS7nb2bGEcNJPWcbIcl3OYvnk6QwVVjJmt6O2pAX+Mtvg==}
applesauce-core@0.0.0-next-20250313155042:
resolution: {integrity: sha512-HZeDganvR9kdAA7qexnkEXzSG+bdqMGOvUWgljZrvhjoAwazwX7XaOyj2vVmyQdmA8jY0V6pz+5RRTDgqCrx5A==}
applesauce-factory@0.0.0-next-20250312201602:
resolution: {integrity: sha512-PCEpm7l7jm+oh8eKltPERVZaVBSLQgBu84FKbO1M0K7LDVQANAkL0Q06vBdiOzbwEuEH2rbYRFsshJeRKfSuIQ==}
applesauce-factory@0.0.0-next-20250313155042:
resolution: {integrity: sha512-4xLAhram5hxgFkw2ATRIrAAg0r7FAVFLbwRfn6rxasYjrC0NMGGURWULWpHs/o3L2VQGM0iDcdhEYKvn+zIIrQ==}
applesauce-loaders@0.0.0-next-20250312201602:
resolution: {integrity: sha512-VnpzR+Awf4B6cX9FQMK8iMBigsWf64ve6nvjN31LRCZaH5rZBdQkd+Vu8Sy6uOuCktoen7lFWkcWb0H6jAfnCQ==}
applesauce-loaders@0.0.0-next-20250313155042:
resolution: {integrity: sha512-oyLU8fNObK/bfrLS099dfE3HlLj2Btc/MDBhN4FGBCnu8E3ya/r6bFyt0NOcUhxelJle4qi/pWMaRNUhiHdaqQ==}
applesauce-react@0.0.0-next-20250312201602:
resolution: {integrity: sha512-khhLt7zIb+FPX6l3N0YweqeehRgYNJ49dahW4VFqsVRwjaoo4qwKsrO4hzyxj9sdg2tBUTM6UyS9xeDUmxSIWg==}
applesauce-react@0.0.0-next-20250313155042:
resolution: {integrity: sha512-ry8xatCIBQm/+1SlG8NMchFCeQMOSPZwUAnO/qw1etW4X5c8PuIkYdQCOQYBoLp37YW0nXGgxImNwxhiz5FoGA==}
applesauce-relay@0.0.0-next-20250312201602:
resolution: {integrity: sha512-zyU4/joBrcgVb1NlgYawMeXigNrszRpZTmzCbFhqMRlIQ/ELPBcEuNSGvPUfiPH4qTU7IopBHNRRunFprjT7UQ==}
applesauce-relay@0.0.0-next-20250313155042:
resolution: {integrity: sha512-0ZL1iHm0FcsFkO2d7Vjl06mBC2QugtD0+coHy1xP0fs92zGQKnoTXyAS6B3M014vg2x+mH1hwHuOCaQ9rjeEbw==}
applesauce-signers@0.0.0-next-20250312201602:
resolution: {integrity: sha512-L4wCgv9gRdbj9sAA6d9hmpR6uQEDlaWORJ5GNie//PTA8GUzs5QQcQ2fGvNazns7pZKWH/Vhniwr7Q+rewz4jA==}
applesauce-signers@0.0.0-next-20250313155042:
resolution: {integrity: sha512-yvaxUFgmZhbN28B/6skhgoZxdF4/aU9Y2DXoh4ZvmSiJaNJLv1MVE2s7c/vblcsyO7OqQCYKA78jryDLHh3l+w==}
applesauce-wallet@0.0.0-next-20250312201602:
resolution: {integrity: sha512-g3uhg3TqQ/XHoH7wAy8ho7sj/6VMCyTLqeYPwVxZUgKDCWOZHAwki0oThBFYGV5B97+6Lqp9U1q8lMN2lQ9tuQ==}
applesauce-wallet@0.0.0-next-20250313155042:
resolution: {integrity: sha512-a5wk1LgHk4dr0B9+Jc2aIy4SglWv7y95Y/jtArWFLA75K3J0Y00NgJ69xsp5ZOQfwBV/iDfQTfOlIqSD9u+HLA==}
arg@4.1.3:
resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
@ -2361,6 +2368,9 @@ packages:
resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==}
engines: {node: '>=0.6'}
bignumber.js@9.1.2:
resolution: {integrity: sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==}
bindings@1.5.0:
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
@ -2463,8 +2473,8 @@ packages:
resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==}
engines: {node: '>=16'}
caniuse-lite@1.0.30001703:
resolution: {integrity: sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ==}
caniuse-lite@1.0.30001704:
resolution: {integrity: sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==}
canvas-color-tracker@1.3.1:
resolution: {integrity: sha512-eNycxGS7oQ3IS/9QQY41f/aQjiO9Y/MtedhCgSdsbLSxC9EyUD8L3ehl/Q3Kfmvt8um79S45PBV+5Rxm5ztdSw==}
@ -2473,6 +2483,13 @@ packages:
canvas-confetti@1.9.3:
resolution: {integrity: sha512-rFfTURMvmVEX1gyXFgn5QMn81bYk70qa0HLzcIOSVEyl57n6o9ItHeBtUSWdvKAPY0xlvBHno4/v3QPrT83q9g==}
cbor-sync@1.0.4:
resolution: {integrity: sha512-GWlXN4wiz0vdWWXBU71Dvc1q3aBo0HytqwAZnXF1wOwjqNnDWA1vZ1gDMFLlqohak31VQzmhiYfiCX5QSSfagA==}
cborg@4.2.8:
resolution: {integrity: sha512-z9M+TZCWQbf89Gl8ulpYThM9fqmkjBDdMiq+wS72OAK2zqDaXNquoAWFDrAKHQAukVtPspmadB9chuFC0ut7ew==}
hasBin: true
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@ -3042,8 +3059,8 @@ packages:
engines: {node: '>=0.10.0'}
hasBin: true
electron-to-chromium@1.5.115:
resolution: {integrity: sha512-MN1nahVHAQMOz6dz6bNZ7apgqc9InZy7Ja4DBEVCTdeiUcegbyOYE9bi/f2Z/z6ZxLi0RxLpyJ3EGe+4h3w73A==}
electron-to-chromium@1.5.116:
resolution: {integrity: sha512-mufxTCJzLBQVvSdZzX1s5YAuXsN1M4tTyYxOOL1TcSKtIzQ9rjIrm7yFK80rN5dwGTePgdoABDSHpuVtRQh0Zw==}
elementtree@0.1.7:
resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==}
@ -3860,6 +3877,9 @@ packages:
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==}
hasBin: true
jsbi@3.1.5:
resolution: {integrity: sha512-w2BY0VOYC1ahe+w6Qhl4SFoPvPsZ9NPHY4bwass+LCgU7RK3PBoVQlQ3G1s7vI8W3CYyJiEXcbKF7FIM/L8q3Q==}
jsesc@3.0.2:
resolution: {integrity: sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==}
engines: {node: '>=6'}
@ -5730,8 +5750,8 @@ packages:
undici-types@6.20.0:
resolution: {integrity: sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==}
undici@6.21.1:
resolution: {integrity: sha512-q/1rj5D0/zayJB2FraXdaWxbhWiNKDvu8naDT2dl1yTlvJp4BLtOcp2a5BvgGNQpYYJzau7tf1WgKv3b+7mqpQ==}
undici@6.21.2:
resolution: {integrity: sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g==}
engines: {node: '>=18.17'}
unicode-canonical-property-names-ecmascript@2.0.1:
@ -6142,12 +6162,19 @@ packages:
yauzl@2.10.0:
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
yet-another-react-lightbox@3.21.7:
resolution: {integrity: sha512-dcdokNuCIl92f0Vl+uzeKULnQhztIGpoZFUMvtVNUPmtwsQWpqWufeieDPeg9JtFyVCcbj4vYw3V00DS0QNoWA==}
yet-another-react-lightbox@3.21.8:
resolution: {integrity: sha512-8DnjpSmWF+WjGXX+NIJx0V/naUhUYxLt6RIBJZoQ4y1GJVKwiUO2RuRvUBvYQTwAwFqwhJSvxfIhCU3VyCj9WQ==}
engines: {node: '>=14'}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@types/react': ^18.2.22
'@types/react-dom': ^18.2.7
react: ^16.8.0 || ^17 || ^18 || ^19
react-dom: ^16.8.0 || ^17 || ^18 || ^19
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
yn@3.1.1:
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
@ -6196,6 +6223,8 @@ snapshots:
jsonpointer: 5.0.1
leven: 3.1.0
'@apocentre/alias-sampling@0.5.3': {}
'@babel/code-frame@7.26.2':
dependencies:
'@babel/helper-validator-identifier': 7.25.9
@ -7310,27 +7339,27 @@ snapshots:
'@codemirror/autocomplete@6.18.6':
dependencies:
'@codemirror/language': 6.10.8
'@codemirror/language': 6.11.0
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.4
'@lezer/common': 1.2.3
'@codemirror/commands@6.8.0':
dependencies:
'@codemirror/language': 6.10.8
'@codemirror/language': 6.11.0
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.4
'@lezer/common': 1.2.3
'@codemirror/lang-json@6.0.1':
dependencies:
'@codemirror/language': 6.10.8
'@codemirror/language': 6.11.0
'@lezer/json': 1.0.3
'@codemirror/lang-yaml@6.1.2':
dependencies:
'@codemirror/autocomplete': 6.18.6
'@codemirror/language': 6.10.8
'@codemirror/language': 6.11.0
'@codemirror/state': 6.5.2
'@lezer/common': 1.2.3
'@lezer/highlight': 1.2.1
@ -7338,7 +7367,7 @@ snapshots:
'@lezer/yaml': 1.0.3
optional: true
'@codemirror/language@6.10.8':
'@codemirror/language@6.11.0':
dependencies:
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.4
@ -7365,7 +7394,7 @@ snapshots:
'@codemirror/theme-one-dark@6.1.2':
dependencies:
'@codemirror/language': 6.10.8
'@codemirror/language': 6.11.0
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.4
'@lezer/highlight': 1.2.1
@ -7557,6 +7586,16 @@ snapshots:
'@esbuild/win32-x64@0.21.5':
optional: true
'@gandlaf21/bc-ur@1.1.12':
dependencies:
'@apocentre/alias-sampling': 0.5.3
'@noble/hashes': 1.7.1
bignumber.js: 9.1.2
buffer: 6.0.3
cbor-sync: 1.0.4
cborg: 4.2.8
jsbi: 3.1.5
'@getalby/bitcoin-connect-react@3.7.0(@types/react@18.3.18)(react@19.0.0)(typescript@5.8.2)':
dependencies:
'@getalby/bitcoin-connect': 3.7.0(@types/react@18.3.18)(react@19.0.0)(typescript@5.8.2)
@ -8304,38 +8343,38 @@ snapshots:
'@types/webxr@0.5.21': {}
'@uiw/codemirror-extensions-basic-setup@4.23.10(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)':
'@uiw/codemirror-extensions-basic-setup@4.23.10(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.0)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)':
dependencies:
'@codemirror/autocomplete': 6.18.6
'@codemirror/commands': 6.8.0
'@codemirror/language': 6.10.8
'@codemirror/language': 6.11.0
'@codemirror/lint': 6.8.4
'@codemirror/search': 6.5.10
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.4
'@uiw/codemirror-theme-github@4.23.10(@codemirror/language@6.10.8)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)':
'@uiw/codemirror-theme-github@4.23.10(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)':
dependencies:
'@uiw/codemirror-themes': 4.23.10(@codemirror/language@6.10.8)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)
'@uiw/codemirror-themes': 4.23.10(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)
transitivePeerDependencies:
- '@codemirror/language'
- '@codemirror/state'
- '@codemirror/view'
'@uiw/codemirror-themes@4.23.10(@codemirror/language@6.10.8)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)':
'@uiw/codemirror-themes@4.23.10(@codemirror/language@6.11.0)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)':
dependencies:
'@codemirror/language': 6.10.8
'@codemirror/language': 6.11.0
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.4
'@uiw/react-codemirror@4.23.10(@babel/runtime@7.26.10)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.4)(codemirror@6.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
'@uiw/react-codemirror@4.23.10(@babel/runtime@7.26.10)(@codemirror/autocomplete@6.18.6)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/theme-one-dark@6.1.2)(@codemirror/view@6.36.4)(codemirror@6.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
dependencies:
'@babel/runtime': 7.26.10
'@codemirror/commands': 6.8.0
'@codemirror/state': 6.5.2
'@codemirror/theme-one-dark': 6.1.2
'@codemirror/view': 6.36.4
'@uiw/codemirror-extensions-basic-setup': 4.23.10(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.0)(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)
'@uiw/codemirror-extensions-basic-setup': 4.23.10(@codemirror/autocomplete@6.18.6)(@codemirror/commands@6.8.0)(@codemirror/language@6.11.0)(@codemirror/lint@6.8.4)(@codemirror/search@6.5.10)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)
codemirror: 6.0.1
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
@ -8441,10 +8480,10 @@ snapshots:
dependencies:
entities: 2.2.0
applesauce-accounts@0.0.0-next-20250312201602(typescript@5.8.2):
applesauce-accounts@0.0.0-next-20250313155042(typescript@5.8.2):
dependencies:
'@noble/hashes': 1.7.1
applesauce-signers: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-signers: 0.0.0-next-20250313155042(typescript@5.8.2)
nanoid: 5.1.3
nostr-tools: 2.10.4(typescript@5.8.2)
rxjs: 7.8.2
@ -8452,22 +8491,22 @@ snapshots:
- supports-color
- typescript
applesauce-actions@0.0.0-next-20250312201602(typescript@5.8.2):
applesauce-actions@0.0.0-next-20250313155042(typescript@5.8.2):
dependencies:
applesauce-core: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250313155042(typescript@5.8.2)
nostr-tools: 2.10.4(typescript@5.8.2)
transitivePeerDependencies:
- supports-color
- typescript
applesauce-content@0.0.0-next-20250312201602(typescript@5.8.2):
applesauce-content@0.0.0-next-20250313155042(typescript@5.8.2):
dependencies:
'@cashu/cashu-ts': 2.0.0-rc1
'@types/hast': 3.0.4
'@types/mdast': 4.0.4
'@types/unist': 3.0.3
applesauce-core: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
mdast-util-find-and-replace: 3.0.2
nostr-tools: 2.10.4(typescript@5.8.2)
remark: 15.0.1
@ -8478,7 +8517,7 @@ snapshots:
- supports-color
- typescript
applesauce-core@0.0.0-next-20250312201602(typescript@5.8.2):
applesauce-core@0.0.0-next-20250313155042(typescript@5.8.2):
dependencies:
'@noble/hashes': 1.7.1
'@scure/base': 1.2.4
@ -8493,19 +8532,19 @@ snapshots:
- supports-color
- typescript
applesauce-factory@0.0.0-next-20250312201602(typescript@5.8.2):
applesauce-factory@0.0.0-next-20250313155042(typescript@5.8.2):
dependencies:
applesauce-content: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-content: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
nanoid: 5.1.3
nostr-tools: 2.10.4(typescript@5.8.2)
transitivePeerDependencies:
- supports-color
- typescript
applesauce-loaders@0.0.0-next-20250312201602(typescript@5.8.2):
applesauce-loaders@0.0.0-next-20250313155042(typescript@5.8.2):
dependencies:
applesauce-core: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
nanoid: 5.1.3
nostr-tools: 2.10.4(typescript@5.8.2)
rx-nostr: 3.5.0
@ -8514,13 +8553,13 @@ snapshots:
- supports-color
- typescript
applesauce-react@0.0.0-next-20250312201602(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2):
applesauce-react@0.0.0-next-20250313155042(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2):
dependencies:
applesauce-accounts: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-actions: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-content: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-accounts: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-actions: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-content: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250313155042(typescript@5.8.2)
nostr-tools: 2.10.4(typescript@5.8.2)
observable-hooks: 4.2.4(react-dom@19.0.0(react@19.0.0))(react@18.3.1)(rxjs@7.8.2)
react: 18.3.1
@ -8530,9 +8569,9 @@ snapshots:
- supports-color
- typescript
applesauce-relay@0.0.0-next-20250312201602(typescript@5.8.2):
applesauce-relay@0.0.0-next-20250313155042(typescript@5.8.2):
dependencies:
applesauce-core: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
nanoid: 5.1.3
nostr-tools: 2.10.4(typescript@5.8.2)
rxjs: 7.8.2
@ -8540,12 +8579,12 @@ snapshots:
- supports-color
- typescript
applesauce-signers@0.0.0-next-20250312201602(typescript@5.8.2):
applesauce-signers@0.0.0-next-20250313155042(typescript@5.8.2):
dependencies:
'@noble/hashes': 1.7.1
'@noble/secp256k1': 1.7.1
'@scure/base': 1.2.4
applesauce-core: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
debug: 4.4.0
nanoid: 5.1.3
nostr-tools: 2.10.4(typescript@5.8.2)
@ -8553,13 +8592,14 @@ snapshots:
- supports-color
- typescript
applesauce-wallet@0.0.0-next-20250312201602(typescript@5.8.2):
applesauce-wallet@0.0.0-next-20250313155042(typescript@5.8.2):
dependencies:
'@cashu/cashu-ts': 2.0.0-rc1
'@gandlaf21/bc-ur': 1.1.12
'@noble/hashes': 1.7.1
applesauce-actions: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250312201602(typescript@5.8.2)
applesauce-actions: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-core: 0.0.0-next-20250313155042(typescript@5.8.2)
applesauce-factory: 0.0.0-next-20250313155042(typescript@5.8.2)
nostr-tools: 2.10.4(typescript@5.8.2)
rxjs: 7.8.2
transitivePeerDependencies:
@ -8697,6 +8737,8 @@ snapshots:
big-integer@1.6.52: {}
bignumber.js@9.1.2: {}
bindings@1.5.0:
dependencies:
file-uri-to-path: 1.0.0
@ -8782,8 +8824,8 @@ snapshots:
browserslist@4.24.4:
dependencies:
caniuse-lite: 1.0.30001703
electron-to-chromium: 1.5.115
caniuse-lite: 1.0.30001704
electron-to-chromium: 1.5.116
node-releases: 2.0.19
update-browserslist-db: 1.1.3(browserslist@4.24.4)
@ -8832,7 +8874,7 @@ snapshots:
camelcase@8.0.0: {}
caniuse-lite@1.0.30001703: {}
caniuse-lite@1.0.30001704: {}
canvas-color-tracker@1.3.1:
dependencies:
@ -8840,6 +8882,10 @@ snapshots:
canvas-confetti@1.9.3: {}
cbor-sync@1.0.4: {}
cborg@4.2.8: {}
ccount@2.0.1: {}
chalk@2.4.2:
@ -8893,7 +8939,7 @@ snapshots:
parse5: 7.2.1
parse5-htmlparser2-tree-adapter: 7.1.0
parse5-parser-stream: 7.1.2
undici: 6.21.1
undici: 6.21.2
whatwg-mimetype: 4.0.0
chevrotain@7.1.1:
@ -8930,9 +8976,9 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
codemirror-json-schema@0.7.9(@codemirror/language@6.10.8)(@codemirror/lint@6.8.4)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3):
codemirror-json-schema@0.7.9(@codemirror/language@6.11.0)(@codemirror/lint@6.8.4)(@codemirror/state@6.5.2)(@codemirror/view@6.36.4)(@lezer/common@1.2.3):
dependencies:
'@codemirror/language': 6.10.8
'@codemirror/language': 6.11.0
'@codemirror/lint': 6.8.4
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.4
@ -8955,7 +9001,7 @@ snapshots:
codemirror-json5@1.0.3:
dependencies:
'@codemirror/language': 6.10.8
'@codemirror/language': 6.11.0
'@codemirror/state': 6.5.2
'@codemirror/view': 6.36.4
'@lezer/common': 1.2.3
@ -8974,7 +9020,7 @@ snapshots:
dependencies:
'@codemirror/autocomplete': 6.18.6
'@codemirror/commands': 6.8.0
'@codemirror/language': 6.10.8
'@codemirror/language': 6.11.0
'@codemirror/lint': 6.8.4
'@codemirror/search': 6.5.10
'@codemirror/state': 6.5.2
@ -9480,7 +9526,7 @@ snapshots:
dependencies:
jake: 10.9.2
electron-to-chromium@1.5.115: {}
electron-to-chromium@1.5.116: {}
elementtree@0.1.7:
dependencies:
@ -10379,6 +10425,8 @@ snapshots:
argparse: 1.0.10
esprima: 4.0.1
jsbi@3.1.5: {}
jsesc@3.0.2: {}
jsesc@3.1.0: {}
@ -12639,7 +12687,7 @@ snapshots:
undici-types@6.20.0: {}
undici@6.21.1: {}
undici@6.21.2: {}
unicode-canonical-property-names-ecmascript@2.0.1: {}
@ -13110,10 +13158,13 @@ snapshots:
buffer-crc32: 0.2.13
fd-slicer: 1.1.0
yet-another-react-lightbox@3.21.7(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
yet-another-react-lightbox@3.21.8(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
dependencies:
react: 19.0.0
react-dom: 19.0.0(react@19.0.0)
optionalDependencies:
'@types/react': 18.3.18
'@types/react-dom': 18.3.5(@types/react@18.3.18)
yn@3.1.1: {}

View File

@ -0,0 +1,78 @@
import { Barcode, BarcodeScannerPlugin } from "@capacitor-mlkit/barcode-scanning";
import { from, Observable, switchMap } from "rxjs";
import { PluginListenerHandle } from "@capacitor/core";
import { logger } from "../../helpers/debug";
const log = logger.extend("NativeQrCodeScanner");
export async function getNativeScanner(): Promise<BarcodeScannerPlugin> {
const { BarcodeScanner, GoogleBarcodeScannerModuleInstallState } = await import("@capacitor-mlkit/barcode-scanning");
const { available } = await BarcodeScanner.isGoogleBarcodeScannerModuleAvailable();
if (!available) {
// install barcode scanner
await BarcodeScanner.installGoogleBarcodeScannerModule();
await new Promise<void>(async (res, rej) => {
const sub = await BarcodeScanner.addListener("googleBarcodeScannerModuleInstallProgress", (event) => {
log("Installing google barcode scanner", event.progress);
switch (event.state) {
case GoogleBarcodeScannerModuleInstallState.COMPLETED:
sub.remove();
res();
break;
case GoogleBarcodeScannerModuleInstallState.PENDING:
log("Pending download");
break;
case GoogleBarcodeScannerModuleInstallState.DOWNLOADING:
log("Downloading");
break;
case GoogleBarcodeScannerModuleInstallState.DOWNLOAD_PAUSED:
log("Download paused");
break;
case GoogleBarcodeScannerModuleInstallState.INSTALLING:
log("Installing");
break;
case GoogleBarcodeScannerModuleInstallState.FAILED:
sub.remove();
rej(new Error("Failed to install"));
break;
case GoogleBarcodeScannerModuleInstallState.CANCELED:
sub.remove();
rej(new Error("Canceled install"));
break;
}
});
});
}
const { supported } = await BarcodeScanner.isSupported();
if (!supported) throw new Error("Unsupported");
const { camera } = await BarcodeScanner.requestPermissions();
const granted = camera === "granted" || camera === "limited";
if (!granted) throw new Error("Camera access denied");
return BarcodeScanner;
}
export function getNativeScanStream(scanner: BarcodeScannerPlugin): Observable<Barcode> {
return new Observable<Barcode>((observer) => {
const sub = scanner.addListener("barcodesScanned", (event) => {
for (const barcode of event.barcodes) {
observer.next(barcode);
}
});
scanner.startScan();
let handle: PluginListenerHandle | undefined = undefined;
sub.then((e) => (handle = e));
return () => {
if (handle) handle.remove();
else sub.then((handle) => handle.remove);
scanner.stopScan();
};
});
}

View File

@ -1,103 +1,128 @@
import { Suspense, lazy, useCallback } from "react";
import { IconButton, IconButtonProps, useDisclosure, useToast } from "@chakra-ui/react";
import { Suspense, lazy, useCallback, useEffect, useState } from "react";
import { filter, map, merge, Observable, Subject } from "rxjs";
import {
Button,
IconButton,
IconButtonProps,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalOverlay,
Progress,
useDisclosure,
useToast,
} from "@chakra-ui/react";
import { receiveAnimated } from "applesauce-wallet/helpers/animated-qr";
import { type QrScannerModalProps } from "./qr-scanner-modal";
import { CAP_IS_NATIVE } from "../../env";
import { logger } from "../../helpers/debug";
import { QrCodeIcon } from "../icons";
import { getNativeScanner, getNativeScanStream } from "./native-scanner";
const QrScannerModal = lazy(() => import("./qr-scanner-modal"));
const BarcodeScannerComponent = lazy(() => import("react-qr-barcode-scanner"));
const log = logger.extend("QRCodeScanner");
async function scanWithNative() {
const { BarcodeScanner, BarcodeFormat, GoogleBarcodeScannerModuleInstallState } = await import(
"@capacitor-mlkit/barcode-scanning"
);
const { available } = await BarcodeScanner.isGoogleBarcodeScannerModuleAvailable();
if (!available) {
await BarcodeScanner.installGoogleBarcodeScannerModule();
await new Promise<void>(async (res, rej) => {
const sub = await BarcodeScanner.addListener("googleBarcodeScannerModuleInstallProgress", (event) => {
log("Installing google barcode scanner", event.progress);
switch (event.state) {
case GoogleBarcodeScannerModuleInstallState.COMPLETED:
sub.remove();
res();
break;
case GoogleBarcodeScannerModuleInstallState.PENDING:
log("Pending download");
break;
case GoogleBarcodeScannerModuleInstallState.DOWNLOADING:
log("Downloading");
break;
case GoogleBarcodeScannerModuleInstallState.DOWNLOAD_PAUSED:
log("Download paused");
break;
case GoogleBarcodeScannerModuleInstallState.INSTALLING:
log("Installing");
break;
case GoogleBarcodeScannerModuleInstallState.FAILED:
sub.remove();
rej(new Error("Failed to install"));
break;
case GoogleBarcodeScannerModuleInstallState.CANCELED:
sub.remove();
rej(new Error("Canceled install"));
break;
}
});
});
}
const { supported } = await BarcodeScanner.isSupported();
if (!supported) throw new Error("Unsupported");
const { camera } = await BarcodeScanner.requestPermissions();
const granted = camera === "granted" || camera === "limited";
if (!granted) throw new Error("Camera access denied");
try {
const { barcodes } = await BarcodeScanner.scan({
formats: [BarcodeFormat.QrCode],
});
const barcode = barcodes[0];
if (!barcode) return null;
return barcode.rawValue;
} catch (error) {
// user closed scanner
return null;
}
}
export default function QRCodeScannerButton({
onData,
onResult,
...props
}: { onData: QrScannerModalProps["onData"] } & Omit<IconButtonProps, "icon" | "aria-label">) {
}: { onResult: (data: string) => void } & Omit<IconButtonProps, "icon" | "aria-label">) {
const toast = useToast();
const modal = useDisclosure();
const [progress, setProgress] = useState<number>();
const [stream, setStream] = useState<Observable<string> | Subject<string>>();
const openModal = useCallback(() => {
setStream(new Subject());
modal.onOpen();
}, [modal.onOpen, setStream]);
const [stopStream, setStopStream] = useState(false);
const closeModal = useCallback(() => {
// Stop the QR Reader stream (fixes issue where the browser freezes when closing the modal) and then dismiss the modal one tick later
setStopStream(true);
setTimeout(() => modal.onClose(), 0);
}, [setStopStream, modal.onClose]);
const openNative = useCallback(async () => {
const scanner = await getNativeScanner();
const stream = getNativeScanStream(scanner);
setStream(stream.pipe(map((barcode) => barcode.rawValue)));
}, [setStream]);
const handleClick = useCallback(async () => {
if (CAP_IS_NATIVE) {
try {
const result = await scanWithNative();
if (result) onData(result);
await openNative();
} catch (error) {
log(error);
if (import.meta.env.DEV && error instanceof Error) toast({ status: "error", description: error.message });
modal.onOpen();
openModal();
}
} else modal.onOpen();
}, [modal.onOpen]);
} else openModal();
}, [openModal, openNative]);
// listen to the scanning stream
useEffect(() => {
if (stream) {
setProgress(undefined);
const normal = stream.pipe(filter((part) => !part.startsWith("ur:bytes")));
const animated = stream.pipe(receiveAnimated);
const sub = merge(normal, animated).subscribe({
next: (part) => {
if (typeof part === "number") {
// progress
setProgress(part);
} else if (part) {
// close the javascript scanner
closeModal();
// wait for steam to be stopped before returning data
setTimeout(() => {
onResult(part);
}, 0);
}
},
error: (err) => {
if (err instanceof Error) toast({ status: "error", description: err.message });
closeModal();
},
});
return () => sub.unsubscribe();
}
}, [stream, closeModal, onResult, setProgress]);
return (
<>
<IconButton onClick={handleClick} icon={<QrCodeIcon boxSize={6} />} aria-label="Qr Scanner" {...props} />
{modal.isOpen && (
<Suspense fallback={null}>
<QrScannerModal isOpen={modal.isOpen} onClose={modal.onClose} onData={onData} />
<Modal isOpen={modal.isOpen} onClose={closeModal}>
<ModalOverlay />
<ModalContent>
<ModalBody p="2">
<BarcodeScannerComponent
stopStream={stopStream}
onUpdate={(err, result) => {
if (stream instanceof Subject && result && result.getText()) stream.next(result.getText());
}}
onError={(err) => {
if (!(stream instanceof Subject)) return;
if (err instanceof Error) stream.error(err);
else stream.error(new Error(err));
}}
/>
</ModalBody>
<ModalFooter px="2" pb="2" pt="0" alignItems="center" gap="2">
{progress !== undefined && <Progress hasStripe value={progress * 100} w="full" />}
<Button onClick={closeModal}>Cancel</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Suspense>
)}
</>

View File

@ -1,37 +0,0 @@
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalOverlay, ModalProps } from "@chakra-ui/react";
import { useState } from "react";
import BarcodeScannerComponent from "react-qr-barcode-scanner";
export type QrScannerModalProps = { onData: (text: string) => void } & Pick<ModalProps, "isOpen" | "onClose">;
export default function QrScannerModal({ isOpen, onClose, onData }: QrScannerModalProps) {
const [stopStream, setStopStream] = useState(false);
const handleClose = () => {
// Stop the QR Reader stream (fixes issue where the browser freezes when closing the modal) and then dismiss the modal one tick later
setStopStream(true);
setTimeout(() => onClose(), 0);
};
return (
<Modal isOpen={isOpen} onClose={handleClose}>
<ModalOverlay />
<ModalContent>
<ModalBody p="2">
<BarcodeScannerComponent
stopStream={stopStream}
onUpdate={(err, result) => {
if (result && result.getText()) {
handleClose();
// wait for steam to be stopped before returning data
setTimeout(() => onData(result.getText()), 0);
}
}}
/>
</ModalBody>
<ModalFooter px="2" pb="2" pt="0">
<Button onClick={handleClose}>Cancel</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}

View File

@ -48,7 +48,7 @@ export default function WebRtcConnectView() {
<Flex as="form" gap="2" onSubmit={connect}>
<Input placeholder="webrtc+nostr:npub1..." {...register("uri")} autoComplete="off" />
<QRCodeScannerButton onData={(data) => setValue("uri", data)} />
<QRCodeScannerButton onResult={(data) => setValue("uri", data)} />
<Button colorScheme="primary" type="submit" isLoading={formState.isSubmitting}>
Connect
</Button>

View File

@ -76,7 +76,7 @@ export function SearchPage() {
<VerticalPageLayout>
<Flex as="form" gap="2" wrap="wrap" onSubmit={submit}>
<ButtonGroup>
<QRCodeScannerButton onData={handleSearchText} />
<QRCodeScannerButton onResult={handleSearchText} />
{!!navigator.clipboard?.readText && (
<IconButton
onClick={readClipboard}

View File

@ -37,7 +37,7 @@ function ConnectForm() {
<FormLabel>Bakery URL</FormLabel>
<Flex gap="2">
<Input type="text" {...register("url", { required: true })} isRequired placeholder="ws://localhost:2012" />
<QRCodeScannerButton onData={handleScanData} />
<QRCodeScannerButton onResult={handleScanData} />
</Flex>
</FormControl>

View File

@ -93,7 +93,7 @@ export default function BakerySetupView() {
<FormLabel>Owner</FormLabel>
<Flex gap="2">
<Input {...register("owner", { required: true })} isRequired placeholder="john@example.com" />
<QRCodeScannerButton onData={(url) => setValue("owner", url)} />
<QRCodeScannerButton onResult={(url) => setValue("owner", url)} />
</Flex>
<FormHelperText>Enter the NIP-05, npub, or hex pubkey of the owner of this node</FormHelperText>
</FormControl>

View File

@ -131,7 +131,7 @@ export default function LoginNostrAddressView() {
onChange={(e) => setAddress(e.target.value)}
autoComplete="off"
/>
<QRCodeScannerButton onData={(v) => setAddress(v)} />
<QRCodeScannerButton onResult={(v) => setAddress(v)} />
</Flex>
</FormControl>
{renderStatus()}

View File

@ -151,7 +151,7 @@ export default function LoginNostrConnectView() {
onChange={(e) => setConnection(e.target.value)}
autoComplete="off"
/>
<QRCodeScannerButton onData={(v) => setConnection(v)} />
<QRCodeScannerButton onResult={(v) => setConnection(v)} />
</Flex>
</FormControl>
)}

View File

@ -32,7 +32,7 @@ export default function LoginNpubView() {
<FormLabel>Enter user npub</FormLabel>
<Flex gap="2">
<Input type="text" placeholder="npub1" isRequired value={npub} onChange={(e) => setNpub(e.target.value)} />
<QRCodeScannerButton onData={(v) => setNpub(v)} />
<QRCodeScannerButton onResult={(v) => setNpub(v)} />
</Flex>
<FormHelperText>
Enter any npub you want.{" "}

View File

@ -127,7 +127,7 @@ export default function LoginNsecView() {
/>
</InputRightElement>
</InputGroup>
<QRCodeScannerButton onData={(v) => setValue("value", v)} />
<QRCodeScannerButton onResult={(v) => setValue("value", v)} />
</Flex>
</FormControl>

View File

@ -1,19 +1,31 @@
import { useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { Button, Card, CardBody, CardHeader, CardProps, Flex, Text } from "@chakra-ui/react";
import { useStoreQuery } from "applesauce-react/hooks";
import { WalletBalanceQuery } from "applesauce-wallet/queries";
import { ECashIcon } from "../../components/icons";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import { WALLET_KIND } from "applesauce-wallet/helpers";
import useEventUpdate from "../../hooks/use-event-update";
import QRCodeScannerButton from "../../components/qr-code/qr-code-scanner-button";
import RouterLink from "../../components/router-link";
import { ECashIcon } from "../../../components/icons";
import useReplaceableEvent from "../../../hooks/use-replaceable-event";
import useEventUpdate from "../../../hooks/use-event-update";
import QRCodeScannerButton from "../../../components/qr-code/qr-code-scanner-button";
import RouterLink from "../../../components/router-link";
export default function WalletBalanceCard({ pubkey, ...props }: { pubkey: string } & Omit<CardProps, "children">) {
const navigate = useNavigate();
const wallet = useReplaceableEvent({ kind: WALLET_KIND, pubkey });
useEventUpdate(wallet?.id);
const balance = useStoreQuery(WalletBalanceQuery, [pubkey]);
const handleScan = useCallback(
(data: string) => {
if (data.startsWith("cashuA") || data.startsWith("cashuB"))
navigate("/wallet/receive", { state: { input: data } });
},
[navigate],
);
return (
<Card {...props}>
<CardHeader gap="4" display="flex" justifyContent="center" alignItems="center" pt="10">
@ -24,10 +36,10 @@ export default function WalletBalanceCard({ pubkey, ...props }: { pubkey: string
</CardHeader>
<CardBody>
<Flex gap="2" w="full">
<Button isDisabled w="full" size="lg">
<Button as={RouterLink} w="full" size="lg" to="/wallet/send">
Send
</Button>
<QRCodeScannerButton onData={() => {}} isDisabled size="lg" />
<QRCodeScannerButton onResult={handleScan} size="lg" />
<Button as={RouterLink} w="full" size="lg" to="/wallet/receive">
Receive
</Button>

View File

@ -0,0 +1,25 @@
import { Button, ButtonProps } from "@chakra-ui/react";
import { useActionHub, useActiveAccount } from "applesauce-react/hooks";
import { UnlockWallet } from "applesauce-wallet/actions";
import useUserWallet from "../../../hooks/use-user-wallet";
import useAsyncErrorHandler from "../../../hooks/use-async-error-handler";
export default function WalletUnlockButton({ children, ...props }: Omit<ButtonProps, "onClick" | "isLoading">) {
const account = useActiveAccount()!;
const wallet = useUserWallet(account.pubkey);
const actions = useActionHub();
const unlock = useAsyncErrorHandler(async () => {
if (!wallet) throw new Error("Missing wallet");
if (wallet.locked === false) return;
await actions.run(UnlockWallet, { history: true, tokens: true });
}, [wallet, actions]);
return (
<Button onClick={unlock} {...props}>
{children || "Unlock"}
</Button>
);
}

View File

@ -1,22 +1,21 @@
import { Button, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
import { Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import { WalletBalanceQuery } from "applesauce-wallet/queries";
import { UnlockWallet } from "applesauce-wallet/actions";
import { WALLET_HISTORY_KIND, WALLET_TOKEN_KIND } from "applesauce-wallet/helpers";
import { useActiveAccount, useStoreQuery, useActionHub } from "applesauce-react/hooks";
import useAsyncErrorHandler from "../../hooks/use-async-error-handler";
import { useActiveAccount, useStoreQuery } from "applesauce-react/hooks";
import SimpleView from "../../components/layout/presets/simple-view";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import useUserMailboxes from "../../hooks/use-user-mailboxes";
import { useReadRelays } from "../../hooks/use-client-relays";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import WalletBalanceCard from "./balance-card";
import WalletBalanceCard from "./components/balance-card";
import WalletTokensTab from "./tabs/tokens";
import WalletHistoryTab from "./tabs/history";
import WalletMintsTab from "./tabs/mints";
import useUserWallet from "../../hooks/use-user-wallet";
import WalletUnlockButton from "./components/wallet-unlock-button";
export default function WalletHomeView() {
const account = useActiveAccount()!;
@ -33,34 +32,16 @@ export default function WalletHomeView() {
]);
const balance = useStoreQuery(WalletBalanceQuery, [account.pubkey]);
const actions = useActionHub();
const unlock = useAsyncErrorHandler(async () => {
if (!wallet) throw new Error("Missing wallet");
if (wallet.locked === false) return;
await actions.run(UnlockWallet, { history: true, tokens: true });
}, [wallet, actions]);
const callback = useTimelineCurserIntersectionCallback(loader);
return (
<IntersectionObserverProvider callback={callback}>
<SimpleView
title="Wallet"
actions={
wallet?.locked && (
<Button onClick={unlock} colorScheme="primary" ms="auto" size="sm">
Unlock
</Button>
)
}
actions={wallet?.locked && <WalletUnlockButton colorScheme="primary" ms="auto" size="sm" />}
>
<WalletBalanceCard pubkey={account.pubkey} w="full" maxW="2xl" mx="auto" />
{wallet?.locked && (
<Button onClick={unlock} colorScheme="primary" mx="auto" size="lg" w="sm">
Unlock
</Button>
)}
{wallet?.locked && <WalletUnlockButton colorScheme="primary" mx="auto" size="lg" w="sm" />}
<Tabs isFitted maxW="2xl" mx="auto" w="full" isLazy>
<TabList mb="1em">

View File

@ -0,0 +1,5 @@
import SimpleView from "../../../components/layout/presets/simple-view";
export default function WalletPayLightning() {
return <SimpleView title="Pay lightning" maxW="2xl" center></SimpleView>;
}

View File

@ -1,36 +1,41 @@
import { useState } from "react";
import { useActionHub } from "applesauce-react/hooks";
import { Button, Flex, Textarea, useToast } from "@chakra-ui/react";
import { Button, Flex, Spacer, Textarea, useToast } from "@chakra-ui/react";
import { getDecodedToken, Token } from "@cashu/cashu-ts";
import { ReceiveToken } from "applesauce-wallet/actions";
import { useNavigate } from "react-router-dom";
import { useLocation, useNavigate } from "react-router-dom";
import SimpleView from "../../components/layout/presets/simple-view";
import { getCashuWallet } from "../../services/cashu-mints";
import RouterLink from "../../components/router-link";
import QRCodeScannerButton from "../../components/qr-code/qr-code-scanner-button";
export default function WalletReceiveView() {
const location = useLocation();
const actions = useActionHub();
const navigate = useNavigate();
const toast = useToast();
const [input, setInput] = useState("");
const [input, setInput] = useState(location.state?.input ?? "");
const [loading, setLoading] = useState(false);
const receive = async () => {
setLoading(true);
try {
const decoded = getDecodedToken(input.trim());
const originalAmount = decoded.proofs.reduce((t, p) => t + p.amount, 0);
// swap tokens
const wallet = await getCashuWallet(decoded.mint);
const proofs = await wallet.receive(decoded);
const token: Token = { mint: decoded.mint, proofs };
// save new tokens
await actions.run(ReceiveToken, token);
const amount = token.proofs.reduce((t, p) => t + p.amount, 0);
const fee = originalAmount - amount;
// save new tokens
await actions.run(ReceiveToken, token, undefined, fee || undefined);
toast({ status: "success", description: `Received ${amount} sats` });
navigate("/wallet");
@ -48,7 +53,9 @@ export default function WalletReceiveView() {
<Button as={RouterLink} to="/wallet">
Back
</Button>
<Button colorScheme="primary" onClick={receive} isLoading={loading} ms="auto">
<Spacer />
<QRCodeScannerButton onResult={setInput} />
<Button colorScheme="primary" onClick={receive} isLoading={loading}>
Receive
</Button>
</Flex>

View File

@ -4,6 +4,9 @@ import { lazy } from "react";
const WalletHomeView = lazy(() => import("."));
const WalletReceiveView = lazy(() => import("./receive"));
const WalletSendView = lazy(() => import("./send/index"));
const WalletSendCashuView = lazy(() => import("./send/cashu"));
const WalletSendTokenView = lazy(() => import("./send/token"));
export default [
{
@ -15,4 +18,12 @@ export default [
),
},
{ path: "receive", Component: WalletReceiveView },
{
path: "send",
children: [
{ index: true, Component: WalletSendView },
{ path: "cashu", Component: WalletSendCashuView },
{ path: "token", Component: WalletSendTokenView },
],
},
] satisfies RouteObject[];

View File

@ -0,0 +1,101 @@
import { useState } from "react";
import {
Button,
Flex,
Input,
NumberDecrementStepper,
NumberIncrementStepper,
NumberInput,
NumberInputField,
NumberInputStepper,
Select,
} from "@chakra-ui/react";
import { WalletBalanceQuery, WalletQuery, WalletTokensQuery } from "applesauce-wallet/queries";
import { useActionHub, useActiveAccount, useStoreQuery } from "applesauce-react/hooks";
import { CompleteSpend } from "applesauce-wallet/actions";
import { useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import SimpleView from "../../../components/layout/presets/simple-view";
import CashuMintName from "../../../components/cashu/cashu-mint-name";
import WalletUnlockButton from "../components/wallet-unlock-button";
import RouterLink from "../../../components/router-link";
import { getEncodedToken, Proof, Token } from "@cashu/cashu-ts";
import { dumbTokenSelection, getTokenContent } from "applesauce-wallet/helpers";
import { getCashuWallet } from "../../../services/cashu-mints";
export default function WalletSendCashuView() {
const navigate = useNavigate();
const account = useActiveAccount()!;
const wallet = useStoreQuery(WalletQuery, [account.pubkey]);
const balance = useStoreQuery(WalletBalanceQuery, [account.pubkey]);
const tokens = useStoreQuery(WalletTokensQuery, [account.pubkey, false]);
const { register, getValues, watch, handleSubmit, formState } = useForm({
defaultValues: { amount: 0, mint: "" },
mode: "all",
});
watch("mint");
const actions = useActionHub();
const submit = handleSubmit(async (values) => {
if (!tokens) return;
const selected = dumbTokenSelection(tokens, values.amount, values.mint);
const wallet = await getCashuWallet(values.mint);
// get the proofs
const selectedProofs = selected
.map((t) => getTokenContent(t)!)
.reduce((arr, token) => [...arr, ...token.proofs], [] as Proof[]);
// swap
const send = await wallet.send(values.amount, selectedProofs);
// save the change
await actions.run(CompleteSpend, selected, { proofs: send.keep, mint: values.mint });
// redirect to the token view
const token: Token = {
mint: values.mint,
proofs: send.send,
};
navigate("/wallet/send/token", { state: { token: getEncodedToken(token) } });
});
return (
<SimpleView as="form" title="Send Cashu" maxW="xl" center onSubmit={submit}>
{wallet?.locked && <WalletUnlockButton colorScheme="primary" mx="auto" size="lg" w="sm" />}
<Select {...register("mint", { required: true })} isRequired>
{balance &&
Object.entries(balance).map(([mint, amount]) => (
<option key={mint} value={mint}>
<CashuMintName mint={mint} /> ({amount})
</option>
))}
</Select>
<Input
size="lg"
type="number"
min={1}
max={getValues("mint") && balance ? balance[getValues("mint")] : undefined}
{...register("amount", {
required: true,
min: 1,
max: getValues("mint") && balance ? balance[getValues("mint")] : undefined,
valueAsNumber: true,
})}
/>
<Flex direction="row-reverse">
<Button type="submit" colorScheme="primary" isLoading={formState.isSubmitting} isDisabled={!formState.isValid}>
Create
</Button>
<Button as={RouterLink} to="/wallet" me="auto">
Cancel
</Button>
</Flex>
</SimpleView>
);
}

View File

@ -0,0 +1,32 @@
import { Button, Card, LinkBox, Text } from "@chakra-ui/react";
import SimpleView from "../../../components/layout/presets/simple-view";
import { ECashIcon, LightningIcon } from "../../../components/icons";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
import RouterLink from "../../../components/router-link";
export default function WalletSendView() {
return (
<SimpleView title="Send" maxW="xl" center>
<Card as={LinkBox} p="4" gap="4" display="flex" flexDirection="row" alignItems="center">
<ECashIcon boxSize={10} />
<HoverLinkOverlay as={RouterLink} to="/wallet/send/cashu">
<Text fontWeight="bold" fontSize="xl">
ECash
</Text>
</HoverLinkOverlay>
</Card>
<Card as={LinkBox} p="4" gap="4" display="flex" flexDirection="row" alignItems="center">
<LightningIcon boxSize={10} color="yellow.400" />
<HoverLinkOverlay as={RouterLink} to="/wallet/pay/lightning">
<Text fontWeight="bold" fontSize="xl">
Lightning
</Text>
</HoverLinkOverlay>
</Card>
<Button as={RouterLink} to="/wallet" me="auto">
Back
</Button>
</SimpleView>
);
}

View File

@ -0,0 +1,116 @@
import { useEffect, useState } from "react";
import { Button, ButtonGroup, Flex, Spacer, useToast } from "@chakra-ui/react";
import { ANIMATED_QR_INTERVAL, sendAnimated } from "applesauce-wallet/helpers";
import { getDecodedToken, Proof, ProofState } from "@cashu/cashu-ts";
import { ReceiveToken } from "applesauce-wallet/actions";
import { useActionHub } from "applesauce-react/hooks";
import { Navigate, useLocation, useNavigate } from "react-router-dom";
import SimpleView from "../../../components/layout/presets/simple-view";
import RouterLink from "../../../components/router-link";
import { CopyIconButton } from "../../../components/copy-icon-button";
import QrCodeSvg from "../../../components/qr-code/qr-code-svg";
import { filter, from, Observable, switchMap, take } from "rxjs";
import { getCashuWallet } from "../../../services/cashu-mints";
export default function WalletSendTokenView() {
const toast = useToast();
const navigate = useNavigate();
const location = useLocation();
const token: string = location.state?.token;
if (!token) return <Navigate to="/wallet" />;
const actions = useActionHub();
const [speed, setSpeed] = useState(ANIMATED_QR_INTERVAL.MEDIUM);
const [data, setData] = useState<string>();
const shouldAnimate = token.length > 256;
// update qr code data
useEffect(() => {
if (shouldAnimate) {
const sub = sendAnimated(token, { interval: speed }).subscribe((part) => setData(part));
return () => sub.unsubscribe();
} else setData(token);
}, [token, speed, shouldAnimate]);
// subscribe to redeemed state
useEffect(() => {
const decoded = getDecodedToken(token);
const sub = from(getCashuWallet(decoded.mint))
.pipe(
switchMap((wallet) => {
// subscribe to proof states
return new Observable<ProofState & { proof: Proof }>((observer) => {
// TODO: cancel subscription
wallet.onProofStateUpdates(
decoded.proofs,
(state) => observer.next(state),
(err) => observer.error(err),
);
});
}),
// look for spent proofs
filter((state) => state.state === "SPENT"),
// only wait for one to be spent
take(1),
)
.subscribe(() => {
toast({ status: "success", description: "Tokens sent" });
navigate("/wallet");
});
return () => sub.unsubscribe();
}, [token]);
const [canceling, setCanceling] = useState(false);
const cancel = async () => {
setCanceling(true);
try {
await actions.run(ReceiveToken, getDecodedToken(token));
navigate("/wallet");
} catch (error) {}
setCanceling(false);
};
return (
<SimpleView title="Cashu Token" maxW="xl" center>
{data && <QrCodeSvg content={data} w="full" aspectRatio={1} />}
{shouldAnimate && (
<ButtonGroup size="xs" mx="auto">
<Button
colorScheme={speed === ANIMATED_QR_INTERVAL.SLOW ? "primary" : undefined}
onClick={() => setSpeed(ANIMATED_QR_INTERVAL.SLOW)}
>
Slow
</Button>
<Button
colorScheme={speed === ANIMATED_QR_INTERVAL.MEDIUM ? "primary" : undefined}
onClick={() => setSpeed(ANIMATED_QR_INTERVAL.MEDIUM)}
>
Normal
</Button>
<Button
colorScheme={speed === ANIMATED_QR_INTERVAL.FAST ? "primary" : undefined}
onClick={() => setSpeed(ANIMATED_QR_INTERVAL.FAST)}
>
Fast
</Button>
</ButtonGroup>
)}
<Flex gap="2">
<CopyIconButton value={token} aria-label="Copy token" />
<Spacer />
<Button onClick={cancel} isLoading={canceling}>
Cancel
</Button>
<Button as={RouterLink} to="/wallet" colorScheme="primary">
Done
</Button>
</Flex>
</SimpleView>
);
}

View File

@ -34,12 +34,15 @@ import useSingleEvents from "../../../hooks/use-single-events";
import UserAvatarLink from "../../../components/user/user-avatar-link";
import CashuMintFavicon from "../../../components/cashu/cashu-mint-favicon";
import CashuMintName from "../../../components/cashu/cashu-mint-name";
import { usePublishEvent } from "../../../providers/global/publish-provider";
import factory from "../../../services/event-factory";
function HistoryEntry({ entry }: { entry: NostrEvent }) {
const account = useActiveAccount()!;
const eventStore = useEventStore();
const locked = isHistoryContentLocked(entry);
const details = !locked ? getHistoryContent(entry) : undefined;
const publish = usePublishEvent();
useEventUpdate(entry.id);
const ref = useEventIntersectionRef(entry);
@ -95,7 +98,7 @@ function HistoryEntry({ entry }: { entry: NostrEvent }) {
<Text mr="2">Redeemed zaps from:</Text>
<AvatarGroup size="sm">
{redeemed.map((event) => (
<UserAvatarLink pubkey={event.pubkey} />
<UserAvatarLink key={event.id} pubkey={event.pubkey} />
))}
</AvatarGroup>
</>
@ -114,6 +117,7 @@ function HistoryEntry({ entry }: { entry: NostrEvent }) {
export default function WalletHistoryTab() {
const account = useActiveAccount()!;
const eventStore = useEventStore();
const publish = usePublishEvent();
const history = useStoreQuery(WalletHistoryQuery, [account.pubkey]) ?? [];
const locked = useStoreQuery(WalletHistoryQuery, [account.pubkey, true]) ?? [];
@ -126,6 +130,12 @@ export default function WalletHistoryTab() {
}
}, [locked, account, eventStore]);
const clear = useAsyncErrorHandler(async () => {
if (confirm("Are you sure you want to clear history?") !== true) return;
const draft = await factory.delete(history);
await publish("Clear history", draft);
}, [factory, publish, history]);
return (
<Flex direction="column" gap="2" w="full">
{locked && locked.length > 0 && (
@ -134,6 +144,11 @@ export default function WalletHistoryTab() {
</Button>
)}
{history?.map((entry) => <HistoryEntry key={entry.id} entry={entry} />)}
{history.length > 0 && (
<Button variant="link" onClick={clear} ms="auto">
Clear history
</Button>
)}
</Flex>
);
}