Compare commits
48 Commits
fix/modal-
...
feature/ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec544a0592 | ||
|
|
72d561f8c9 | ||
|
|
f408fd1b38 | ||
|
|
2b6a1e1e5d | ||
|
|
104404b04c | ||
|
|
e4fdb7794a | ||
|
|
1566592683 | ||
|
|
52b119b424 | ||
|
|
12afbaa76b | ||
|
|
4aa4f7f175 | ||
|
|
7aaea89f21 | ||
|
|
89fc5b0ae0 | ||
|
|
48c07ad1c0 | ||
|
|
caf8f9a82b | ||
|
|
449bdb79ce | ||
|
|
d16c3cd9b0 | ||
|
|
d00e16139e | ||
|
|
fe4705afc8 | ||
|
|
326d824451 | ||
|
|
9d565ddbde | ||
|
|
c5c5843cb8 | ||
|
|
cc9840760b | ||
|
|
34bf3f7c12 | ||
|
|
be8cfcb3a5 | ||
|
|
14940a4345 | ||
|
|
d3ab9174e1 | ||
|
|
fa4c5d3532 | ||
|
|
8faccc383b | ||
|
|
e80a41bfa0 | ||
|
|
1305af6896 | ||
|
|
6c2a12c924 | ||
|
|
8aabb45917 | ||
|
|
593fafd9f8 | ||
|
|
5b57b42111 | ||
|
|
2ba1eaef65 | ||
|
|
9c18310fd9 | ||
|
|
c5af7d377d | ||
|
|
f2e70a998d | ||
|
|
b2e1a43f1b | ||
|
|
878bae6c2f | ||
|
|
1c6947d549 | ||
|
|
fabc920563 | ||
|
|
020ab18e56 | ||
|
|
41de75ff6e | ||
|
|
8ae416047d | ||
|
|
cddf0b7805 | ||
|
|
c28ef815ac | ||
|
|
50e31ceb1c |
3
.env
3
.env
@@ -2,5 +2,6 @@
|
||||
# change if you're using a different noauthd server
|
||||
REACT_APP_WEB_PUSH_PUBKEY=BNW_39YcKbV4KunFxFhvMW5JUs8AljfFnGUeZpaerO-gwCoWyQat5ol0xOGB8MLaqqCbz0iptd2Qv3SToSGynMk
|
||||
#REACT_APP_NOAUTHD_URL=http://localhost:8000
|
||||
REACT_APP_NOAUTHD_URL=https://noauthd.login.nostrapps.org
|
||||
REACT_APP_NOAUTHD_URL=https://noauthd.nsec.app
|
||||
REACT_APP_DOMAIN=nsec.app
|
||||
REACT_APP_RELAY=wss://relay.nsec.app
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
node_modules/
|
||||
9
.prettierrc
Normal file
9
.prettierrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"bracketSpacing": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
146
package-lock.json
generated
146
package-lock.json
generated
@@ -10,9 +10,10 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@mui/icons-material": "^5.14.19",
|
||||
"@mui/material": "^5.14.20",
|
||||
"@nostr-dev-kit/ndk": "^2.0.5",
|
||||
"@nostr-dev-kit/ndk": "^2.4.0",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
@@ -33,6 +34,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.50.0",
|
||||
"react-redux": "^9.0.3",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"react-scripts": "5.0.1",
|
||||
@@ -51,7 +53,8 @@
|
||||
"workbox-range-requests": "^6.6.0",
|
||||
"workbox-routing": "^6.6.0",
|
||||
"workbox-strategies": "^6.6.0",
|
||||
"workbox-streams": "^6.6.0"
|
||||
"workbox-streams": "^6.6.0",
|
||||
"yup": "^1.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash.isequal": "^4.5.8",
|
||||
@@ -61,6 +64,7 @@
|
||||
"customize-cra": "^1.0.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"prettier": "^3.2.5",
|
||||
"process": "^0.11.10",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"serve": "^14.2.1",
|
||||
@@ -2738,6 +2742,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
|
||||
"integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz",
|
||||
"integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==",
|
||||
"peerDependencies": {
|
||||
"react-hook-form": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.13",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||
@@ -3541,9 +3553,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@nostr-dev-kit/ndk": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.1.1.tgz",
|
||||
"integrity": "sha512-sMD4re3QVpSVMzoyvJAFiftPxJBXkhjRInrK5DcjhSkkPTlJhI+oiVXCpcvCJ3PHT5PilhDgKkg3LyOvv135UQ==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.4.0.tgz",
|
||||
"integrity": "sha512-IdL6F/o0p1oVmII6om5ErJ+LarO2hjbRoqYfz8DlSBWVNT/uAx/GdtlJG0qlAdg5PPzDyf3Unrk0Pi/WermVjw==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "^1.3.1",
|
||||
"@noble/secp256k1": "^2.0.0",
|
||||
@@ -14193,6 +14205,21 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
|
||||
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-bytes": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||
@@ -14286,6 +14313,11 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/property-expr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -14604,6 +14636,21 @@
|
||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
||||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.50.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.50.0.tgz",
|
||||
"integrity": "sha512-AOhuzM3RdP09ZCnq+Z0yvKGHK25yiOX5phwxjV9L7U6HMla10ezkBnvQ+Pk4GTuDfsC5P2zza3k8mawFwFLVuQ==",
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
@@ -16678,6 +16725,11 @@
|
||||
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
|
||||
},
|
||||
"node_modules/tiny-case": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
|
||||
},
|
||||
"node_modules/tmpl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
@@ -16715,6 +16767,11 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
@@ -18316,6 +18373,28 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yup": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/yup/-/yup-1.3.3.tgz",
|
||||
"integrity": "sha512-v8QwZSsHH2K3/G9WSkp6mZKO+hugKT1EmnMqLNUcfu51HU9MDyhlETT/JgtzprnrnQHPWsjc6MUDMBp/l9fNnw==",
|
||||
"dependencies": {
|
||||
"property-expr": "^2.0.5",
|
||||
"tiny-case": "^1.0.3",
|
||||
"toposort": "^2.0.2",
|
||||
"type-fest": "^2.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/yup/node_modules/type-fest": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -20090,6 +20169,12 @@
|
||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.1.6.tgz",
|
||||
"integrity": "sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A=="
|
||||
},
|
||||
"@hookform/resolvers": {
|
||||
"version": "3.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz",
|
||||
"integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"@humanwhocodes/config-array": {
|
||||
"version": "0.11.13",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
|
||||
@@ -20620,9 +20705,9 @@
|
||||
}
|
||||
},
|
||||
"@nostr-dev-kit/ndk": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.1.1.tgz",
|
||||
"integrity": "sha512-sMD4re3QVpSVMzoyvJAFiftPxJBXkhjRInrK5DcjhSkkPTlJhI+oiVXCpcvCJ3PHT5PilhDgKkg3LyOvv135UQ==",
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk/-/ndk-2.4.0.tgz",
|
||||
"integrity": "sha512-IdL6F/o0p1oVmII6om5ErJ+LarO2hjbRoqYfz8DlSBWVNT/uAx/GdtlJG0qlAdg5PPzDyf3Unrk0Pi/WermVjw==",
|
||||
"requires": {
|
||||
"@noble/hashes": "^1.3.1",
|
||||
"@noble/secp256k1": "^2.0.0",
|
||||
@@ -28255,6 +28340,12 @@
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="
|
||||
},
|
||||
"prettier": {
|
||||
"version": "3.2.5",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
|
||||
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
|
||||
"dev": true
|
||||
},
|
||||
"pretty-bytes": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz",
|
||||
@@ -28331,6 +28422,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"property-expr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA=="
|
||||
},
|
||||
"proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -28585,6 +28681,12 @@
|
||||
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.11.tgz",
|
||||
"integrity": "sha512-/6UZ2qgEyH2aqzYZgQPxEnz33NJ2gNsnHA2o5+o4wW9bLM/JYQitNP9xPhsXwC08hMMovfGe/8retsdDsczPRg=="
|
||||
},
|
||||
"react-hook-form": {
|
||||
"version": "7.50.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.50.0.tgz",
|
||||
"integrity": "sha512-AOhuzM3RdP09ZCnq+Z0yvKGHK25yiOX5phwxjV9L7U6HMla10ezkBnvQ+Pk4GTuDfsC5P2zza3k8mawFwFLVuQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
@@ -30123,6 +30225,11 @@
|
||||
"resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz",
|
||||
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA=="
|
||||
},
|
||||
"tiny-case": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="
|
||||
},
|
||||
"tmpl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
|
||||
@@ -30151,6 +30258,11 @@
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="
|
||||
},
|
||||
"toposort": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg=="
|
||||
},
|
||||
"tough-cookie": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
||||
@@ -31375,6 +31487,24 @@
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
|
||||
},
|
||||
"yup": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/yup/-/yup-1.3.3.tgz",
|
||||
"integrity": "sha512-v8QwZSsHH2K3/G9WSkp6mZKO+hugKT1EmnMqLNUcfu51HU9MDyhlETT/JgtzprnrnQHPWsjc6MUDMBp/l9fNnw==",
|
||||
"requires": {
|
||||
"property-expr": "^2.0.5",
|
||||
"tiny-case": "^1.0.3",
|
||||
"toposort": "^2.0.2",
|
||||
"type-fest": "^2.19.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"type-fest": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
package.json
11
package.json
@@ -5,9 +5,10 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@mui/icons-material": "^5.14.19",
|
||||
"@mui/material": "^5.14.20",
|
||||
"@nostr-dev-kit/ndk": "^2.0.5",
|
||||
"@nostr-dev-kit/ndk": "^2.4.0",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
@@ -28,6 +29,7 @@
|
||||
"react": "^18.2.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hook-form": "^7.50.0",
|
||||
"react-redux": "^9.0.3",
|
||||
"react-router-dom": "^6.20.1",
|
||||
"react-scripts": "5.0.1",
|
||||
@@ -46,7 +48,8 @@
|
||||
"workbox-range-requests": "^6.6.0",
|
||||
"workbox-routing": "^6.6.0",
|
||||
"workbox-strategies": "^6.6.0",
|
||||
"workbox-streams": "^6.6.0"
|
||||
"workbox-streams": "^6.6.0",
|
||||
"yup": "^1.3.3"
|
||||
},
|
||||
"overrides": {
|
||||
"react-scripts": {
|
||||
@@ -58,7 +61,8 @@
|
||||
"build": "react-app-rewired build",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-app-rewired eject",
|
||||
"serve": "npm run build && serve -s build"
|
||||
"serve": "npm run build && serve -s build",
|
||||
"format": "npx prettier --write src"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
@@ -86,6 +90,7 @@
|
||||
"customize-cra": "^1.0.0",
|
||||
"https-browserify": "^1.0.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"prettier": "^3.2.5",
|
||||
"process": "^0.11.10",
|
||||
"react-app-rewired": "^2.2.1",
|
||||
"serve": "^14.2.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Noauth",
|
||||
"short_name": "Noauth Nostr key manager",
|
||||
"name": "Nsec.app",
|
||||
"short_name": "Nsec.app - Nostr key management tool",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import App from './App';
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import App from './App'
|
||||
|
||||
test('renders learn react link', () => {
|
||||
render(<App />);
|
||||
const linkElement = screen.getByText(/learn react/i);
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
render(<App />)
|
||||
const linkElement = screen.getByText(/learn react/i)
|
||||
expect(linkElement).toBeInTheDocument()
|
||||
})
|
||||
|
||||
15
src/App.tsx
15
src/App.tsx
@@ -2,12 +2,7 @@ import { DbKey, dbi } from './modules/db'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { swicOnRender } from './modules/swic'
|
||||
import { useAppDispatch } from './store/hooks/redux'
|
||||
import {
|
||||
setApps,
|
||||
setKeys,
|
||||
setPending,
|
||||
setPerms,
|
||||
} from './store/reducers/content.slice'
|
||||
import { setApps, setKeys, setPending, setPerms } from './store/reducers/content.slice'
|
||||
import AppRoutes from './routes/AppRoutes'
|
||||
import { fetchProfile, ndk } from './modules/nostr'
|
||||
import { useModalSearchParams } from './hooks/useModalSearchParams'
|
||||
@@ -50,12 +45,8 @@ function App() {
|
||||
const apps = await dbi.listApps()
|
||||
dispatch(
|
||||
setApps({
|
||||
apps: apps.map((app) => ({
|
||||
...app,
|
||||
// MOCK IMAGE
|
||||
icon: 'https://nostr.band/android-chrome-192x192.png',
|
||||
})),
|
||||
}),
|
||||
apps,
|
||||
})
|
||||
)
|
||||
|
||||
const perms = await dbi.listPerms()
|
||||
|
||||
173
src/components/Modal/ModalAppDetails/ModalAppDetails.tsx
Normal file
173
src/components/Modal/ModalAppDetails/ModalAppDetails.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Autocomplete, CircularProgress, Stack, Typography } from '@mui/material'
|
||||
import { StyledInput } from './styled'
|
||||
import { FormEvent, useEffect, useState } from 'react'
|
||||
import { isEmptyString } from '@/utils/helpers/helpers'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectApps } from '@/store'
|
||||
import { dbi } from '@/modules/db'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { setApps } from '@/store/reducers/content.slice'
|
||||
|
||||
export const ModalAppDetails = () => {
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.APP_DETAILS)
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.APP_DETAILS)
|
||||
|
||||
const { appNpub = '' } = useParams()
|
||||
const apps = useAppSelector(selectApps)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
const [details, setDetails] = useState({
|
||||
url: '',
|
||||
name: '',
|
||||
icon: '',
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const currentApp = apps.find((app) => app.appNpub === appNpub)
|
||||
if (!currentApp) return
|
||||
|
||||
setDetails({
|
||||
icon: currentApp.icon || '',
|
||||
name: currentApp.name || '',
|
||||
url: currentApp.url || '',
|
||||
})
|
||||
|
||||
// eslint-disable-next-line
|
||||
}, [appNpub, isModalOpened])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isModalOpened) {
|
||||
// modal closed
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
}, [isModalOpened])
|
||||
|
||||
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||||
|
||||
if (isModalOpened && !isAppNpubExists) {
|
||||
handleCloseModal()
|
||||
return null
|
||||
}
|
||||
|
||||
const { icon, name, url } = details
|
||||
|
||||
const handleInputBlur = () => {
|
||||
if (isEmptyString(url)) return
|
||||
|
||||
try {
|
||||
const u = new URL(url)
|
||||
|
||||
if (isEmptyString(name)) setDetails((prev) => ({ ...prev, name: u.hostname }))
|
||||
if (isEmptyString(icon)) {
|
||||
const iconUrl = `https://${u.hostname}/favicon.ico`
|
||||
setDetails((prev) => ({ ...prev, icon: iconUrl }))
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputChange = (key: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDetails((prevState) => {
|
||||
return { ...prevState, [key]: e.target.value }
|
||||
})
|
||||
}
|
||||
|
||||
const handleAutocompletInputChange = (e: unknown, value: string) => {
|
||||
setDetails((prevState) => {
|
||||
return { ...prevState, url: value }
|
||||
})
|
||||
}
|
||||
|
||||
const submitHandler = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (isLoading) return undefined
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const updatedApp = {
|
||||
url,
|
||||
name,
|
||||
icon,
|
||||
appNpub,
|
||||
}
|
||||
await dbi.updateApp(updatedApp)
|
||||
const apps = await dbi.listApps()
|
||||
dispatch(
|
||||
setApps({
|
||||
apps,
|
||||
})
|
||||
)
|
||||
notify(`App successfully updated!`, 'success')
|
||||
setIsLoading(false)
|
||||
handleCloseModal()
|
||||
} catch (error: any) {
|
||||
setIsLoading(false)
|
||||
notify(error?.message || 'Something went wrong!', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const isFormValid = !isEmptyString(url) && !isEmptyString(name) && !isEmptyString(icon)
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack gap={'1rem'} component={'form'} onSubmit={submitHandler}>
|
||||
<Stack alignItems={'center'}>
|
||||
<Typography fontWeight={600} variant="h5">
|
||||
App details
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Autocomplete
|
||||
options={[]}
|
||||
freeSolo
|
||||
onBlur={handleInputBlur}
|
||||
onInputChange={handleAutocompletInputChange}
|
||||
inputValue={details.url}
|
||||
renderInput={({ inputProps, disabled, id, InputProps }) => {
|
||||
return (
|
||||
<StyledInput
|
||||
{...InputProps}
|
||||
className="input"
|
||||
inputProps={inputProps}
|
||||
disabled={disabled}
|
||||
label="URL"
|
||||
fullWidth
|
||||
placeholder="Enter URL"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label="Name"
|
||||
fullWidth
|
||||
placeholder="Enter app name"
|
||||
onChange={handleInputChange('name')}
|
||||
value={details.name}
|
||||
/>
|
||||
<Input
|
||||
label="Icon"
|
||||
fullWidth
|
||||
placeholder="Enter app icon url"
|
||||
onChange={handleInputChange('icon')}
|
||||
value={details.icon}
|
||||
/>
|
||||
|
||||
<Button varianttype="secondary" type="submit" fullWidth disabled={!isFormValid || isLoading}>
|
||||
Save changes {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
11
src/components/Modal/ModalAppDetails/styled.tsx
Normal file
11
src/components/Modal/ModalAppDetails/styled.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AppInputProps, Input } from '@/shared/Input/Input'
|
||||
import { styled } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const StyledInput = styled(
|
||||
forwardRef<HTMLInputElement, AppInputProps>((props, ref) => <Input {...props} ref={ref} />)
|
||||
)(() => ({
|
||||
'& .MuiAutocomplete-endAdornment': {
|
||||
right: '1rem',
|
||||
},
|
||||
}))
|
||||
@@ -1,143 +1,191 @@
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { call, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { call, getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { Avatar, Box, Stack, Typography } from '@mui/material'
|
||||
import { useParams, useSearchParams } from 'react-router-dom'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectAppsByNpub } from '@/store'
|
||||
import { selectAppsByNpub, selectKeys, selectPendingsByNpub } from '@/store'
|
||||
import { StyledButton, StyledToggleButtonsGroup } from './styled'
|
||||
import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
||||
import { useState } from 'react'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { ACTION_TYPE } from '@/utils/consts'
|
||||
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
|
||||
export const ModalConfirmConnect = () => {
|
||||
const keys = useAppSelector(selectKeys)
|
||||
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const notify = useEnqueueSnackbar()
|
||||
const navigate = useNavigate()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
||||
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
|
||||
|
||||
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(
|
||||
ACTION_TYPE.BASIC,
|
||||
)
|
||||
|
||||
const [searchParams] = useSearchParams()
|
||||
const paramNpub = searchParams.get('npub') || ''
|
||||
const { npub = paramNpub } = useParams<{ npub: string }>()
|
||||
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
|
||||
const pending = useAppSelector((state) => selectPendingsByNpub(state, npub))
|
||||
|
||||
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.BASIC)
|
||||
|
||||
const appNpub = searchParams.get('appNpub') || ''
|
||||
const pendingReqId = searchParams.get('reqId') || ''
|
||||
const isPopup = searchParams.get('popup') === 'true'
|
||||
const token = searchParams.get('token') || ''
|
||||
|
||||
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
||||
const { name, icon = '' } = triggerApp || {}
|
||||
const appName = name || getShortenNpub(appNpub)
|
||||
const { name, url = '', icon = '' } = triggerApp || {}
|
||||
|
||||
let appUrl = url || searchParams.get('appUrl') || ''
|
||||
// console.log('referrer', window.document.referrer, appUrl)
|
||||
if (!appUrl && window.document.referrer) {
|
||||
try {
|
||||
const u = new URL(window.document.referrer)
|
||||
appUrl = u.origin
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const appDomain = getDomain(appUrl)
|
||||
const appName = name || appDomain || getShortenNpub(appNpub)
|
||||
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||
const appIcon = icon || (appDomain ? `https://${appDomain}/favicon.ico` : '')
|
||||
|
||||
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
sp.delete('popup')
|
||||
sp.delete('npub')
|
||||
sp.delete('appUrl')
|
||||
},
|
||||
})
|
||||
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||||
const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId)
|
||||
if (isModalOpened && (!isNpubExists || !isAppNpubExists || !isPendingReqIdExists)) {
|
||||
closeModalAfterRequest()
|
||||
return null
|
||||
}
|
||||
|
||||
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
||||
if (!value) return undefined
|
||||
return setSelectedActionType(value)
|
||||
}
|
||||
|
||||
const handleCloseModal = createHandleCloseReplace(
|
||||
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
||||
{
|
||||
onClose: async (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
await swicCall('confirm', pendingReqId, false, false)
|
||||
}
|
||||
},
|
||||
)
|
||||
const closeModalAfterRequest = createHandleCloseReplace(
|
||||
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
||||
{
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
async function confirmPending(
|
||||
id: string,
|
||||
allow: boolean,
|
||||
remember: boolean,
|
||||
options?: any
|
||||
) {
|
||||
async function confirmPending(id: string, allow: boolean, remember: boolean, options?: any) {
|
||||
call(async () => {
|
||||
await swicCall('confirm', id, allow, remember, options)
|
||||
console.log('confirmed', id, allow, remember, options)
|
||||
closeModalAfterRequest()
|
||||
})
|
||||
if (isPopup) window.close()
|
||||
}
|
||||
|
||||
const allow = () => {
|
||||
const options: any = {};
|
||||
if (selectedActionType === ACTION_TYPE.BASIC)
|
||||
options.perm = ACTION_TYPE.BASIC;
|
||||
confirmPending(pendingReqId, true, true, options)
|
||||
const allow = async () => {
|
||||
let perms = ['connect', 'get_public_key']
|
||||
if (selectedActionType === ACTION_TYPE.BASIC) perms = [ACTION_TYPE.BASIC]
|
||||
|
||||
if (pendingReqId) {
|
||||
const options = { perms }
|
||||
await confirmPending(pendingReqId, true, true, options)
|
||||
} else {
|
||||
try {
|
||||
await swicCall('enablePush')
|
||||
console.log('enablePush done')
|
||||
} catch (e: any) {
|
||||
console.log('error', e)
|
||||
notify('Please enable Notifications in website settings!', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await swicCall('connectApp', { npub, appNpub, appUrl, perms })
|
||||
console.log('connectApp done', npub, appNpub, appUrl, perms)
|
||||
} catch (e: any) {
|
||||
notify(e.toString(), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
await swicCall('redeemToken', npub, token)
|
||||
console.log('redeemToken done')
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
notify('App did not reply. Please try to log in now.', 'error')
|
||||
navigate(`/key/${npub}`, { replace: true })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
notify('App connected! Closing...', 'success')
|
||||
|
||||
if (isPopup) setTimeout(() => window.close(), 3000)
|
||||
else navigate(`/key/${npub}`, { replace: true })
|
||||
}
|
||||
}
|
||||
|
||||
const disallow = () => {
|
||||
if (pendingReqId) confirmPending(pendingReqId, false, true)
|
||||
else closeModalAfterRequest()
|
||||
}
|
||||
|
||||
if (isPopup) {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
disallow()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Modal title="Connection request" open={isModalOpened} withCloseButton={false}>
|
||||
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
gap={'1rem'}
|
||||
alignItems={'center'}
|
||||
marginBottom={'1rem'}
|
||||
>
|
||||
{!pendingReqId && (
|
||||
<Typography variant="body1" color={'GrayText'}>
|
||||
You will be asked to <b>enable notifications</b>, this is needed for a reliable communication with Nostr
|
||||
apps.
|
||||
</Typography>
|
||||
)}
|
||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
|
||||
<Avatar
|
||||
variant='square'
|
||||
variant="rounded"
|
||||
sx={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
}}
|
||||
src={icon}
|
||||
/>
|
||||
src={appIcon}
|
||||
>
|
||||
{appAvatarTitle}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant='h5' fontWeight={600}>
|
||||
<Typography variant="h5" fontWeight={600}>
|
||||
{appName}
|
||||
</Typography>
|
||||
<Typography variant='body2' color={'GrayText'}>
|
||||
Would like to connect to your account
|
||||
<Typography variant="body2" color={'GrayText'}>
|
||||
New app would like to connect
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<StyledToggleButtonsGroup
|
||||
value={selectedActionType}
|
||||
onChange={handleActionTypeChange}
|
||||
exclusive
|
||||
>
|
||||
<StyledToggleButtonsGroup value={selectedActionType} onChange={handleActionTypeChange} exclusive>
|
||||
<ActionToggleButton
|
||||
value={ACTION_TYPE.BASIC}
|
||||
title='Basic permissions'
|
||||
description='Read your public key, sign notes and reactions'
|
||||
// hasinfo
|
||||
title="Basic permissions"
|
||||
description="Read your public key, sign notes, reactions, zaps, etc"
|
||||
/>
|
||||
{/* <ActionToggleButton
|
||||
value={ACTION_TYPE.ADVANCED}
|
||||
title='Advanced'
|
||||
description='Use for trusted apps only'
|
||||
hasinfo
|
||||
/> */}
|
||||
<ActionToggleButton
|
||||
value={ACTION_TYPE.CUSTOM}
|
||||
title='On demand'
|
||||
description='Assign permissions when the app asks for them'
|
||||
title="On demand"
|
||||
description="Confirm permissions when the app asks for them"
|
||||
/>
|
||||
</StyledToggleButtonsGroup>
|
||||
<Stack direction={'row'} gap={'1rem'}>
|
||||
<StyledButton
|
||||
onClick={() => confirmPending(pendingReqId, false, true)}
|
||||
varianttype='secondary'
|
||||
>
|
||||
<StyledButton onClick={disallow} varianttype="secondary">
|
||||
Disallow
|
||||
</StyledButton>
|
||||
<StyledButton
|
||||
fullWidth
|
||||
onClick={allow}
|
||||
>
|
||||
{/* Allow {selectedActionType} actions */}
|
||||
<StyledButton fullWidth onClick={allow}>
|
||||
Connect
|
||||
</StyledButton>
|
||||
</Stack>
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
||||
import {
|
||||
ToggleButtonGroup,
|
||||
ToggleButtonGroupProps,
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
import { ToggleButtonGroup, ToggleButtonGroupProps, styled } from '@mui/material'
|
||||
|
||||
export const StyledButton = styled((props: AppButtonProps) => (
|
||||
<Button {...props} />
|
||||
))(() => ({
|
||||
export const StyledButton = styled((props: AppButtonProps) => <Button {...props} />)(() => ({
|
||||
borderRadius: '19px',
|
||||
fontWeight: 600,
|
||||
padding: '0.75rem 1rem',
|
||||
maxHeight: '41px',
|
||||
}))
|
||||
|
||||
export const StyledToggleButtonsGroup = styled(
|
||||
(props: ToggleButtonGroupProps) => <ToggleButtonGroup {...props} />,
|
||||
)(() => ({
|
||||
export const StyledToggleButtonsGroup = styled((props: ToggleButtonGroupProps) => <ToggleButtonGroup {...props} />)(
|
||||
() => ({
|
||||
gap: '0.75rem',
|
||||
marginBottom: '1rem',
|
||||
justifyContent: 'space-between',
|
||||
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped:not(:first-of-type)':
|
||||
{
|
||||
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped:not(:first-of-type)': {
|
||||
margin: '0',
|
||||
border: 'initial',
|
||||
},
|
||||
@@ -29,4 +21,5 @@ export const StyledToggleButtonsGroup = styled(
|
||||
border: 'initial',
|
||||
borderRadius: '1rem',
|
||||
},
|
||||
}))
|
||||
})
|
||||
)
|
||||
|
||||
@@ -7,23 +7,16 @@ type ActionToggleButtonProps = ToggleButtonProps & {
|
||||
hasinfo?: boolean
|
||||
}
|
||||
|
||||
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({
|
||||
hasinfo = false,
|
||||
...props
|
||||
}) => {
|
||||
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({ hasinfo = false, ...props }) => {
|
||||
const { title, description = '' } = props
|
||||
return (
|
||||
<StyledToggleButton {...props}>
|
||||
<Typography variant='body2'>{title}</Typography>
|
||||
<Typography
|
||||
className='description'
|
||||
variant='caption'
|
||||
color={'GrayText'}
|
||||
>
|
||||
<Typography variant="body2">{title}</Typography>
|
||||
<Typography className="description" variant="caption" color={'GrayText'}>
|
||||
{description}
|
||||
</Typography>
|
||||
{hasinfo && (
|
||||
<Typography className='info' color={'GrayText'}>
|
||||
<Typography className="info" color={'GrayText'}>
|
||||
Info
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -1,32 +1,18 @@
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { call, getShortenNpub, getSignReqKind } from '@/utils/helpers/helpers'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { call, getAppIconTitle, getReqActionName, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { Avatar, Box, List, ListItem, ListItemIcon, ListItemText, Stack, Typography } from '@mui/material'
|
||||
import { useParams, useSearchParams } from 'react-router-dom'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectAppsByNpub } from '@/store'
|
||||
import { selectAppsByNpub, selectKeys } from '@/store'
|
||||
import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
||||
import { FC, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
StyledActionsListContainer,
|
||||
StyledButton,
|
||||
StyledToggleButtonsGroup,
|
||||
} from './styled'
|
||||
import { StyledActionsListContainer, StyledButton, StyledToggleButtonsGroup } from './styled'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
||||
import { DbPending } from '@/modules/db'
|
||||
import { ACTIONS } from '@/utils/consts'
|
||||
import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal'
|
||||
|
||||
enum ACTION_TYPE {
|
||||
@@ -47,37 +33,46 @@ type ModalConfirmEventProps = {
|
||||
|
||||
type PendingRequest = DbPending & { checked: boolean }
|
||||
|
||||
export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
||||
confirmEventReqs,
|
||||
}) => {
|
||||
export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs }) => {
|
||||
const keys = useAppSelector(selectKeys)
|
||||
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
const appNpub = searchParams.get('appNpub') || ''
|
||||
|
||||
const isPopup = searchParams.get('popup') === 'true'
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
|
||||
|
||||
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(
|
||||
ACTION_TYPE.ALWAYS,
|
||||
)
|
||||
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.ALWAYS)
|
||||
const [pendingRequests, setPendingRequests] = useState<PendingRequest[]>([])
|
||||
|
||||
const currentAppPendingReqs = useMemo(
|
||||
() => confirmEventReqs[appNpub]?.pending || [],
|
||||
[confirmEventReqs, appNpub],
|
||||
)
|
||||
const currentAppPendingReqs = useMemo(() => confirmEventReqs[appNpub]?.pending || [], [confirmEventReqs, appNpub])
|
||||
|
||||
useEffect(() => {
|
||||
setPendingRequests(
|
||||
currentAppPendingReqs.map((pr) => ({ ...pr, checked: true })),
|
||||
)
|
||||
setPendingRequests(currentAppPendingReqs.map((pr) => ({ ...pr, checked: true })))
|
||||
}, [currentAppPendingReqs])
|
||||
|
||||
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
},
|
||||
})
|
||||
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||||
|
||||
if (isModalOpened && (!isNpubExists || !isAppNpubExists)) {
|
||||
closeModalAfterRequest()
|
||||
return null
|
||||
}
|
||||
|
||||
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
||||
const { name, icon = '' } = triggerApp || {}
|
||||
const appName = name || getShortenNpub(appNpub)
|
||||
const appAvatarTitle = getAppIconTitle(name, appNpub)
|
||||
|
||||
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
||||
if (!value) return undefined
|
||||
@@ -86,29 +81,6 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
||||
|
||||
const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked)
|
||||
|
||||
const handleCloseModal = createHandleCloseReplace(
|
||||
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
||||
{
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
selectedPendingRequests.forEach(
|
||||
async (req) => await swicCall('confirm', req.id, false, false),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const closeModalAfterRequest = createHandleCloseReplace(
|
||||
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
||||
{
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function confirmPending(allow: boolean) {
|
||||
selectedPendingRequests.forEach((req) => {
|
||||
call(async () => {
|
||||
@@ -118,6 +90,7 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
||||
})
|
||||
})
|
||||
closeModalAfterRequest()
|
||||
if (isPopup) window.close()
|
||||
}
|
||||
|
||||
const handleChangeCheckbox = (reqId: string) => () => {
|
||||
@@ -128,39 +101,35 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
||||
setPendingRequests(newPendingRequests)
|
||||
}
|
||||
|
||||
const getAction = (req: PendingRequest) => {
|
||||
const action = ACTIONS[req.method]
|
||||
if (req.method === 'sign_event') {
|
||||
const kind = getSignReqKind(req)
|
||||
if (kind !== undefined) return `${action} of kind ${kind}`
|
||||
if (isPopup) {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
confirmPending(false)
|
||||
}
|
||||
return action
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Modal title="Permission request" open={isModalOpened} withCloseButton={false}>
|
||||
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
gap={'1rem'}
|
||||
alignItems={'center'}
|
||||
marginBottom={'1rem'}
|
||||
>
|
||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
|
||||
<Avatar
|
||||
variant='square'
|
||||
variant="square"
|
||||
sx={{
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: '12px',
|
||||
}}
|
||||
src={icon}
|
||||
/>
|
||||
>
|
||||
{appAvatarTitle}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant='h5' fontWeight={600}>
|
||||
<Typography variant="h5" fontWeight={600}>
|
||||
{appName}
|
||||
</Typography>
|
||||
<Typography variant='body2' color={'GrayText'}>
|
||||
Would like your permission to
|
||||
<Typography variant="body2" color={'GrayText'}>
|
||||
App wants to perform these actions
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
@@ -172,51 +141,24 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
||||
return (
|
||||
<ListItem key={req.id}>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
checked={req.checked}
|
||||
onChange={handleChangeCheckbox(
|
||||
req.id,
|
||||
)}
|
||||
/>
|
||||
<Checkbox checked={req.checked} onChange={handleChangeCheckbox(req.id)} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>
|
||||
{getAction(req)}
|
||||
</ListItemText>
|
||||
<ListItemText>{getReqActionName(req)}</ListItemText>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</StyledActionsListContainer>
|
||||
<StyledToggleButtonsGroup
|
||||
value={selectedActionType}
|
||||
onChange={handleActionTypeChange}
|
||||
exclusive
|
||||
>
|
||||
<ActionToggleButton
|
||||
value={ACTION_TYPE.ALWAYS}
|
||||
title='Always'
|
||||
/>
|
||||
<ActionToggleButton
|
||||
value={ACTION_TYPE.ONCE}
|
||||
title='Just once'
|
||||
/>
|
||||
{/* <ActionToggleButton
|
||||
value={ACTION_TYPE.ALLOW_ALL}
|
||||
title='Allow All Advanced Actions'
|
||||
hasinfo
|
||||
/> */}
|
||||
<StyledToggleButtonsGroup value={selectedActionType} onChange={handleActionTypeChange} exclusive>
|
||||
<ActionToggleButton value={ACTION_TYPE.ALWAYS} title="Always" />
|
||||
<ActionToggleButton value={ACTION_TYPE.ONCE} title="Just once" />
|
||||
</StyledToggleButtonsGroup>
|
||||
|
||||
<Stack direction={'row'} gap={'1rem'}>
|
||||
<StyledButton
|
||||
onClick={() => confirmPending(false)}
|
||||
varianttype='secondary'
|
||||
>
|
||||
<StyledButton onClick={() => confirmPending(false)} varianttype="secondary">
|
||||
Disallow {ACTION_LABELS[selectedActionType]}
|
||||
</StyledButton>
|
||||
<StyledButton onClick={() => confirmPending(true)}>
|
||||
Allow {ACTION_LABELS[selectedActionType]}
|
||||
</StyledButton>
|
||||
<StyledButton onClick={() => confirmPending(true)}>Allow {ACTION_LABELS[selectedActionType]}</StyledButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
||||
import {
|
||||
Stack,
|
||||
StackProps,
|
||||
ToggleButtonGroup,
|
||||
ToggleButtonGroupProps,
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
import { Stack, StackProps, ToggleButtonGroup, ToggleButtonGroupProps, styled } from '@mui/material'
|
||||
|
||||
export const StyledButton = styled((props: AppButtonProps) => (
|
||||
<Button {...props} />
|
||||
))(() => ({
|
||||
export const StyledButton = styled((props: AppButtonProps) => <Button {...props} />)(() => ({
|
||||
borderRadius: '19px',
|
||||
fontWeight: 600,
|
||||
padding: '0.75rem 1rem',
|
||||
maxHeight: '41px',
|
||||
}))
|
||||
|
||||
export const StyledToggleButtonsGroup = styled(
|
||||
(props: ToggleButtonGroupProps) => <ToggleButtonGroup {...props} />,
|
||||
)(() => ({
|
||||
export const StyledToggleButtonsGroup = styled((props: ToggleButtonGroupProps) => <ToggleButtonGroup {...props} />)(
|
||||
() => ({
|
||||
gap: '0.75rem',
|
||||
marginBottom: '1rem',
|
||||
justifyContent: 'space-between',
|
||||
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped:not(:first-of-type)':
|
||||
{
|
||||
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped:not(:first-of-type)': {
|
||||
margin: '0',
|
||||
border: 'initial',
|
||||
},
|
||||
@@ -31,11 +21,10 @@ export const StyledToggleButtonsGroup = styled(
|
||||
border: 'initial',
|
||||
borderRadius: '1rem',
|
||||
},
|
||||
}))
|
||||
})
|
||||
)
|
||||
|
||||
export const StyledActionsListContainer = styled((props: StackProps) => (
|
||||
<Stack {...props} />
|
||||
))(({ theme }) => ({
|
||||
export const StyledActionsListContainer = styled((props: StackProps) => <Stack {...props} />)(({ theme }) => ({
|
||||
padding: '0.75rem',
|
||||
background: theme.palette.backgroundSecondary.default,
|
||||
borderRadius: '1rem',
|
||||
|
||||
@@ -6,16 +6,13 @@ type ActionToggleButtonProps = ToggleButtonProps & {
|
||||
hasinfo?: boolean
|
||||
}
|
||||
|
||||
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({
|
||||
hasinfo = false,
|
||||
...props
|
||||
}) => {
|
||||
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({ hasinfo = false, ...props }) => {
|
||||
const { title } = props
|
||||
return (
|
||||
<StyledToggleButton {...props}>
|
||||
<Typography variant='body2'>{title}</Typography>
|
||||
<Typography variant="body2">{title}</Typography>
|
||||
{hasinfo && (
|
||||
<Typography className='info' color={'GrayText'}>
|
||||
<Typography className="info" color={'GrayText'}>
|
||||
Info
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -5,31 +5,35 @@ import { Button } from '@/shared/Button/Button'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { selectKeys } from '@/store'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { getBunkerLink } from '@/utils/helpers/helpers'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
import { useRef } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
export const ModalConnectApp = () => {
|
||||
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||
const timerRef = useRef<NodeJS.Timeout>()
|
||||
const keys = useAppSelector(selectKeys)
|
||||
|
||||
const timerRef = useRef<NodeJS.Timeout>()
|
||||
const notify = useEnqueueSnackbar()
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const bunkerStr = getBunkerLink(npub)
|
||||
|
||||
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||
const handleCloseModal = createHandleCloseReplace(
|
||||
MODAL_PARAMS_KEYS.CONNECT_APP,
|
||||
{
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONNECT_APP, {
|
||||
onClose: () => {
|
||||
clearTimeout(timerRef.current)
|
||||
},
|
||||
})
|
||||
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
if (isModalOpened && !isNpubExists) {
|
||||
handleCloseModal()
|
||||
return null
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
|
||||
const bunkerStr = getBunkerLink(npub)
|
||||
|
||||
const handleShareBunker = async () => {
|
||||
const shareData = {
|
||||
@@ -54,31 +58,20 @@ export const ModalConnectApp = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isModalOpened}
|
||||
title='Share your profile'
|
||||
onClose={handleCloseModal}
|
||||
>
|
||||
<Modal open={isModalOpened} title="Share your profile" onClose={handleCloseModal}>
|
||||
<Stack gap={'1rem'} alignItems={'center'}>
|
||||
<Typography variant='caption'>
|
||||
Please, copy this code and paste it into the app to log in
|
||||
</Typography>
|
||||
<Typography variant="caption">Please, copy this code and paste it into the app to log in</Typography>
|
||||
<Input
|
||||
sx={{
|
||||
gap: '0.5rem',
|
||||
}}
|
||||
fullWidth
|
||||
value={bunkerStr}
|
||||
endAdornment={
|
||||
<InputCopyButton
|
||||
value={bunkerStr}
|
||||
onCopy={handleCopy}
|
||||
/>
|
||||
}
|
||||
endAdornment={<InputCopyButton value={bunkerStr} onCopy={handleCopy} />}
|
||||
/>
|
||||
<AppLink
|
||||
title='What is this?'
|
||||
onClick={() => handleOpen(MODAL_PARAMS_KEYS.EXPLANATION)}
|
||||
title="What is this?"
|
||||
onClick={() => handleOpen(MODAL_PARAMS_KEYS.EXPLANATION, { search: { type: EXPLANATION_MODAL_KEYS.BUNKER } })}
|
||||
/>
|
||||
<Button fullWidth onClick={handleShareBunker}>
|
||||
Share it
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FC } from 'react'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
@@ -10,9 +10,7 @@ type ModalExplanationProps = {
|
||||
explanationText?: string
|
||||
}
|
||||
|
||||
export const ModalExplanation: FC<ModalExplanationProps> = ({
|
||||
explanationText = '',
|
||||
}) => {
|
||||
export const ModalExplanation: FC<ModalExplanationProps> = () => {
|
||||
const { getModalOpened } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EXPLANATION)
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
@@ -20,21 +18,76 @@ export const ModalExplanation: FC<ModalExplanationProps> = ({
|
||||
const handleCloseModal = () => {
|
||||
searchParams.delete('type')
|
||||
searchParams.delete(MODAL_PARAMS_KEYS.EXPLANATION)
|
||||
setSearchParams(searchParams)
|
||||
setSearchParams(searchParams, { replace: true })
|
||||
}
|
||||
|
||||
const type = searchParams.get('type')
|
||||
|
||||
let title = ''
|
||||
let explanationText
|
||||
switch (type) {
|
||||
case EXPLANATION_MODAL_KEYS.NPUB: {
|
||||
title = 'What is NPUB?'
|
||||
explanationText = (
|
||||
<>
|
||||
NPUB is your Nostr PUBlic key.
|
||||
<br />
|
||||
<br />
|
||||
It is your global unique identifier on the Nostr network, and is derived from your private key.
|
||||
<br />
|
||||
<br />
|
||||
You can share your NPUB with other people so that they could unambiguously find you on the network.
|
||||
</>
|
||||
)
|
||||
break
|
||||
}
|
||||
case EXPLANATION_MODAL_KEYS.LOGIN: {
|
||||
title = 'What is Login?'
|
||||
explanationText = (
|
||||
<>
|
||||
Login (username) is your human-readable name on the Nostr network.
|
||||
<br />
|
||||
<br />
|
||||
Unlike your NPUB, which is a long string of random symbols, your login is a meaningful name tied to a website
|
||||
address (like name@nsec.app).
|
||||
<br />
|
||||
<br />
|
||||
Use your username to log in to Nostr apps.
|
||||
<br />
|
||||
<br />
|
||||
You can have many usernames all pointing to your NPUB. People also refer to these names as nostr-addresses or
|
||||
NIP05 names.
|
||||
</>
|
||||
)
|
||||
break
|
||||
}
|
||||
case EXPLANATION_MODAL_KEYS.BUNKER: {
|
||||
title = 'What is Bunker URL?'
|
||||
explanationText = (
|
||||
<>
|
||||
Bunker URL is a string used to connect to Nostr apps.
|
||||
<br />
|
||||
<br />
|
||||
Some apps require bunker URL to connect to your keys. Paste it to the app and then confirm a connection
|
||||
request.
|
||||
</>
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
title='What is this?'
|
||||
title={title}
|
||||
open={isModalOpened}
|
||||
onClose={handleCloseModal}
|
||||
withCloseButton={false}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
minHeight: '60%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack height={'100%'}>
|
||||
<Stack height={'100%'} gap={2}>
|
||||
<Typography flex={1}>{explanationText}</Typography>
|
||||
<Button fullWidth onClick={handleCloseModal}>
|
||||
Got it!
|
||||
|
||||
@@ -5,60 +5,151 @@ import { Button } from '@/shared/Button/Button'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
import React, { ChangeEvent, FormEvent, useState } from 'react'
|
||||
import { CircularProgress, Stack, Typography, useTheme } from '@mui/material'
|
||||
import { StyledAppLogo } from './styled'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { FormInputType, schema } from './const'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { usePassword } from '@/hooks/usePassword'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { CheckmarkIcon } from '@/assets'
|
||||
|
||||
const FORM_DEFAULT_VALUES = {
|
||||
username: '',
|
||||
nsec: '',
|
||||
}
|
||||
|
||||
export const ModalImportKeys = () => {
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
||||
const { hidePassword, inputProps } = usePassword()
|
||||
const theme = useTheme()
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
register,
|
||||
formState: { errors },
|
||||
watch,
|
||||
} = useForm<FormInputType>({
|
||||
defaultValues: FORM_DEFAULT_VALUES,
|
||||
resolver: yupResolver(schema),
|
||||
mode: 'onSubmit',
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isAvailable, setIsAvailable] = useState(false)
|
||||
const enteredUsername = watch('username')
|
||||
const [debouncedUsername] = useDebounce(enteredUsername, 100)
|
||||
|
||||
const checkIsUsernameAvailable = useCallback(async () => {
|
||||
if (!debouncedUsername.trim().length) return undefined
|
||||
const npubNip05 = await fetchNip05(`${debouncedUsername}@${DOMAIN}`)
|
||||
|
||||
setIsAvailable(!npubNip05)
|
||||
}, [debouncedUsername])
|
||||
|
||||
useEffect(() => {
|
||||
checkIsUsernameAvailable()
|
||||
}, [checkIsUsernameAvailable])
|
||||
|
||||
const cleanUpStates = useCallback(() => {
|
||||
hidePassword()
|
||||
reset()
|
||||
setIsLoading(false)
|
||||
setIsAvailable(false)
|
||||
}, [reset, hidePassword])
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [enteredNsec, setEnteredNsec] = useState('')
|
||||
|
||||
const handleNsecChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setEnteredNsec(e.target.value)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
const submitHandler = async (values: FormInputType) => {
|
||||
if (isLoading || !isAvailable) return undefined
|
||||
try {
|
||||
if (!enteredNsec.trim().length) return
|
||||
const k: any = await swicCall('importKey', enteredNsec)
|
||||
const { nsec, username } = values
|
||||
setIsLoading(true)
|
||||
const k: any = await swicCall('importKey', username, nsec)
|
||||
notify('Key imported!', 'success')
|
||||
navigate(`/key/${k.npub}`)
|
||||
cleanUpStates()
|
||||
} catch (error: any) {
|
||||
notify(error.message, 'error')
|
||||
notify(error?.message || 'Something went wrong!', 'error')
|
||||
cleanUpStates()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isModalOpened && cleanUpStates()
|
||||
}
|
||||
}, [isModalOpened, cleanUpStates])
|
||||
|
||||
const getInputHelperText = () => {
|
||||
if (!enteredUsername) return "Don't worry, username can be changed later."
|
||||
if (!isAvailable) return 'Already taken'
|
||||
return (
|
||||
<>
|
||||
<CheckmarkIcon /> Available
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const inputHelperText = getInputHelperText()
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
gap={'1rem'}
|
||||
alignItems={'center'}
|
||||
alignSelf={'flex-start'}
|
||||
>
|
||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
||||
<StyledAppLogo />
|
||||
<Typography fontWeight={600} variant='h5'>
|
||||
Import keys
|
||||
<Typography fontWeight={600} variant="h5">
|
||||
Import key
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Input
|
||||
label='Enter a NSEC'
|
||||
placeholder='Your NSEC'
|
||||
value={enteredNsec}
|
||||
onChange={handleNsecChange}
|
||||
label="Choose a username"
|
||||
fullWidth
|
||||
type='password'
|
||||
placeholder="Enter a Username"
|
||||
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||
{...register('username')}
|
||||
error={!!errors.username}
|
||||
helperText={inputHelperText}
|
||||
helperTextProps={{
|
||||
sx: {
|
||||
'&.helper_text': {
|
||||
color:
|
||||
enteredUsername && isAvailable
|
||||
? theme.palette.success.main
|
||||
: enteredUsername && !isAvailable
|
||||
? theme.palette.error.main
|
||||
: theme.palette.textSecondaryDecorate.main,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button type='submit'>Import nsec</Button>
|
||||
<Input
|
||||
label="Paste your private key"
|
||||
placeholder="nsec1..."
|
||||
fullWidth
|
||||
{...register('nsec')}
|
||||
error={!!errors.nsec}
|
||||
{...inputProps}
|
||||
helperText="Keys stay on your device."
|
||||
helperTextProps={{
|
||||
sx: {
|
||||
'&.helper_text': {
|
||||
color: theme.palette.textSecondaryDecorate.main,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
Import key {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
)
|
||||
|
||||
8
src/components/Modal/ModalImportKeys/const.ts
Normal file
8
src/components/Modal/ModalImportKeys/const.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as yup from 'yup'
|
||||
|
||||
export const schema = yup.object().shape({
|
||||
username: yup.string().required(),
|
||||
nsec: yup.string().required(),
|
||||
})
|
||||
|
||||
export type FormInputType = yup.InferType<typeof schema>
|
||||
@@ -29,27 +29,13 @@ export const ModalInitial = () => {
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack paddingTop={'0.5rem'} gap={'1rem'}>
|
||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.SIGN_UP)}>
|
||||
Sign up
|
||||
</Button>
|
||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.LOGIN)}>
|
||||
Login
|
||||
</Button>
|
||||
<AppLink
|
||||
title='Advanced'
|
||||
alignSelf={'center'}
|
||||
onClick={handleShowAdvanced}
|
||||
/>
|
||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.SIGN_UP)}>Sign up</Button>
|
||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.LOGIN)}>Login</Button>
|
||||
<AppLink title="Advanced" alignSelf={'center'} onClick={handleShowAdvanced} />
|
||||
|
||||
{showAdvancedContent && (
|
||||
<Fade in>
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
||||
}
|
||||
>
|
||||
Import keys
|
||||
</Button>
|
||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS)}>Import key</Button>
|
||||
</Fade>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { IconButton, Stack, Typography } from '@mui/material'
|
||||
import React, { ChangeEvent, useState } from 'react'
|
||||
import { CircularProgress, Stack, Typography } from '@mui/material'
|
||||
import { StyledAppLogo } from './styled'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { FormInputType, schema } from './const'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||
import { usePassword } from '@/hooks/usePassword'
|
||||
import { dbi } from '@/modules/db'
|
||||
|
||||
const FORM_DEFAULT_VALUES = {
|
||||
username: '',
|
||||
password: '',
|
||||
}
|
||||
|
||||
export const ModalLogin = () => {
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
@@ -19,95 +28,99 @@ export const ModalLogin = () => {
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN)
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { hidePassword, inputProps } = usePassword()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const [enteredUsername, setEnteredUsername] = useState('')
|
||||
const [enteredPassword, setEnteredPassword] = useState('')
|
||||
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useForm<FormInputType>({
|
||||
defaultValues: FORM_DEFAULT_VALUES,
|
||||
resolver: yupResolver(schema),
|
||||
mode: 'onSubmit',
|
||||
})
|
||||
|
||||
const handleUsernameChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setEnteredUsername(e.target.value)
|
||||
}
|
||||
const cleanUpStates = useCallback(() => {
|
||||
hidePassword()
|
||||
reset()
|
||||
setIsLoading(false)
|
||||
}, [reset, hidePassword])
|
||||
|
||||
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setEnteredPassword(e.target.value)
|
||||
}
|
||||
const submitHandler = async (values: FormInputType) => {
|
||||
if (isLoading) return undefined
|
||||
|
||||
const handlePasswordTypeChange = () =>
|
||||
setIsPasswordShown((prevState) => !prevState)
|
||||
|
||||
const isFormValid =
|
||||
enteredUsername.trim().length > 0 && enteredPassword.trim().length > 0
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!isFormValid) return undefined
|
||||
try {
|
||||
const [username, domain] = enteredUsername.split('@')
|
||||
const response = await fetch(
|
||||
`https://${domain}/.well-known/nostr.json?name=${username}`,
|
||||
)
|
||||
const getNpub: {
|
||||
names: {
|
||||
[name: string]: string
|
||||
}
|
||||
} = await response.json()
|
||||
setIsLoading(true)
|
||||
let npub = values.username
|
||||
let name = ''
|
||||
|
||||
const pubkey = getNpub.names[username]
|
||||
const npub = nip19.npubEncode(pubkey)
|
||||
const passphrase = enteredPassword
|
||||
console.log('fetch', npub, passphrase)
|
||||
const k: any = await swicCall('fetchKey', npub, passphrase)
|
||||
if (!npub.startsWith('npub1')) {
|
||||
name = npub
|
||||
if (!npub.includes('@')) {
|
||||
npub += '@' + DOMAIN
|
||||
} else {
|
||||
const nameDomain = npub.split('@')
|
||||
if (nameDomain[1] === DOMAIN) name = nameDomain[0]
|
||||
}
|
||||
}
|
||||
if (npub.includes('@')) {
|
||||
const npubNip05 = await fetchNip05(npub)
|
||||
if (!npubNip05) throw new Error(`Username ${npub} not found`)
|
||||
npub = npubNip05
|
||||
}
|
||||
const passphrase = values.password
|
||||
|
||||
console.log('fetch', npub, name)
|
||||
const k: any = await swicCall('fetchKey', npub, passphrase, name)
|
||||
notify(`Fetched ${k.npub}`, 'success')
|
||||
dbi.addSynced(k.npub)
|
||||
cleanUpStates()
|
||||
navigate(`/key/${k.npub}`)
|
||||
} catch (error: any) {
|
||||
notify(error.message, 'error')
|
||||
console.log('error', error)
|
||||
notify(error?.message || 'Something went wrong!', 'error')
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isModalOpened) {
|
||||
// modal closed
|
||||
cleanUpStates()
|
||||
}
|
||||
}
|
||||
}, [isModalOpened, cleanUpStates])
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
gap={'1rem'}
|
||||
alignItems={'center'}
|
||||
alignSelf={'flex-start'}
|
||||
>
|
||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
||||
<StyledAppLogo />
|
||||
<Typography fontWeight={600} variant='h5'>
|
||||
<Typography fontWeight={600} variant="h5">
|
||||
Login
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Input
|
||||
label='Enter a Username'
|
||||
label="Username or nip05 or npub"
|
||||
fullWidth
|
||||
placeholder='user@nsec.app'
|
||||
onChange={handleUsernameChange}
|
||||
value={enteredUsername}
|
||||
placeholder="name or name@domain.com or npub1..."
|
||||
{...register('username')}
|
||||
error={!!errors.username}
|
||||
/>
|
||||
<Input
|
||||
label='Password'
|
||||
label="Password"
|
||||
fullWidth
|
||||
placeholder='Your password'
|
||||
onChange={handlePasswordChange}
|
||||
value={enteredPassword}
|
||||
endAdornment={
|
||||
<IconButton
|
||||
size='small'
|
||||
onClick={handlePasswordTypeChange}
|
||||
>
|
||||
{isPasswordShown ? (
|
||||
<VisibilityOffOutlinedIcon />
|
||||
) : (
|
||||
<VisibilityOutlinedIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
}
|
||||
type={isPasswordShown ? 'text' : 'password'}
|
||||
placeholder="Your password"
|
||||
{...register('password')}
|
||||
{...inputProps}
|
||||
error={!!errors.password}
|
||||
/>
|
||||
<Button type='submit' fullWidth disabled={!isFormValid}>
|
||||
Login
|
||||
<Button type="submit" fullWidth disabled={isLoading}>
|
||||
Add account {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
8
src/components/Modal/ModalLogin/const.ts
Normal file
8
src/components/Modal/ModalLogin/const.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as yup from 'yup'
|
||||
|
||||
export const schema = yup.object().shape({
|
||||
username: yup.string().required(),
|
||||
password: yup.string().required().min(4),
|
||||
})
|
||||
|
||||
export type FormInputType = yup.InferType<typeof schema>
|
||||
@@ -2,29 +2,20 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
StyledButton,
|
||||
StyledSettingContainer,
|
||||
StyledSynchedText,
|
||||
} from './styled'
|
||||
import { Box, CircularProgress, Stack, Typography } from '@mui/material'
|
||||
import { StyledButton, StyledSettingContainer, StyledSynchedText } from './styled'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { CheckmarkIcon } from '@/assets'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
||||
import { ChangeEvent, FC, useEffect, useState } from 'react'
|
||||
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { dbi } from '@/modules/db'
|
||||
import { usePassword } from '@/hooks/usePassword'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectKeys } from '@/store'
|
||||
|
||||
type ModalSettingsProps = {
|
||||
isSynced: boolean
|
||||
@@ -33,31 +24,44 @@ type ModalSettingsProps = {
|
||||
export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const keys = useAppSelector(selectKeys)
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS)
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SETTINGS)
|
||||
|
||||
const { hidePassword, inputProps } = usePassword()
|
||||
|
||||
const [enteredPassword, setEnteredPassword] = useState('')
|
||||
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
||||
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
|
||||
|
||||
const [isChecked, setIsChecked] = useState(false)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
|
||||
useEffect(() => setIsChecked(isSynced), [isModalOpened, isSynced])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isModalOpened) {
|
||||
// modal closed
|
||||
hidePassword()
|
||||
}
|
||||
}
|
||||
}, [hidePassword, isModalOpened])
|
||||
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
|
||||
if (isModalOpened && !isNpubExists) {
|
||||
handleCloseModal()
|
||||
return null
|
||||
}
|
||||
|
||||
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setIsPasswordInvalid(false)
|
||||
setEnteredPassword(e.target.value)
|
||||
}
|
||||
|
||||
const handlePasswordTypeChange = () =>
|
||||
setIsPasswordShown((prevState) => !prevState)
|
||||
|
||||
const onClose = () => {
|
||||
handleCloseModal()
|
||||
setEnteredPassword('')
|
||||
@@ -90,7 +94,7 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={onClose} title='Settings'>
|
||||
<Modal open={isModalOpened} onClose={onClose} title="Settings">
|
||||
<Stack gap={'1rem'}>
|
||||
<StyledSettingContainer onSubmit={handleSubmit}>
|
||||
<Stack direction={'row'} justifyContent={'space-between'}>
|
||||
@@ -102,35 +106,16 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||
)}
|
||||
</Stack>
|
||||
<Box>
|
||||
<Checkbox
|
||||
onChange={handleChangeCheckbox}
|
||||
checked={isChecked}
|
||||
/>
|
||||
<Typography variant='caption'>
|
||||
Use this key on multiple devices
|
||||
</Typography>
|
||||
<Checkbox onChange={handleChangeCheckbox} checked={isChecked} />
|
||||
<Typography variant="caption">Use this key on multiple devices</Typography>
|
||||
</Box>
|
||||
<Input
|
||||
fullWidth
|
||||
endAdornment={
|
||||
<IconButton
|
||||
size='small'
|
||||
onClick={handlePasswordTypeChange}
|
||||
>
|
||||
{isPasswordShown ? (
|
||||
<VisibilityOffOutlinedIcon htmlColor='#6b6b6b' />
|
||||
) : (
|
||||
<VisibilityOutlinedIcon htmlColor='#6b6b6b' />
|
||||
)}
|
||||
</IconButton>
|
||||
}
|
||||
type={isPasswordShown ? 'text' : 'password'}
|
||||
{...inputProps}
|
||||
onChange={handlePasswordChange}
|
||||
value={enteredPassword}
|
||||
helperText={
|
||||
isPasswordInvalid ? 'Invalid password' : ''
|
||||
}
|
||||
placeholder='Enter a password'
|
||||
helperText={isPasswordInvalid ? 'Invalid password' : ''}
|
||||
placeholder="Enter a password"
|
||||
helperTextProps={{
|
||||
sx: {
|
||||
'&.helper_text': {
|
||||
@@ -141,26 +126,17 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||
disabled={!isChecked}
|
||||
/>
|
||||
{isSynced ? (
|
||||
<Typography variant='body2' color={'GrayText'}>
|
||||
<Typography variant="body2" color={'GrayText'}>
|
||||
To change your password, type a new one and sync.
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant='body2' color={'GrayText'}>
|
||||
This key will be encrypted and stored on our server. You can use the password to download this key onto another device.
|
||||
<Typography variant="body2" color={'GrayText'}>
|
||||
This key will be encrypted and stored on our server. You can use the password to download this key onto
|
||||
another device.
|
||||
</Typography>
|
||||
)}
|
||||
<StyledButton
|
||||
type='submit'
|
||||
fullWidth
|
||||
disabled={!isChecked}
|
||||
>
|
||||
Sync{' '}
|
||||
{isLoading && (
|
||||
<CircularProgress
|
||||
sx={{ marginLeft: '0.5rem' }}
|
||||
size={'1rem'}
|
||||
/>
|
||||
)}
|
||||
<StyledButton type="submit" fullWidth disabled={!isChecked}>
|
||||
Sync {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
</StyledButton>
|
||||
</StyledSettingContainer>
|
||||
<Button onClick={onClose}>Done</Button>
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import {
|
||||
Stack,
|
||||
StackProps,
|
||||
Typography,
|
||||
TypographyProps,
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
import { Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material'
|
||||
|
||||
export const StyledSettingContainer = styled((props: StackProps) => (
|
||||
<Stack gap={'0.75rem'} component={'form'} {...props} />
|
||||
@@ -28,9 +22,9 @@ export const StyledButton = styled(Button)(({ theme }) => {
|
||||
}
|
||||
})
|
||||
|
||||
export const StyledSynchedText = styled((props: TypographyProps) => (
|
||||
<Typography variant='caption' {...props} />
|
||||
))(({ theme }) => {
|
||||
export const StyledSynchedText = styled((props: TypographyProps) => <Typography variant="caption" {...props} />)(({
|
||||
theme,
|
||||
}) => {
|
||||
return {
|
||||
color: theme.palette.success.main,
|
||||
}
|
||||
|
||||
@@ -2,14 +2,16 @@ import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Stack, Typography, useTheme } from '@mui/material'
|
||||
import React, { ChangeEvent, useState } from 'react'
|
||||
import { CircularProgress, Stack, Typography, useTheme } from '@mui/material'
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react'
|
||||
import { StyledAppLogo } from './styled'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { CheckmarkIcon } from '@/assets'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||
|
||||
export const ModalSignUp = () => {
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
@@ -21,73 +23,94 @@ export const ModalSignUp = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [enteredValue, setEnteredValue] = useState('')
|
||||
const [isAvailable, setIsAvailable] = useState(false)
|
||||
|
||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setEnteredValue(e.target.value)
|
||||
const name = e.target.value.trim()
|
||||
if (name) {
|
||||
const npubNip05 = await fetchNip05(`${name}@${DOMAIN}`)
|
||||
setIsAvailable(!npubNip05)
|
||||
} else {
|
||||
setIsAvailable(false)
|
||||
}
|
||||
}
|
||||
|
||||
const isAvailable = enteredValue.trim().length > 2
|
||||
|
||||
const inputHelperText = isAvailable ? (
|
||||
const getInputHelperText = () => {
|
||||
if (!enteredValue) return "Don't worry, username can be changed later."
|
||||
if (!isAvailable) return 'Already taken'
|
||||
return (
|
||||
<>
|
||||
<CheckmarkIcon /> Available
|
||||
</>
|
||||
) : (
|
||||
"Don't worry, username can be changed later."
|
||||
)
|
||||
}
|
||||
|
||||
const inputHelperText = getInputHelperText()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (isLoading || !isAvailable) return undefined
|
||||
|
||||
const name = enteredValue.trim()
|
||||
if (!name.length) return
|
||||
|
||||
try {
|
||||
const k: any = await swicCall('generateKey')
|
||||
notify(`New key ${k.npub}`, 'success')
|
||||
setIsLoading(true)
|
||||
const k: any = await swicCall('generateKey', name)
|
||||
notify(`Account created for "${name}"`, 'success')
|
||||
navigate(`/key/${k.npub}`)
|
||||
setIsLoading(false)
|
||||
} catch (error: any) {
|
||||
notify(error.message, 'error')
|
||||
notify(error?.message || 'Something went wrong!', 'error')
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isModalOpened) {
|
||||
// modal closed
|
||||
setIsLoading(false)
|
||||
setIsAvailable(false)
|
||||
}
|
||||
}
|
||||
}, [isModalOpened])
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack
|
||||
paddingTop={'1rem'}
|
||||
gap={'1rem'}
|
||||
component={'form'}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
gap={'1rem'}
|
||||
alignItems={'center'}
|
||||
alignSelf={'flex-start'}
|
||||
>
|
||||
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
||||
<StyledAppLogo />
|
||||
<Typography fontWeight={600} variant='h5'>
|
||||
<Typography fontWeight={600} variant="h5">
|
||||
Sign up
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Input
|
||||
label='Enter a Username'
|
||||
label="Username"
|
||||
fullWidth
|
||||
placeholder='Username'
|
||||
placeholder="Enter a Username"
|
||||
helperText={inputHelperText}
|
||||
endAdornment={
|
||||
<Typography color={'#FFFFFFA8'}>@nsec.app</Typography>
|
||||
}
|
||||
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||
onChange={handleInputChange}
|
||||
value={enteredValue}
|
||||
helperTextProps={{
|
||||
sx: {
|
||||
'&.helper_text': {
|
||||
color: isAvailable
|
||||
color:
|
||||
enteredValue && isAvailable
|
||||
? theme.palette.success.main
|
||||
: enteredValue && !isAvailable
|
||||
? theme.palette.error.main
|
||||
: theme.palette.textSecondaryDecorate.main,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button fullWidth type='submit'>
|
||||
Sign up
|
||||
<Button fullWidth type="submit" disabled={isLoading}>
|
||||
Create account {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
@@ -5,8 +5,7 @@ import CloseIcon from '@mui/icons-material/Close'
|
||||
import { NotificationProps } from './types'
|
||||
import { StyledAlert, StyledContainer } from './styled'
|
||||
|
||||
export const Notification = forwardRef<HTMLDivElement, NotificationProps>(
|
||||
({ message, alertvariant, id }, ref) => {
|
||||
export const Notification = forwardRef<HTMLDivElement, NotificationProps>(({ message, alertvariant, id }, ref) => {
|
||||
const { closeSnackbar } = useSnackbar()
|
||||
|
||||
const closeSnackBarHandler = () => closeSnackbar(id)
|
||||
@@ -14,12 +13,11 @@ export const Notification = forwardRef<HTMLDivElement, NotificationProps>(
|
||||
return (
|
||||
<StyledAlert alertvariant={alertvariant} ref={ref}>
|
||||
<StyledContainer>
|
||||
<Typography variant='body1'>{message}</Typography>
|
||||
<IconButton onClick={closeSnackBarHandler} color='inherit'>
|
||||
<CloseIcon color='inherit' />
|
||||
<Typography variant="body1">{message}</Typography>
|
||||
<IconButton onClick={closeSnackBarHandler} color="inherit">
|
||||
<CloseIcon color="inherit" />
|
||||
</IconButton>
|
||||
</StyledContainer>
|
||||
</StyledAlert>
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -4,9 +4,7 @@ import { BORDER_STYLES } from './const'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const StyledAlert = styled(
|
||||
forwardRef<HTMLDivElement, StyledAlertProps>((props, ref) => (
|
||||
<Alert {...props} ref={ref} icon={false} />
|
||||
)),
|
||||
forwardRef<HTMLDivElement, StyledAlertProps>((props, ref) => <Alert {...props} ref={ref} icon={false} />)
|
||||
)(({ alertvariant }) => ({
|
||||
width: '100%',
|
||||
maxHeight: 56,
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import React, { FC, ReactNode } from 'react'
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { IconContainer, StyledContainer } from './styled'
|
||||
import { BoxProps, Typography } from '@mui/material'
|
||||
import { BoxProps, Stack, Typography } from '@mui/material'
|
||||
|
||||
type WarningProps = {
|
||||
message: string | ReactNode
|
||||
Icon?: ReactNode
|
||||
message?: string | ReactNode
|
||||
hint?: string | ReactNode
|
||||
icon?: ReactNode
|
||||
} & BoxProps
|
||||
|
||||
export const Warning: FC<WarningProps> = ({ message, Icon, ...restProps }) => {
|
||||
export const Warning: FC<WarningProps> = ({ hint, message, icon, ...restProps }) => {
|
||||
return (
|
||||
<StyledContainer {...restProps}>
|
||||
{Icon && <IconContainer>{Icon}</IconContainer>}
|
||||
<Typography flex={1} noWrap>
|
||||
{icon && <IconContainer>{icon}</IconContainer>}
|
||||
<Stack flex={1} direction={'column'} gap={'0.2rem'}>
|
||||
<Typography noWrap>
|
||||
{message}
|
||||
</Typography>
|
||||
{hint && (
|
||||
<Typography>
|
||||
{hint}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
</StyledContainer>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Box, BoxProps, styled } from '@mui/material'
|
||||
|
||||
export const StyledContainer = styled((props: BoxProps) => <Box {...props} />)(
|
||||
() => {
|
||||
export const StyledContainer = styled((props: BoxProps) => <Box {...props} />)(() => {
|
||||
return {
|
||||
borderRadius: '4px',
|
||||
border: '1px solid grey',
|
||||
@@ -11,16 +10,13 @@ export const StyledContainer = styled((props: BoxProps) => <Box {...props} />)(
|
||||
gap: '1rem',
|
||||
cursor: 'pointer',
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
export const IconContainer = styled((props: BoxProps) => <Box {...props} />)(
|
||||
() => ({
|
||||
export const IconContainer = styled((props: BoxProps) => <Box {...props} />)(() => ({
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
borderRadius: '50%',
|
||||
background: 'blue',
|
||||
background: 'grey',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
}),
|
||||
)
|
||||
}))
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
import {
|
||||
useSnackbar as useDefaultSnackbar,
|
||||
OptionsObject,
|
||||
VariantType,
|
||||
} from 'notistack'
|
||||
import { useSnackbar as useDefaultSnackbar, OptionsObject, VariantType } from 'notistack'
|
||||
import { Notification } from '../components/Notification/Notification'
|
||||
|
||||
export const useEnqueueSnackbar = () => {
|
||||
const { enqueueSnackbar } = useDefaultSnackbar()
|
||||
|
||||
const showSnackbar = (
|
||||
message: string,
|
||||
variant: Exclude<VariantType, 'default' | 'info'> = 'success',
|
||||
) => {
|
||||
const showSnackbar = (message: string, variant: Exclude<VariantType, 'default' | 'info'> = 'success') => {
|
||||
enqueueSnackbar(message, {
|
||||
anchorOrigin: {
|
||||
vertical: 'top',
|
||||
horizontal: 'right',
|
||||
},
|
||||
content: (id) => {
|
||||
return (
|
||||
<Notification
|
||||
id={id}
|
||||
message={message}
|
||||
alertvariant={variant}
|
||||
/>
|
||||
)
|
||||
return <Notification id={id} message={message} alertvariant={variant} />
|
||||
},
|
||||
} as OptionsObject)
|
||||
}
|
||||
|
||||
@@ -12,8 +12,7 @@ function useIsIOS() {
|
||||
|
||||
useEffect(() => {
|
||||
const isIOSUserAgent =
|
||||
iOSRegex.test(navigator.userAgent) ||
|
||||
(navigator.userAgent.includes('Mac') && 'ontouchend' in document)
|
||||
iOSRegex.test(navigator.userAgent) || (navigator.userAgent.includes('Mac') && 'ontouchend' in document)
|
||||
setIsIOS(isIOSUserAgent)
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
createSearchParams,
|
||||
useLocation,
|
||||
useNavigate,
|
||||
useSearchParams,
|
||||
} from 'react-router-dom'
|
||||
import { createSearchParams, useLocation, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
|
||||
type SearchParamsType = {
|
||||
[key: string]: string
|
||||
@@ -29,14 +24,10 @@ export const useModalSearchParams = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const getEnumParam = useCallback((modal: MODAL_PARAMS_KEYS) => {
|
||||
return Object.values(MODAL_PARAMS_KEYS)[
|
||||
Object.values(MODAL_PARAMS_KEYS).indexOf(modal)
|
||||
]
|
||||
return Object.values(MODAL_PARAMS_KEYS)[Object.values(MODAL_PARAMS_KEYS).indexOf(modal)]
|
||||
}, [])
|
||||
|
||||
const createHandleClose =
|
||||
(modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraCloseOptions) =>
|
||||
() => {
|
||||
const createHandleClose = (modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraCloseOptions) => () => {
|
||||
const enumKey = getEnumParam(modal)
|
||||
searchParams.delete(enumKey)
|
||||
extraOptions?.onClose && extraOptions?.onClose(searchParams)
|
||||
@@ -44,9 +35,7 @@ export const useModalSearchParams = () => {
|
||||
setSearchParams(searchParams, { replace: !!extraOptions?.replace })
|
||||
}
|
||||
|
||||
const createHandleCloseReplace =
|
||||
(modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraCloseOptions) =>
|
||||
() => {
|
||||
const createHandleCloseReplace = (modal: MODAL_PARAMS_KEYS, extraOptions: IExtraCloseOptions = {}) => {
|
||||
return createHandleClose(modal, { ...extraOptions, replace: true })
|
||||
}
|
||||
|
||||
@@ -64,19 +53,17 @@ export const useModalSearchParams = () => {
|
||||
|
||||
const searchString = !extraOptions?.append
|
||||
? createSearchParams(searchParamsData).toString()
|
||||
: `${location.search}&${createSearchParams(
|
||||
searchParamsData,
|
||||
).toString()}`
|
||||
: `${location.search}&${createSearchParams(searchParamsData).toString()}`
|
||||
|
||||
navigate(
|
||||
{
|
||||
pathname: location.pathname,
|
||||
search: searchString,
|
||||
},
|
||||
{ replace: !!extraOptions?.replace },
|
||||
{ replace: !!extraOptions?.replace }
|
||||
)
|
||||
},
|
||||
[location, navigate, getEnumParam],
|
||||
[location, navigate, getEnumParam]
|
||||
)
|
||||
|
||||
const getModalOpened = useCallback(
|
||||
@@ -85,7 +72,7 @@ export const useModalSearchParams = () => {
|
||||
const modalOpened = searchParams.get(enumKey) === 'true'
|
||||
return modalOpened
|
||||
},
|
||||
[getEnumParam, searchParams],
|
||||
[getEnumParam, searchParams]
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
30
src/hooks/usePassword.tsx
Normal file
30
src/hooks/usePassword.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { IconButton } from '@mui/material'
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
||||
|
||||
export const usePassword = () => {
|
||||
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
||||
|
||||
const handlePasswordTypeChange = useCallback(() => setIsPasswordShown((prevState) => !prevState), [])
|
||||
|
||||
const hidePassword = useCallback(() => setIsPasswordShown(false), [])
|
||||
|
||||
const inputProps = useMemo(
|
||||
() => ({
|
||||
endAdornment: (
|
||||
<IconButton size="small" onClick={handlePasswordTypeChange}>
|
||||
{isPasswordShown ? (
|
||||
<VisibilityOffOutlinedIcon htmlColor="#6b6b6b" />
|
||||
) : (
|
||||
<VisibilityOutlinedIcon htmlColor="#6b6b6b" />
|
||||
)}
|
||||
</IconButton>
|
||||
),
|
||||
type: isPasswordShown ? 'text' : 'password',
|
||||
}),
|
||||
[handlePasswordTypeChange, isPasswordShown]
|
||||
)
|
||||
|
||||
return { inputProps, hidePassword }
|
||||
}
|
||||
41
src/hooks/useProfile.ts
Normal file
41
src/hooks/useProfile.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { fetchProfile } from '@/modules/nostr'
|
||||
import { MetaEvent } from '@/types/meta-event'
|
||||
import { getProfileUsername, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectKeyByNpub } from '@/store'
|
||||
|
||||
const getFirstLetter = (text: string | undefined): string | null => {
|
||||
if (!text || text.trim().length === 0) return null
|
||||
return text.substring(0, 1).toUpperCase()
|
||||
}
|
||||
|
||||
export const useProfile = (npub: string) => {
|
||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||
const currentKey = useAppSelector((state) => selectKeyByNpub(state, npub))
|
||||
|
||||
const userName = getProfileUsername(profile) || currentKey?.name
|
||||
const userAvatar = profile?.info?.picture || ''
|
||||
const avatarTitle = getFirstLetter(userName)
|
||||
|
||||
const loadProfile = useCallback(async () => {
|
||||
if (!npub) return undefined
|
||||
try {
|
||||
const response = await fetchProfile(npub)
|
||||
setProfile(response)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error)
|
||||
}
|
||||
}, [npub])
|
||||
|
||||
useEffect(() => {
|
||||
loadProfile()
|
||||
}, [loadProfile])
|
||||
|
||||
return {
|
||||
profile,
|
||||
userName: userName || getShortenNpub(npub),
|
||||
userAvatar,
|
||||
avatarTitle,
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,14 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
|
||||
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
|
||||
'Helvetica Neue', sans-serif;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
|
||||
'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
||||
}
|
||||
|
||||
html,
|
||||
|
||||
@@ -25,7 +25,7 @@ root.render(
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
</React.StrictMode>
|
||||
)
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
|
||||
@@ -1,63 +1,59 @@
|
||||
import { Avatar, Stack, Toolbar, Typography } from '@mui/material'
|
||||
import { AppLogo } from '../../assets'
|
||||
import { StyledAppBar, StyledAppName } from './styled'
|
||||
import { StyledAppBar, StyledAppName, StyledProfileContainer, StyledThemeButton } from './styled'
|
||||
import { Menu } from './components/Menu'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { MetaEvent } from '@/types/meta-event'
|
||||
import { fetchProfile } from '@/modules/nostr'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { ProfileMenu } from './components/ProfileMenu'
|
||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { useProfile } from '@/hooks/useProfile'
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode'
|
||||
import LightModeIcon from '@mui/icons-material/LightMode'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
||||
import { setThemeMode } from '@/store/reducers/ui.slice'
|
||||
|
||||
export const Header = () => {
|
||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||
const showProfile = Boolean(npub)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!npub) return setProfile(null)
|
||||
|
||||
try {
|
||||
const response = await fetchProfile(npub)
|
||||
setProfile(response as any)
|
||||
} catch (e) {
|
||||
return setProfile(null)
|
||||
const handleNavigate = () => {
|
||||
navigate(`/key/${npub}`)
|
||||
}
|
||||
}, [npub])
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
const isDarkMode = themeMode === 'dark'
|
||||
const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#000" />
|
||||
|
||||
const showProfile = Boolean(npub || profile)
|
||||
const userName = profile?.info?.name || getShortenNpub(npub)
|
||||
const userAvatar = profile?.info?.picture || ''
|
||||
const handleChangeMode = () => {
|
||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledAppBar position='fixed'>
|
||||
<StyledAppBar position="fixed">
|
||||
<Toolbar sx={{ padding: '12px' }}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
justifyContent={'space-between'}
|
||||
alignItems={'center'}
|
||||
width={'100%'}
|
||||
>
|
||||
{showProfile ? (
|
||||
<Stack
|
||||
gap={'1rem'}
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
flex={1}
|
||||
>
|
||||
<Avatar src={userAvatar} alt={userName} />
|
||||
<Typography fontWeight={600}>{userName}</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'} width={'100%'}>
|
||||
{showProfile && (
|
||||
<StyledProfileContainer>
|
||||
<Avatar src={userAvatar} alt={userName} onClick={handleNavigate} className="avatar">
|
||||
{avatarTitle}
|
||||
</Avatar>
|
||||
<Typography fontWeight={600} onClick={handleNavigate} className="username">
|
||||
{userName}
|
||||
</Typography>
|
||||
</StyledProfileContainer>
|
||||
)}
|
||||
|
||||
{!showProfile && (
|
||||
<StyledAppName>
|
||||
<AppLogo />
|
||||
<span>Nsec.app</span>
|
||||
</StyledAppName>
|
||||
)}
|
||||
|
||||
<StyledThemeButton onClick={handleChangeMode}>{themeIcon}</StyledThemeButton>
|
||||
|
||||
{showProfile ? <ProfileMenu /> : <Menu />}
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
|
||||
31
src/layout/Header/components/ListItemProfile.tsx
Normal file
31
src/layout/Header/components/ListItemProfile.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useProfile } from '@/hooks/useProfile'
|
||||
import { DbKey } from '@/modules/db'
|
||||
import { Avatar, ListItemIcon, MenuItem, Typography } from '@mui/material'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
type ListItemProfileProps = {
|
||||
onClickItem: () => void
|
||||
} & DbKey
|
||||
|
||||
export const ListItemProfile: FC<ListItemProfileProps> = ({
|
||||
onClickItem,
|
||||
npub,
|
||||
}) => {
|
||||
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||
return (
|
||||
<MenuItem sx={{ gap: '0.5rem' }} onClick={onClickItem}>
|
||||
<ListItemIcon>
|
||||
<Avatar
|
||||
src={userAvatar}
|
||||
alt={userName}
|
||||
sx={{ width: 36, height: 36 }}
|
||||
>
|
||||
{avatarTitle}
|
||||
</Avatar>
|
||||
</ListItemIcon>
|
||||
<Typography variant='body2' noWrap>
|
||||
{userName}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
@@ -1,47 +1,18 @@
|
||||
import { DbKey } from '@/modules/db'
|
||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import {
|
||||
Avatar,
|
||||
ListItemIcon,
|
||||
MenuItem,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import React, { FC } from 'react'
|
||||
import { Stack } from '@mui/material'
|
||||
import { FC } from 'react'
|
||||
import { ListItemProfile } from './ListItemProfile'
|
||||
|
||||
type ListProfilesProps = {
|
||||
keys: DbKey[]
|
||||
onClickItem: (key: DbKey) => void
|
||||
}
|
||||
|
||||
export const ListProfiles: FC<ListProfilesProps> = ({
|
||||
keys = [],
|
||||
onClickItem,
|
||||
}) => {
|
||||
export const ListProfiles: FC<ListProfilesProps> = ({ keys = [], onClickItem }) => {
|
||||
return (
|
||||
<Stack maxHeight={'10rem'} overflow={'auto'}>
|
||||
{keys.map((key) => {
|
||||
const userName =
|
||||
key?.profile?.info?.name || getShortenNpub(key.npub)
|
||||
const userAvatar = key?.profile?.info?.picture || ''
|
||||
return (
|
||||
<MenuItem
|
||||
sx={{ gap: '0.5rem' }}
|
||||
onClick={() => onClickItem(key)}
|
||||
key={key.npub}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Avatar
|
||||
src={userAvatar}
|
||||
alt={userName}
|
||||
sx={{ width: 36, height: 36 }}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
<Typography variant='body2' noWrap>
|
||||
{userName}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
)
|
||||
return <ListItemProfile {...key} key={key.npub} onClickItem={() => onClickItem(key)} />
|
||||
})}
|
||||
</Stack>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import { Menu as MuiMenu } from '@mui/material'
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode'
|
||||
import LightModeIcon from '@mui/icons-material/LightMode'
|
||||
import LoginIcon from '@mui/icons-material/Login'
|
||||
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
||||
import { setThemeMode } from '@/store/reducers/ui.slice'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { MenuButton } from './styled'
|
||||
@@ -14,39 +11,21 @@ import MenuRoundedIcon from '@mui/icons-material/MenuRounded'
|
||||
import { selectKeys } from '@/store'
|
||||
|
||||
export const Menu = () => {
|
||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||
const keys = useAppSelector(selectKeys)
|
||||
const dispatch = useAppDispatch()
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
const { anchorEl, handleClose, handleOpen: handleOpenMenu, open } = useOpenMenu()
|
||||
|
||||
const isDarkMode = themeMode === 'dark'
|
||||
const isNoKeys = !keys || keys.length === 0
|
||||
|
||||
const {
|
||||
anchorEl,
|
||||
handleClose,
|
||||
handleOpen: handleOpenMenu,
|
||||
open,
|
||||
} = useOpenMenu()
|
||||
|
||||
const handleChangeMode = () => {
|
||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
||||
}
|
||||
const handleNavigateToAuth = () => {
|
||||
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const themeIcon = isDarkMode ? (
|
||||
<DarkModeIcon htmlColor='#fff' />
|
||||
) : (
|
||||
<LightModeIcon htmlColor='#feb94a' />
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuButton onClick={handleOpenMenu}>
|
||||
<MenuRoundedIcon color='inherit' />
|
||||
<MenuRoundedIcon color="inherit" />
|
||||
</MenuButton>
|
||||
<MuiMenu
|
||||
anchorEl={anchorEl}
|
||||
@@ -57,17 +36,10 @@ export const Menu = () => {
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
Icon={
|
||||
isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />
|
||||
}
|
||||
Icon={isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />}
|
||||
onClick={handleNavigateToAuth}
|
||||
title={isNoKeys ? 'Sign up' : 'Add account'}
|
||||
/>
|
||||
<MenuItem
|
||||
Icon={themeIcon}
|
||||
onClick={handleChangeMode}
|
||||
title='Change theme'
|
||||
/>
|
||||
</MuiMenu>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import React, { FC, ReactNode } from 'react'
|
||||
import { StyledMenuItem } from './styled'
|
||||
import {
|
||||
ListItemIcon,
|
||||
MenuItemProps as MuiMenuItemProps,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import { ListItemIcon, MenuItemProps as MuiMenuItemProps, Typography } from '@mui/material'
|
||||
|
||||
type MenuItemProps = {
|
||||
onClick: () => void
|
||||
@@ -16,7 +12,7 @@ export const MenuItem: FC<MenuItemProps> = ({ onClick, Icon, title }) => {
|
||||
return (
|
||||
<StyledMenuItem onClick={onClick}>
|
||||
<ListItemIcon>{Icon}</ListItemIcon>
|
||||
<Typography fontWeight={500} variant='body2' noWrap>
|
||||
<Typography fontWeight={500} variant="body2" noWrap>
|
||||
{title}
|
||||
</Typography>
|
||||
</StyledMenuItem>
|
||||
|
||||
@@ -9,29 +9,19 @@ import LoginIcon from '@mui/icons-material/Login'
|
||||
import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded'
|
||||
import HomeRoundedIcon from '@mui/icons-material/HomeRounded'
|
||||
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectKeys } from '@/store'
|
||||
import { setThemeMode } from '@/store/reducers/ui.slice'
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode'
|
||||
import LightModeIcon from '@mui/icons-material/LightMode'
|
||||
|
||||
import { ListProfiles } from './ListProfiles'
|
||||
import { DbKey } from '@/modules/db'
|
||||
|
||||
export const ProfileMenu = () => {
|
||||
const {
|
||||
anchorEl,
|
||||
handleOpen: handleOpenMenu,
|
||||
open,
|
||||
handleClose,
|
||||
} = useOpenMenu()
|
||||
const { anchorEl, handleOpen: handleOpenMenu, open, handleClose } = useOpenMenu()
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
|
||||
const keys = useAppSelector(selectKeys)
|
||||
const isNoKeys = !keys || keys.length === 0
|
||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||
const isDarkMode = themeMode === 'dark'
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleNavigateToAuth = () => {
|
||||
@@ -44,28 +34,15 @@ export const ProfileMenu = () => {
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const handleChangeMode = () => {
|
||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
||||
}
|
||||
|
||||
const handleNavigateToKeyInnerPage = (key: DbKey) => {
|
||||
navigate('/key/' + key.npub)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const themeIcon = isDarkMode ? (
|
||||
<DarkModeIcon htmlColor='#fff' />
|
||||
) : (
|
||||
<LightModeIcon htmlColor='#feb94a' />
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuButton onClick={handleOpenMenu}>
|
||||
<KeyboardArrowDownRoundedIcon
|
||||
color='inherit'
|
||||
fontSize='large'
|
||||
/>
|
||||
<KeyboardArrowDownRoundedIcon color="inherit" fontSize="large" />
|
||||
</MenuButton>
|
||||
<Menu
|
||||
open={open}
|
||||
@@ -75,28 +52,14 @@ export const ProfileMenu = () => {
|
||||
zIndex: 1302,
|
||||
}}
|
||||
>
|
||||
<ListProfiles
|
||||
keys={keys}
|
||||
onClickItem={handleNavigateToKeyInnerPage}
|
||||
/>
|
||||
<ListProfiles keys={keys} onClickItem={handleNavigateToKeyInnerPage} />
|
||||
<Divider />
|
||||
<MenuItem Icon={<HomeRoundedIcon />} onClick={handleNavigateHome} title="Home" />
|
||||
<MenuItem
|
||||
Icon={<HomeRoundedIcon />}
|
||||
onClick={handleNavigateHome}
|
||||
title='Home'
|
||||
/>
|
||||
<MenuItem
|
||||
Icon={
|
||||
isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />
|
||||
}
|
||||
Icon={isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />}
|
||||
onClick={handleNavigateToAuth}
|
||||
title={isNoKeys ? 'Sign up' : 'Add account'}
|
||||
/>
|
||||
<MenuItem
|
||||
Icon={themeIcon}
|
||||
onClick={handleChangeMode}
|
||||
title='Change theme'
|
||||
/>
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import {
|
||||
IconButton,
|
||||
IconButtonProps,
|
||||
MenuItem,
|
||||
MenuItemProps,
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
import { IconButton, IconButtonProps, MenuItem, MenuItemProps, styled } from '@mui/material'
|
||||
|
||||
export const MenuButton = styled((props: IconButtonProps) => (
|
||||
<IconButton {...props} />
|
||||
))(({ theme }) => {
|
||||
export const MenuButton = styled((props: IconButtonProps) => <IconButton {...props} />)(({ theme }) => {
|
||||
const isDark = theme.palette.mode === 'dark'
|
||||
return {
|
||||
borderRadius: '1rem',
|
||||
@@ -19,8 +11,6 @@ export const MenuButton = styled((props: IconButtonProps) => (
|
||||
}
|
||||
})
|
||||
|
||||
export const StyledMenuItem = styled((props: MenuItemProps) => (
|
||||
<MenuItem {...props} />
|
||||
))(() => ({
|
||||
export const StyledMenuItem = styled((props: MenuItemProps) => <MenuItem {...props} />)(() => ({
|
||||
padding: '0.5rem 1rem',
|
||||
}))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppBar, Typography, TypographyProps, styled } from '@mui/material'
|
||||
import { AppBar, IconButton, Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export const StyledAppBar = styled(AppBar)(({ theme }) => {
|
||||
@@ -11,6 +11,7 @@ export const StyledAppBar = styled(AppBar)(({ theme }) => {
|
||||
maxWidth: 'inherit',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
borderRadius: '8px',
|
||||
}
|
||||
})
|
||||
|
||||
@@ -29,3 +30,20 @@ export const StyledAppName = styled((props: TypographyProps) => (
|
||||
lineHeight: '22.4px',
|
||||
marginLeft: '0.5rem',
|
||||
}))
|
||||
|
||||
export const StyledProfileContainer = styled((props: StackProps) => <Stack {...props} />)(() => ({
|
||||
gap: '1rem',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
'& .avatar': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'& .username': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}))
|
||||
|
||||
export const StyledThemeButton = styled(IconButton)({
|
||||
margin: '0 0.5rem',
|
||||
})
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { FC } from 'react'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { Header } from './Header/Header'
|
||||
import {
|
||||
Container,
|
||||
ContainerProps,
|
||||
Divider,
|
||||
DividerProps,
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
import { Container, ContainerProps, Divider, DividerProps, styled } from '@mui/material'
|
||||
|
||||
export const Layout: FC = () => {
|
||||
return (
|
||||
<StyledContainer maxWidth='md'>
|
||||
<StyledContainer maxWidth="md">
|
||||
<Header />
|
||||
<StyledDivider />
|
||||
<main>
|
||||
@@ -21,9 +15,7 @@ export const Layout: FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const StyledContainer = styled((props: ContainerProps) => (
|
||||
<Container maxWidth='sm' {...props} />
|
||||
))({
|
||||
const StyledContainer = styled((props: ContainerProps) => <Container maxWidth="sm" {...props} />)({
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
@@ -8,9 +8,10 @@ import NDK, {
|
||||
NDKPrivateKeySigner,
|
||||
NDKSigner,
|
||||
} from '@nostr-dev-kit/ndk'
|
||||
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS } from '../utils/consts'
|
||||
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN } from '../utils/consts'
|
||||
import { Nip04 } from './nip04'
|
||||
import { getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
|
||||
import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
|
||||
import { NostrPowEvent, minePow } from './pow'
|
||||
//import { PrivateKeySigner } from './signer'
|
||||
|
||||
//const PERF_TEST = false
|
||||
@@ -36,6 +37,7 @@ interface Pending {
|
||||
}
|
||||
|
||||
interface IAllowCallbackParams {
|
||||
backend: NDKNip46Backend
|
||||
npub: string
|
||||
id: string
|
||||
method: string
|
||||
@@ -52,12 +54,7 @@ class Nip04KeyHandlingStrategy implements IEventHandlingStrategy {
|
||||
this.privkey = privkey
|
||||
}
|
||||
|
||||
private async getKey(
|
||||
backend: NDKNip46Backend,
|
||||
id: string,
|
||||
remotePubkey: string,
|
||||
recipientPubkey: string,
|
||||
) {
|
||||
private async getKey(backend: NDKNip46Backend, id: string, remotePubkey: string, recipientPubkey: string) {
|
||||
if (
|
||||
!(await backend.pubkeyAllowed({
|
||||
id,
|
||||
@@ -71,34 +68,30 @@ class Nip04KeyHandlingStrategy implements IEventHandlingStrategy {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return Buffer.from(
|
||||
this.nip04.createKey(this.privkey, recipientPubkey),
|
||||
).toString('hex')
|
||||
return Buffer.from(this.nip04.createKey(this.privkey, recipientPubkey)).toString('hex')
|
||||
}
|
||||
|
||||
async handle(
|
||||
backend: NDKNip46Backend,
|
||||
id: string,
|
||||
remotePubkey: string,
|
||||
params: string[],
|
||||
) {
|
||||
async handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]) {
|
||||
const [recipientPubkey] = params
|
||||
return await this.getKey(backend, id, remotePubkey, recipientPubkey)
|
||||
}
|
||||
}
|
||||
|
||||
class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
|
||||
readonly backend: NDKNip46Backend
|
||||
readonly npub: string
|
||||
readonly method: string
|
||||
private body: IEventHandlingStrategy
|
||||
private allowCb: (params: IAllowCallbackParams) => Promise<boolean>
|
||||
|
||||
constructor(
|
||||
backend: NDKNip46Backend,
|
||||
npub: string,
|
||||
method: string,
|
||||
body: IEventHandlingStrategy,
|
||||
allowCb: (params: IAllowCallbackParams) => Promise<boolean>,
|
||||
allowCb: (params: IAllowCallbackParams) => Promise<boolean>
|
||||
) {
|
||||
this.backend = backend
|
||||
this.npub = npub
|
||||
this.method = method
|
||||
this.body = body
|
||||
@@ -109,7 +102,7 @@ class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
|
||||
backend: NDKNip46Backend,
|
||||
id: string,
|
||||
remotePubkey: string,
|
||||
params: string[],
|
||||
params: string[]
|
||||
): Promise<string | undefined> {
|
||||
console.log(Date.now(), 'handle', {
|
||||
method: this.method,
|
||||
@@ -118,6 +111,7 @@ class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
|
||||
params,
|
||||
})
|
||||
const allow = await this.allowCb({
|
||||
backend: this.backend,
|
||||
npub: this.npub,
|
||||
id,
|
||||
method: this.method,
|
||||
@@ -126,15 +120,7 @@ class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
|
||||
})
|
||||
if (!allow) return undefined
|
||||
return this.body.handle(backend, id, remotePubkey, params).then((r) => {
|
||||
console.log(
|
||||
Date.now(),
|
||||
'req',
|
||||
id,
|
||||
'method',
|
||||
this.method,
|
||||
'result',
|
||||
r,
|
||||
)
|
||||
console.log(Date.now(), 'req', id, 'method', this.method, 'result', r)
|
||||
return r
|
||||
})
|
||||
}
|
||||
@@ -159,10 +145,12 @@ export class NoauthBackend {
|
||||
const self = this
|
||||
swg.addEventListener('activate', (event) => {
|
||||
console.log('activate')
|
||||
// swg.addEventListener('activate', event => event.waitUntil(swg.clients.claim()));
|
||||
})
|
||||
|
||||
swg.addEventListener('install', (event) => {
|
||||
console.log('install')
|
||||
// swg.addEventListener('install', event => event.waitUntil(swg.skipWaiting()));
|
||||
})
|
||||
|
||||
swg.addEventListener('push', (event) => {
|
||||
@@ -171,7 +159,7 @@ export class NoauthBackend {
|
||||
event.waitUntil(
|
||||
new Promise((ok: any) => {
|
||||
self.setNotifCallback(ok)
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -191,18 +179,13 @@ export class NoauthBackend {
|
||||
self.confirm(event.action.split(':')[1], false, false)
|
||||
} else {
|
||||
event.waitUntil(
|
||||
self.swg.clients
|
||||
.matchAll({ type: 'window' })
|
||||
.then((clientList) => {
|
||||
self.swg.clients.matchAll({ type: 'window' }).then((clientList) => {
|
||||
console.log('clients', clientList.length)
|
||||
// FIXME find a client that has our
|
||||
// key page
|
||||
for (const client of clientList) {
|
||||
console.log('client', client.url)
|
||||
if (
|
||||
new URL(client.url).pathname === '/' &&
|
||||
'focus' in client
|
||||
) {
|
||||
if (new URL(client.url).pathname === '/' && 'focus' in client) {
|
||||
client.focus()
|
||||
return
|
||||
}
|
||||
@@ -210,15 +193,15 @@ export class NoauthBackend {
|
||||
|
||||
// confirm screen url
|
||||
const req = event.notification.data.req
|
||||
console.log("req", req)
|
||||
console.log('req', req)
|
||||
// const url = `${self.swg.location.origin}/key/${req.npub}?confirm-connect=true&appNpub=${req.appNpub}&reqId=${req.id}`
|
||||
const url = `${self.swg.location.origin}/key/${req.npub}`
|
||||
self.swg.clients.openWindow(url)
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
},
|
||||
false, // ???
|
||||
false // ???
|
||||
)
|
||||
}
|
||||
|
||||
@@ -242,7 +225,7 @@ export class NoauthBackend {
|
||||
|
||||
public setNotifCallback(cb: () => void) {
|
||||
if (this.notifCallback) {
|
||||
this.notify()
|
||||
// this.notify()
|
||||
}
|
||||
this.notifCallback = cb
|
||||
}
|
||||
@@ -260,22 +243,17 @@ export class NoauthBackend {
|
||||
}
|
||||
|
||||
private async sha256(s: string) {
|
||||
return Buffer.from(
|
||||
await this.swg.crypto.subtle.digest('SHA-256', Buffer.from(s)),
|
||||
).toString('hex')
|
||||
return Buffer.from(await this.swg.crypto.subtle.digest('SHA-256', Buffer.from(s))).toString('hex')
|
||||
}
|
||||
|
||||
private async sendPost({
|
||||
url,
|
||||
method,
|
||||
headers,
|
||||
body,
|
||||
}: {
|
||||
url: string
|
||||
method: string
|
||||
headers: any
|
||||
body: string
|
||||
}) {
|
||||
private async fetchNpubName(npub: string) {
|
||||
const url = `${NOAUTHD_URL}/name?npub=${npub}`
|
||||
const r = await fetch(url)
|
||||
const d = await r.json()
|
||||
return d?.names?.length ? (d.names[0] as string) : ''
|
||||
}
|
||||
|
||||
private async sendPost({ url, method, headers, body }: { url: string; method: string; headers: any; body: string }) {
|
||||
const r = await fetch(url, {
|
||||
method,
|
||||
headers: {
|
||||
@@ -286,7 +264,8 @@ export class NoauthBackend {
|
||||
})
|
||||
if (r.status !== 200 && r.status !== 201) {
|
||||
console.log('Fetch error', url, method, r.status)
|
||||
throw new Error('Failed to fetch' + url)
|
||||
const body = await r.json()
|
||||
throw new Error('Failed to fetch ' + url, { cause: body })
|
||||
}
|
||||
|
||||
return await r.json()
|
||||
@@ -297,11 +276,13 @@ export class NoauthBackend {
|
||||
url,
|
||||
method = 'GET',
|
||||
body = '',
|
||||
pow = 0,
|
||||
}: {
|
||||
npub: string
|
||||
url: string
|
||||
method: string
|
||||
body: string
|
||||
pow?: number
|
||||
}) {
|
||||
const { data: pubkey } = nip19.decode(npub)
|
||||
|
||||
@@ -320,6 +301,15 @@ export class NoauthBackend {
|
||||
})
|
||||
if (body) authEvent.tags.push(['payload', await this.sha256(body)])
|
||||
|
||||
// generate pow on auth evevnt
|
||||
if (pow) {
|
||||
const start = Date.now()
|
||||
const powEvent: NostrPowEvent = authEvent.rawEvent()
|
||||
const minedEvent = minePow(powEvent, pow)
|
||||
console.log('mined pow of', pow, 'in', Date.now() - start, 'ms', minedEvent)
|
||||
authEvent.tags = minedEvent.tags
|
||||
}
|
||||
|
||||
authEvent.sig = await authEvent.sign(key.signer)
|
||||
|
||||
const auth = this.swg.btoa(JSON.stringify(authEvent.rawEvent()))
|
||||
@@ -334,10 +324,7 @@ export class NoauthBackend {
|
||||
})
|
||||
}
|
||||
|
||||
private async sendSubscriptionToServer(
|
||||
npub: string,
|
||||
pushSubscription: PushSubscription,
|
||||
) {
|
||||
private async sendSubscriptionToServer(npub: string, pushSubscription: PushSubscription) {
|
||||
const body = JSON.stringify({
|
||||
npub,
|
||||
relays: NIP46_RELAYS,
|
||||
@@ -354,6 +341,7 @@ export class NoauthBackend {
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
private async sendKeyToServer(npub: string, enckey: string, pwh: string) {
|
||||
const body = JSON.stringify({
|
||||
npub,
|
||||
@@ -389,22 +377,68 @@ export class NoauthBackend {
|
||||
})
|
||||
}
|
||||
|
||||
private async sendNameToServer(npub: string, name: string) {
|
||||
const body = JSON.stringify({
|
||||
npub,
|
||||
name,
|
||||
})
|
||||
|
||||
const method = 'POST'
|
||||
const url = `${NOAUTHD_URL}/name`
|
||||
|
||||
// mas pow should be 21 or something like that
|
||||
let pow = MIN_POW
|
||||
while (pow <= MAX_POW) {
|
||||
console.log('Try name', name, 'pow', pow)
|
||||
try {
|
||||
return await this.sendPostAuthd({
|
||||
npub,
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
pow,
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.log('error', e.cause)
|
||||
if (e.cause && e.cause.minPow > pow) pow = e.cause.minPow
|
||||
else throw e
|
||||
}
|
||||
}
|
||||
throw new Error('Too many requests, retry later')
|
||||
}
|
||||
|
||||
private async sendTokenToServer(npub: string, token: string) {
|
||||
const body = JSON.stringify({
|
||||
npub,
|
||||
token,
|
||||
})
|
||||
|
||||
const method = 'POST'
|
||||
const url = `${NOAUTHD_URL}/created`
|
||||
|
||||
return this.sendPostAuthd({
|
||||
npub,
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
private notify() {
|
||||
// FIXME collect info from accessBuffer and confirmBuffer
|
||||
// and update the notifications
|
||||
|
||||
for (const r of this.confirmBuffer) {
|
||||
|
||||
if (r.notified) continue
|
||||
|
||||
const key = this.keys.find(k => k.npub === r.req.npub)
|
||||
const key = this.keys.find((k) => k.npub === r.req.npub)
|
||||
if (!key) continue
|
||||
|
||||
const app = this.apps.find(a => a.appNpub === r.req.appNpub)
|
||||
const app = this.apps.find((a) => a.appNpub === r.req.appNpub)
|
||||
if (r.req.method !== 'connect' && !app) continue
|
||||
|
||||
// FIXME use Nsec.app icon!
|
||||
const icon = 'https://nostr.band/android-chrome-192x192.png'
|
||||
// FIXME check
|
||||
const icon = 'assets/icons/logo.svg'
|
||||
|
||||
const appName = app?.name || getShortenNpub(r.req.appNpub)
|
||||
// FIXME load profile?
|
||||
@@ -460,6 +494,7 @@ export class NoauthBackend {
|
||||
}
|
||||
|
||||
if (this.notifCallback) this.notifCallback()
|
||||
this.notifCallback = null
|
||||
}
|
||||
|
||||
private keyInfo(k: DbKey): KeyInfo {
|
||||
@@ -474,7 +509,18 @@ export class NoauthBackend {
|
||||
return generatePrivateKey()
|
||||
}
|
||||
|
||||
public async addKey(nsec?: string): Promise<KeyInfo> {
|
||||
public async addKey({
|
||||
name,
|
||||
nsec,
|
||||
existingName,
|
||||
}: {
|
||||
name: string
|
||||
nsec?: string
|
||||
existingName?: boolean
|
||||
}): Promise<KeyInfo> {
|
||||
// lowercase
|
||||
name = name.trim().toLocaleLowerCase()
|
||||
|
||||
let sk = ''
|
||||
if (nsec) {
|
||||
const { type, data } = nip19.decode(nsec)
|
||||
@@ -485,14 +531,23 @@ export class NoauthBackend {
|
||||
}
|
||||
const pubkey = getPublicKey(sk)
|
||||
const npub = nip19.npubEncode(pubkey)
|
||||
|
||||
const localKey = await this.keysModule.generateLocalKey()
|
||||
const enckey = await this.keysModule.encryptKeyLocal(sk, localKey)
|
||||
// @ts-ignore
|
||||
const dbKey: DbKey = { npub, enckey, localKey }
|
||||
const dbKey: DbKey = { npub, name, enckey, localKey }
|
||||
await dbi.addKey(dbKey)
|
||||
this.enckeys.push(dbKey)
|
||||
await this.startKey({ npub, sk })
|
||||
|
||||
// assign nip05 before adding the key
|
||||
// FIXME set name to db and if this call to 'send' fails
|
||||
// then retry later
|
||||
if (!existingName && name && !name.includes('@')) {
|
||||
console.log('adding key', npub, name)
|
||||
await this.sendNameToServer(npub, name)
|
||||
}
|
||||
|
||||
const sub = await this.swg.registration.pushManager.getSubscription()
|
||||
if (sub) await this.sendSubscriptionToServer(npub, sub)
|
||||
|
||||
@@ -501,9 +556,7 @@ export class NoauthBackend {
|
||||
|
||||
private getPerm(req: DbPending): string {
|
||||
const reqPerm = getReqPerm(req)
|
||||
const appPerms = this.perms.filter(
|
||||
(p) => p.npub === req.npub && p.appNpub === req.appNpub,
|
||||
)
|
||||
const appPerms = this.perms.filter((p) => p.npub === req.npub && p.appNpub === req.appNpub)
|
||||
|
||||
// exact match first
|
||||
let perm = appPerms.find((p) => p.perm === reqPerm)
|
||||
@@ -514,7 +567,52 @@ export class NoauthBackend {
|
||||
return perm?.value || ''
|
||||
}
|
||||
|
||||
private async connectApp({
|
||||
npub,
|
||||
appNpub,
|
||||
appUrl,
|
||||
perms,
|
||||
appName = '',
|
||||
appIcon = ''
|
||||
}: {
|
||||
npub: string,
|
||||
appNpub: string,
|
||||
appUrl: string,
|
||||
appName?: string,
|
||||
appIcon?: string,
|
||||
perms: string[]
|
||||
}) {
|
||||
|
||||
await dbi.addApp({
|
||||
appNpub: appNpub,
|
||||
npub: npub,
|
||||
timestamp: Date.now(),
|
||||
name: appName,
|
||||
icon: appIcon,
|
||||
url: appUrl,
|
||||
})
|
||||
|
||||
// reload
|
||||
this.apps = await dbi.listApps()
|
||||
|
||||
// write new perms confirmed by user
|
||||
for (const p of perms) {
|
||||
await dbi.addPerm({
|
||||
id: Math.random().toString(36).substring(7),
|
||||
npub: npub,
|
||||
appNpub: appNpub,
|
||||
perm: p,
|
||||
value: '1',
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
// reload
|
||||
this.perms = await dbi.listPerms()
|
||||
}
|
||||
|
||||
private async allowPermitCallback({
|
||||
backend,
|
||||
npub,
|
||||
id,
|
||||
method,
|
||||
@@ -529,6 +627,12 @@ export class NoauthBackend {
|
||||
}
|
||||
|
||||
const appNpub = nip19.npubEncode(remotePubkey)
|
||||
const connected = !!this.apps.find((a) => a.appNpub === appNpub)
|
||||
if (!connected && method !== 'connect') {
|
||||
console.log('ignoring request before connect', method, id, appNpub, npub)
|
||||
return false
|
||||
}
|
||||
|
||||
const req: DbPending = {
|
||||
id,
|
||||
npub,
|
||||
@@ -541,27 +645,16 @@ export class NoauthBackend {
|
||||
const self = this
|
||||
return new Promise(async (ok) => {
|
||||
// called when it's decided whether to allow this or not
|
||||
const onAllow = async (
|
||||
manual: boolean,
|
||||
allow: boolean,
|
||||
remember: boolean,
|
||||
options?: any,
|
||||
) => {
|
||||
const onAllow = async (manual: boolean, allow: boolean, remember: boolean, options?: any) => {
|
||||
// confirm
|
||||
console.log(
|
||||
Date.now(),
|
||||
allow ? 'allowed' : 'disallowed',
|
||||
npub,
|
||||
method,
|
||||
options,
|
||||
params,
|
||||
)
|
||||
console.log(Date.now(), allow ? 'allowed' : 'disallowed', npub, method, options, params)
|
||||
|
||||
if (manual) {
|
||||
await dbi.confirmPending(id, allow)
|
||||
|
||||
if (!(method === 'connect' && !allow)) {
|
||||
// only add app if it's not 'disallow connect'
|
||||
if (!(await dbi.getApp(req.appNpub))) {
|
||||
// add app on 'allow connect'
|
||||
if (method === 'connect' && allow) {
|
||||
// if (!(await dbi.getApp(req.appNpub))) {
|
||||
await dbi.addApp({
|
||||
appNpub: req.appNpub,
|
||||
npub: req.npub,
|
||||
@@ -574,10 +667,8 @@ export class NoauthBackend {
|
||||
// reload
|
||||
self.apps = await dbi.listApps()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// just send to db w/o waiting for it
|
||||
// if (!PERF_TEST)
|
||||
dbi.addConfirmed({
|
||||
...req,
|
||||
allowed: allow,
|
||||
@@ -588,38 +679,34 @@ export class NoauthBackend {
|
||||
self.accessBuffer.push(req)
|
||||
|
||||
// clear from pending
|
||||
const index = self.confirmBuffer.findIndex(
|
||||
(r) => r.req.id === id,
|
||||
)
|
||||
const index = self.confirmBuffer.findIndex((r) => r.req.id === id)
|
||||
if (index >= 0) self.confirmBuffer.splice(index, 1)
|
||||
|
||||
if (remember) {
|
||||
let perm = getReqPerm(req)
|
||||
if (allow && options && options.perm) perm = options.perm
|
||||
let newPerms = [getReqPerm(req)]
|
||||
if (allow && options && options.perms) newPerms = options.perms
|
||||
|
||||
// write new perms confirmed by user
|
||||
for (const p of newPerms) {
|
||||
await dbi.addPerm({
|
||||
id: req.id,
|
||||
npub: req.npub,
|
||||
appNpub: req.appNpub,
|
||||
perm,
|
||||
perm: p,
|
||||
value: allow ? '1' : '0',
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
// reload
|
||||
this.perms = await dbi.listPerms()
|
||||
|
||||
const otherReqs = self.confirmBuffer.filter(
|
||||
(r) => r.req.appNpub === req.appNpub,
|
||||
)
|
||||
console.log(
|
||||
'updated perms',
|
||||
this.perms,
|
||||
'otherReqs',
|
||||
otherReqs,
|
||||
)
|
||||
// confirm pending requests that might now have
|
||||
// the proper perms
|
||||
const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub)
|
||||
console.log('updated perms', this.perms, 'otherReqs', otherReqs, 'connected', connected)
|
||||
for (const r of otherReqs) {
|
||||
const perm = this.getPerm(r.req)
|
||||
// if (r.req.method === req.method) {
|
||||
let perm = this.getPerm(r.req)
|
||||
if (perm) {
|
||||
r.cb(perm === '1', false)
|
||||
}
|
||||
@@ -652,29 +739,30 @@ export class NoauthBackend {
|
||||
// put to a list of pending requests
|
||||
this.confirmBuffer.push({
|
||||
req,
|
||||
cb: (allow, remember, options) =>
|
||||
onAllow(true, allow, remember, options),
|
||||
cb: (allow, remember, options) => onAllow(true, allow, remember, options),
|
||||
})
|
||||
|
||||
// OAuth flow
|
||||
const confirmMethod = method === 'connect' ? 'confirm-connect' : 'confirm-event'
|
||||
const authUrl = `${self.swg.location.origin}/key/${npub}?${confirmMethod}=true&appNpub=${appNpub}&reqId=${id}&popup=true`
|
||||
console.log('sending authUrl', authUrl, 'for', req)
|
||||
// NOTE: if you set 'Update on reload' in the Chrome SW console
|
||||
// then this message will cause a new tab opened by the peer,
|
||||
// which will cause SW (this code) to reload, to fetch
|
||||
// the pending requests and to re-send this event,
|
||||
// looping for 10 seconds (our request age threshold)
|
||||
backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl)
|
||||
|
||||
// show notifs
|
||||
this.notify()
|
||||
// this.notify()
|
||||
|
||||
// notify main thread to ask for user concent
|
||||
// FIXME show a 'confirm' notification?
|
||||
this.updateUI()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private async startKey({
|
||||
npub,
|
||||
sk,
|
||||
backoff = 1000,
|
||||
}: {
|
||||
npub: string
|
||||
sk: string
|
||||
backoff?: number
|
||||
}) {
|
||||
private async startKey({ npub, sk, backoff = 1000 }: { npub: string; sk: string; backoff?: number }) {
|
||||
const ndk = new NDK({
|
||||
explicitRelayUrls: NIP46_RELAYS,
|
||||
})
|
||||
@@ -683,9 +771,7 @@ export class NoauthBackend {
|
||||
ndk.connect()
|
||||
|
||||
const signer = new NDKPrivateKeySigner(sk) // PrivateKeySigner
|
||||
const backend = new NDKNip46Backend(ndk, sk, () =>
|
||||
Promise.resolve(true),
|
||||
)
|
||||
const backend = new NDKNip46Backend(ndk, signer, () => Promise.resolve(true))
|
||||
this.keys.push({ npub, backend, signer, ndk, backoff })
|
||||
|
||||
// new method
|
||||
@@ -694,10 +780,11 @@ export class NoauthBackend {
|
||||
// assign our own permission callback
|
||||
for (const method in backend.handlers) {
|
||||
backend.handlers[method] = new EventHandlingStrategyWrapper(
|
||||
backend,
|
||||
npub,
|
||||
method,
|
||||
backend.handlers[method],
|
||||
this.allowPermitCallback.bind(this),
|
||||
this.allowPermitCallback.bind(this)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -725,13 +812,7 @@ export class NoauthBackend {
|
||||
// run full restart after a pause
|
||||
const bo = self.keys.find((k) => k.npub === npub)?.backoff || 1000
|
||||
setTimeout(() => {
|
||||
console.log(
|
||||
new Date(),
|
||||
'reconnect relays for key',
|
||||
npub,
|
||||
'backoff',
|
||||
bo,
|
||||
)
|
||||
console.log(new Date(), 'reconnect relays for key', npub, 'backoff', bo)
|
||||
// @ts-ignore
|
||||
for (const r of ndk.pool.relays.values()) r.disconnect()
|
||||
// make sure it no longer activates
|
||||
@@ -751,8 +832,7 @@ export class NoauthBackend {
|
||||
|
||||
public async unlock(npub: string) {
|
||||
console.log('unlocking', npub)
|
||||
if (!this.isLocked(npub))
|
||||
throw new Error(`Key ${npub} already unlocked`)
|
||||
if (!this.isLocked(npub)) throw new Error(`Key ${npub} already unlocked`)
|
||||
const info = this.enckeys.find((k) => k.npub === npub)
|
||||
if (!info) throw new Error(`Key ${npub} not found`)
|
||||
const { type } = nip19.decode(npub)
|
||||
@@ -765,14 +845,19 @@ export class NoauthBackend {
|
||||
await this.startKey({ npub, sk })
|
||||
}
|
||||
|
||||
private async generateKey() {
|
||||
const k = await this.addKey()
|
||||
private async generateKey(name: string) {
|
||||
const k = await this.addKey({ name })
|
||||
this.updateUI()
|
||||
return k
|
||||
}
|
||||
|
||||
private async importKey(nsec: string) {
|
||||
const k = await this.addKey(nsec)
|
||||
private async redeemToken(npub: string, token: string) {
|
||||
console.log('redeeming token', npub, token)
|
||||
await this.sendTokenToServer(npub, token)
|
||||
}
|
||||
|
||||
private async importKey(name: string, nsec: string) {
|
||||
const k = await this.addKey({ name, nsec })
|
||||
this.updateUI()
|
||||
return k
|
||||
}
|
||||
@@ -792,36 +877,65 @@ export class NoauthBackend {
|
||||
await this.sendKeyToServer(npub, enckey, pwh)
|
||||
}
|
||||
|
||||
private async fetchKey(npub: string, passphrase: string) {
|
||||
private async fetchKey(npub: string, passphrase: string, nip05: string) {
|
||||
const { type, data: pubkey } = nip19.decode(npub)
|
||||
if (type !== 'npub') throw new Error(`Invalid npub ${npub}`)
|
||||
const { pwh } = await this.keysModule.generatePassKey(
|
||||
pubkey,
|
||||
passphrase,
|
||||
)
|
||||
const { pwh } = await this.keysModule.generatePassKey(pubkey, passphrase)
|
||||
const { data: enckey } = await this.fetchKeyFromServer(npub, pwh)
|
||||
|
||||
// key already exists?
|
||||
const key = this.enckeys.find((k) => k.npub === npub)
|
||||
if (key) return this.keyInfo(key)
|
||||
|
||||
let name = ''
|
||||
let existingName = true
|
||||
// check name - user might have provided external nip05,
|
||||
// or just his npub - we must fetch their name from our
|
||||
// server, and if not exists - try to assign one
|
||||
const npubName = await this.fetchNpubName(npub)
|
||||
if (npubName) {
|
||||
// already have name for this npub
|
||||
console.log('existing npub name', npub, npubName)
|
||||
name = npubName
|
||||
} else if (nip05.includes('@')) {
|
||||
// no name for them?
|
||||
const [nip05name, domain] = nip05.split('@')
|
||||
if (domain === DOMAIN) {
|
||||
// wtf? how did we learn their npub if
|
||||
// it's the name on our server but we can't fetch it?
|
||||
console.log('existing name', nip05name)
|
||||
name = nip05name
|
||||
} else {
|
||||
// try to take same name on our domain
|
||||
existingName = false
|
||||
name = nip05name
|
||||
let takenName = await fetchNip05(`${name}@${DOMAIN}`)
|
||||
if (takenName) {
|
||||
// already taken? try name_domain as name
|
||||
name = `${nip05name}_${domain}`
|
||||
takenName = await fetchNip05(`${name}@${DOMAIN}`)
|
||||
}
|
||||
if (takenName) {
|
||||
console.log('All names taken, leave without a name?')
|
||||
name = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('fetch', { name, existingName })
|
||||
|
||||
// add new key
|
||||
const nsec = await this.keysModule.decryptKeyPass({
|
||||
pubkey,
|
||||
enckey,
|
||||
passphrase,
|
||||
})
|
||||
const k = await this.addKey(nsec)
|
||||
const k = await this.addKey({ name, nsec, existingName })
|
||||
this.updateUI()
|
||||
return k
|
||||
}
|
||||
|
||||
private async confirm(
|
||||
id: string,
|
||||
allow: boolean,
|
||||
remember: boolean,
|
||||
options?: any,
|
||||
) {
|
||||
private async confirm(id: string, allow: boolean, remember: boolean, options?: any) {
|
||||
const req = this.confirmBuffer.find((r) => r.req.id === id)
|
||||
if (!req) {
|
||||
console.log('req ', id, 'not found')
|
||||
@@ -854,8 +968,7 @@ export class NoauthBackend {
|
||||
applicationServerKey: WEB_PUSH_PUBKEY,
|
||||
}
|
||||
|
||||
const pushSubscription =
|
||||
await this.swg.registration.pushManager.subscribe(options)
|
||||
const pushSubscription = await this.swg.registration.pushManager.subscribe(options)
|
||||
console.log('push endpoint', JSON.stringify(pushSubscription))
|
||||
|
||||
if (!pushSubscription) {
|
||||
@@ -878,15 +991,19 @@ export class NoauthBackend {
|
||||
//console.log("UI message", id, method, args)
|
||||
let result = undefined
|
||||
if (method === 'generateKey') {
|
||||
result = await this.generateKey()
|
||||
result = await this.generateKey(args[0])
|
||||
} else if (method === 'redeemToken') {
|
||||
result = await this.redeemToken(args[0], args[1])
|
||||
} else if (method === 'importKey') {
|
||||
result = await this.importKey(args[0])
|
||||
result = await this.importKey(args[0], args[1])
|
||||
} else if (method === 'saveKey') {
|
||||
result = await this.saveKey(args[0], args[1])
|
||||
} else if (method === 'fetchKey') {
|
||||
result = await this.fetchKey(args[0], args[1])
|
||||
result = await this.fetchKey(args[0], args[1], args[2])
|
||||
} else if (method === 'confirm') {
|
||||
result = await this.confirm(args[0], args[1], args[2], args[3])
|
||||
} else if (method === 'connectApp') {
|
||||
result = await this.connectApp(args[0])
|
||||
} else if (method === 'deleteApp') {
|
||||
result = await this.deleteApp(args[0])
|
||||
} else if (method === 'deletePerm') {
|
||||
@@ -901,6 +1018,7 @@ export class NoauthBackend {
|
||||
result,
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.log('backend error', e)
|
||||
event.source.postMessage({
|
||||
id,
|
||||
error: e.toString(),
|
||||
@@ -909,7 +1027,9 @@ export class NoauthBackend {
|
||||
}
|
||||
|
||||
private async updateUI() {
|
||||
const clients = await this.swg.clients.matchAll()
|
||||
const clients = await this.swg.clients.matchAll({
|
||||
includeUncontrolled: true,
|
||||
})
|
||||
console.log('updateUI clients', clients.length)
|
||||
for (const client of clients) {
|
||||
client.postMessage({})
|
||||
|
||||
@@ -103,6 +103,17 @@ export const dbi = {
|
||||
console.log(`db addApp error: ${error}`)
|
||||
}
|
||||
},
|
||||
updateApp: async (app: Omit<DbApp, 'npub' | 'timestamp'>) => {
|
||||
try {
|
||||
await db.apps.where({ appNpub: app.appNpub }).modify({
|
||||
name: app.name,
|
||||
icon: app.icon,
|
||||
url: app.url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(`db updateApp error: ${error}`)
|
||||
}
|
||||
},
|
||||
listApps: async (): Promise<DbApp[]> => {
|
||||
try {
|
||||
return await db.apps.toArray()
|
||||
@@ -151,10 +162,8 @@ export const dbi = {
|
||||
try {
|
||||
return db.transaction('rw', db.pending, db.history, async () => {
|
||||
const exists =
|
||||
(await db.pending.where('id').equals(r.id).toArray())
|
||||
.length > 0 ||
|
||||
(await db.history.where('id').equals(r.id).toArray())
|
||||
.length > 0
|
||||
(await db.pending.where('id').equals(r.id).toArray()).length > 0 ||
|
||||
(await db.history.where('id').equals(r.id).toArray()).length > 0
|
||||
if (exists) return false
|
||||
|
||||
await db.pending.add(r)
|
||||
@@ -183,10 +192,7 @@ export const dbi = {
|
||||
confirmPending: async (id: string, allowed: boolean) => {
|
||||
try {
|
||||
db.transaction('rw', db.pending, db.history, async () => {
|
||||
const r: DbPending | undefined = await db.pending
|
||||
.where('id')
|
||||
.equals(id)
|
||||
.first()
|
||||
const r: DbPending | undefined = await db.pending.where('id').equals(id).first()
|
||||
if (!r) throw new Error('Pending not found ' + id)
|
||||
const h: DbHistory = {
|
||||
...r,
|
||||
|
||||
@@ -4,123 +4,92 @@ ende stands for encryption decryption
|
||||
import { secp256k1 as secp } from '@noble/curves/secp256k1'
|
||||
//import * as secp from "./vendor/secp256k1.js";
|
||||
|
||||
export async function encrypt(
|
||||
publicKey: string,
|
||||
message: string,
|
||||
privateKey: string,
|
||||
): Promise<string> {
|
||||
const key = secp.getSharedSecret(privateKey, "02" + publicKey);
|
||||
const normalizedKey = getNormalizedX(key);
|
||||
const encoder = new TextEncoder();
|
||||
const iv = Uint8Array.from(randomBytes(16));
|
||||
const plaintext = encoder.encode(message);
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
normalizedKey,
|
||||
{ name: "AES-CBC" },
|
||||
false,
|
||||
["encrypt"],
|
||||
);
|
||||
const ciphertext = await crypto.subtle.encrypt(
|
||||
{ name: "AES-CBC", iv },
|
||||
cryptoKey,
|
||||
plaintext,
|
||||
);
|
||||
export async function encrypt(publicKey: string, message: string, privateKey: string): Promise<string> {
|
||||
const key = secp.getSharedSecret(privateKey, '02' + publicKey)
|
||||
const normalizedKey = getNormalizedX(key)
|
||||
const encoder = new TextEncoder()
|
||||
const iv = Uint8Array.from(randomBytes(16))
|
||||
const plaintext = encoder.encode(message)
|
||||
const cryptoKey = await crypto.subtle.importKey('raw', normalizedKey, { name: 'AES-CBC' }, false, ['encrypt'])
|
||||
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, cryptoKey, plaintext)
|
||||
|
||||
const ctb64 = toBase64(new Uint8Array(ciphertext));
|
||||
const ivb64 = toBase64(new Uint8Array(iv.buffer));
|
||||
return `${ctb64}?iv=${ivb64}`;
|
||||
const ctb64 = toBase64(new Uint8Array(ciphertext))
|
||||
const ivb64 = toBase64(new Uint8Array(iv.buffer))
|
||||
return `${ctb64}?iv=${ivb64}`
|
||||
}
|
||||
|
||||
export async function decrypt(
|
||||
privateKey: string,
|
||||
publicKey: string,
|
||||
data: string,
|
||||
): Promise<string | Error> {
|
||||
const key = secp.getSharedSecret(privateKey, "02" + publicKey); // this line is very slow
|
||||
return decrypt_with_shared_secret(data, key);
|
||||
export async function decrypt(privateKey: string, publicKey: string, data: string): Promise<string | Error> {
|
||||
const key = secp.getSharedSecret(privateKey, '02' + publicKey) // this line is very slow
|
||||
return decrypt_with_shared_secret(data, key)
|
||||
}
|
||||
|
||||
export async function decrypt_with_shared_secret(
|
||||
data: string,
|
||||
sharedSecret: Uint8Array,
|
||||
): Promise<string | Error> {
|
||||
const [ctb64, ivb64] = data.split("?iv=");
|
||||
const normalizedKey = getNormalizedX(sharedSecret);
|
||||
export async function decrypt_with_shared_secret(data: string, sharedSecret: Uint8Array): Promise<string | Error> {
|
||||
const [ctb64, ivb64] = data.split('?iv=')
|
||||
const normalizedKey = getNormalizedX(sharedSecret)
|
||||
|
||||
const cryptoKey = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
normalizedKey,
|
||||
{ name: "AES-CBC" },
|
||||
false,
|
||||
["decrypt"],
|
||||
);
|
||||
let ciphertext: BufferSource;
|
||||
let iv: BufferSource;
|
||||
const cryptoKey = await crypto.subtle.importKey('raw', normalizedKey, { name: 'AES-CBC' }, false, ['decrypt'])
|
||||
let ciphertext: BufferSource
|
||||
let iv: BufferSource
|
||||
try {
|
||||
ciphertext = decodeBase64(ctb64);
|
||||
iv = decodeBase64(ivb64);
|
||||
ciphertext = decodeBase64(ctb64)
|
||||
iv = decodeBase64(ivb64)
|
||||
} catch (e) {
|
||||
return new Error(`failed to decode, ${e}`);
|
||||
return new Error(`failed to decode, ${e}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const plaintext = await crypto.subtle.decrypt(
|
||||
{ name: "AES-CBC", iv },
|
||||
cryptoKey,
|
||||
ciphertext,
|
||||
);
|
||||
const text = utf8Decode(plaintext);
|
||||
return text;
|
||||
const plaintext = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, ciphertext)
|
||||
const text = utf8Decode(plaintext)
|
||||
return text
|
||||
} catch (e) {
|
||||
return new Error(`failed to decrypt, ${e}`);
|
||||
return new Error(`failed to decrypt, ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
export function utf8Encode(str: string) {
|
||||
let encoder = new TextEncoder();
|
||||
return encoder.encode(str);
|
||||
let encoder = new TextEncoder()
|
||||
return encoder.encode(str)
|
||||
}
|
||||
|
||||
export function utf8Decode(bin: Uint8Array | ArrayBuffer): string {
|
||||
let decoder = new TextDecoder();
|
||||
return decoder.decode(bin);
|
||||
let decoder = new TextDecoder()
|
||||
return decoder.decode(bin)
|
||||
}
|
||||
|
||||
function toBase64(uInt8Array: Uint8Array) {
|
||||
let strChunks = new Array(uInt8Array.length);
|
||||
let i = 0;
|
||||
let strChunks = new Array(uInt8Array.length)
|
||||
let i = 0
|
||||
// @ts-ignore
|
||||
for (let byte of uInt8Array) {
|
||||
strChunks[i] = String.fromCharCode(byte); // bytes to utf16 string
|
||||
i++;
|
||||
strChunks[i] = String.fromCharCode(byte) // bytes to utf16 string
|
||||
i++
|
||||
}
|
||||
return btoa(strChunks.join(""));
|
||||
return btoa(strChunks.join(''))
|
||||
}
|
||||
|
||||
function decodeBase64(base64String: string) {
|
||||
const binaryString = atob(base64String);
|
||||
const length = binaryString.length;
|
||||
const bytes = new Uint8Array(length);
|
||||
const binaryString = atob(base64String)
|
||||
const length = binaryString.length
|
||||
const bytes = new Uint8Array(length)
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
bytes[i] = binaryString.charCodeAt(i)
|
||||
}
|
||||
return bytes;
|
||||
return bytes
|
||||
}
|
||||
|
||||
function getNormalizedX(key: Uint8Array): Uint8Array {
|
||||
return key.slice(1, 33);
|
||||
return key.slice(1, 33)
|
||||
}
|
||||
|
||||
function randomBytes(bytesLength: number = 32) {
|
||||
return crypto.getRandomValues(new Uint8Array(bytesLength));
|
||||
return crypto.getRandomValues(new Uint8Array(bytesLength))
|
||||
}
|
||||
|
||||
export function utf16Encode(str: string): number[] {
|
||||
let array = new Array(str.length);
|
||||
let array = new Array(str.length)
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
array[i] = str.charCodeAt(i);
|
||||
array[i] = str.charCodeAt(i)
|
||||
}
|
||||
return array;
|
||||
return array
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import crypto, { pbkdf2 } from 'crypto';
|
||||
import { getPublicKey, nip19 } from 'nostr-tools';
|
||||
import crypto, { pbkdf2 } from 'crypto'
|
||||
import { getPublicKey, nip19 } from 'nostr-tools'
|
||||
|
||||
// encrypted keys have a prefix and version
|
||||
// so that we'd be able to switch to a better
|
||||
@@ -17,14 +17,14 @@ const ITERATIONS_PWH = 100000
|
||||
const HASH_SIZE = 32
|
||||
const HASH_ALGO = 'sha256'
|
||||
// encryption
|
||||
const ALGO = 'aes-256-cbc';
|
||||
const ALGO = 'aes-256-cbc'
|
||||
const IV_SIZE = 16
|
||||
|
||||
// valid passwords are a limited ASCII only, see notes below
|
||||
const ASCII_REGEX = /^[A-Za-z0-9!@#$%^&*()]{4,}$/
|
||||
|
||||
const ALGO_LOCAL = 'AES-CBC';
|
||||
const KEY_SIZE_LOCAL = 256;
|
||||
const ALGO_LOCAL = 'AES-CBC'
|
||||
const KEY_SIZE_LOCAL = 256
|
||||
|
||||
export class Keys {
|
||||
subtle: any
|
||||
@@ -37,9 +37,7 @@ export class Keys {
|
||||
return ASCII_REGEX.test(passphrase)
|
||||
}
|
||||
|
||||
public async generatePassKey(pubkey: string, passphrase: string)
|
||||
: Promise<{ passkey: Buffer, pwh: string }> {
|
||||
|
||||
public async generatePassKey(pubkey: string, passphrase: string): Promise<{ passkey: Buffer; pwh: string }> {
|
||||
const salt = Buffer.from(pubkey, 'hex')
|
||||
|
||||
// https://nodejs.org/api/crypto.html#using-strings-as-inputs-to-cryptographic-apis
|
||||
@@ -47,7 +45,7 @@ export class Keys {
|
||||
// We could use string.normalize() to make sure all JS implementations
|
||||
// are compatible, but since we're looking to make this thing a standard
|
||||
// then the simplest way is to exclude unicode and only work with ASCII
|
||||
if (!this.isValidPassphase(passphrase)) throw new Error("Password must be 4+ ASCII chars")
|
||||
if (!this.isValidPassphase(passphrase)) throw new Error('Password must be 4+ ASCII chars')
|
||||
|
||||
return new Promise((ok, fail) => {
|
||||
// NOTE: we should use Argon2 or scrypt later, for now
|
||||
@@ -57,7 +55,11 @@ export class Keys {
|
||||
else {
|
||||
pbkdf2(key, passphrase, ITERATIONS_PWH, HASH_SIZE, HASH_ALGO, (err, hash) => {
|
||||
if (err) fail(err)
|
||||
else ok({ passkey: key, pwh: hash.toString('hex') })
|
||||
else
|
||||
ok({
|
||||
passkey: key,
|
||||
pwh: hash.toString('hex'),
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -65,8 +67,8 @@ export class Keys {
|
||||
}
|
||||
|
||||
private isSafari() {
|
||||
const chrome = navigator.userAgent.indexOf("Chrome") > -1;
|
||||
const safari = navigator.userAgent.indexOf("Safari") > -1;
|
||||
const chrome = navigator.userAgent.indexOf('Chrome') > -1
|
||||
const safari = navigator.userAgent.indexOf('Safari') > -1
|
||||
return safari && !chrome
|
||||
}
|
||||
|
||||
@@ -81,8 +83,8 @@ export class Keys {
|
||||
{ name: ALGO_LOCAL, length: KEY_SIZE_LOCAL },
|
||||
// NOTE: important to make sure it's not visible in
|
||||
// dev console in IndexedDB
|
||||
/*extractable*/false,
|
||||
["encrypt", "decrypt"]
|
||||
/*extractable*/ false,
|
||||
['encrypt', 'decrypt']
|
||||
)
|
||||
}
|
||||
|
||||
@@ -94,25 +96,30 @@ export class Keys {
|
||||
return `${PREFIX_LOCAL}:${VERSION_LOCAL}:${iv.toString('hex')}:${Buffer.from(encrypted).toString('hex')}}`
|
||||
}
|
||||
|
||||
public async decryptKeyLocal({ enckey, localKey }: { enckey: string, localKey: CryptoKey | {} }): Promise<string> {
|
||||
public async decryptKeyLocal({ enckey, localKey }: { enckey: string; localKey: CryptoKey | {} }): Promise<string> {
|
||||
if (this.isSafari()) return enckey
|
||||
const parts = enckey.split(':')
|
||||
if (parts.length !== 4) throw new Error("Bad encrypted key")
|
||||
if (parts[0] !== PREFIX_LOCAL) throw new Error("Bad encrypted key prefix")
|
||||
if (parts[1] !== VERSION_LOCAL) throw new Error("Bad encrypted key version")
|
||||
if (parts[2].length !== IV_SIZE * 2) throw new Error("Bad encrypted key iv")
|
||||
if (parts[3].length < 30) throw new Error("Bad encrypted key data")
|
||||
const iv = Buffer.from(parts[2], 'hex');
|
||||
const data = Buffer.from(parts[3], 'hex');
|
||||
if (parts.length !== 4) throw new Error('Bad encrypted key')
|
||||
if (parts[0] !== PREFIX_LOCAL) throw new Error('Bad encrypted key prefix')
|
||||
if (parts[1] !== VERSION_LOCAL) throw new Error('Bad encrypted key version')
|
||||
if (parts[2].length !== IV_SIZE * 2) throw new Error('Bad encrypted key iv')
|
||||
if (parts[3].length < 30) throw new Error('Bad encrypted key data')
|
||||
const iv = Buffer.from(parts[2], 'hex')
|
||||
const data = Buffer.from(parts[3], 'hex')
|
||||
const decrypted = await this.subtle.decrypt({ name: ALGO_LOCAL, iv }, localKey, data)
|
||||
const { type, data: value } = nip19.decode(Buffer.from(decrypted).toString())
|
||||
if (type !== "nsec") throw new Error("Bad encrypted key payload type")
|
||||
if ((value as string).length !== 64) throw new Error("Bad encrypted key payload length")
|
||||
return (value as string)
|
||||
if (type !== 'nsec') throw new Error('Bad encrypted key payload type')
|
||||
if ((value as string).length !== 64) throw new Error('Bad encrypted key payload length')
|
||||
return value as string
|
||||
}
|
||||
|
||||
public async encryptKeyPass({ key, passphrase }: { key: string, passphrase: string })
|
||||
: Promise<{ enckey: string, pwh: string }> {
|
||||
public async encryptKeyPass({
|
||||
key,
|
||||
passphrase,
|
||||
}: {
|
||||
key: string
|
||||
passphrase: string
|
||||
}): Promise<{ enckey: string; pwh: string }> {
|
||||
const start = Date.now()
|
||||
const nsec = nip19.nsecEncode(key)
|
||||
const pubkey = getPublicKey(key)
|
||||
@@ -120,21 +127,29 @@ export class Keys {
|
||||
const iv = crypto.randomBytes(IV_SIZE)
|
||||
const cipher = crypto.createCipheriv(ALGO, passkey, iv)
|
||||
const encrypted = Buffer.concat([cipher.update(nsec), cipher.final()])
|
||||
console.log("encrypted key in ", Date.now() - start)
|
||||
console.log('encrypted key in ', Date.now() - start)
|
||||
return {
|
||||
enckey: `${PREFIX}:${VERSION}:${iv.toString('hex')}:${encrypted.toString('hex')}}`,
|
||||
pwh
|
||||
enckey: `${PREFIX}:${VERSION}:${iv.toString('hex')}:${encrypted.toString('hex')}`,
|
||||
pwh,
|
||||
}
|
||||
}
|
||||
|
||||
public async decryptKeyPass({ pubkey, enckey, passphrase }: { pubkey: string, enckey: string, passphrase: string }): Promise<string> {
|
||||
public async decryptKeyPass({
|
||||
pubkey,
|
||||
enckey,
|
||||
passphrase,
|
||||
}: {
|
||||
pubkey: string
|
||||
enckey: string
|
||||
passphrase: string
|
||||
}): Promise<string> {
|
||||
const start = Date.now()
|
||||
const parts = enckey.split(':')
|
||||
if (parts.length !== 4) throw new Error("Bad encrypted key")
|
||||
if (parts[0] !== PREFIX) throw new Error("Bad encrypted key prefix")
|
||||
if (parts[1] !== VERSION) throw new Error("Bad encrypted key version")
|
||||
if (parts[2].length !== IV_SIZE * 2) throw new Error("Bad encrypted key iv")
|
||||
if (parts[3].length < 30) throw new Error("Bad encrypted key data")
|
||||
if (parts.length !== 4) throw new Error('Bad encrypted key')
|
||||
if (parts[0] !== PREFIX) throw new Error('Bad encrypted key prefix')
|
||||
if (parts[1] !== VERSION) throw new Error('Bad encrypted key version')
|
||||
if (parts[2].length !== IV_SIZE * 2) throw new Error('Bad encrypted key iv')
|
||||
if (parts[3].length < 30) throw new Error('Bad encrypted key data')
|
||||
const { passkey } = await this.generatePassKey(pubkey, passphrase)
|
||||
const iv = Buffer.from(parts[2], 'hex')
|
||||
const data = Buffer.from(parts[3], 'hex')
|
||||
@@ -142,9 +157,9 @@ export class Keys {
|
||||
const decrypted = Buffer.concat([decipher.update(data), decipher.final()])
|
||||
const nsec = decrypted.toString()
|
||||
const { type, data: value } = nip19.decode(nsec)
|
||||
if (type !== "nsec") throw new Error("Bad encrypted key payload type")
|
||||
if (value.length !== 64) throw new Error("Bad encrypted key payload length")
|
||||
console.log("decrypted key in ", Date.now() - start)
|
||||
return nsec;
|
||||
if (type !== 'nsec') throw new Error('Bad encrypted key payload type')
|
||||
if (value.length !== 64) throw new Error('Bad encrypted key payload length')
|
||||
console.log('decrypted key in ', Date.now() - start)
|
||||
return nsec
|
||||
}
|
||||
}
|
||||
@@ -7,25 +7,25 @@ export const utf8Decoder = new TextDecoder('utf-8')
|
||||
export const utf8Encoder = new TextEncoder()
|
||||
|
||||
function toBase64(uInt8Array: Uint8Array) {
|
||||
let strChunks = new Array(uInt8Array.length);
|
||||
let i = 0;
|
||||
let strChunks = new Array(uInt8Array.length)
|
||||
let i = 0
|
||||
// @ts-ignore
|
||||
for (let byte of uInt8Array) {
|
||||
strChunks[i] = String.fromCharCode(byte); // bytes to utf16 string
|
||||
i++;
|
||||
strChunks[i] = String.fromCharCode(byte) // bytes to utf16 string
|
||||
i++
|
||||
}
|
||||
return btoa(strChunks.join(""));
|
||||
return btoa(strChunks.join(''))
|
||||
}
|
||||
|
||||
function fromBase64(base64String: string) {
|
||||
const binaryString = atob(base64String);
|
||||
const length = binaryString.length;
|
||||
const bytes = new Uint8Array(length);
|
||||
const binaryString = atob(base64String)
|
||||
const length = binaryString.length
|
||||
const bytes = new Uint8Array(length)
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
bytes[i] = binaryString.charCodeAt(i);
|
||||
bytes[i] = binaryString.charCodeAt(i)
|
||||
}
|
||||
return bytes;
|
||||
return bytes
|
||||
}
|
||||
|
||||
function getNormalizedX(key: Uint8Array): Uint8Array {
|
||||
@@ -65,7 +65,7 @@ export class Nip04 {
|
||||
// let ctb64 = toBase64(new Uint8Array(ciphertext))
|
||||
// let ivb64 = toBase64(new Uint8Array(iv.buffer))
|
||||
|
||||
console.log("nip04_encrypt", text, "t1", t2 - t1, "t2", t3 - t2, "t3", Date.now() - t3)
|
||||
console.log('nip04_encrypt', text, 't1', t2 - t1, 't2', t3 - t2, 't3', Date.now() - t3)
|
||||
|
||||
return `${ctb64}?iv=${ivb64}`
|
||||
}
|
||||
@@ -85,7 +85,4 @@ export class Nip04 {
|
||||
let text = utf8Decoder.decode(plaintext)
|
||||
return text
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,12 +5,7 @@ import NDK, { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk'
|
||||
import { nip19 } from 'nostr-tools'
|
||||
|
||||
export const ndk = new NDK({
|
||||
explicitRelayUrls: [
|
||||
'wss://relay.nostr.band/all',
|
||||
'wss://relay.nostr.band',
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol',
|
||||
],
|
||||
explicitRelayUrls: ['wss://relay.nostr.band/all', 'wss://relay.nostr.band', 'wss://relay.damus.io', 'wss://nos.lol'],
|
||||
})
|
||||
|
||||
export function nostrEvent(e: Required<NDKEvent>) {
|
||||
@@ -41,17 +36,11 @@ function parseContentJson(c: string): object {
|
||||
}
|
||||
}
|
||||
|
||||
export function getTags(
|
||||
e: AugmentedEvent | NDKEvent | MetaEvent,
|
||||
name: string,
|
||||
): string[][] {
|
||||
export function getTags(e: AugmentedEvent | NDKEvent | MetaEvent, name: string): string[][] {
|
||||
return e.tags.filter((t: string[]) => t.length > 0 && t[0] === name)
|
||||
}
|
||||
|
||||
export function getTag(
|
||||
e: AugmentedEvent | NDKEvent | MetaEvent,
|
||||
name: string,
|
||||
): string[] | null {
|
||||
export function getTag(e: AugmentedEvent | NDKEvent | MetaEvent, name: string): string[] | null {
|
||||
const tags = getTags(e, name)
|
||||
if (tags.length === 0) return null
|
||||
return tags[0]
|
||||
@@ -61,11 +50,10 @@ export function getTagValue(
|
||||
e: AugmentedEvent | NDKEvent | MetaEvent,
|
||||
name: string,
|
||||
index: number = 0,
|
||||
def: string = '',
|
||||
def: string = ''
|
||||
): string {
|
||||
const tag = getTag(e, name)
|
||||
if (tag === null || !tag.length || (index && index >= tag.length))
|
||||
return def
|
||||
if (tag === null || !tag.length || (index && index >= tag.length)) return def
|
||||
return tag[1 + index]
|
||||
}
|
||||
|
||||
|
||||
51
src/modules/pow.ts
Normal file
51
src/modules/pow.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// based on https://git.v0l.io/Kieran/snort/src/branch/main/packages/system/src/pow-util.ts
|
||||
|
||||
import { sha256 } from '@noble/hashes/sha256'
|
||||
import { bytesToHex } from '@noble/hashes/utils'
|
||||
|
||||
export interface NostrPowEvent {
|
||||
id?: string
|
||||
pubkey: string
|
||||
created_at: number
|
||||
kind?: number
|
||||
tags: Array<Array<string>>
|
||||
content: string
|
||||
sig?: string
|
||||
}
|
||||
|
||||
export function minePow(e: NostrPowEvent, target: number) {
|
||||
let ctr = 0
|
||||
|
||||
let nonceTagIdx = e.tags.findIndex((a) => a[0] === 'nonce')
|
||||
if (nonceTagIdx === -1) {
|
||||
nonceTagIdx = e.tags.length
|
||||
e.tags.push(['nonce', ctr.toString(), target.toString()])
|
||||
}
|
||||
do {
|
||||
e.tags[nonceTagIdx][1] = (++ctr).toString()
|
||||
e.id = createId(e)
|
||||
} while (countLeadingZeros(e.id) < target)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
function createId(e: NostrPowEvent) {
|
||||
const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content]
|
||||
return bytesToHex(sha256(JSON.stringify(payload)))
|
||||
}
|
||||
|
||||
export function countLeadingZeros(hex: string) {
|
||||
let count = 0
|
||||
|
||||
for (let i = 0; i < hex.length; i++) {
|
||||
const nibble = parseInt(hex[i], 16)
|
||||
if (nibble === 0) {
|
||||
count += 4
|
||||
} else {
|
||||
count += Math.clz32(nibble) - 28
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return count
|
||||
}
|
||||
@@ -53,11 +53,7 @@ export class PrivateKeySigner implements NDKSigner {
|
||||
}
|
||||
|
||||
const recipientHexPubKey = recipient.hexpubkey
|
||||
return await this.nip04.encrypt(
|
||||
this.privateKey,
|
||||
recipientHexPubKey,
|
||||
value,
|
||||
)
|
||||
return await this.nip04.encrypt(this.privateKey, recipientHexPubKey, value)
|
||||
// return await encrypt(recipientHexPubKey, value, this.privateKey);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,15 @@ export async function swicRegister() {
|
||||
},
|
||||
})
|
||||
|
||||
navigator.serviceWorker.ready.then((r) => (swr = r))
|
||||
navigator.serviceWorker.ready.then((r) => {
|
||||
console.log('sw ready')
|
||||
swr = r
|
||||
if (navigator.serviceWorker.controller) {
|
||||
console.log(`This page is currently controlled by: ${navigator.serviceWorker.controller}`)
|
||||
} else {
|
||||
console.log('This page is not currently controlled by a service worker.')
|
||||
}
|
||||
})
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
onMessage((event as MessageEvent).data)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useParams } from 'react-router'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectAppByAppNpub, selectPermsByNpubAndAppNpub } from '@/store'
|
||||
import { selectAppByAppNpub, selectKeys, selectPermsByNpubAndAppNpub } from '@/store'
|
||||
import { Navigate, useNavigate } from 'react-router-dom'
|
||||
import { formatTimestampDate } from '@/utils/helpers/date'
|
||||
import { Box, Stack, Typography } from '@mui/material'
|
||||
import { Box, IconButton, Stack, Typography } from '@mui/material'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { ACTION_TYPE } from '@/utils/consts'
|
||||
import { Permissions } from './components/Permissions/Permissions'
|
||||
@@ -18,103 +18,92 @@ import { IOSBackButton } from '@/shared/IOSBackButton/IOSBackButton'
|
||||
import { ModalActivities } from './components/Activities/ModalActivities'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import MoreIcon from '@mui/icons-material/MoreVertRounded'
|
||||
import { ModalAppDetails } from '@/components/Modal/ModalAppDetails/ModalAppDetails'
|
||||
|
||||
const AppPage = () => {
|
||||
const keys = useAppSelector(selectKeys)
|
||||
|
||||
const { appNpub = '', npub = '' } = useParams()
|
||||
const currentApp = useAppSelector((state) => selectAppByAppNpub(state, appNpub))
|
||||
const perms = useAppSelector((state) => selectPermsByNpubAndAppNpub(state, npub, appNpub))
|
||||
|
||||
const navigate = useNavigate()
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
const perms = useAppSelector((state) =>
|
||||
selectPermsByNpubAndAppNpub(state, npub, appNpub),
|
||||
)
|
||||
const currentApp = useAppSelector((state) =>
|
||||
selectAppByAppNpub(state, appNpub),
|
||||
)
|
||||
|
||||
const { open, handleClose, handleShow } = useToggleConfirm()
|
||||
const { handleOpen: handleOpenModal } = useModalSearchParams()
|
||||
|
||||
const connectPerm = perms.find(
|
||||
(perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC,
|
||||
)
|
||||
const connectPerm = perms.find((perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC)
|
||||
|
||||
if (!currentApp) {
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
|
||||
if (!isNpubExists || !currentApp) {
|
||||
return <Navigate to={`/key/${npub}`} />
|
||||
}
|
||||
|
||||
const { icon = '', name = '' } = currentApp || {}
|
||||
const appName = name || getShortenNpub(appNpub)
|
||||
const { timestamp } = connectPerm || {}
|
||||
const { icon = '', name = '', url = '' } = currentApp || {}
|
||||
const appDomain = getDomain(url)
|
||||
const appName = name || appDomain || getShortenNpub(appNpub)
|
||||
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||
|
||||
const connectedOn =
|
||||
connectPerm && timestamp
|
||||
? `Connected at ${formatTimestampDate(timestamp)}`
|
||||
: 'Not connected'
|
||||
const { timestamp } = connectPerm || {}
|
||||
const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected'
|
||||
|
||||
const handleDeleteApp = async () => {
|
||||
try {
|
||||
await swicCall('deleteApp', appNpub)
|
||||
notify(`App: «${appName}» successfully deleted!`, 'success')
|
||||
navigate(`key/${npub}`)
|
||||
navigate(`/key/${npub}`)
|
||||
} catch (error: any) {
|
||||
notify(error?.message || 'Failed to delete app', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowAppDetailsModal = () => handleOpenModal(MODAL_PARAMS_KEYS.APP_DETAILS)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
maxHeight={'100%'}
|
||||
overflow={'auto'}
|
||||
alignItems={'flex-start'}
|
||||
height={'100%'}
|
||||
>
|
||||
<Stack maxHeight={'100%'} overflow={'auto'} alignItems={'flex-start'} height={'100%'}>
|
||||
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
|
||||
<Stack
|
||||
marginBottom={'1rem'}
|
||||
direction={'row'}
|
||||
gap={'1rem'}
|
||||
width={'100%'}
|
||||
>
|
||||
<StyledAppIcon src={icon} />
|
||||
<Stack marginBottom={'1rem'} direction={'row'} gap={'1rem'} width={'100%'}>
|
||||
<StyledAppIcon src={icon}>{appAvatarTitle}</StyledAppIcon>
|
||||
<Box flex={'1'} overflow={'hidden'}>
|
||||
<Typography variant='h4' noWrap>
|
||||
<Stack direction={'row'} alignItems={'center'} gap={'0.5rem'}>
|
||||
<Typography variant="h4" noWrap flex={1}>
|
||||
{appName}
|
||||
</Typography>
|
||||
<Typography variant='body2' noWrap>
|
||||
<IconButton onClick={handleShowAppDetailsModal}>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Typography variant="body2" noWrap>
|
||||
{connectedOn}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box marginBottom={'1rem'}>
|
||||
<SectionTitle marginBottom={'0.5rem'}>
|
||||
Disconnect
|
||||
</SectionTitle>
|
||||
<SectionTitle marginBottom={'0.5rem'}>Disconnect</SectionTitle>
|
||||
<Button fullWidth onClick={handleShow}>
|
||||
Delete app
|
||||
</Button>
|
||||
</Box>
|
||||
<Permissions perms={perms} />
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() =>
|
||||
handleOpenModal(MODAL_PARAMS_KEYS.ACTIVITY)
|
||||
}
|
||||
>
|
||||
<Button fullWidth onClick={() => handleOpenModal(MODAL_PARAMS_KEYS.ACTIVITY)}>
|
||||
Activity
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<ConfirmModal
|
||||
open={open}
|
||||
headingText='Delete app'
|
||||
description='Are you sure you want to delete this app?'
|
||||
headingText="Delete app"
|
||||
description="Are you sure you want to delete this app?"
|
||||
onCancel={handleClose}
|
||||
onConfirm={handleDeleteApp}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
<ModalActivities appNpub={appNpub} />
|
||||
<ModalAppDetails />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,33 +10,16 @@ import { ACTIONS } from '@/utils/consts'
|
||||
|
||||
type ItemActivityProps = DbHistory
|
||||
|
||||
export const ItemActivity: FC<ItemActivityProps> = ({
|
||||
allowed,
|
||||
method,
|
||||
timestamp,
|
||||
}) => {
|
||||
export const ItemActivity: FC<ItemActivityProps> = ({ allowed, method, timestamp }) => {
|
||||
return (
|
||||
<StyledActivityItem>
|
||||
<Box
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
gap={'0.5rem'}
|
||||
flex={1}
|
||||
>
|
||||
<Box display={'flex'} flexDirection={'column'} gap={'0.5rem'} flex={1}>
|
||||
<Typography flex={1} fontWeight={700}>
|
||||
{ACTIONS[method] || method}
|
||||
</Typography>
|
||||
<Typography variant='body2'>
|
||||
{formatTimestampDate(timestamp)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
{allowed ? (
|
||||
<DoneRoundedIcon htmlColor='green' />
|
||||
) : (
|
||||
<ClearRoundedIcon htmlColor='red' />
|
||||
)}
|
||||
<Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
|
||||
</Box>
|
||||
<Box>{allowed ? <DoneRoundedIcon htmlColor="green" /> : <ClearRoundedIcon htmlColor="red" />}</Box>
|
||||
<IconButton>
|
||||
<MoreVertRoundedIcon />
|
||||
</IconButton>
|
||||
|
||||
@@ -16,19 +16,10 @@ export const ModalActivities: FC<ModalActivitiesProps> = ({ appNpub }) => {
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.ACTIVITY)
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.ACTIVITY)
|
||||
|
||||
const history = useLiveQuery(
|
||||
getActivityHistoryQuerier(appNpub),
|
||||
[],
|
||||
HistoryDefaultValue,
|
||||
)
|
||||
const history = useLiveQuery(getActivityHistoryQuerier(appNpub), [], HistoryDefaultValue)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={isModalOpened}
|
||||
onClose={handleCloseModal}
|
||||
fixedHeight='calc(100% - 5rem)'
|
||||
title='Activity history'
|
||||
>
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal} fixedHeight="calc(100% - 5rem)" title="Activity history">
|
||||
<Box overflow={'auto'}>
|
||||
{history.map((item) => {
|
||||
return <ItemActivity {...item} key={item.id} />
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import styled from '@emotion/styled'
|
||||
import { Box, BoxProps } from '@mui/material'
|
||||
|
||||
export const StyledActivityItem = styled((props: BoxProps) => (
|
||||
<Box {...props} />
|
||||
))(() => ({
|
||||
export const StyledActivityItem = styled((props: BoxProps) => <Box {...props} />)(() => ({
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
@@ -2,20 +2,20 @@ import { FC } from 'react'
|
||||
import { Box, IconButton, Typography } from '@mui/material'
|
||||
import { DbPerm } from '@/modules/db'
|
||||
import { formatTimestampDate } from '@/utils/helpers/date'
|
||||
import { ACTIONS } from '@/utils/consts'
|
||||
import { StyledPermissionItem } from './styled'
|
||||
import ClearRoundedIcon from '@mui/icons-material/ClearRounded'
|
||||
import DoneRoundedIcon from '@mui/icons-material/DoneRounded'
|
||||
import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded'
|
||||
import { ItemPermissionMenu } from './ItemPermissionMenu'
|
||||
import { useOpenMenu } from '@/hooks/useOpenMenu'
|
||||
import { getPermActionName } from '@/utils/helpers/helpers'
|
||||
|
||||
type ItemPermissionProps = {
|
||||
permission: DbPerm
|
||||
}
|
||||
|
||||
export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => {
|
||||
const { perm, value, timestamp, id } = permission || {}
|
||||
const { value, timestamp, id } = permission || {}
|
||||
|
||||
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()
|
||||
|
||||
@@ -24,36 +24,18 @@ export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => {
|
||||
return (
|
||||
<>
|
||||
<StyledPermissionItem>
|
||||
<Box
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
gap={'0.5rem'}
|
||||
flex={1}
|
||||
>
|
||||
<Box display={'flex'} flexDirection={'column'} gap={'0.5rem'} flex={1}>
|
||||
<Typography flex={1} fontWeight={700}>
|
||||
{ACTIONS[perm] || perm}
|
||||
</Typography>
|
||||
<Typography variant='body2'>
|
||||
{formatTimestampDate(timestamp)}
|
||||
{getPermActionName(permission)}
|
||||
</Typography>
|
||||
<Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
{isAllowed ? (
|
||||
<DoneRoundedIcon htmlColor='green' />
|
||||
) : (
|
||||
<ClearRoundedIcon htmlColor='red' />
|
||||
)}
|
||||
</Box>
|
||||
<Box>{isAllowed ? <DoneRoundedIcon htmlColor="green" /> : <ClearRoundedIcon htmlColor="red" />}</Box>
|
||||
<IconButton onClick={handleOpen}>
|
||||
<MoreVertRoundedIcon />
|
||||
</IconButton>
|
||||
</StyledPermissionItem>
|
||||
<ItemPermissionMenu
|
||||
anchorEl={anchorEl}
|
||||
open={open}
|
||||
handleClose={handleClose}
|
||||
permId={id}
|
||||
/>
|
||||
<ItemPermissionMenu anchorEl={anchorEl} open={open} handleClose={handleClose} permId={id} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,12 +9,7 @@ type ItemPermissionMenuProps = {
|
||||
handleClose: () => void
|
||||
} & MenuProps
|
||||
|
||||
export const ItemPermissionMenu: FC<ItemPermissionMenuProps> = ({
|
||||
open,
|
||||
anchorEl,
|
||||
handleClose,
|
||||
permId,
|
||||
}) => {
|
||||
export const ItemPermissionMenu: FC<ItemPermissionMenuProps> = ({ open, anchorEl, handleClose, permId }) => {
|
||||
const [showConfirm, setShowConfirm] = useState(false)
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
@@ -45,16 +40,14 @@ export const ItemPermissionMenu: FC<ItemPermissionMenuProps> = ({
|
||||
vertical: 'bottom',
|
||||
}}
|
||||
>
|
||||
<MenuItem onClick={handleShowConfirm}>
|
||||
Delete permission
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleShowConfirm}>Delete permission</MenuItem>
|
||||
</Menu>
|
||||
<ConfirmModal
|
||||
open={showConfirm}
|
||||
onClose={handleCloseConfirm}
|
||||
onCancel={handleCloseConfirm}
|
||||
headingText='Delete permission'
|
||||
description='Are you sure you want to delete this permission?'
|
||||
headingText="Delete permission"
|
||||
description="Are you sure you want to delete this permission?"
|
||||
onConfirm={handleDeletePerm}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -12,13 +12,7 @@ export const Permissions: FC<PermissionsProps> = ({ perms }) => {
|
||||
return (
|
||||
<Box width={'100%'} marginBottom={'1rem'} flex={1} overflow={'auto'}>
|
||||
<SectionTitle marginBottom={'0.5rem'}>Permissions</SectionTitle>
|
||||
<Box
|
||||
flex={1}
|
||||
overflow={'auto'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
gap={'0.5rem'}
|
||||
>
|
||||
<Box flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={'0.5rem'}>
|
||||
{perms.map((perm) => {
|
||||
return <ItemPermission key={perm.id} permission={perm} />
|
||||
})}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Box, BoxProps, styled } from '@mui/material'
|
||||
|
||||
export const StyledPermissionItem = styled((props: BoxProps) => (
|
||||
<Box {...props} />
|
||||
))(() => ({
|
||||
export const StyledPermissionItem = styled((props: BoxProps) => <Box {...props} />)(() => ({
|
||||
display: 'flex',
|
||||
gap: '0.5rem',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Avatar, AvatarProps, styled } from '@mui/material'
|
||||
|
||||
export const StyledAppIcon = styled((props: AvatarProps) => (
|
||||
<Avatar {...props} variant='rounded' />
|
||||
))(() => ({
|
||||
export const StyledAppIcon = styled((props: AvatarProps) => <Avatar {...props} variant="rounded" />)(() => ({
|
||||
width: 70,
|
||||
height: 70,
|
||||
}))
|
||||
|
||||
@@ -8,7 +8,7 @@ export const getActivityHistoryQuerier = (appNpub: string) => () => {
|
||||
.equals(appNpub)
|
||||
.reverse()
|
||||
.sortBy('timestamp')
|
||||
.then(a => a.slice(0, 30))
|
||||
.then((a) => a.slice(0, 30))
|
||||
// .limit(30)
|
||||
// .toArray()
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { StyledAppLogo, StyledContent } from './styled'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { ChangeEvent, useState } from 'react'
|
||||
import { CheckmarkIcon } from '@/assets'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
|
||||
const AuthPage = () => {
|
||||
const isMobile = useMediaQuery('(max-width:600px)')
|
||||
@@ -29,21 +30,17 @@ const AuthPage = () => {
|
||||
const mainContent = (
|
||||
<>
|
||||
<Input
|
||||
label='Enter a Username'
|
||||
label="Enter a Username"
|
||||
fullWidth
|
||||
placeholder='Username'
|
||||
placeholder="Username"
|
||||
helperText={inputHelperText}
|
||||
endAdornment={
|
||||
<Typography color={'#FFFFFFA8'}>@nsec.app</Typography>
|
||||
}
|
||||
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||
onChange={handleInputChange}
|
||||
value={enteredValue}
|
||||
helperTextProps={{
|
||||
sx: {
|
||||
'&.helper_text': {
|
||||
color: isAvailable
|
||||
? theme.palette.success.main
|
||||
: theme.palette.textSecondaryDecorate.main,
|
||||
color: isAvailable ? theme.palette.success.main : theme.palette.textSecondaryDecorate.main,
|
||||
},
|
||||
},
|
||||
}}
|
||||
@@ -56,14 +53,9 @@ const AuthPage = () => {
|
||||
<Stack height={'100%'} position={'relative'}>
|
||||
{isMobile ? (
|
||||
<StyledContent>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
gap={'1rem'}
|
||||
alignItems={'center'}
|
||||
alignSelf={'flex-start'}
|
||||
>
|
||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
||||
<StyledAppLogo />
|
||||
<Typography fontWeight={600} variant='h5'>
|
||||
<Typography fontWeight={600} variant="h5">
|
||||
Sign up
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { AppLogo } from '@/assets'
|
||||
import { Stack, styled, StackProps, Box } from '@mui/material'
|
||||
|
||||
export const StyledContent = styled((props: StackProps) => (
|
||||
<Stack {...props} gap={'1rem'} alignItems={'center'} />
|
||||
))(({ theme }) => {
|
||||
export const StyledContent = styled((props: StackProps) => <Stack {...props} gap={'1rem'} alignItems={'center'} />)(({
|
||||
theme,
|
||||
}) => {
|
||||
return {
|
||||
background: theme.palette.secondary.main,
|
||||
position: 'absolute',
|
||||
|
||||
119
src/pages/CreatePage/Create.Page.tsx
Normal file
119
src/pages/CreatePage/Create.Page.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
import { GetStartedButton, LearnMoreButton } from './styled'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { ModalConfirmConnect } from '@/components/Modal/ModalConfirmConnect/ModalConfirmConnect'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { useState } from 'react'
|
||||
|
||||
const CreatePage = () => {
|
||||
const notify = useEnqueueSnackbar()
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
const [created, setCreated] = useState(false)
|
||||
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
const name = searchParams.get('name') || ''
|
||||
const token = searchParams.get('token') || ''
|
||||
const appNpub = searchParams.get('appNpub') || ''
|
||||
const isValid = name && token && appNpub
|
||||
|
||||
const nip05 = `${name}@${DOMAIN}`
|
||||
|
||||
const handleLearnMore = () => {
|
||||
// @ts-ignore
|
||||
window.open(`https://${DOMAIN}`, '_blank').focus()
|
||||
}
|
||||
|
||||
const handleClickAddAccount = async () => {
|
||||
try {
|
||||
const key: any = await swicCall('generateKey', name)
|
||||
|
||||
let appUrl = ''
|
||||
if (window.document.referrer) {
|
||||
try {
|
||||
const u = new URL(window.document.referrer)
|
||||
appUrl = u.origin
|
||||
} catch {}
|
||||
}
|
||||
|
||||
console.log('Created', key.npub, 'app', appUrl)
|
||||
setCreated(true)
|
||||
|
||||
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||
search: {
|
||||
npub: key.npub,
|
||||
appNpub,
|
||||
appUrl,
|
||||
token,
|
||||
// needed for this screen itself
|
||||
name,
|
||||
// will close after all done
|
||||
popup: 'true',
|
||||
},
|
||||
replace: true,
|
||||
})
|
||||
} catch (error: any) {
|
||||
notify(error.message || error.toString(), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
return (
|
||||
<Stack maxHeight={'100%'} overflow={'auto'}>
|
||||
<Typography textAlign={'center'} variant="h6" paddingTop="1em">
|
||||
Bad parameters.
|
||||
</Typography>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack maxHeight={'100%'} overflow={'auto'}>
|
||||
{created && (
|
||||
<>
|
||||
<Typography textAlign={'center'} variant="h4" paddingTop="0.5em">
|
||||
Account created!
|
||||
</Typography>
|
||||
<Typography textAlign={'center'} variant="body1" paddingTop="0.5em">
|
||||
User name: <b>{nip05}</b>
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{!created && (
|
||||
<>
|
||||
<Typography textAlign={'center'} variant="h4" paddingTop="0.5em">
|
||||
Welcome to Nostr!
|
||||
</Typography>
|
||||
<Stack gap={'0.5rem'} overflow={'auto'}>
|
||||
<Typography textAlign={'left'} variant="h6" paddingTop="0.5em">
|
||||
Chosen name: <b>{nip05}</b>
|
||||
</Typography>
|
||||
<GetStartedButton onClick={handleClickAddAccount}>Create account</GetStartedButton>
|
||||
|
||||
<Typography textAlign={'left'} variant="h5" paddingTop="1em">
|
||||
What you need to know:
|
||||
</Typography>
|
||||
|
||||
<ol style={{ marginLeft: '1em' }}>
|
||||
<li>Nostr accounts are based on cryptographic keys.</li>
|
||||
<li>All your actions on Nostr will be signed by your keys.</li>
|
||||
<li>Nsec.app is one of many services to manage Nostr keys.</li>
|
||||
<li>When you create an account, a new key will be created.</li>
|
||||
<li>This key can later be used with other Nostr websites.</li>
|
||||
</ol>
|
||||
<LearnMoreButton onClick={handleLearnMore}>Learn more</LearnMoreButton>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<ModalConfirmConnect />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreatePage
|
||||
26
src/pages/CreatePage/styled.tsx
Normal file
26
src/pages/CreatePage/styled.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
||||
import { styled } from '@mui/material'
|
||||
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
||||
import PlayArrowOutlinedIcon from '@mui/icons-material/PlayArrowOutlined'
|
||||
import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'
|
||||
|
||||
export const AddAccountButton = styled((props: AppButtonProps) => (
|
||||
<Button {...props} startIcon={<PersonAddAltRoundedIcon />} />
|
||||
))(() => ({
|
||||
alignSelf: 'center',
|
||||
padding: '0.35rem 1rem',
|
||||
}))
|
||||
|
||||
export const GetStartedButton = styled((props: AppButtonProps) => (
|
||||
<Button {...props} startIcon={<PlayArrowOutlinedIcon />} />
|
||||
))(() => ({
|
||||
alignSelf: 'left',
|
||||
padding: '0.35rem 1rem',
|
||||
}))
|
||||
|
||||
export const LearnMoreButton = styled((props: AppButtonProps) => (
|
||||
<Button {...props} startIcon={<HelpOutlineOutlinedIcon />} />
|
||||
))(() => ({
|
||||
alignSelf: 'left',
|
||||
padding: '0.35rem 1rem',
|
||||
}))
|
||||
@@ -18,48 +18,34 @@ const HomePage = () => {
|
||||
|
||||
const handleLearnMore = () => {
|
||||
// @ts-ignore
|
||||
window.open(`https://info.${DOMAIN}`, '_blank').focus();
|
||||
window.open(`https://${DOMAIN}`, '_blank').focus()
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack maxHeight={'100%'} overflow={'auto'}>
|
||||
<SectionTitle marginBottom={'0.5rem'}>
|
||||
{isNoKeys ? 'Welcome' : 'Keys:'}
|
||||
</SectionTitle>
|
||||
<SectionTitle marginBottom={'0.5rem'}>{isNoKeys ? 'Welcome' : 'Accounts:'}</SectionTitle>
|
||||
<Stack gap={'0.5rem'} overflow={'auto'}>
|
||||
{isNoKeys && (
|
||||
<>
|
||||
<Typography textAlign={'left'} variant='h6' paddingTop='1em'>
|
||||
<Typography textAlign={'left'} variant="h6" paddingTop="1em">
|
||||
Nsec.app is a novel key storage app for Nostr.
|
||||
</Typography>
|
||||
<GetStartedButton onClick={handleClickAddAccount}>
|
||||
Get started
|
||||
</GetStartedButton>
|
||||
<Typography textAlign={'left'} variant='h6' paddingTop='2em'>
|
||||
Your keys are stored in your browser and
|
||||
can be used in many Nostr apps without the
|
||||
need for a browser extension.
|
||||
<GetStartedButton onClick={handleClickAddAccount}>Get started</GetStartedButton>
|
||||
<Typography textAlign={'left'} variant="h6" paddingTop="2em">
|
||||
Your keys are stored in your browser and can be used in many Nostr apps without the need for a browser
|
||||
extension.
|
||||
</Typography>
|
||||
<LearnMoreButton onClick={handleLearnMore}>
|
||||
Learn more
|
||||
</LearnMoreButton>
|
||||
<LearnMoreButton onClick={handleLearnMore}>Learn more</LearnMoreButton>
|
||||
</>
|
||||
)}
|
||||
{!isNoKeys && (
|
||||
<Fragment>
|
||||
<Box
|
||||
flex={1}
|
||||
overflow={'auto'}
|
||||
borderRadius={'8px'}
|
||||
padding={'0.25rem'}
|
||||
>
|
||||
<Box flex={1} overflow={'auto'} borderRadius={'8px'} padding={'0.25rem'}>
|
||||
{keys.map((key) => (
|
||||
<ItemKey {...key} key={key.npub} />
|
||||
))}
|
||||
</Box>
|
||||
<AddAccountButton onClick={handleClickAddAccount}>
|
||||
Add account
|
||||
</AddAccountButton>
|
||||
<AddAccountButton onClick={handleClickAddAccount}>Add account</AddAccountButton>
|
||||
</Fragment>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -1,47 +1,37 @@
|
||||
import { FC } from 'react'
|
||||
import { DbKey } from '../../../modules/db'
|
||||
import {
|
||||
Avatar,
|
||||
Stack,
|
||||
StackProps,
|
||||
Typography,
|
||||
TypographyProps,
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
import { getShortenNpub } from '../../../utils/helpers/helpers'
|
||||
import { Avatar, Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useProfile } from '@/hooks/useProfile'
|
||||
|
||||
type ItemKeyProps = DbKey
|
||||
|
||||
export const ItemKey: FC<ItemKeyProps> = (props) => {
|
||||
const { npub, profile } = props
|
||||
const { npub } = props
|
||||
const navigate = useNavigate()
|
||||
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||
|
||||
const handleNavigate = () => {
|
||||
navigate('/key/' + npub)
|
||||
}
|
||||
const { name = '', picture = '' } = profile?.info || {}
|
||||
const userName = name || getShortenNpub(npub)
|
||||
const userAvatar = picture || ''
|
||||
|
||||
return (
|
||||
<StyledKeyContainer onClick={handleNavigate}>
|
||||
<Stack direction={'row'} alignItems={'center'} gap='1rem'>
|
||||
<Avatar src={userAvatar} alt={userName} />
|
||||
<StyledText variant='body1'>{userName}</StyledText>
|
||||
<Stack direction={'row'} alignItems={'center'} gap="1rem">
|
||||
<Avatar src={userAvatar} alt={userName}>
|
||||
{avatarTitle}
|
||||
</Avatar>
|
||||
<StyledText variant="body1">{userName}</StyledText>
|
||||
</Stack>
|
||||
</StyledKeyContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledKeyContainer = styled((props: StackProps) => (
|
||||
<Stack marginBottom={'0.5rem'} gap={'0.25rem'} {...props} />
|
||||
))(({ theme }) => {
|
||||
const StyledKeyContainer = styled((props: StackProps) => <Stack marginBottom={'0.5rem'} gap={'0.25rem'} {...props} />)(
|
||||
({ theme }) => {
|
||||
return {
|
||||
boxShadow:
|
||||
theme.palette.mode === 'dark'
|
||||
? '0px 1px 6px 0px rgba(92, 92, 92, 0.2)'
|
||||
: '0px 1px 6px 0px rgba(0, 0, 0, 0.2)',
|
||||
theme.palette.mode === 'dark' ? '0px 1px 6px 0px rgba(92, 92, 92, 0.2)' : '0px 1px 6px 0px rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: '12px',
|
||||
padding: '0.5rem 1rem',
|
||||
background: theme.palette.background.paper,
|
||||
@@ -50,11 +40,10 @@ const StyledKeyContainer = styled((props: StackProps) => (
|
||||
},
|
||||
cursor: 'pointer',
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export const StyledText = styled((props: TypographyProps) => (
|
||||
<Typography {...props} />
|
||||
))({
|
||||
export const StyledText = styled((props: TypographyProps) => <Typography {...props} />)({
|
||||
fontWeight: 500,
|
||||
width: '100%',
|
||||
wordBreak: 'break-all',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useAppSelector } from '../../store/hooks/redux'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Navigate, useParams } from 'react-router-dom'
|
||||
import { Stack } from '@mui/material'
|
||||
import { StyledIconButton } from './styled'
|
||||
import { SettingsIcon, ShareIcon } from '@/assets'
|
||||
@@ -11,54 +11,55 @@ import { ModalSettings } from '@/components/Modal/ModalSettings/ModalSettings'
|
||||
import { ModalExplanation } from '@/components/Modal/ModalExplanation/ModalExplanation'
|
||||
import { ModalConfirmConnect } from '@/components/Modal/ModalConfirmConnect/ModalConfirmConnect'
|
||||
import { ModalConfirmEvent } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent'
|
||||
import { useProfile } from './hooks/useProfile'
|
||||
import { useBackgroundSigning } from './hooks/useBackgroundSigning'
|
||||
import { BackgroundSigningWarning } from './components/BackgroundSigningWarning'
|
||||
import UserValueSection from './components/UserValueSection'
|
||||
import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { checkNpubSyncQuerier } from './utils'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
const KeyPage = () => {
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const { apps, pending, perms } = useAppSelector((state) => state.content)
|
||||
const { keys, apps, pending, perms } = useAppSelector((state) => state.content)
|
||||
|
||||
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
|
||||
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning()
|
||||
|
||||
const { userNameWithPrefix } = useProfile(npub)
|
||||
const { handleEnableBackground, showWarning, isEnabling } =
|
||||
useBackgroundSigning()
|
||||
const key = keys.find((k) => k.npub === npub)
|
||||
|
||||
const getUsername = useCallback(() => {
|
||||
if (!key || !key?.name) return ''
|
||||
if (key.name.includes('@')) return key.name
|
||||
return `${key?.name}@${DOMAIN}`
|
||||
}, [key])
|
||||
const username = getUsername()
|
||||
|
||||
const filteredApps = apps.filter((a) => a.npub === npub)
|
||||
const { prepareEventPendings } = useTriggerConfirmModal(
|
||||
npub,
|
||||
pending,
|
||||
perms,
|
||||
)
|
||||
const { prepareEventPendings } = useTriggerConfirmModal(npub, pending, perms)
|
||||
|
||||
const handleOpenConnectAppModal = () =>
|
||||
handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||
const isKeyExists = npub.trim().length && key
|
||||
if (!isKeyExists) return <Navigate to={`/home`} />
|
||||
|
||||
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap={'1rem'} height={'100%'}>
|
||||
{showWarning && (
|
||||
<BackgroundSigningWarning
|
||||
isEnabling={isEnabling}
|
||||
onEnableBackSigning={handleEnableBackground}
|
||||
/>
|
||||
<BackgroundSigningWarning isEnabling={isEnabling} onEnableBackSigning={handleEnableBackground} />
|
||||
)}
|
||||
<UserValueSection
|
||||
title='Your login'
|
||||
value={userNameWithPrefix}
|
||||
copyValue={npub + '@nsec.app'}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||
title="Your login"
|
||||
value={username}
|
||||
copyValue={username}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.LOGIN}
|
||||
/>
|
||||
<UserValueSection
|
||||
title='Your NPUB'
|
||||
title="Your NPUB"
|
||||
value={npub}
|
||||
copyValue={npub}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||
@@ -70,11 +71,7 @@ const KeyPage = () => {
|
||||
Connect app
|
||||
</StyledIconButton>
|
||||
|
||||
<StyledIconButton
|
||||
bgcolor_variant='secondary'
|
||||
onClick={handleOpenSettingsModal}
|
||||
withBadge={!isSynced}
|
||||
>
|
||||
<StyledIconButton bgcolor_variant="secondary" onClick={handleOpenSettingsModal} withBadge={!isSynced}>
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</StyledIconButton>
|
||||
|
||||
@@ -15,7 +15,7 @@ type AppsProps = {
|
||||
npub: string
|
||||
}
|
||||
|
||||
export const Apps: FC<AppsProps> = ({ apps = [], npub = '' }) => {
|
||||
export const Apps: FC<AppsProps> = ({ apps = [] }) => {
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
// eslint-disable-next-line
|
||||
@@ -26,34 +26,22 @@ export const Apps: FC<AppsProps> = ({ apps = [], npub = '' }) => {
|
||||
})
|
||||
}
|
||||
|
||||
const openAppStore = () => {
|
||||
window.open('https://nostrapp.link', '_blank')
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
flex={1}
|
||||
marginBottom={'1rem'}
|
||||
display={'flex'}
|
||||
flexDirection={'column'}
|
||||
overflow={'auto'}
|
||||
>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
marginBottom={'0.5rem'}
|
||||
>
|
||||
<Box flex={1} marginBottom={'1rem'} display={'flex'} flexDirection={'column'} overflow={'auto'}>
|
||||
<Stack direction={'row'} alignItems={'center'} justifyContent={'space-between'} marginBottom={'0.5rem'}>
|
||||
<SectionTitle>Connected apps</SectionTitle>
|
||||
<AppLink title='Discover Apps' />
|
||||
<AppLink title="Discover Apps" onClick={openAppStore} />
|
||||
</Stack>
|
||||
{!apps.length && (
|
||||
<StyledEmptyAppsBox>
|
||||
<Typography
|
||||
className='message'
|
||||
variant='h5'
|
||||
fontWeight={600}
|
||||
textAlign={'center'}
|
||||
>
|
||||
<Typography className="message" variant="h5" fontWeight={600} textAlign={'center'}>
|
||||
No connected apps
|
||||
</Typography>
|
||||
<Button>Discover Nostr Apps</Button>
|
||||
<Button onClick={openAppStore}>Discover Nostr Apps</Button>
|
||||
</StyledEmptyAppsBox>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import React, { FC } from 'react'
|
||||
import { FC } from 'react'
|
||||
import { Warning } from '@/components/Warning/Warning'
|
||||
import { CircularProgress, Stack } from '@mui/material'
|
||||
import GppMaybeIcon from '@mui/icons-material/GppMaybe'
|
||||
import { CircularProgress, Stack, Typography } from '@mui/material'
|
||||
import AutoModeOutlinedIcon from '@mui/icons-material/AutoModeOutlined'
|
||||
|
||||
type BackgroundSigningWarningProps = {
|
||||
isEnabling: boolean
|
||||
onEnableBackSigning: () => void
|
||||
}
|
||||
|
||||
export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({
|
||||
isEnabling,
|
||||
onEnableBackSigning,
|
||||
}) => {
|
||||
export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({ isEnabling, onEnableBackSigning }) => {
|
||||
return (
|
||||
<Warning
|
||||
message={
|
||||
<Stack direction={'row'} alignItems={'center'} gap={'1rem'}>
|
||||
Please enable push notifications{' '}
|
||||
{isEnabling ? <CircularProgress size={'1.5rem'} /> : null}
|
||||
Enable background service {isEnabling ? <CircularProgress size={'1.5rem'} /> : null}
|
||||
</Stack>
|
||||
}
|
||||
Icon={<GppMaybeIcon htmlColor='white' />}
|
||||
hint={
|
||||
<Typography variant='body2'>
|
||||
Please allow notifications
|
||||
for background operation.
|
||||
</Typography>
|
||||
}
|
||||
icon={<AutoModeOutlinedIcon htmlColor="white" />}
|
||||
onClick={isEnabling ? undefined : onEnableBackSigning}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -2,14 +2,16 @@ import { DbApp } from '@/modules/db'
|
||||
import { Avatar, Stack, Typography } from '@mui/material'
|
||||
import { FC } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
// import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'
|
||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { StyledItemAppContainer } from './styled'
|
||||
|
||||
type ItemAppProps = DbApp
|
||||
|
||||
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => {
|
||||
const appName = name || getShortenNpub(appNpub)
|
||||
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name, url }) => {
|
||||
const appDomain = getDomain(url)
|
||||
const appName = name || appDomain || getShortenNpub(appNpub)
|
||||
const appIcon = icon || `https://${appDomain}/favicon.ico`
|
||||
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||
return (
|
||||
<StyledItemAppContainer
|
||||
direction={'row'}
|
||||
@@ -20,21 +22,18 @@ export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => {
|
||||
to={`/key/${npub}/app/${appNpub}`}
|
||||
>
|
||||
<Avatar
|
||||
variant='square'
|
||||
variant="rounded"
|
||||
sx={{ width: 56, height: 56 }}
|
||||
src={icon}
|
||||
alt={name}
|
||||
/>
|
||||
src={appIcon}
|
||||
alt={appName}
|
||||
>
|
||||
{appAvatarTitle}
|
||||
</Avatar>
|
||||
<Stack>
|
||||
<Typography noWrap display={'block'} variant='body2'>
|
||||
<Typography noWrap display={'block'} variant="body2">
|
||||
{appName}
|
||||
</Typography>
|
||||
<Typography
|
||||
noWrap
|
||||
display={'block'}
|
||||
variant='caption'
|
||||
color={'GrayText'}
|
||||
>
|
||||
<Typography noWrap display={'block'} variant="caption" color={'GrayText'}>
|
||||
Basic actions
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
@@ -14,12 +14,7 @@ type UserValueSectionProps = {
|
||||
copyValue: string
|
||||
}
|
||||
|
||||
const UserValueSection: FC<UserValueSectionProps> = ({
|
||||
title,
|
||||
value,
|
||||
explanationType,
|
||||
copyValue,
|
||||
}) => {
|
||||
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, copyValue }) => {
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
|
||||
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
|
||||
@@ -31,23 +26,11 @@ const UserValueSection: FC<UserValueSectionProps> = ({
|
||||
}
|
||||
return (
|
||||
<Box>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
alignItems={'center'}
|
||||
justifyContent={'space-between'}
|
||||
marginBottom={'0.5rem'}
|
||||
>
|
||||
<Stack direction={'row'} alignItems={'center'} justifyContent={'space-between'} marginBottom={'0.5rem'}>
|
||||
<SectionTitle>{title}</SectionTitle>
|
||||
<AppLink
|
||||
title='What is this?'
|
||||
onClick={() => handleOpenExplanationModal(explanationType)}
|
||||
/>
|
||||
<AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} />
|
||||
</Stack>
|
||||
<StyledInput
|
||||
value={value}
|
||||
readOnly
|
||||
endAdornment={<InputCopyButton value={copyValue} />}
|
||||
/>
|
||||
<StyledInput value={value} readOnly endAdornment={<InputCopyButton value={copyValue} />} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { Input, InputProps } from '@/shared/Input/Input'
|
||||
import { Input, AppInputProps } from '@/shared/Input/Input'
|
||||
import { Stack, StackProps, styled } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const StyledInput = styled(({ className, ...props }: InputProps) => {
|
||||
export const StyledInput = styled(
|
||||
forwardRef<HTMLInputElement, AppInputProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
className='input'
|
||||
ref={ref}
|
||||
className="input"
|
||||
containerProps={{
|
||||
className,
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
)
|
||||
})(({ theme }) => ({
|
||||
})
|
||||
)(({ theme }) => ({
|
||||
'& > .input': {
|
||||
border: 'none',
|
||||
background: theme.palette.secondary.main,
|
||||
@@ -23,11 +27,9 @@ export const StyledInput = styled(({ className, ...props }: InputProps) => {
|
||||
},
|
||||
}))
|
||||
|
||||
export const StyledItemAppContainer = styled(
|
||||
<C extends React.ElementType>(props: StackProps<C, { component?: C }>) => (
|
||||
export const StyledItemAppContainer = styled(<C extends React.ElementType>(props: StackProps<C, { component?: C }>) => (
|
||||
<Stack {...props} />
|
||||
),
|
||||
)(({ theme }) => ({
|
||||
))(({ theme }) => ({
|
||||
textDecoration: 'none',
|
||||
boxShadow: 'none',
|
||||
color: theme.palette.text.primary,
|
||||
|
||||
@@ -20,13 +20,10 @@ export const useBackgroundSigning = () => {
|
||||
await askNotificationPermission()
|
||||
const result = await swicCall('enablePush')
|
||||
if (!result) throw new Error('Failed to activate the push subscription')
|
||||
notify('Push notifications enabled!', 'success')
|
||||
notify('Background service enabled!', 'success')
|
||||
setShowWarning(false)
|
||||
} catch (error: any) {
|
||||
notify(
|
||||
`Failed to enable push subscription: ${error}`,
|
||||
'error',
|
||||
)
|
||||
notify(`Failed to enable push subscription: ${error}`, 'error')
|
||||
}
|
||||
setIsLoading(false)
|
||||
checkBackgroundSigning()
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { fetchProfile } from '@/modules/nostr'
|
||||
import { MetaEvent } from '@/types/meta-event'
|
||||
import { getProfileUsername } from '@/utils/helpers/helpers'
|
||||
|
||||
export const useProfile = (npub: string) => {
|
||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||
|
||||
const userName = getProfileUsername(profile, npub)
|
||||
const userNameWithPrefix = userName + '@nsec.app'
|
||||
|
||||
const loadProfile = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetchProfile(npub)
|
||||
setProfile(response)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error)
|
||||
}
|
||||
}, [npub])
|
||||
|
||||
useEffect(() => {
|
||||
loadProfile()
|
||||
}, [loadProfile])
|
||||
|
||||
return {
|
||||
profile,
|
||||
userNameWithPrefix,
|
||||
}
|
||||
}
|
||||
@@ -15,38 +15,21 @@ type IShownConfirmModals = {
|
||||
[reqId: string]: boolean
|
||||
}
|
||||
|
||||
export const useTriggerConfirmModal = (
|
||||
npub: string,
|
||||
pending: DbPending[],
|
||||
perms: DbPerm[],
|
||||
) => {
|
||||
export const useTriggerConfirmModal = (npub: string, pending: DbPending[], perms: DbPerm[]) => {
|
||||
const { handleOpen, getModalOpened } = useModalSearchParams()
|
||||
|
||||
const isConfirmConnectModalOpened = getModalOpened(
|
||||
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
||||
)
|
||||
const isConfirmEventModalOpened = getModalOpened(
|
||||
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
||||
)
|
||||
const isConfirmConnectModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
||||
const isConfirmEventModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
||||
|
||||
const filteredPendingReqs = pending.filter((p) => p.npub === npub)
|
||||
const filteredPerms = perms.filter((p) => p.npub === npub)
|
||||
|
||||
const npubConnectPerms = filteredPerms.filter(
|
||||
(perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC,
|
||||
)
|
||||
const excludeConnectPendings = filteredPendingReqs.filter(
|
||||
(pr) => pr.method !== 'connect',
|
||||
)
|
||||
const connectPendings = filteredPendingReqs.filter(
|
||||
(pr) => pr.method === 'connect',
|
||||
)
|
||||
const npubConnectPerms = filteredPerms.filter((perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC)
|
||||
const excludeConnectPendings = filteredPendingReqs.filter((pr) => pr.method !== 'connect')
|
||||
const connectPendings = filteredPendingReqs.filter((pr) => pr.method === 'connect')
|
||||
|
||||
const prepareEventPendings =
|
||||
excludeConnectPendings.reduce<IPendingsByAppNpub>((acc, current) => {
|
||||
const isConnected = npubConnectPerms.some(
|
||||
(cp) => cp.appNpub === current.appNpub,
|
||||
)
|
||||
const prepareEventPendings = excludeConnectPendings.reduce<IPendingsByAppNpub>((acc, current) => {
|
||||
const isConnected = npubConnectPerms.some((cp) => cp.appNpub === current.appNpub)
|
||||
if (!acc[current.appNpub]) {
|
||||
acc[current.appNpub] = {
|
||||
pending: [current],
|
||||
@@ -70,12 +53,7 @@ export const useTriggerConfirmModal = (
|
||||
}, [npub, pending.length])
|
||||
|
||||
const handleOpenConfirmConnectModal = useCallback(() => {
|
||||
if (
|
||||
!filteredPendingReqs.length ||
|
||||
isConfirmEventModalOpened ||
|
||||
isConfirmConnectModalOpened
|
||||
)
|
||||
return undefined
|
||||
if (!filteredPendingReqs.length || isConfirmEventModalOpened || isConfirmConnectModalOpened) return undefined
|
||||
|
||||
for (let i = 0; i < connectPendings.length; i++) {
|
||||
const req = connectPendings[i]
|
||||
@@ -92,25 +70,15 @@ export const useTriggerConfirmModal = (
|
||||
})
|
||||
break
|
||||
}
|
||||
}, [
|
||||
connectPendings,
|
||||
filteredPendingReqs.length,
|
||||
handleOpen,
|
||||
isConfirmEventModalOpened,
|
||||
isConfirmConnectModalOpened,
|
||||
])
|
||||
}, [connectPendings, filteredPendingReqs.length, handleOpen, isConfirmEventModalOpened, isConfirmConnectModalOpened])
|
||||
|
||||
const handleOpenConfirmEventModal = useCallback(() => {
|
||||
if (!filteredPendingReqs.length || connectPendings.length)
|
||||
return undefined
|
||||
if (!filteredPendingReqs.length || connectPendings.length) return undefined
|
||||
|
||||
for (let i = 0; i < Object.keys(prepareEventPendings).length; i++) {
|
||||
const appNpub = Object.keys(prepareEventPendings)[i]
|
||||
|
||||
if (
|
||||
shownConfirmEventModals.current[appNpub] ||
|
||||
!prepareEventPendings[appNpub].isConnected
|
||||
) {
|
||||
if (shownConfirmEventModals.current[appNpub] || !prepareEventPendings[appNpub].isConnected) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -122,12 +90,7 @@ export const useTriggerConfirmModal = (
|
||||
})
|
||||
break
|
||||
}
|
||||
}, [
|
||||
connectPendings.length,
|
||||
filteredPendingReqs.length,
|
||||
handleOpen,
|
||||
prepareEventPendings,
|
||||
])
|
||||
}, [connectPendings.length, filteredPendingReqs.length, handleOpen, prepareEventPendings])
|
||||
|
||||
useEffect(() => {
|
||||
handleOpenConfirmEventModal()
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { Input, InputProps } from '@/shared/Input/Input'
|
||||
import { Input, AppInputProps } from '@/shared/Input/Input'
|
||||
import { Box, Button, ButtonProps, styled, Badge } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
type StyledIconButtonProps = ButtonProps & {
|
||||
bgcolor_variant?: 'primary' | 'secondary'
|
||||
withBadge?: boolean
|
||||
}
|
||||
|
||||
export const StyledIconButton = styled(
|
||||
({ withBadge, ...props }: StyledIconButtonProps) => {
|
||||
export const StyledIconButton = styled(({ withBadge, ...props }: StyledIconButtonProps) => {
|
||||
if (withBadge) {
|
||||
return (
|
||||
<Badge sx={{ flex: 1 }} badgeContent={''} color='error'>
|
||||
<Badge sx={{ flex: 1 }} badgeContent={''} color="error">
|
||||
<Button {...props} />
|
||||
</Badge>
|
||||
)
|
||||
}
|
||||
return <Button {...props} />
|
||||
},
|
||||
)(({ bgcolor_variant = 'primary', theme }) => {
|
||||
})(({ bgcolor_variant = 'primary', theme }) => {
|
||||
const isPrimary = bgcolor_variant === 'primary'
|
||||
return {
|
||||
flex: '1',
|
||||
@@ -29,13 +28,9 @@ export const StyledIconButton = styled(
|
||||
borderRadius: '1rem',
|
||||
fontSize: '0.875rem',
|
||||
'&:is(:hover, :active, &)': {
|
||||
background: isPrimary
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.secondary.main,
|
||||
background: isPrimary ? theme.palette.primary.main : theme.palette.secondary.main,
|
||||
},
|
||||
color: isPrimary
|
||||
? theme.palette.text.secondary
|
||||
: theme.palette.text.primary,
|
||||
color: isPrimary ? theme.palette.text.secondary : theme.palette.text.primary,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -57,18 +52,21 @@ export const StyledEmptyAppsBox = styled(Box)(({ theme }) => {
|
||||
}
|
||||
})
|
||||
|
||||
export const StyledInput = styled(({ className, ...props }: InputProps) => {
|
||||
export const StyledInput = styled(
|
||||
forwardRef<HTMLInputElement, AppInputProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
className='input'
|
||||
ref={ref}
|
||||
className="input"
|
||||
containerProps={{
|
||||
className,
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
)
|
||||
})(({ theme }) => ({
|
||||
})
|
||||
)(({ theme }) => ({
|
||||
'& > .input': {
|
||||
border: 'none',
|
||||
background: theme.palette.secondary.main,
|
||||
|
||||
@@ -50,42 +50,24 @@ const WelcomePage = () => {
|
||||
return (
|
||||
<Stack gap={'1.5rem'}>
|
||||
<Box alignSelf={'center'}>
|
||||
<Button size='small' variant='contained' onClick={generateKey}>
|
||||
<Button size="small" variant="contained" onClick={generateKey}>
|
||||
generate key
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Stack alignItems={'center'} gap='0.5rem'>
|
||||
<TextField
|
||||
variant='outlined'
|
||||
ref={nsecInputRef}
|
||||
placeholder='Enter nsec...'
|
||||
fullWidth
|
||||
size='small'
|
||||
/>
|
||||
<Button size='small' variant='contained' onClick={importKey}>
|
||||
<Stack alignItems={'center'} gap="0.5rem">
|
||||
<TextField variant="outlined" ref={nsecInputRef} placeholder="Enter nsec..." fullWidth size="small" />
|
||||
<Button size="small" variant="contained" onClick={importKey}>
|
||||
import key (DANGER!)
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Stack alignItems={'center'} gap='0.5rem'>
|
||||
<Stack width={'100%'} gap='0.5rem'>
|
||||
<TextField
|
||||
variant='outlined'
|
||||
ref={npubInputRef}
|
||||
placeholder='Enter npub...'
|
||||
fullWidth
|
||||
size='small'
|
||||
/>
|
||||
<TextField
|
||||
variant='outlined'
|
||||
ref={passwordInputRef}
|
||||
placeholder='Enter password'
|
||||
fullWidth
|
||||
size='small'
|
||||
/>
|
||||
<Stack alignItems={'center'} gap="0.5rem">
|
||||
<Stack width={'100%'} gap="0.5rem">
|
||||
<TextField variant="outlined" ref={npubInputRef} placeholder="Enter npub..." fullWidth size="small" />
|
||||
<TextField variant="outlined" ref={passwordInputRef} placeholder="Enter password" fullWidth size="small" />
|
||||
</Stack>
|
||||
<Button size='small' variant='contained' onClick={fetchNewKey}>
|
||||
<Button size="small" variant="contained" onClick={fetchNewKey}>
|
||||
fetch key
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
import { ReportHandler } from 'web-vitals'
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
getCLS(onPerfEntry)
|
||||
getFID(onPerfEntry)
|
||||
getFCP(onPerfEntry)
|
||||
getLCP(onPerfEntry)
|
||||
getTTFB(onPerfEntry)
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default reportWebVitals;
|
||||
export default reportWebVitals
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Suspense, lazy } from 'react'
|
||||
import { Route, Routes, Navigate } from 'react-router-dom'
|
||||
import HomePage from '../pages/HomePage/Home.Page'
|
||||
import WelcomePage from '../pages/Welcome.Page'
|
||||
import { Layout } from '../layout/Layout'
|
||||
import { CircularProgress, Stack } from '@mui/material'
|
||||
import CreatePage from '@/pages/CreatePage/Create.Page'
|
||||
|
||||
const KeyPage = lazy(() => import('../pages/KeyPage/Key.Page'))
|
||||
const ConfirmPage = lazy(() => import('../pages/Confirm.Page'))
|
||||
@@ -19,21 +19,16 @@ const AppRoutes = () => {
|
||||
return (
|
||||
<Suspense fallback={<LoadingSpinner />}>
|
||||
<Routes>
|
||||
<Route path='/' element={<Layout />}>
|
||||
<Route path='/' element={<Navigate to={'/home'} />} />
|
||||
<Route path="/" element={<Layout />}>
|
||||
<Route path="/" element={<Navigate to={'/home'} />} />
|
||||
{/* <Route path='/welcome' element={<WelcomePage />} /> */}
|
||||
<Route path='/home' element={<HomePage />} />
|
||||
<Route path='/key/:npub' element={<KeyPage />} />
|
||||
<Route
|
||||
path='/key/:npub/app/:appNpub'
|
||||
element={<AppPage />}
|
||||
/>
|
||||
<Route
|
||||
path='/key/:npub/:req_id'
|
||||
element={<ConfirmPage />}
|
||||
/>
|
||||
<Route path="/home" element={<HomePage />} />
|
||||
<Route path="/key/:npub" element={<KeyPage />} />
|
||||
<Route path="/key/:npub/app/:appNpub" element={<AppPage />} />
|
||||
<Route path="/key/:npub/:req_id" element={<ConfirmPage />} />
|
||||
<Route path="/create" element={<CreatePage />} />
|
||||
</Route>
|
||||
<Route path='*' element={<Navigate to={'/home'} />} />
|
||||
<Route path="*" element={<Navigate to={'/home'} />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
)
|
||||
|
||||
@@ -51,15 +51,14 @@ registerRoute(
|
||||
// Return true to signal that we want to use the handler.
|
||||
return true
|
||||
},
|
||||
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html'),
|
||||
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
|
||||
)
|
||||
|
||||
// An example runtime caching route for requests that aren't handled by the
|
||||
// precache, in this case same-origin .png requests like those from in public/
|
||||
registerRoute(
|
||||
// Add in any other file extensions or routing criteria as needed.
|
||||
({ url }) =>
|
||||
url.origin === self.location.origin && url.pathname.endsWith('.png'),
|
||||
({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
|
||||
// Customize this strategy as needed, e.g., by changing to CacheFirst.
|
||||
new StaleWhileRevalidate({
|
||||
cacheName: 'images',
|
||||
@@ -68,7 +67,7 @@ registerRoute(
|
||||
// least-recently used images are removed.
|
||||
new ExpirationPlugin({ maxEntries: 50 }),
|
||||
],
|
||||
}),
|
||||
})
|
||||
)
|
||||
|
||||
// This allows the web app to trigger skipWaiting via
|
||||
|
||||
@@ -16,34 +16,34 @@ const isLocalhost = Boolean(
|
||||
window.location.hostname === '[::1]' ||
|
||||
// 127.0.0.0/8 are considered localhost for IPv4.
|
||||
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
||||
);
|
||||
)
|
||||
|
||||
type Config = {
|
||||
onSuccess?: (registration: ServiceWorkerRegistration) => void;
|
||||
onUpdate?: (registration: ServiceWorkerRegistration) => void;
|
||||
onError?: (e: any) => void;
|
||||
};
|
||||
onSuccess?: (registration: ServiceWorkerRegistration) => void
|
||||
onUpdate?: (registration: ServiceWorkerRegistration) => void
|
||||
onError?: (e: any) => void
|
||||
}
|
||||
|
||||
export function register(config?: Config) {
|
||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||
// The URL constructor is available in all browsers that support SW.
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
|
||||
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
|
||||
if (publicUrl.origin !== window.location.origin) {
|
||||
// Our service worker won't work if PUBLIC_URL is on a different origin
|
||||
// from what our page is served on. This might happen if a CDN is used to
|
||||
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
|
||||
if (config && config.onError) {
|
||||
config.onError(new Error("Wrong origin"));
|
||||
config.onError(new Error('Wrong origin'))
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
|
||||
window.addEventListener('load', () => {
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
|
||||
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
|
||||
|
||||
if (isLocalhost) {
|
||||
// This is running on localhost. Let's check if a service worker still exists or not.
|
||||
checkValidServiceWorker(swUrl, config);
|
||||
checkValidServiceWorker(swUrl, config)
|
||||
|
||||
// Add some additional logging to localhost, pointing developers to the
|
||||
// service worker/PWA documentation.
|
||||
@@ -51,16 +51,16 @@ export function register(config?: Config) {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://cra.link/PWA'
|
||||
);
|
||||
});
|
||||
)
|
||||
})
|
||||
} else {
|
||||
// Is not localhost. Just register service worker
|
||||
registerValidSW(swUrl, config);
|
||||
registerValidSW(swUrl, config)
|
||||
}
|
||||
});
|
||||
})
|
||||
} else {
|
||||
if (config && config.onError) {
|
||||
config.onError(new Error("No service worker"));
|
||||
config.onError(new Error('No service worker'))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,9 +70,9 @@ function registerValidSW(swUrl: string, config?: Config) {
|
||||
.register(swUrl)
|
||||
.then((registration) => {
|
||||
registration.onupdatefound = () => {
|
||||
const installingWorker = registration.installing;
|
||||
const installingWorker = registration.installing
|
||||
if (installingWorker == null) {
|
||||
return;
|
||||
return
|
||||
}
|
||||
installingWorker.onstatechange = () => {
|
||||
if (installingWorker.state === 'installed') {
|
||||
@@ -83,33 +83,33 @@ function registerValidSW(swUrl: string, config?: Config) {
|
||||
console.log(
|
||||
'New content is available and will be used when all ' +
|
||||
'tabs for this page are closed. See https://cra.link/PWA.'
|
||||
);
|
||||
)
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onUpdate) {
|
||||
config.onUpdate(registration);
|
||||
config.onUpdate(registration)
|
||||
}
|
||||
} else {
|
||||
// At this point, everything has been precached.
|
||||
// It's the perfect time to display a
|
||||
// "Content is cached for offline use." message.
|
||||
console.log('Content is cached for offline use.');
|
||||
console.log('Content is cached for offline use.')
|
||||
|
||||
// Execute callback
|
||||
if (config && config.onSuccess) {
|
||||
config.onSuccess(registration);
|
||||
config.onSuccess(registration)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error during service worker registration:', error);
|
||||
console.error('Error during service worker registration:', error)
|
||||
if (config && config.onError) {
|
||||
config.onError(new Error(`Install error: ${error}`));
|
||||
config.onError(new Error(`Install error: ${error}`))
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||
@@ -119,35 +119,32 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
|
||||
})
|
||||
.then((response) => {
|
||||
// Ensure service worker exists, and that we really are getting a JS file.
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (
|
||||
response.status === 404 ||
|
||||
(contentType != null && contentType.indexOf('javascript') === -1)
|
||||
) {
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
|
||||
// No service worker found. Probably a different app. Reload the page.
|
||||
navigator.serviceWorker.ready.then((registration) => {
|
||||
registration.unregister().then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
window.location.reload()
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// Service worker found. Proceed as normal.
|
||||
registerValidSW(swUrl, config);
|
||||
registerValidSW(swUrl, config)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.log('No internet connection found. App is running in offline mode.');
|
||||
});
|
||||
console.log('No internet connection found. App is running in offline mode.')
|
||||
})
|
||||
}
|
||||
|
||||
export function unregister() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.ready
|
||||
.then((registration) => {
|
||||
registration.unregister();
|
||||
registration.unregister()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error.message);
|
||||
});
|
||||
console.error(error.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
@@ -9,9 +9,9 @@ export const AppLink: FC<AppLinkProps> = ({ title = '', ...rest }) => {
|
||||
return <StyledTypography {...rest}>{title}</StyledTypography>
|
||||
}
|
||||
|
||||
const StyledTypography = styled((props: TypographyProps) => (
|
||||
<Typography {...props} variant='caption' />
|
||||
))(({ theme }) => {
|
||||
const StyledTypography = styled((props: TypographyProps) => <Typography {...props} variant="caption" />)(({
|
||||
theme,
|
||||
}) => {
|
||||
return {
|
||||
color: theme.palette.textSecondaryDecorate.main,
|
||||
cursor: 'pointer',
|
||||
|
||||
@@ -1,28 +1,20 @@
|
||||
import {
|
||||
styled,
|
||||
Button as MuiButton,
|
||||
ButtonProps as MuiButtonProps,
|
||||
} from '@mui/material'
|
||||
import { styled, Button as MuiButton, ButtonProps as MuiButtonProps } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export type AppButtonProps = MuiButtonProps & {
|
||||
varianttype?: 'light' | 'default' | 'dark' | 'secondary'
|
||||
}
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, AppButtonProps>(
|
||||
({ children, ...restProps }, ref) => {
|
||||
export const Button = forwardRef<HTMLButtonElement, AppButtonProps>(({ children, ...restProps }, ref) => {
|
||||
return (
|
||||
<StyledButton classes={{ root: 'button' }} {...restProps} ref={ref}>
|
||||
<StyledButton classes={{ root: 'button', disabled: 'disabled' }} {...restProps} ref={ref}>
|
||||
{children}
|
||||
</StyledButton>
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const StyledButton = styled(
|
||||
forwardRef<HTMLButtonElement, AppButtonProps>((props, ref) => (
|
||||
<MuiButton ref={ref} {...props} />
|
||||
)),
|
||||
forwardRef<HTMLButtonElement, AppButtonProps>((props, ref) => <MuiButton ref={ref} {...props} />)
|
||||
)(({ theme, varianttype = 'default' }) => {
|
||||
const commonStyles = {
|
||||
fontWeight: 500,
|
||||
@@ -35,19 +27,22 @@ const StyledButton = styled(
|
||||
background: theme.palette.backgroundSecondary.default,
|
||||
},
|
||||
color: theme.palette.text.primary,
|
||||
'&.disabled': {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
...commonStyles,
|
||||
'&.button:is(:hover, :active, &)': {
|
||||
'&.button:is(:hover, :active, &, .disabled)': {
|
||||
background: theme.palette.primary.main,
|
||||
},
|
||||
color: theme.palette.text.secondary,
|
||||
':disabled': {
|
||||
'&.button:is(:hover, :active, &)': {
|
||||
background: theme.palette.backgroundSecondary.default,
|
||||
},
|
||||
color: theme.palette.backgroundSecondary.paper,
|
||||
'&.disabled': {
|
||||
color: theme.palette.text.secondary,
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,37 +1,26 @@
|
||||
import { forwardRef } from 'react'
|
||||
import { Checkbox as MuiCheckbox, CheckboxProps, styled } from '@mui/material'
|
||||
import {
|
||||
CheckedIcon,
|
||||
CheckedLightIcon,
|
||||
UnchekedIcon,
|
||||
UnchekedLightIcon,
|
||||
} from '@/assets'
|
||||
import { CheckedIcon, CheckedLightIcon, UnchekedIcon, UnchekedLightIcon } from '@/assets'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
|
||||
export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
|
||||
(props, ref) => {
|
||||
export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>((props, ref) => {
|
||||
const { themeMode } = useAppSelector((state) => state.ui)
|
||||
|
||||
return <StyledCheckbox ref={ref} {...props} mode={themeMode} />
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const StyledCheckbox = styled(
|
||||
forwardRef<HTMLButtonElement, CheckboxProps & { mode: 'dark' | 'light' }>(
|
||||
({ mode, ...restProps }, ref) => {
|
||||
forwardRef<HTMLButtonElement, CheckboxProps & { mode: 'dark' | 'light' }>(({ mode, ...restProps }, ref) => {
|
||||
const isDarkMode = mode === 'dark'
|
||||
return (
|
||||
<MuiCheckbox
|
||||
{...restProps}
|
||||
ref={ref}
|
||||
icon={isDarkMode ? <UnchekedLightIcon /> : <UnchekedIcon />}
|
||||
checkedIcon={
|
||||
isDarkMode ? <CheckedLightIcon /> : <CheckedIcon />
|
||||
}
|
||||
checkedIcon={isDarkMode ? <CheckedLightIcon /> : <CheckedIcon />}
|
||||
/>
|
||||
)
|
||||
},
|
||||
),
|
||||
})
|
||||
)(() => ({
|
||||
'& .MuiSvgIcon-root': { fontSize: '1.5rem' },
|
||||
marginLeft: '-10px',
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import React, { FC } from 'react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogProps,
|
||||
DialogTitle,
|
||||
Slide,
|
||||
} from '@mui/material'
|
||||
import { Dialog, DialogActions, DialogContent, DialogProps, DialogTitle, Slide } from '@mui/material'
|
||||
import { Button } from '../Button/Button'
|
||||
import { TransitionProps } from '@mui/material/transitions'
|
||||
import { StyledDialogContentText } from './styled'
|
||||
@@ -15,9 +8,9 @@ const Transition = React.forwardRef(function Transition(
|
||||
props: TransitionProps & {
|
||||
children: React.ReactElement<any, any>
|
||||
},
|
||||
ref: React.Ref<unknown>,
|
||||
ref: React.Ref<unknown>
|
||||
) {
|
||||
return <Slide direction='up' ref={ref} {...props} />
|
||||
return <Slide direction="up" ref={ref} {...props} />
|
||||
})
|
||||
|
||||
type ConfirmModalProps = {
|
||||
@@ -55,7 +48,7 @@ export const ConfirmModal: FC<ConfirmModalProps> = ({
|
||||
<StyledDialogContentText>{description}</StyledDialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button varianttype='secondary' onClick={onCancel}>
|
||||
<Button varianttype="secondary" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={onConfirm}>Confirm</Button>
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import {
|
||||
DialogContentText,
|
||||
DialogContentTextProps,
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
import { DialogContentText, DialogContentTextProps, styled } from '@mui/material'
|
||||
|
||||
export const StyledDialogContentText = styled(
|
||||
(props: DialogContentTextProps) => <DialogContentText {...props} />,
|
||||
)(({ theme }) => ({
|
||||
export const StyledDialogContentText = styled((props: DialogContentTextProps) => <DialogContentText {...props} />)(
|
||||
({ theme }) => ({
|
||||
color: theme.palette.primary.main,
|
||||
}))
|
||||
})
|
||||
)
|
||||
|
||||
26
src/shared/DebounceInput/DebounceInput.tsx
Normal file
26
src/shared/DebounceInput/DebounceInput.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useRef } from 'react'
|
||||
import { Input, AppInputProps } from '../Input/Input'
|
||||
|
||||
export type DebounceProps = {
|
||||
handleDebounce: (value: string) => void
|
||||
debounceTimeout: number
|
||||
}
|
||||
|
||||
export const DebounceInput = (props: AppInputProps & DebounceProps) => {
|
||||
const { handleDebounce, debounceTimeout, ...rest } = props
|
||||
|
||||
const timerRef = useRef<number>()
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
|
||||
timerRef.current = window.setTimeout(() => {
|
||||
handleDebounce(event.target.value)
|
||||
}, debounceTimeout)
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return <Input {...rest} onChange={handleChange} />
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, ReactNode } from 'react'
|
||||
import { ReactNode, forwardRef } from 'react'
|
||||
import {
|
||||
Box,
|
||||
BoxProps,
|
||||
@@ -10,39 +10,34 @@ import {
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
|
||||
export type InputProps = InputBaseProps & {
|
||||
export type AppInputProps = InputBaseProps & {
|
||||
helperText?: string | ReactNode
|
||||
helperTextProps?: FormHelperTextProps
|
||||
containerProps?: BoxProps
|
||||
label?: string
|
||||
}
|
||||
|
||||
export const Input: FC<InputProps> = ({
|
||||
helperText,
|
||||
containerProps,
|
||||
helperTextProps,
|
||||
label,
|
||||
...props
|
||||
}) => {
|
||||
export const Input = forwardRef<HTMLInputElement, AppInputProps>(
|
||||
({ helperText, containerProps, helperTextProps, label, ...props }, ref) => {
|
||||
return (
|
||||
<StyledInputContainer {...containerProps}>
|
||||
{label ? (
|
||||
<FormLabel className='label' htmlFor={props.id}>
|
||||
<FormLabel className="label" htmlFor={props.id}>
|
||||
{label}
|
||||
</FormLabel>
|
||||
) : null}
|
||||
<InputBase className='input' {...props} />
|
||||
<InputBase autoComplete="off" className="input" {...props} classes={{ error: 'error' }} ref={ref} />
|
||||
{helperText ? (
|
||||
<FormHelperText {...helperTextProps} className='helper_text'>
|
||||
<FormHelperText {...helperTextProps} className="helper_text">
|
||||
{helperText}
|
||||
</FormHelperText>
|
||||
) : null}
|
||||
</StyledInputContainer>
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(
|
||||
({ theme }) => {
|
||||
const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({ theme }) => {
|
||||
const isDark = theme.palette.mode === 'dark'
|
||||
return {
|
||||
width: '100%',
|
||||
@@ -56,6 +51,9 @@ const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(
|
||||
'& input::placeholder': {
|
||||
color: '#fff',
|
||||
},
|
||||
'&.error': {
|
||||
border: '0.3px solid ' + theme.palette.error.main,
|
||||
},
|
||||
},
|
||||
'& > .helper_text': {
|
||||
margin: '0.5rem 1rem 0',
|
||||
@@ -68,5 +66,4 @@ const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
@@ -9,10 +9,7 @@ type InputCopyButtonProps = {
|
||||
onCopy?: () => void
|
||||
}
|
||||
|
||||
export const InputCopyButton: FC<InputCopyButtonProps> = ({
|
||||
value,
|
||||
onCopy = () => undefined,
|
||||
}) => {
|
||||
export const InputCopyButton: FC<InputCopyButtonProps> = ({ value, onCopy = () => undefined }) => {
|
||||
const [isCopied, setIsCopied] = useState(false)
|
||||
|
||||
const handleCopy = () => {
|
||||
@@ -37,17 +34,13 @@ export const InputCopyButton: FC<InputCopyButtonProps> = ({
|
||||
<StyledContainer copied={isCopied ? 1 : 0}>
|
||||
{isCopied && (
|
||||
<Fade in exit>
|
||||
<Typography
|
||||
marginLeft={'0.5rem'}
|
||||
variant='body2'
|
||||
color={'inherit'}
|
||||
>
|
||||
<Typography marginLeft={'0.5rem'} variant="body2" color={'inherit'}>
|
||||
Copied
|
||||
</Typography>
|
||||
</Fade>
|
||||
)}
|
||||
<CopyToClipboard text={value} onCopy={handleCopy}>
|
||||
<IconButton color='inherit'>
|
||||
<IconButton color="inherit">
|
||||
<CopyIcon />
|
||||
</IconButton>
|
||||
</CopyToClipboard>
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { Stack, StackProps, styled } from '@mui/material'
|
||||
|
||||
export const StyledContainer = styled(
|
||||
(props: StackProps & { copied: number }) => (
|
||||
export const StyledContainer = styled((props: StackProps & { copied: number }) => (
|
||||
<Stack {...props} direction={'row'} alignItems={'center'} />
|
||||
),
|
||||
)(({ theme, copied }) => ({
|
||||
color: copied
|
||||
? theme.palette.success.main
|
||||
: theme.palette.textSecondaryDecorate.main,
|
||||
))(({ theme, copied }) => ({
|
||||
color: copied ? theme.palette.success.main : theme.palette.textSecondaryDecorate.main,
|
||||
}))
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import { DialogProps, IconButton, Slide } from '@mui/material'
|
||||
import { TransitionProps } from '@mui/material/transitions'
|
||||
import { FC, forwardRef } from 'react'
|
||||
import {
|
||||
StyledCloseButtonWrapper,
|
||||
StyledDialog,
|
||||
StyledDialogContent,
|
||||
StyledDialogTitle,
|
||||
} from './styled'
|
||||
import { StyledCloseButtonWrapper, StyledDialog, StyledDialogContent, StyledDialogTitle } from './styled'
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded'
|
||||
|
||||
type ModalProps = DialogProps & {
|
||||
@@ -18,32 +13,17 @@ const Transition = forwardRef(function Transition(
|
||||
props: TransitionProps & {
|
||||
children: React.ReactElement
|
||||
},
|
||||
ref: React.Ref<unknown>,
|
||||
ref: React.Ref<unknown>
|
||||
) {
|
||||
return <Slide direction='up' ref={ref} {...props} />
|
||||
return <Slide direction="up" ref={ref} {...props} />
|
||||
})
|
||||
|
||||
export const Modal: FC<ModalProps> = ({
|
||||
children,
|
||||
title,
|
||||
onClose,
|
||||
withCloseButton = true,
|
||||
fixedHeight,
|
||||
...props
|
||||
}) => {
|
||||
export const Modal: FC<ModalProps> = ({ children, title, onClose, withCloseButton = true, fixedHeight, ...props }) => {
|
||||
return (
|
||||
<StyledDialog
|
||||
fixedHeight={fixedHeight}
|
||||
{...props}
|
||||
onClose={onClose}
|
||||
TransitionComponent={Transition}
|
||||
>
|
||||
<StyledDialog fixedheight={fixedHeight} {...props} onClose={onClose} TransitionComponent={Transition}>
|
||||
{withCloseButton && (
|
||||
<StyledCloseButtonWrapper>
|
||||
<IconButton
|
||||
onClick={() => onClose && onClose({}, 'backdropClick')}
|
||||
className='close_btn'
|
||||
>
|
||||
<IconButton onClick={() => onClose && onClose({}, 'backdropClick')} className="close_btn">
|
||||
<CloseRoundedIcon />
|
||||
</IconButton>
|
||||
</StyledCloseButtonWrapper>
|
||||
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
|
||||
export const StyledDialog = styled(
|
||||
(props: DialogProps & { fixedHeight?: string }) => (
|
||||
export const StyledDialog = styled((props: DialogProps & { fixedheight?: string }) => (
|
||||
<Dialog
|
||||
{...props}
|
||||
classes={{
|
||||
@@ -27,9 +26,8 @@ export const StyledDialog = styled(
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
),
|
||||
)(({ theme, fixedHeight = '' }) => {
|
||||
const fixedHeightStyles = fixedHeight ? { height: fixedHeight } : {}
|
||||
))(({ theme, fixedheight = '' }) => {
|
||||
const fixedHeightStyles = fixedheight ? { height: fixedheight } : {}
|
||||
return {
|
||||
'& .container': {
|
||||
alignItems: 'flex-end',
|
||||
@@ -39,27 +37,20 @@ export const StyledDialog = styled(
|
||||
width: '100%',
|
||||
borderTopLeftRadius: '2rem',
|
||||
borderTopRightRadius: '2rem',
|
||||
background:
|
||||
theme.palette.mode === 'light'
|
||||
? '#fff'
|
||||
: theme.palette.secondary.main,
|
||||
background: theme.palette.mode === 'light' ? '#fff' : theme.palette.secondary.main,
|
||||
...fixedHeightStyles,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export const StyledDialogTitle = styled((props: DialogTitleProps) => (
|
||||
<DialogTitle {...props} variant='h5' />
|
||||
))(() => {
|
||||
export const StyledDialogTitle = styled((props: DialogTitleProps) => <DialogTitle {...props} variant="h5" />)(() => {
|
||||
return {
|
||||
textAlign: 'center',
|
||||
fontWeight: 600,
|
||||
}
|
||||
})
|
||||
|
||||
export const StyledDialogContent = styled((props: DialogContentProps) => (
|
||||
<DialogContent {...props} />
|
||||
))(() => {
|
||||
export const StyledDialogContent = styled((props: DialogContentProps) => <DialogContent {...props} />)(() => {
|
||||
return {
|
||||
padding: '0 1rem 1rem',
|
||||
display: 'flex',
|
||||
@@ -67,9 +58,7 @@ export const StyledDialogContent = styled((props: DialogContentProps) => (
|
||||
}
|
||||
})
|
||||
|
||||
export const StyledCloseButtonWrapper = styled((props: BoxProps) => (
|
||||
<Box {...props} />
|
||||
))(() => ({
|
||||
export const StyledCloseButtonWrapper = styled((props: BoxProps) => <Box {...props} />)(() => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-end',
|
||||
|
||||
@@ -7,9 +7,9 @@ export const SectionTitle: FC<SectionTitleProps> = ({ children, ...rest }) => {
|
||||
return <StyledTypography {...rest}>{children}</StyledTypography>
|
||||
}
|
||||
|
||||
const StyledTypography = styled((props: TypographyProps) => (
|
||||
<Typography {...props} variant='caption' />
|
||||
))(({ theme }) => {
|
||||
const StyledTypography = styled((props: TypographyProps) => <Typography {...props} variant="caption" />)(({
|
||||
theme,
|
||||
}) => {
|
||||
const isDark = theme.palette.mode === 'dark'
|
||||
return {
|
||||
textTransform: 'uppercase',
|
||||
|
||||
@@ -43,37 +43,28 @@ export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
export const selectKeys = (state: RootState) => state.content.keys
|
||||
export const selectApps = (state: RootState) => state.content.apps
|
||||
|
||||
export const selectKeyByNpub = (state: RootState, npub: string) => {
|
||||
return state.content.keys.find((key) => key.npub === npub)
|
||||
}
|
||||
|
||||
export const selectAppsByNpub = memoizeOne((state: RootState, npub: string) => {
|
||||
return state.content.apps.filter((app) => app.npub === npub)
|
||||
}, isDeepEqual)
|
||||
|
||||
export const selectPermsByNpub = memoizeOne(
|
||||
(state: RootState, npub: string) => {
|
||||
export const selectPermsByNpub = memoizeOne((state: RootState, npub: string) => {
|
||||
return state.content.perms.filter((perm) => perm.npub === npub)
|
||||
},
|
||||
isDeepEqual,
|
||||
)
|
||||
}, isDeepEqual)
|
||||
|
||||
export const selectPermsByNpubAndAppNpub = memoizeOne(
|
||||
(state: RootState, npub: string, appNpub: string) => {
|
||||
return state.content.perms.filter(
|
||||
(perm) => perm.npub === npub && perm.appNpub === appNpub,
|
||||
)
|
||||
},
|
||||
isDeepEqual,
|
||||
)
|
||||
export const selectPermsByNpubAndAppNpub = memoizeOne((state: RootState, npub: string, appNpub: string) => {
|
||||
return state.content.perms.filter((perm) => perm.npub === npub && perm.appNpub === appNpub)
|
||||
}, isDeepEqual)
|
||||
|
||||
export const selectPendingsByNpub = memoizeOne(
|
||||
(state: RootState, npub: string) => {
|
||||
export const selectPendingsByNpub = memoizeOne((state: RootState, npub: string) => {
|
||||
return state.content.pending.filter((pending) => pending.npub === npub)
|
||||
},
|
||||
isDeepEqual,
|
||||
)
|
||||
}, isDeepEqual)
|
||||
|
||||
export const selectAppByAppNpub = memoizeOne(
|
||||
(state: RootState, appNpub: string) => {
|
||||
export const selectAppByAppNpub = memoizeOne((state: RootState, appNpub: string) => {
|
||||
return state.content.apps.find((app) => app.appNpub === appNpub)
|
||||
},
|
||||
isDeepEqual,
|
||||
)
|
||||
}, isDeepEqual)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user