diff --git a/package.json b/package.json index b676471be..e43c7e241 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a751b35d..7660bc042 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/src/components/qr-code/native-scanner.ts b/src/components/qr-code/native-scanner.ts new file mode 100644 index 000000000..33733804c --- /dev/null +++ b/src/components/qr-code/native-scanner.ts @@ -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 { + 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(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 { + return new Observable((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(); + }; + }); +} diff --git a/src/components/qr-code/qr-code-scanner-button.tsx b/src/components/qr-code/qr-code-scanner-button.tsx index c5d16ea42..fc8be9c22 100644 --- a/src/components/qr-code/qr-code-scanner-button.tsx +++ b/src/components/qr-code/qr-code-scanner-button.tsx @@ -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(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) { +}: { onResult: (data: string) => void } & Omit) { const toast = useToast(); const modal = useDisclosure(); + const [progress, setProgress] = useState(); + const [stream, setStream] = useState | Subject>(); + + 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 ( <> } aria-label="Qr Scanner" {...props} /> {modal.isOpen && ( - + + + + + { + 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)); + }} + /> + + + + {progress !== undefined && } + + + + )} diff --git a/src/components/qr-code/qr-scanner-modal.tsx b/src/components/qr-code/qr-scanner-modal.tsx deleted file mode 100644 index 8207c5baf..000000000 --- a/src/components/qr-code/qr-scanner-modal.tsx +++ /dev/null @@ -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; -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 ( - - - - - { - if (result && result.getText()) { - handleClose(); - // wait for steam to be stopped before returning data - setTimeout(() => onData(result.getText()), 0); - } - }} - /> - - - - - - - - ); -} diff --git a/src/views/relays/webrtc/connect.tsx b/src/views/relays/webrtc/connect.tsx index 55ceba978..91cc32a38 100644 --- a/src/views/relays/webrtc/connect.tsx +++ b/src/views/relays/webrtc/connect.tsx @@ -48,7 +48,7 @@ export default function WebRtcConnectView() { - setValue("uri", data)} /> + setValue("uri", data)} /> diff --git a/src/views/search/index.tsx b/src/views/search/index.tsx index c525e5a8c..6f4f1af32 100644 --- a/src/views/search/index.tsx +++ b/src/views/search/index.tsx @@ -76,7 +76,7 @@ export function SearchPage() { - + {!!navigator.clipboard?.readText && ( Bakery URL - + diff --git a/src/views/settings/bakery/setup/index.tsx b/src/views/settings/bakery/setup/index.tsx index eb12fd6b3..aea88d163 100644 --- a/src/views/settings/bakery/setup/index.tsx +++ b/src/views/settings/bakery/setup/index.tsx @@ -93,7 +93,7 @@ export default function BakerySetupView() { Owner - setValue("owner", url)} /> + setValue("owner", url)} /> Enter the NIP-05, npub, or hex pubkey of the owner of this node diff --git a/src/views/signin/address/index.tsx b/src/views/signin/address/index.tsx index 56806f87a..e44e6c555 100644 --- a/src/views/signin/address/index.tsx +++ b/src/views/signin/address/index.tsx @@ -131,7 +131,7 @@ export default function LoginNostrAddressView() { onChange={(e) => setAddress(e.target.value)} autoComplete="off" /> - setAddress(v)} /> + setAddress(v)} /> {renderStatus()} diff --git a/src/views/signin/nostr-connect.tsx b/src/views/signin/nostr-connect.tsx index bc2614438..aedbf4cd9 100644 --- a/src/views/signin/nostr-connect.tsx +++ b/src/views/signin/nostr-connect.tsx @@ -151,7 +151,7 @@ export default function LoginNostrConnectView() { onChange={(e) => setConnection(e.target.value)} autoComplete="off" /> - setConnection(v)} /> + setConnection(v)} /> )} diff --git a/src/views/signin/npub.tsx b/src/views/signin/npub.tsx index 0c32dc3f4..4fb6a21b4 100644 --- a/src/views/signin/npub.tsx +++ b/src/views/signin/npub.tsx @@ -32,7 +32,7 @@ export default function LoginNpubView() { Enter user npub setNpub(e.target.value)} /> - setNpub(v)} /> + setNpub(v)} /> Enter any npub you want.{" "} diff --git a/src/views/signin/nsec.tsx b/src/views/signin/nsec.tsx index 045335b11..503492d0b 100644 --- a/src/views/signin/nsec.tsx +++ b/src/views/signin/nsec.tsx @@ -127,7 +127,7 @@ export default function LoginNsecView() { /> - setValue("value", v)} /> + setValue("value", v)} /> diff --git a/src/views/wallet/balance-card.tsx b/src/views/wallet/components/balance-card.tsx similarity index 58% rename from src/views/wallet/balance-card.tsx rename to src/views/wallet/components/balance-card.tsx index bf83d0e00..9ce17e82e 100644 --- a/src/views/wallet/balance-card.tsx +++ b/src/views/wallet/components/balance-card.tsx @@ -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) { + 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 ( @@ -24,10 +36,10 @@ export default function WalletBalanceCard({ pubkey, ...props }: { pubkey: string - - {}} isDisabled size="lg" /> + diff --git a/src/views/wallet/components/wallet-unlock-button.tsx b/src/views/wallet/components/wallet-unlock-button.tsx new file mode 100644 index 000000000..169b7fb88 --- /dev/null +++ b/src/views/wallet/components/wallet-unlock-button.tsx @@ -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) { + 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 ( + + ); +} diff --git a/src/views/wallet/index.tsx b/src/views/wallet/index.tsx index 676fd9ca9..816976fc6 100644 --- a/src/views/wallet/index.tsx +++ b/src/views/wallet/index.tsx @@ -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 ( - Unlock - - ) - } + actions={wallet?.locked && } > - {wallet?.locked && ( - - )} + {wallet?.locked && } diff --git a/src/views/wallet/pay/lightning.tsx b/src/views/wallet/pay/lightning.tsx new file mode 100644 index 000000000..58ef3a37a --- /dev/null +++ b/src/views/wallet/pay/lightning.tsx @@ -0,0 +1,5 @@ +import SimpleView from "../../../components/layout/presets/simple-view"; + +export default function WalletPayLightning() { + return ; +} diff --git a/src/views/wallet/receive.tsx b/src/views/wallet/receive.tsx index 39e49a5b6..31d6350c0 100644 --- a/src/views/wallet/receive.tsx +++ b/src/views/wallet/receive.tsx @@ -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() { - diff --git a/src/views/wallet/routes.tsx b/src/views/wallet/routes.tsx index 2c120d42c..e2fd9530b 100644 --- a/src/views/wallet/routes.tsx +++ b/src/views/wallet/routes.tsx @@ -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[]; diff --git a/src/views/wallet/send/cashu.tsx b/src/views/wallet/send/cashu.tsx new file mode 100644 index 000000000..85e45869f --- /dev/null +++ b/src/views/wallet/send/cashu.tsx @@ -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 ( + + {wallet?.locked && } + + + + + + + + + + ); +} diff --git a/src/views/wallet/send/index.tsx b/src/views/wallet/send/index.tsx new file mode 100644 index 000000000..50ff50590 --- /dev/null +++ b/src/views/wallet/send/index.tsx @@ -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 ( + + + + + + ECash + + + + + + + + Lightning + + + + + + ); +} diff --git a/src/views/wallet/send/token.tsx b/src/views/wallet/send/token.tsx new file mode 100644 index 000000000..2e0f6796c --- /dev/null +++ b/src/views/wallet/send/token.tsx @@ -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 ; + + const actions = useActionHub(); + + const [speed, setSpeed] = useState(ANIMATED_QR_INTERVAL.MEDIUM); + const [data, setData] = useState(); + + 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((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 ( + + {data && } + + {shouldAnimate && ( + + + + + + )} + + + + + + + + + ); +} diff --git a/src/views/wallet/tabs/history.tsx b/src/views/wallet/tabs/history.tsx index 67d5ac792..eba3889d0 100644 --- a/src/views/wallet/tabs/history.tsx +++ b/src/views/wallet/tabs/history.tsx @@ -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 }) { Redeemed zaps from: {redeemed.map((event) => ( - + ))} @@ -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 ( {locked && locked.length > 0 && ( @@ -134,6 +144,11 @@ export default function WalletHistoryTab() { )} {history?.map((entry) => )} + {history.length > 0 && ( + + )} ); }