Merge pull request 'feat: add the feature to zap admin/app account' (#4) from zap into master
Reviewed-on: #4
This commit is contained in:
commit
0591dfecd6
318
package-lock.json
generated
318
package-lock.json
generated
@ -8,8 +8,12 @@
|
|||||||
"name": "degmods.com",
|
"name": "degmods.com",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nostr-dev-kit/ndk": "2.8.2",
|
"@getalby/lightning-tools": "5.0.3",
|
||||||
|
"@nostr-dev-kit/ndk": "2.10.0",
|
||||||
"@reduxjs/toolkit": "2.2.6",
|
"@reduxjs/toolkit": "2.2.6",
|
||||||
|
"axios": "1.7.3",
|
||||||
|
"bech32": "2.0.0",
|
||||||
|
"buffer": "6.0.3",
|
||||||
"date-fns": "3.6.0",
|
"date-fns": "3.6.0",
|
||||||
"dompurify": "3.1.6",
|
"dompurify": "3.1.6",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
@ -17,14 +21,17 @@
|
|||||||
"nostr-login": "1.5.2",
|
"nostr-login": "1.5.2",
|
||||||
"nostr-tools": "2.7.1",
|
"nostr-tools": "2.7.1",
|
||||||
"papaparse": "5.4.1",
|
"papaparse": "5.4.1",
|
||||||
|
"qrcode.react": "3.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-countdown": "2.3.5",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-quill": "2.0.0",
|
"react-quill": "2.0.0",
|
||||||
"react-redux": "9.1.2",
|
"react-redux": "9.1.2",
|
||||||
"react-router-dom": "^6.24.1",
|
"react-router-dom": "^6.24.1",
|
||||||
"react-toastify": "10.0.5",
|
"react-toastify": "10.0.5",
|
||||||
"react-window": "1.8.10",
|
"react-window": "1.8.10",
|
||||||
"uuid": "10.0.0"
|
"uuid": "10.0.0",
|
||||||
|
"webln": "0.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dompurify": "3.0.5",
|
"@types/dompurify": "3.0.5",
|
||||||
@ -903,6 +910,18 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@getalby/lightning-tools": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@getalby/lightning-tools/-/lightning-tools-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-QG3/SBI5n2py5IgsjP3K+c8eq55eiI3PQB12yo9Pot0b5hcN7TNNoTKn0fgLJjO1iEVCUkF513kDOpjjXwK0hQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "lightning",
|
||||||
|
"url": "lightning:hello@getalby.com"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.11.14",
|
"version": "0.11.14",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||||
@ -1082,9 +1101,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@nostr-dev-kit/ndk": {
|
"node_modules/@nostr-dev-kit/ndk": {
|
||||||
"version": "2.8.2",
|
"version": "2.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.10.0.tgz",
|
||||||
"integrity": "sha512-+dOEyuYvO5/MoI5iTi8C5HifmvfeEvpybNesluVYyu+o+koFdfc+WSYH050V8+9KlOgx8nOZAaqXnHz0KY1gBA==",
|
"integrity": "sha512-TqCAAo6ylORraAXrzRkCGFN2xTMiFbdER8Y8CtUT0HwOpFG/Wn+PBNeDeDmqkl/6LaPdeyXmVwCWj2KcUjIwYA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/curves": "^1.4.0",
|
"@noble/curves": "^1.4.0",
|
||||||
"@noble/hashes": "^1.3.1",
|
"@noble/hashes": "^1.3.1",
|
||||||
@ -1093,73 +1112,14 @@
|
|||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"light-bolt11-decoder": "^3.0.0",
|
"light-bolt11-decoder": "^3.0.0",
|
||||||
"node-fetch": "^3.3.1",
|
"node-fetch": "^3.3.1",
|
||||||
"nostr-tools": "^1.15.0",
|
"nostr-tools": "^2.7.1",
|
||||||
"tseep": "^1.1.1",
|
"tseep": "^1.1.1",
|
||||||
"typescript-lru-cache": "^2.0.0",
|
"typescript-lru-cache": "^2.0.0",
|
||||||
"utf8-buffer": "^1.0.0",
|
"utf8-buffer": "^1.0.0",
|
||||||
"websocket-polyfill": "^0.0.3"
|
"websocket-polyfill": "^0.0.3"
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"node_modules/@nostr-dev-kit/ndk/node_modules/@noble/ciphers": {
|
|
||||||
"version": "0.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-0.2.0.tgz",
|
|
||||||
"integrity": "sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==",
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nostr-dev-kit/ndk/node_modules/@noble/hashes": {
|
|
||||||
"version": "1.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.1.tgz",
|
|
||||||
"integrity": "sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nostr-dev-kit/ndk/node_modules/@scure/base": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools": {
|
|
||||||
"version": "1.17.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/nostr-tools/-/nostr-tools-1.17.0.tgz",
|
|
||||||
"integrity": "sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==",
|
|
||||||
"dependencies": {
|
|
||||||
"@noble/ciphers": "0.2.0",
|
|
||||||
"@noble/curves": "1.1.0",
|
|
||||||
"@noble/hashes": "1.3.1",
|
|
||||||
"@scure/base": "1.1.1",
|
|
||||||
"@scure/bip32": "1.3.1",
|
|
||||||
"@scure/bip39": "1.2.1"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"typescript": ">=5.0.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"typescript": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@nostr-dev-kit/ndk/node_modules/nostr-tools/node_modules/@noble/curves": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==",
|
|
||||||
"dependencies": {
|
|
||||||
"@noble/hashes": "1.3.1"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@reduxjs/toolkit": {
|
"node_modules/@reduxjs/toolkit": {
|
||||||
@ -1543,6 +1503,14 @@
|
|||||||
"@babel/types": "^7.20.7"
|
"@babel/types": "^7.20.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/chrome": {
|
||||||
|
"version": "0.0.74",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chrome/-/chrome-0.0.74.tgz",
|
||||||
|
"integrity": "sha512-hzosS5CkQcIKCgxcsV2AzbJ36KNxG/Db2YEN/erEu7Boprg+KpMDLBQqKFmSo+JkQMGqRcicUyqCowJpuT+C6A==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/filesystem": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/dompurify": {
|
"node_modules/@types/dompurify": {
|
||||||
"version": "3.0.5",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz",
|
||||||
@ -1564,6 +1532,19 @@
|
|||||||
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
"integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/filesystem": {
|
||||||
|
"version": "0.0.36",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/filesystem/-/filesystem-0.0.36.tgz",
|
||||||
|
"integrity": "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/filewriter": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/filewriter": {
|
||||||
|
"version": "0.0.33",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/filewriter/-/filewriter-0.0.33.tgz",
|
||||||
|
"integrity": "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g=="
|
||||||
|
},
|
||||||
"node_modules/@types/lodash": {
|
"node_modules/@types/lodash": {
|
||||||
"version": "4.17.7",
|
"version": "4.17.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
|
||||||
@ -1961,12 +1942,51 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz",
|
||||||
|
"integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/bech32": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/bech32/-/bech32-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-LcknSilhIGatDAsY1ak2I8VtGaHNhgMSYVxFrGLXv+xLHytaKZKcaUJJUE7qmBr7h33o5YQwP55pMI0xmkpJwg=="
|
||||||
|
},
|
||||||
"node_modules/binary-extensions": {
|
"node_modules/binary-extensions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||||
@ -2032,6 +2052,29 @@
|
|||||||
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/buffer": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "^1.3.1",
|
||||||
|
"ieee754": "^1.2.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/bufferutil": {
|
"node_modules/bufferutil": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
|
||||||
@ -2181,6 +2224,17 @@
|
|||||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@ -2321,6 +2375,14 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
@ -2947,6 +3009,38 @@
|
|||||||
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
"integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz",
|
||||||
|
"integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/formdata-polyfill": {
|
"node_modules/formdata-polyfill": {
|
||||||
"version": "4.0.10",
|
"version": "4.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||||
@ -3189,6 +3283,25 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ieee754": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/ignore": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.1",
|
"version": "5.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
|
||||||
@ -3540,6 +3653,25 @@
|
|||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/minimatch": {
|
"node_modules/minimatch": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||||
@ -3795,6 +3927,14 @@
|
|||||||
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/object-assign": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/object-is": {
|
"node_modules/object-is": {
|
||||||
"version": "1.1.6",
|
"version": "1.1.6",
|
||||||
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
|
||||||
@ -4006,6 +4146,21 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prop-types": {
|
||||||
|
"version": "15.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"react-is": "^16.13.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
@ -4015,6 +4170,14 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode.react": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/queue-microtask": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
@ -4072,6 +4235,18 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-countdown": {
|
||||||
|
"version": "2.3.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-countdown/-/react-countdown-2.3.5.tgz",
|
||||||
|
"integrity": "sha512-K26ENYEesMfPxhRRtm1r+Pf70SErrvW3g4CArLi/x6MPFjgfDFYePT4UghEj8p2nI0cqVV7/JjDgjyr//U60Og==",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "^15.7.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 15",
|
||||||
|
"react-dom": ">= 15"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
@ -4084,6 +4259,11 @@
|
|||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
|
},
|
||||||
"node_modules/react-quill": {
|
"node_modules/react-quill": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-quill/-/react-quill-2.0.0.tgz",
|
||||||
@ -4786,6 +4966,14 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/webln": {
|
||||||
|
"version": "0.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/webln/-/webln-0.3.2.tgz",
|
||||||
|
"integrity": "sha512-YYT83aOCLup2AmqvJdKtdeBTaZpjC6/JDMe8o6x1kbTYWwiwrtWHyO//PAsPixF3jwFsAkj5DmiceB6w/QSe7Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/chrome": "^0.0.74"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/websocket": {
|
"node_modules/websocket": {
|
||||||
"version": "1.0.35",
|
"version": "1.0.35",
|
||||||
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz",
|
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.35.tgz",
|
||||||
|
11
package.json
11
package.json
@ -10,8 +10,12 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nostr-dev-kit/ndk": "2.8.2",
|
"@getalby/lightning-tools": "5.0.3",
|
||||||
|
"@nostr-dev-kit/ndk": "2.10.0",
|
||||||
"@reduxjs/toolkit": "2.2.6",
|
"@reduxjs/toolkit": "2.2.6",
|
||||||
|
"axios": "1.7.3",
|
||||||
|
"bech32": "2.0.0",
|
||||||
|
"buffer": "6.0.3",
|
||||||
"date-fns": "3.6.0",
|
"date-fns": "3.6.0",
|
||||||
"dompurify": "3.1.6",
|
"dompurify": "3.1.6",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
@ -19,14 +23,17 @@
|
|||||||
"nostr-login": "1.5.2",
|
"nostr-login": "1.5.2",
|
||||||
"nostr-tools": "2.7.1",
|
"nostr-tools": "2.7.1",
|
||||||
"papaparse": "5.4.1",
|
"papaparse": "5.4.1",
|
||||||
|
"qrcode.react": "3.1.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
"react-countdown": "2.3.5",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-quill": "2.0.0",
|
"react-quill": "2.0.0",
|
||||||
"react-redux": "9.1.2",
|
"react-redux": "9.1.2",
|
||||||
"react-router-dom": "^6.24.1",
|
"react-router-dom": "^6.24.1",
|
||||||
"react-toastify": "10.0.5",
|
"react-toastify": "10.0.5",
|
||||||
"react-window": "1.8.10",
|
"react-window": "1.8.10",
|
||||||
"uuid": "10.0.0"
|
"uuid": "10.0.0",
|
||||||
|
"webln": "0.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/dompurify": "3.0.5",
|
"@types/dompurify": "3.0.5",
|
||||||
|
@ -1 +1,2 @@
|
|||||||
export const T_TAG_VALUE = 'GameMod'
|
export const T_TAG_VALUE = 'GameMod'
|
||||||
|
export const MOD_FILTER_LIMIT = 20
|
||||||
|
@ -1,2 +1,3 @@
|
|||||||
export * from './metadata'
|
export * from './metadata'
|
||||||
export * from './relay'
|
export * from './relay'
|
||||||
|
export * from './zap'
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import NDK, { NDKList, NDKRelayList, NDKUser } from '@nostr-dev-kit/ndk'
|
import NDK, { getRelayListForUser, NDKList, NDKUser } from '@nostr-dev-kit/ndk'
|
||||||
import { UserProfile } from '../types/user'
|
|
||||||
import { hexToNpub, log, LogType, npubToHex } from '../utils'
|
|
||||||
import { kinds } from 'nostr-tools'
|
import { kinds } from 'nostr-tools'
|
||||||
import { MuteLists } from '../types'
|
import { MuteLists } from '../types'
|
||||||
|
import { UserProfile } from '../types/user'
|
||||||
|
import { hexToNpub, log, LogType, npubToHex } from '../utils'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton class to manage metadata operations using NDK.
|
* Singleton class to manage metadata operations using NDK.
|
||||||
@ -10,6 +10,7 @@ import { MuteLists } from '../types'
|
|||||||
export class MetadataController {
|
export class MetadataController {
|
||||||
private static instance: MetadataController
|
private static instance: MetadataController
|
||||||
private ndk: NDK
|
private ndk: NDK
|
||||||
|
private usersMetadata = new Map<string, UserProfile>()
|
||||||
public adminNpubs: string[]
|
public adminNpubs: string[]
|
||||||
public adminRelays = new Set<string>()
|
public adminRelays = new Set<string>()
|
||||||
|
|
||||||
@ -18,7 +19,8 @@ export class MetadataController {
|
|||||||
explicitRelayUrls: [
|
explicitRelayUrls: [
|
||||||
'wss://user.kindpag.es',
|
'wss://user.kindpag.es',
|
||||||
'wss://purplepag.es',
|
'wss://purplepag.es',
|
||||||
'wss://relay.damus.io/'
|
'wss://relay.damus.io/',
|
||||||
|
import.meta.env.VITE_APP_RELAY
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
this.ndk.connect()
|
this.ndk.connect()
|
||||||
@ -31,7 +33,7 @@ export class MetadataController {
|
|||||||
const hexKey = npubToHex(npub)
|
const hexKey = npubToHex(npub)
|
||||||
if (!hexKey) return null
|
if (!hexKey) return null
|
||||||
|
|
||||||
return NDKRelayList.forUser(hexKey, this.ndk)
|
return getRelayListForUser(hexKey, this.ndk)
|
||||||
.then((ndkRelayList) => {
|
.then((ndkRelayList) => {
|
||||||
if (ndkRelayList) {
|
if (ndkRelayList) {
|
||||||
ndkRelayList.writeRelayUrls.forEach((url) =>
|
ndkRelayList.writeRelayUrls.forEach((url) =>
|
||||||
@ -74,14 +76,34 @@ export class MetadataController {
|
|||||||
*/
|
*/
|
||||||
public findMetadata = async (pubkey: string): Promise<UserProfile> => {
|
public findMetadata = async (pubkey: string): Promise<UserProfile> => {
|
||||||
const npub = hexToNpub(pubkey)
|
const npub = hexToNpub(pubkey)
|
||||||
|
|
||||||
|
const cachedMetadata = this.usersMetadata.get(npub)
|
||||||
|
if (cachedMetadata) {
|
||||||
|
return cachedMetadata
|
||||||
|
}
|
||||||
|
|
||||||
const user = new NDKUser({ npub })
|
const user = new NDKUser({ npub })
|
||||||
user.ndk = this.ndk
|
user.ndk = this.ndk
|
||||||
|
|
||||||
return await user.fetchProfile()
|
const userProfile = await user.fetchProfile()
|
||||||
|
if (userProfile) {
|
||||||
|
this.usersMetadata.set(npub, userProfile)
|
||||||
|
}
|
||||||
|
|
||||||
|
return userProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds metadata for admin user.
|
||||||
|
*
|
||||||
|
* @returns A promise that resolves to the metadata event.
|
||||||
|
*/
|
||||||
|
public findAdminMetadata = async (): Promise<UserProfile> => {
|
||||||
|
return this.findMetadata(this.adminNpubs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
public findWriteRelays = async (hexKey: string) => {
|
public findWriteRelays = async (hexKey: string) => {
|
||||||
const ndkRelayList = await NDKRelayList.forUser(hexKey, this.ndk)
|
const ndkRelayList = await getRelayListForUser(hexKey, this.ndk)
|
||||||
|
|
||||||
if (!ndkRelayList) {
|
if (!ndkRelayList) {
|
||||||
throw new Error(`Couldn't found user's relay list`)
|
throw new Error(`Couldn't found user's relay list`)
|
||||||
|
@ -12,7 +12,19 @@ export class RelayController {
|
|||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
private connectRelay = async (relayUrl: string) => {
|
/**
|
||||||
|
* Provides the singleton instance of RelayController.
|
||||||
|
*
|
||||||
|
* @returns The singleton instance of RelayController.
|
||||||
|
*/
|
||||||
|
public static getInstance(): RelayController {
|
||||||
|
if (!RelayController.instance) {
|
||||||
|
RelayController.instance = new RelayController()
|
||||||
|
}
|
||||||
|
return RelayController.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
public connectRelay = async (relayUrl: string) => {
|
||||||
const relay = this.connectedRelays.find(
|
const relay = this.connectedRelays.find(
|
||||||
(relay) =>
|
(relay) =>
|
||||||
normalizeWebSocketURL(relay.url) === normalizeWebSocketURL(relayUrl)
|
normalizeWebSocketURL(relay.url) === normalizeWebSocketURL(relayUrl)
|
||||||
@ -39,18 +51,6 @@ export class RelayController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides the singleton instance of RelayController.
|
|
||||||
*
|
|
||||||
* @returns The singleton instance of RelayController.
|
|
||||||
*/
|
|
||||||
public static getInstance(): RelayController {
|
|
||||||
if (!RelayController.instance) {
|
|
||||||
RelayController.instance = new RelayController()
|
|
||||||
}
|
|
||||||
return RelayController.instance
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publishes an event to multiple relays.
|
* Publishes an event to multiple relays.
|
||||||
*
|
*
|
||||||
|
349
src/controllers/zap.ts
Normal file
349
src/controllers/zap.ts
Normal file
@ -0,0 +1,349 @@
|
|||||||
|
import { Invoice } from '@getalby/lightning-tools'
|
||||||
|
import axios, { AxiosInstance } from 'axios'
|
||||||
|
import { bech32 } from 'bech32'
|
||||||
|
import { Buffer } from 'buffer'
|
||||||
|
import { Filter, kinds } from 'nostr-tools'
|
||||||
|
import { requestProvider, SendPaymentResponse, WebLNProvider } from 'webln'
|
||||||
|
import {
|
||||||
|
isLnurlResponse,
|
||||||
|
LnurlResponse,
|
||||||
|
PaymentRequest,
|
||||||
|
SignedEvent,
|
||||||
|
ZapReceipt,
|
||||||
|
ZapRequest
|
||||||
|
} from '../types'
|
||||||
|
import { log, LogType, npubToHex } from '../utils'
|
||||||
|
import { RelayController } from './relay'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton class to manage zap related operations.
|
||||||
|
*/
|
||||||
|
export class ZapController {
|
||||||
|
private static instance: ZapController
|
||||||
|
private webln: WebLNProvider | null = null
|
||||||
|
private httpClient: AxiosInstance
|
||||||
|
private appRelay = import.meta.env.VITE_APP_RELAY
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.httpClient = axios.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns The singleton instance of ZapController.
|
||||||
|
*/
|
||||||
|
public static getInstance(): ZapController {
|
||||||
|
if (!ZapController.instance) {
|
||||||
|
ZapController.instance = new ZapController()
|
||||||
|
}
|
||||||
|
return ZapController.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates ZapRequest and payment request string. More info can be found at
|
||||||
|
* https://github.com/nostr-protocol/nips/blob/master/57.md.
|
||||||
|
* @param lud16 - LUD-16 of the recipient.
|
||||||
|
* @param amount - payment amount (will be multiplied by 1000 to represent sats).
|
||||||
|
* @param recipientPubKey - pubKey of the recipient.
|
||||||
|
* @param senderPubkey - pubKey of of the sender.
|
||||||
|
* @param content - optional content (comment).
|
||||||
|
* @param eventId - event id, if zapping an event.
|
||||||
|
* @returns - promise that resolves into object containing zap request and payment
|
||||||
|
* request string
|
||||||
|
*/
|
||||||
|
async getLightningPaymentRequest(
|
||||||
|
lud16: string,
|
||||||
|
amount: number,
|
||||||
|
recipientPubKey: string,
|
||||||
|
senderPubkey: string,
|
||||||
|
content?: string,
|
||||||
|
eventId?: string
|
||||||
|
) {
|
||||||
|
// Check if amount is greater than 0
|
||||||
|
if (amount <= 0) throw 'Amount should be > 0.'
|
||||||
|
|
||||||
|
// convert to mili satoshis
|
||||||
|
amount *= 1000
|
||||||
|
|
||||||
|
// decode lud16 into lnurl
|
||||||
|
const lnurl = this.decodeLud16(lud16)
|
||||||
|
|
||||||
|
// get receiver lightning details from lnurl pay endpoint
|
||||||
|
const lnurlResponse = await this.getLnurlResponse(lnurl)
|
||||||
|
|
||||||
|
const { minSendable, maxSendable, callback } = lnurlResponse
|
||||||
|
|
||||||
|
// check if the amount is within minSendable and maxSendable values
|
||||||
|
if (amount < minSendable || amount > maxSendable) {
|
||||||
|
throw `Amount '${amount}' is not within minSendable and maxSendable values '${minSendable}-${maxSendable}'.`
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode lnurl into bech32 using lnurl prefix
|
||||||
|
const lnurlBech32 = bech32.encode(
|
||||||
|
'lnurl',
|
||||||
|
bech32.toWords(Buffer.from(lnurl, 'utf8'))
|
||||||
|
)
|
||||||
|
|
||||||
|
// generate zap request
|
||||||
|
const zapRequest = await this.createZapRequest(
|
||||||
|
amount,
|
||||||
|
content,
|
||||||
|
lnurlBech32,
|
||||||
|
recipientPubKey,
|
||||||
|
senderPubkey,
|
||||||
|
eventId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!window.nostr?.signEvent) {
|
||||||
|
log(
|
||||||
|
true,
|
||||||
|
LogType.Error,
|
||||||
|
'Failed to sign the zap request!',
|
||||||
|
'window.nostr.signEvent is not defined'
|
||||||
|
)
|
||||||
|
throw 'Failed to sign zap Request!'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign zap request. This is validated by the lightning provider prior to sending the invoice(NIP-57).
|
||||||
|
const signedEvent = await window.nostr
|
||||||
|
.signEvent(zapRequest)
|
||||||
|
.then((event) => event as SignedEvent)
|
||||||
|
.catch((err) => {
|
||||||
|
log(true, LogType.Error, 'Failed to sign the zap request!', err)
|
||||||
|
throw 'Failed to sign the zap request!'
|
||||||
|
})
|
||||||
|
|
||||||
|
// Kind 9734 event must be signed and sent
|
||||||
|
// in order to receive the invoice from the provider.
|
||||||
|
// Encode stringified signed zap request.
|
||||||
|
const encodedEvent = encodeURI(JSON.stringify(signedEvent))
|
||||||
|
|
||||||
|
// send zap request as GET request to callback url received from the lnurl pay endpoint
|
||||||
|
const { data } = await this.httpClient.get(
|
||||||
|
`${callback}?amount=${amount}&nostr=${encodedEvent}&lnurl=${lnurlBech32}`
|
||||||
|
)
|
||||||
|
|
||||||
|
// data object of the response should contain payment request
|
||||||
|
if (data && data.pr) {
|
||||||
|
return Promise.resolve({ ...signedEvent, pr: data.pr })
|
||||||
|
}
|
||||||
|
|
||||||
|
throw 'lnurl callback did not return payment request.'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polls zap receipt.
|
||||||
|
* @param paymentRequest - payment request object containing zap request and
|
||||||
|
* payment request string.
|
||||||
|
* @param pollingTimeout - polling timeout (secs), by default equals to 6min.
|
||||||
|
* @returns - promise that resolves into zap receipt.
|
||||||
|
*/
|
||||||
|
async pollZapReceipt(
|
||||||
|
paymentRequest: PaymentRequest,
|
||||||
|
pollingTimeout?: number
|
||||||
|
) {
|
||||||
|
const { pr, ...zapRequest } = paymentRequest
|
||||||
|
const { created_at } = zapRequest
|
||||||
|
|
||||||
|
// stringify zap request
|
||||||
|
const zapRequestStringified = JSON.stringify(zapRequest)
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-async-promise-executor
|
||||||
|
return new Promise<ZapReceipt>(async (resolve, reject) => {
|
||||||
|
// clear polling timeout
|
||||||
|
const cleanup = () => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
sub.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Polling timeout
|
||||||
|
const timeout = setTimeout(
|
||||||
|
() => {
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
reject('Zap receipt was not received.')
|
||||||
|
},
|
||||||
|
pollingTimeout || 6 * 60 * 1000 // 6 minutes
|
||||||
|
)
|
||||||
|
|
||||||
|
const relay = await RelayController.getInstance().connectRelay(
|
||||||
|
this.appRelay
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!relay) {
|
||||||
|
return reject('Polling Zap Receipt: Could not connect to app relay!')
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter relay for event of kind 9735
|
||||||
|
const filter: Filter = {
|
||||||
|
kinds: [kinds.Zap],
|
||||||
|
since: created_at
|
||||||
|
}
|
||||||
|
|
||||||
|
const sub = relay.subscribe([filter], {
|
||||||
|
// Handle incoming events
|
||||||
|
onevent: async (event) => {
|
||||||
|
// get description tag of the event
|
||||||
|
const description = event.tags.filter(
|
||||||
|
(tag) => tag[0] === 'description'
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
// compare description tag of the event with stringified zap request
|
||||||
|
if (description[1] === zapRequestStringified) {
|
||||||
|
// validate zap receipt
|
||||||
|
if (await this.validateZapReceipt(pr, event as ZapReceipt)) {
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
resolve(event as ZapReceipt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async isWeblnProviderExists(): Promise<boolean> {
|
||||||
|
await this.requestWeblnProvider()
|
||||||
|
return !!this.webln
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendPayment(invoice: string): Promise<SendPaymentResponse> {
|
||||||
|
if (this.webln) {
|
||||||
|
return await this.webln!.sendPayment(invoice).catch((err) => {
|
||||||
|
throw new Error(`Error while sending payment. Error: ${err.message}`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
throw 'Webln is not defined!'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decodes LUD-16 into lnurl.
|
||||||
|
* @param lud16 - LUD-16 that looks like <username>@<domainname>.
|
||||||
|
* @returns - lnurl that looks like 'http://<domain>/.well-known/lnurlp/<username>'.
|
||||||
|
*/
|
||||||
|
private decodeLud16(lud16: string) {
|
||||||
|
const username = lud16.split('@')[0]
|
||||||
|
const domain = lud16.split('@')[1]
|
||||||
|
|
||||||
|
if (!domain || !username) throw `Provided lud16 '${lud16}' is not valid.`
|
||||||
|
|
||||||
|
return `https://${domain}/.well-known/lnurlp/${username}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches and validates response from lnurl pay endpoint.
|
||||||
|
*
|
||||||
|
* @param lnurl - lnurl pay endpoint.
|
||||||
|
* @returns response object that conforms to LnurlResponse interface.
|
||||||
|
*/
|
||||||
|
private async getLnurlResponse(lnurl: string): Promise<LnurlResponse> {
|
||||||
|
// get request from lnurl pay endpoint
|
||||||
|
const { data: lnurlResponse } = await this.httpClient.get(lnurl)
|
||||||
|
|
||||||
|
// validate lnurl response
|
||||||
|
this.validateLnurlResponse(lnurlResponse)
|
||||||
|
|
||||||
|
// return callback URL
|
||||||
|
return Promise.resolve(lnurlResponse)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if response conforms to LnurlResponse interface and if 'allowsNostr'
|
||||||
|
* and 'nostrPubkey' supported.
|
||||||
|
*
|
||||||
|
* @param response - response received from lnurl pay endpoint.
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
private validateLnurlResponse(response: any) {
|
||||||
|
if (!isLnurlResponse(response)) {
|
||||||
|
throw 'Provided response is not LnurlResponse.'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.allowsNostr) throw `'allowsNostr' is not supported.`
|
||||||
|
if (!response.nostrPubkey) throw `'nostrPubkey' is not supported.`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs zap request object.
|
||||||
|
* @param amount - request amount (sats).
|
||||||
|
* @param content - comment.
|
||||||
|
* @param lnurl - lnurl pay endpoint.
|
||||||
|
* @param recipientPubKey - pubKey of the recipient.
|
||||||
|
* @param senderPubkey - pubKey of of the sender.
|
||||||
|
* @param eventId - event id, if zapping an event.
|
||||||
|
* @returns zap request
|
||||||
|
*/
|
||||||
|
private async createZapRequest(
|
||||||
|
amount: number,
|
||||||
|
content = '',
|
||||||
|
lnurl: string,
|
||||||
|
recipientPubKey: string,
|
||||||
|
senderPubkey: string,
|
||||||
|
eventId?: string
|
||||||
|
): Promise<ZapRequest> {
|
||||||
|
const recipientHexKey = npubToHex(recipientPubKey)
|
||||||
|
|
||||||
|
if (!recipientHexKey) throw 'Invalid recipient pubKey.'
|
||||||
|
|
||||||
|
const zapRequest: ZapRequest = {
|
||||||
|
kind: kinds.ZapRequest,
|
||||||
|
content,
|
||||||
|
tags: [
|
||||||
|
['relays', `${this.appRelay}`],
|
||||||
|
['amount', `${amount}`],
|
||||||
|
['lnurl', lnurl],
|
||||||
|
['p', recipientHexKey]
|
||||||
|
],
|
||||||
|
pubkey: senderPubkey,
|
||||||
|
created_at: Math.round(Date.now() / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// add event id to the tags, if zapping an event.
|
||||||
|
if (eventId) zapRequest.tags.push(['e', eventId])
|
||||||
|
|
||||||
|
return zapRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates zap receipt preimage and payment request string
|
||||||
|
* @param paymentRequest - payment request string
|
||||||
|
* @param event - zap receipt.
|
||||||
|
* @returns - boolean indicating if preimage in zap receipt is valid
|
||||||
|
*/
|
||||||
|
private async validateZapReceipt(paymentRequest: string, event: ZapReceipt) {
|
||||||
|
const invoice = new Invoice({ pr: paymentRequest })
|
||||||
|
|
||||||
|
return invoice.validatePreimage(this.getPreimageFromZapReceipt(event))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets preimage from zap receipt.
|
||||||
|
* @param event - zap receipt (9735 kind).
|
||||||
|
* @returns - preimage string.
|
||||||
|
*/
|
||||||
|
private getPreimageFromZapReceipt(event: ZapReceipt) {
|
||||||
|
// filter tags by 1st item
|
||||||
|
const preimageTag = event.tags.filter((tag) => tag[0] === 'preimage')[0]
|
||||||
|
|
||||||
|
// throw an error if 'preimage' tag is not present
|
||||||
|
if (!preimageTag || preimageTag.length != 2) {
|
||||||
|
throw `'preimage' tag is not present.`
|
||||||
|
}
|
||||||
|
|
||||||
|
const preimage = preimageTag[1]
|
||||||
|
|
||||||
|
// throw an error if 'preimage' value is not present
|
||||||
|
if (!preimage) throw `'preimage' tag is not valid.`
|
||||||
|
|
||||||
|
return preimage
|
||||||
|
}
|
||||||
|
|
||||||
|
private async requestWeblnProvider() {
|
||||||
|
if (!this.webln)
|
||||||
|
this.webln = await requestProvider().catch((err) => {
|
||||||
|
console.log('err in requesting WebLNProvider :>> ', err.message)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,17 +1,35 @@
|
|||||||
import navStyles from '../styles/nav.module.scss'
|
|
||||||
import mainStyles from '../styles//main.module.scss'
|
|
||||||
import { Banner } from '../components/Banner'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { appRoutes } from '../routes'
|
|
||||||
import {
|
import {
|
||||||
init as initNostrLogin,
|
init as initNostrLogin,
|
||||||
launch as launchNostrLoginDialog
|
launch as launchNostrLoginDialog
|
||||||
} from 'nostr-login'
|
} from 'nostr-login'
|
||||||
import { useEffect } from 'react'
|
import React, {
|
||||||
import { useAppDispatch, useAppSelector } from '../hooks'
|
Dispatch,
|
||||||
|
SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState
|
||||||
|
} from 'react'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
import { Banner } from '../components/Banner'
|
||||||
|
import { MetadataController, ZapController } from '../controllers'
|
||||||
|
import { useAppDispatch, useAppSelector, useDidMount } from '../hooks'
|
||||||
|
import { appRoutes } from '../routes'
|
||||||
import { setIsAuth, setUser } from '../store/reducers/user'
|
import { setIsAuth, setUser } from '../store/reducers/user'
|
||||||
import { MetadataController } from '../controllers'
|
import mainStyles from '../styles//main.module.scss'
|
||||||
import { npubToHex } from '../utils'
|
import navStyles from '../styles/nav.module.scss'
|
||||||
|
import '../styles/popup.css'
|
||||||
|
import {
|
||||||
|
copyTextToClipboard,
|
||||||
|
formatNumber,
|
||||||
|
npubToHex,
|
||||||
|
unformatNumber
|
||||||
|
} from '../utils'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { PaymentRequest } from '../types'
|
||||||
|
import { LoadingSpinner } from '../components/LoadingSpinner'
|
||||||
|
import { QRCodeSVG } from 'qrcode.react'
|
||||||
|
import Countdown, { CountdownRenderProps } from 'react-countdown'
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
@ -28,7 +46,12 @@ export const Header = () => {
|
|||||||
dispatch(setUser(null))
|
dispatch(setUser(null))
|
||||||
} else {
|
} else {
|
||||||
dispatch(setIsAuth(true))
|
dispatch(setIsAuth(true))
|
||||||
dispatch(setUser({ npub }))
|
dispatch(
|
||||||
|
setUser({
|
||||||
|
npub,
|
||||||
|
pubkey: npubToHex(npub)!
|
||||||
|
})
|
||||||
|
)
|
||||||
MetadataController.getInstance().then((metadataController) => {
|
MetadataController.getInstance().then((metadataController) => {
|
||||||
metadataController.findMetadata(npub).then((userProfile) => {
|
metadataController.findMetadata(npub).then((userProfile) => {
|
||||||
if (userProfile) {
|
if (userProfile) {
|
||||||
@ -72,20 +95,7 @@ export const Header = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={navStyles.NMTI_Sec}>
|
<div className={navStyles.NMTI_Sec}>
|
||||||
<div className={navStyles.NMTI_SecInside}>
|
<div className={navStyles.NMTI_SecInside}>
|
||||||
<a
|
<TipButtonWithDialog />
|
||||||
className={`${navStyles.NMTI_SecInside_Link} ${navStyles.NMTI_SI_LinkTip}`}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
|
||||||
viewBox='-64 0 512 512'
|
|
||||||
width='1em'
|
|
||||||
height='1em'
|
|
||||||
fill='currentColor'
|
|
||||||
>
|
|
||||||
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
|
||||||
</svg>
|
|
||||||
Tip
|
|
||||||
</a>
|
|
||||||
<Link
|
<Link
|
||||||
to={appRoutes.submitMod}
|
to={appRoutes.submitMod}
|
||||||
className={navStyles.NMTI_SecInside_Link}
|
className={navStyles.NMTI_SecInside_Link}
|
||||||
@ -198,3 +208,398 @@ export const Header = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TipButtonWithDialog = React.memo(() => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
|
|
||||||
|
const [amount, setAmount] = useState<number>(0)
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
|
||||||
|
const [paymentRequest, setPaymentRequest] = useState<PaymentRequest>()
|
||||||
|
|
||||||
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => {
|
||||||
|
setPaymentRequest(undefined)
|
||||||
|
setIsLoading(false)
|
||||||
|
setIsOpen(false)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleQRExpiry = useCallback(() => {
|
||||||
|
setPaymentRequest(undefined)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleAmountChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const unformattedValue = unformatNumber(event.target.value)
|
||||||
|
setAmount(unformattedValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const generatePaymentRequest =
|
||||||
|
useCallback(async (): Promise<PaymentRequest | null> => {
|
||||||
|
let userHexKey: string
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Getting user pubkey')
|
||||||
|
|
||||||
|
if (userState.isAuth && userState.user?.pubkey) {
|
||||||
|
userHexKey = userState.user.pubkey as string
|
||||||
|
} else {
|
||||||
|
userHexKey = (await window.nostr?.getPublicKey()) as string
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userHexKey) {
|
||||||
|
setIsLoading(false)
|
||||||
|
toast.error('Could not get pubkey')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Getting admin metadata')
|
||||||
|
const metadataController = await MetadataController.getInstance()
|
||||||
|
|
||||||
|
const adminMetadata = await metadataController.findAdminMetadata()
|
||||||
|
|
||||||
|
if (!adminMetadata?.lud16) {
|
||||||
|
setIsLoading(false)
|
||||||
|
toast.error('Lighting address (lud16) is missing in admin metadata!')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!adminMetadata?.pubkey) {
|
||||||
|
setIsLoading(false)
|
||||||
|
toast.error('pubkey is missing in admin metadata!')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const zapController = ZapController.getInstance()
|
||||||
|
|
||||||
|
setLoadingSpinnerDesc('Creating zap request')
|
||||||
|
return await zapController
|
||||||
|
.getLightningPaymentRequest(
|
||||||
|
adminMetadata.lud16,
|
||||||
|
amount,
|
||||||
|
adminMetadata.pubkey as string,
|
||||||
|
userHexKey,
|
||||||
|
message
|
||||||
|
)
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message || err)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false)
|
||||||
|
})
|
||||||
|
}, [amount, message, userState])
|
||||||
|
|
||||||
|
const handleSend = useCallback(async () => {
|
||||||
|
const pr = await generatePaymentRequest()
|
||||||
|
|
||||||
|
if (!pr) return
|
||||||
|
|
||||||
|
setIsLoading(true)
|
||||||
|
setLoadingSpinnerDesc('Sending payment!')
|
||||||
|
|
||||||
|
const zapController = ZapController.getInstance()
|
||||||
|
|
||||||
|
if (await zapController.isWeblnProviderExists()) {
|
||||||
|
await zapController
|
||||||
|
.sendPayment(pr.pr)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(`Successfully sent ${amount} sats!`)
|
||||||
|
handleClose()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message || err)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.warn('Webln is not present. Use QR code to send zap.')
|
||||||
|
setPaymentRequest(pr)
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(false)
|
||||||
|
}, [amount, handleClose, generatePaymentRequest])
|
||||||
|
|
||||||
|
const handleGenerateQRCode = async () => {
|
||||||
|
const pr = await generatePaymentRequest()
|
||||||
|
|
||||||
|
if (!pr) return
|
||||||
|
|
||||||
|
setPaymentRequest(pr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<a
|
||||||
|
className={`${navStyles.NMTI_SecInside_Link} ${navStyles.NMTI_SI_LinkTip}`}
|
||||||
|
onClick={() => setIsOpen(true)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='-64 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z'></path>
|
||||||
|
</svg>
|
||||||
|
Tip
|
||||||
|
</a>
|
||||||
|
{isOpen && (
|
||||||
|
<div
|
||||||
|
id='PopUpMainZap'
|
||||||
|
className='popUpMain'
|
||||||
|
style={{ display: 'flex' }}
|
||||||
|
>
|
||||||
|
<div className='ContainerMain'>
|
||||||
|
<div className='popUpMainCardWrapper'>
|
||||||
|
<div className='popUpMainCard popUpMainCardQR'>
|
||||||
|
<div className='popUpMainCardTop'>
|
||||||
|
<div className='popUpMainCardTopInfo'>
|
||||||
|
<h3>Tip/Zap DEG Mods</h3>
|
||||||
|
</div>
|
||||||
|
<div className='popUpMainCardTopClose' onClick={handleClose}>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='-96 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
|
>
|
||||||
|
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z' />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='pUMCB_Zaps'>
|
||||||
|
<div className='pUMCB_ZapsInside'>
|
||||||
|
<div className='pUMCB_ZapsInsideAmount'>
|
||||||
|
<div className='inputLabelWrapperMain'>
|
||||||
|
<p
|
||||||
|
className='labelDescriptionMain'
|
||||||
|
style={{ textAlign: 'center' }}
|
||||||
|
>
|
||||||
|
If you want the development and maintenance of DEG
|
||||||
|
Mods to stop, then a tip helps continue it.
|
||||||
|
</p>
|
||||||
|
<label className='form-label labelMain'>
|
||||||
|
Amount (Satoshis)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className='inputMain'
|
||||||
|
type='text'
|
||||||
|
inputMode='numeric'
|
||||||
|
value={amount ? formatNumber(amount) : ''}
|
||||||
|
onChange={handleAmountChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='pUMCB_ZapsInsideAmountOptions'>
|
||||||
|
<PresetAmount
|
||||||
|
label='1K'
|
||||||
|
value={1000}
|
||||||
|
setAmount={setAmount}
|
||||||
|
/>
|
||||||
|
<PresetAmount
|
||||||
|
label='5K'
|
||||||
|
value={5000}
|
||||||
|
setAmount={setAmount}
|
||||||
|
/>
|
||||||
|
<PresetAmount
|
||||||
|
label='10K'
|
||||||
|
value={10000}
|
||||||
|
setAmount={setAmount}
|
||||||
|
/>
|
||||||
|
<PresetAmount
|
||||||
|
label='25K'
|
||||||
|
value={25000}
|
||||||
|
setAmount={setAmount}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='inputLabelWrapperMain'>
|
||||||
|
<label className='form-label labelMain'>
|
||||||
|
Message (optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type='text'
|
||||||
|
className='inputMain'
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className='pUMCB_ZapsInsideBtns'>
|
||||||
|
<button
|
||||||
|
className='btn btnMain pUMCB_ZapsInsideElementBtn'
|
||||||
|
type='button'
|
||||||
|
onClick={handleGenerateQRCode}
|
||||||
|
disabled={!amount}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='-32 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M144 32C170.5 32 192 53.49 192 80V176C192 202.5 170.5 224 144 224H48C21.49 224 0 202.5 0 176V80C0 53.49 21.49 32 48 32H144zM128 96H64V160H128V96zM144 288C170.5 288 192 309.5 192 336V432C192 458.5 170.5 480 144 480H48C21.49 480 0 458.5 0 432V336C0 309.5 21.49 288 48 288H144zM128 352H64V416H128V352zM256 80C256 53.49 277.5 32 304 32H400C426.5 32 448 53.49 448 80V176C448 202.5 426.5 224 400 224H304C277.5 224 256 202.5 256 176V80zM320 160H384V96H320V160zM352 448H384V480H352V448zM448 480H416V448H448V480zM416 288H448V416H352V384H320V480H256V288H352V320H416V288z'></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className='btn btnMain pUMCB_ZapsInsideElementBtn'
|
||||||
|
type='button'
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={!amount}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='-64 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z' />
|
||||||
|
</svg>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{paymentRequest && (
|
||||||
|
<ZapQR
|
||||||
|
paymentRequest={paymentRequest}
|
||||||
|
handleClose={handleClose}
|
||||||
|
handleQRExpiry={handleQRExpiry}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
type PresetAmountProps = {
|
||||||
|
label: string
|
||||||
|
value: number
|
||||||
|
setAmount: Dispatch<SetStateAction<number>>
|
||||||
|
}
|
||||||
|
|
||||||
|
const PresetAmount = React.memo(
|
||||||
|
({ label, value, setAmount }: PresetAmountProps) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className='btn btnMain pUMCB_ZapsInsideAmountOptionsBtn'
|
||||||
|
type='button'
|
||||||
|
onClick={() => setAmount(value)}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='-64 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M240.5 224H352C365.3 224 377.3 232.3 381.1 244.7C386.6 257.2 383.1 271.3 373.1 280.1L117.1 504.1C105.8 513.9 89.27 514.7 77.19 505.9C65.1 497.1 60.7 481.1 66.59 467.4L143.5 288H31.1C18.67 288 6.733 279.7 2.044 267.3C-2.645 254.8 .8944 240.7 10.93 231.9L266.9 7.918C278.2-1.92 294.7-2.669 306.8 6.114C318.9 14.9 323.3 30.87 317.4 44.61L240.5 224z' />
|
||||||
|
</svg>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
type ZapQRProps = {
|
||||||
|
paymentRequest: PaymentRequest
|
||||||
|
handleClose: () => void
|
||||||
|
handleQRExpiry: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ZapQR = React.memo(
|
||||||
|
({ paymentRequest, handleClose, handleQRExpiry }: ZapQRProps) => {
|
||||||
|
useDidMount(() => {
|
||||||
|
ZapController.getInstance()
|
||||||
|
.pollZapReceipt(paymentRequest)
|
||||||
|
.then(() => {
|
||||||
|
toast.success(`Successfully sent sats!`)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.message || err)
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
handleClose()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const onQrCodeClicked = async () => {
|
||||||
|
if (!paymentRequest) return
|
||||||
|
|
||||||
|
const zapController = ZapController.getInstance()
|
||||||
|
|
||||||
|
if (await zapController.isWeblnProviderExists()) {
|
||||||
|
zapController.sendPayment(paymentRequest.pr)
|
||||||
|
} else {
|
||||||
|
console.warn('Webln provider not present')
|
||||||
|
|
||||||
|
const href = `lightning:${paymentRequest.pr}`
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = href
|
||||||
|
a.click()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='inputLabelWrapperMain' style={{ alignItems: 'center' }}>
|
||||||
|
<QRCodeSVG
|
||||||
|
className='popUpMainCardBottomQR'
|
||||||
|
onClick={onQrCodeClicked}
|
||||||
|
value={paymentRequest.pr}
|
||||||
|
height={235}
|
||||||
|
width={235}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
className='popUpMainCardBottomLnurl'
|
||||||
|
onClick={() => {
|
||||||
|
copyTextToClipboard(paymentRequest.pr).then((isCopied) => {
|
||||||
|
if (isCopied) toast.success('Lnurl copied to clipboard!')
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{paymentRequest.pr}
|
||||||
|
</label>
|
||||||
|
<Timer onTimerExpired={handleQRExpiry} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const MAX_POLLING_TIME = 2 * 60 * 1000 // 2 minutes in milliseconds
|
||||||
|
|
||||||
|
const renderer = ({ minutes, seconds }: CountdownRenderProps) => (
|
||||||
|
<span>
|
||||||
|
{minutes}:{seconds}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
type TimerProps = {
|
||||||
|
onTimerExpired: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Timer = React.memo(({ onTimerExpired }: TimerProps) => {
|
||||||
|
const expiryTime = useMemo(() => {
|
||||||
|
return Date.now() + MAX_POLLING_TIME
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<i className='fas fa-clock'></i>
|
||||||
|
<Countdown
|
||||||
|
date={expiryTime}
|
||||||
|
renderer={renderer}
|
||||||
|
onComplete={onTimerExpired}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
@ -111,6 +111,7 @@ export const InnerModPage = () => {
|
|||||||
body={modData.body}
|
body={modData.body}
|
||||||
screenshotsUrls={modData.screenshotsUrls}
|
screenshotsUrls={modData.screenshotsUrls}
|
||||||
tags={modData.tags}
|
tags={modData.tags}
|
||||||
|
nsfw={modData.nsfw}
|
||||||
/>
|
/>
|
||||||
<Interactions />
|
<Interactions />
|
||||||
<PublishDetails
|
<PublishDetails
|
||||||
@ -318,6 +319,7 @@ type BodyProps = {
|
|||||||
body: string
|
body: string
|
||||||
screenshotsUrls: string[]
|
screenshotsUrls: string[]
|
||||||
tags: string[]
|
tags: string[]
|
||||||
|
nsfw: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Body = ({
|
const Body = ({
|
||||||
@ -325,7 +327,8 @@ const Body = ({
|
|||||||
title,
|
title,
|
||||||
body,
|
body,
|
||||||
screenshotsUrls,
|
screenshotsUrls,
|
||||||
tags
|
tags,
|
||||||
|
nsfw
|
||||||
}: BodyProps) => {
|
}: BodyProps) => {
|
||||||
const postBodyRef = useRef<HTMLDivElement>(null)
|
const postBodyRef = useRef<HTMLDivElement>(null)
|
||||||
const viewFullPostBtnRef = useRef<HTMLDivElement>(null)
|
const viewFullPostBtnRef = useRef<HTMLDivElement>(null)
|
||||||
@ -376,6 +379,12 @@ const Body = ({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className='IBMSMSMBSSTags'>
|
<div className='IBMSMSMBSSTags'>
|
||||||
|
{nsfw && (
|
||||||
|
<div className='IBMSMSMBSSTagsTag IBMSMSMBSSTagsTagNSFW'>
|
||||||
|
<p>NSFW</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{tags.map((tag, index) => (
|
{tags.map((tag, index) => (
|
||||||
<a className='IBMSMSMBSSTagsTag' href='#' key={`tag-${index}`}>
|
<a className='IBMSMSMBSSTagsTag' href='#' key={`tag-${index}`}>
|
||||||
{tag}
|
{tag}
|
||||||
|
@ -19,6 +19,7 @@ import '../styles/search.css'
|
|||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
import { ModDetails, MuteLists } from '../types'
|
import { ModDetails, MuteLists } from '../types'
|
||||||
import { fetchMods } from '../utils'
|
import { fetchMods } from '../utils'
|
||||||
|
import { MOD_FILTER_LIMIT } from '../constants'
|
||||||
|
|
||||||
enum SortBy {
|
enum SortBy {
|
||||||
Latest = 'Latest',
|
Latest = 'Latest',
|
||||||
@ -190,7 +191,7 @@ export const ModsPage = () => {
|
|||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
page={page}
|
page={page}
|
||||||
disabledNext={mods.length < 20}
|
disabledNext={mods.length < MOD_FILTER_LIMIT}
|
||||||
handlePrev={handlePrev}
|
handlePrev={handlePrev}
|
||||||
handleNext={handleNext}
|
handleNext={handleNext}
|
||||||
/>
|
/>
|
||||||
|
@ -103,3 +103,7 @@
|
|||||||
max-width: 28px;
|
max-width: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.IBMSMSMBSSTagsTag.IBMSMSMBSSTagsTagNSFW:hover {
|
||||||
|
border: unset;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
@ -59,6 +59,17 @@
|
|||||||
.popUpMainCardBottomQR {
|
.popUpMainCardBottomQR {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popUpMainCardBottomLnurl {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
width: inherit;
|
||||||
|
display: inline;
|
||||||
|
cursor: pointer;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.popUpMainCardTopClose {
|
.popUpMainCardTopClose {
|
||||||
@ -391,3 +402,21 @@
|
|||||||
background: rgba(255, 255, 255, 0.1);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.popUpMainCardBottom.popUpMainCardBottomAlt {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popUpMainCardFooter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 15px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pUMCB_ZapsInsideBtns {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 0.25fr 1.75fr;
|
||||||
|
width: 100%;
|
||||||
|
grid-gap: 15px;
|
||||||
|
}
|
||||||
|
@ -34,3 +34,12 @@
|
|||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.IBMSMSMBSSTagsTag.IBMSMSMBSSTagsTagNSFW {
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
color: rgb(225, 68, 68);
|
||||||
|
font-weight: bold;
|
||||||
|
border: unset;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: default;
|
||||||
|
box-shadow: unset;
|
||||||
|
}
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
export * from './mod'
|
export * from './mod'
|
||||||
|
export * from './nostr'
|
||||||
export * from './user'
|
export * from './user'
|
||||||
|
export * from './zap'
|
||||||
|
9
src/types/nostr.ts
Normal file
9
src/types/nostr.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export interface SignedEvent {
|
||||||
|
kind: number
|
||||||
|
tags: string[][]
|
||||||
|
content: string
|
||||||
|
created_at: number
|
||||||
|
pubkey: string
|
||||||
|
id: string
|
||||||
|
sig: string
|
||||||
|
}
|
42
src/types/zap.ts
Normal file
42
src/types/zap.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import { kinds } from 'nostr-tools'
|
||||||
|
import { SignedEvent } from '.'
|
||||||
|
|
||||||
|
export interface LnurlResponse {
|
||||||
|
callback: string
|
||||||
|
metadata: string
|
||||||
|
minSendable: number
|
||||||
|
maxSendable: number
|
||||||
|
commentAllowed: number
|
||||||
|
payerData: { [key: string]: { [key: string]: boolean } }
|
||||||
|
tag: string
|
||||||
|
nostrPubkey: string
|
||||||
|
allowsNostr: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export const isLnurlResponse = (obj: any): obj is LnurlResponse =>
|
||||||
|
'callback' in obj &&
|
||||||
|
'metadata' in obj &&
|
||||||
|
'minSendable' in obj &&
|
||||||
|
'maxSendable' in obj &&
|
||||||
|
'commentAllowed' in obj &&
|
||||||
|
'payerData' in obj &&
|
||||||
|
'tag' in obj &&
|
||||||
|
'nostrPubkey' in obj &&
|
||||||
|
'allowsNostr' in obj
|
||||||
|
|
||||||
|
export interface ZapRequest {
|
||||||
|
kind: typeof kinds.ZapRequest
|
||||||
|
content: string
|
||||||
|
tags: string[][]
|
||||||
|
pubkey: string
|
||||||
|
created_at: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaymentRequest extends SignedEvent {
|
||||||
|
pr: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ZapReceipt extends SignedEvent {
|
||||||
|
kind: typeof kinds.Zap
|
||||||
|
}
|
@ -4,7 +4,7 @@ import { ModFormState, ModDetails } from '../types'
|
|||||||
import { RelayController } from '../controllers'
|
import { RelayController } from '../controllers'
|
||||||
import { log, LogType } from './utils'
|
import { log, LogType } from './utils'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { T_TAG_VALUE } from '../constants'
|
import { MOD_FILTER_LIMIT, T_TAG_VALUE } from '../constants'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts and normalizes mod data from an event.
|
* Extracts and normalizes mod data from an event.
|
||||||
@ -151,7 +151,7 @@ export const fetchMods = async (
|
|||||||
// Define the filter criteria for fetching mods
|
// Define the filter criteria for fetching mods
|
||||||
const filter: Filter = {
|
const filter: Filter = {
|
||||||
kinds: [kinds.ClassifiedListing], // Specify the kind of events to fetch
|
kinds: [kinds.ClassifiedListing], // Specify the kind of events to fetch
|
||||||
limit: 20, // Limit the number of events fetched to 20
|
limit: MOD_FILTER_LIMIT, // Limit the number of events fetched to 20
|
||||||
'#t': [T_TAG_VALUE],
|
'#t': [T_TAG_VALUE],
|
||||||
until, // Optional filter to fetch events until this timestamp
|
until, // Optional filter to fetch events until this timestamp
|
||||||
since // Optional filter to fetch events from this timestamp
|
since // Optional filter to fetch events from this timestamp
|
||||||
|
@ -67,3 +67,33 @@ export const copyTextToClipboard = async (text: string): Promise<boolean> => {
|
|||||||
return false // Failed to copy
|
return false // Failed to copy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a number with commas as thousands separators.
|
||||||
|
*
|
||||||
|
* @param value - The number to be formatted.
|
||||||
|
* @returns A string representing the formatted number.
|
||||||
|
*/
|
||||||
|
export const formatNumber = (value: number): string => {
|
||||||
|
// Use `Math.round` to ensure the number is rounded to the nearest integer.
|
||||||
|
// `Intl.NumberFormat` creates a number format object for formatting numbers.
|
||||||
|
// The `format` method applies the format to the rounded number.
|
||||||
|
return new Intl.NumberFormat().format(Math.round(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a formatted number string back to a numeric value.
|
||||||
|
*
|
||||||
|
* This function removes any commas used as thousand separators and parses the resulting string
|
||||||
|
* to a floating-point number. If the input is not a valid number, it returns 0.
|
||||||
|
*
|
||||||
|
* @param value - The formatted number string (e.g., "1,234,567.89").
|
||||||
|
* @returns The numeric value represented by the string. Returns 0 if parsing fails.
|
||||||
|
*/
|
||||||
|
export const unformatNumber = (value: string): number => {
|
||||||
|
// Remove commas from the input string. The regular expression `/\,/g` matches all commas.
|
||||||
|
// Replace them with an empty string to get a plain numeric string.
|
||||||
|
// `parseFloat` converts the resulting string to a floating-point number.
|
||||||
|
// If `parseFloat` fails to parse the string, `|| 0` ensures that the function returns 0.
|
||||||
|
return parseFloat(value.replace(/,/g, '')) || 0
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user