Compare commits
103 Commits
feature/ap
...
main
Author | SHA1 | Date | |
---|---|---|---|
|
9b10952749 | ||
|
6a6b18bcad | ||
|
67787c182b | ||
|
8c0b5f379e | ||
|
905dc7ac1b | ||
|
5259e17447 | ||
|
ca25712d20 | ||
|
6e334c5078 | ||
|
4d7b2c5a69 | ||
|
051eaf001f | ||
|
4a2362f6b9 | ||
|
da6f68e00a | ||
|
c08e26629a | ||
|
3de39c35b0 | ||
|
7aba51b103 | ||
|
c61869b2a2 | ||
|
47dc8e20fe | ||
|
f55ba7e6f2 | ||
|
98345037aa | ||
|
05bd08e86d | ||
|
4fa5c57a69 | ||
|
3e86ad37b9 | ||
|
3de4a508be | ||
|
febc91632a | ||
|
ecf27d8d23 | ||
|
59c03d16eb | ||
|
adbc7d455d | ||
|
7379d75002 | ||
|
71accbf983 | ||
|
13f9bb13fd | ||
|
b1dd6a5424 | ||
|
dddc90308a | ||
|
e3feb8b5a2 | ||
|
06fa8ffbd7 | ||
|
648567cac8 | ||
|
22753a8d89 | ||
|
6d4a8b4f64 | ||
|
425f7277fc | ||
|
ba3775e6c6 | ||
|
8c77b10b60 | ||
|
b98339e177 | ||
|
4ad66c8711 | ||
|
f0a7f5c58a | ||
|
a60fcd65b5 | ||
|
6a04c3ec4b | ||
|
64082de238 | ||
|
93f6135baf | ||
|
6d72cf1f82 | ||
|
3813cef605 | ||
|
170daa9ee7 | ||
|
2e522b79ad | ||
|
453a16690f | ||
|
46336d817f | ||
|
8ef8157c38 | ||
|
4f00a014d0 | ||
|
a500a2e2a5 | ||
|
1e6bf8679c | ||
|
04373e7991 | ||
|
6acd00ca3b | ||
|
6186f3dd3d | ||
|
87ec23c737 | ||
|
d199dcf9f7 | ||
|
04c425c32c | ||
|
0f28c80a15 | ||
|
34b516a1e3 | ||
|
aac537c7a2 | ||
|
40f4a9922a | ||
|
2058b900ac | ||
|
4b1f7564e7 | ||
|
32c097c1ee | ||
|
43e375efe9 | ||
|
8b349c0350 | ||
|
83d5c013cf | ||
|
0be2159efb | ||
|
e96edf90fe | ||
|
1a9dc0da82 | ||
|
56e71219a5 | ||
|
676eaf6191 | ||
|
97c3bcc16d | ||
|
67b6a3bfcf | ||
|
a5f7bf2a58 | ||
|
0b56813ece | ||
|
8d205d9d93 | ||
|
9a18e79862 | ||
|
ab2df05d50 | ||
|
163de16a84 | ||
|
e9b290db30 | ||
|
544ac18b59 | ||
|
2551022d5e | ||
|
041b84eb0b | ||
|
69166ff501 | ||
|
043e159e53 | ||
|
d11cccec35 | ||
|
f45300583c | ||
|
977a4b5c93 | ||
|
6589a98d52 | ||
|
e7e3b871e4 | ||
|
063213cb89 | ||
|
0bf6fafb3e | ||
|
14a83ec721 | ||
|
dfb8889b9d | ||
|
b24e3d31b0 | ||
|
b27fb5ec07 |
23
README
23
README
@ -1,23 +0,0 @@
|
|||||||
Noauth - Nostr key manager
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
THIS IS BETA SOFTWARE, DON'T USE WITH REAL KEYS!
|
|
||||||
|
|
||||||
This is a web-based nostr signer app, it uses nip46 signer
|
|
||||||
running inside a service worker, if SW is not running -
|
|
||||||
a noauthd server sends a push message and wakes SW up. Also,
|
|
||||||
keys can be saved to server and fetched later in an end-to-end
|
|
||||||
encrypted way. Keys are encrypted with user-defined password,
|
|
||||||
a good key derivation function is used to resist brute force.
|
|
||||||
|
|
||||||
This app works in Chrome on desktop and Android out of the box,
|
|
||||||
try it with snort.social (use bunker:/... string as 'login string').
|
|
||||||
|
|
||||||
On iOS web push notifications are still experimental, eventually
|
|
||||||
it will work on iOS out of the box too.
|
|
||||||
|
|
||||||
It works across devices, but that's unreliable, especially if
|
|
||||||
signer is on mobile - if smartphone is locked then service worker might
|
|
||||||
not wake up. Thanks to cloud sync/recovery of keys users can import
|
|
||||||
their keys into this app on every device and then it works well.
|
|
||||||
|
|
95
README.md
Normal file
95
README.md
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
Noauth - Nostr key manager
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
Nsec.app is a web app to store your Nostr keys
|
||||||
|
and provide remote access to keys using nip46.
|
||||||
|
|
||||||
|
Features:
|
||||||
|
- non-custodial store for your keys
|
||||||
|
- can store many keys
|
||||||
|
- provides nip46 access to apps
|
||||||
|
- permission management for connected apps
|
||||||
|
- works in any browser or platform
|
||||||
|
- background operation even if app tab is closed
|
||||||
|
- cloud e2ee sync for your keys
|
||||||
|
- support for OAuth-like signin flow
|
||||||
|
|
||||||
|
|
||||||
|
How it works
|
||||||
|
------------
|
||||||
|
|
||||||
|
This is a web-based nostr signer app, it uses nip46 signer
|
||||||
|
running inside a service worker, if SW is not running -
|
||||||
|
a noauthd server sends a push message and wakes SW up. Also,
|
||||||
|
keys can be saved to server and fetched later in an end-to-end
|
||||||
|
encrypted way. Keys are encrypted with user-defined password,
|
||||||
|
a good key derivation function is used to resist brute force.
|
||||||
|
|
||||||
|
It works across devices, but that's unreliable, especially if
|
||||||
|
signer is on mobile - if your phone is locked then service worker might
|
||||||
|
not wake up. Thanks to cloud sync/recovery of keys users can import
|
||||||
|
their keys into this app on every device and then it works well.
|
||||||
|
|
||||||
|
How to self-host
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This app is non-custodial, so there isn't much need for
|
||||||
|
self-hosting. However, if you'd like to run your own version of
|
||||||
|
it, here is how to do it:
|
||||||
|
|
||||||
|
Create web push keys (https://github.com/web-push-libs/web-push):
|
||||||
|
```
|
||||||
|
npm install web-push;
|
||||||
|
web-push generate-vapid-keys --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit .end in noauth:
|
||||||
|
```
|
||||||
|
REACT_APP_WEB_PUSH_PUBKEY=web push public key,
|
||||||
|
REACT_APP_NOAUTHD_URL=address of the noauthd server (see below)
|
||||||
|
REACT_APP_DOMAIN=domain name of your bunker (i.e. nsec.app)
|
||||||
|
REACT_APP_RELAY=relay that you'll use, can use wss://relay.nsec.app - don't use public general-purpose relays, you'll hit rate limits very fast
|
||||||
|
```
|
||||||
|
|
||||||
|
Then do:
|
||||||
|
```
|
||||||
|
npm install;
|
||||||
|
npm run build;
|
||||||
|
```
|
||||||
|
The app is in the `build` folder.
|
||||||
|
|
||||||
|
To run the noauthd server (https://github.com/nostrband/noauthd),
|
||||||
|
edit .env in noauthd:
|
||||||
|
```
|
||||||
|
PUSH_PUBKEY=web push public key, same as above
|
||||||
|
PUSH_SECRET=web push private key that you generated above
|
||||||
|
ORIGIN=address of the server itself, like http://localhost:8000
|
||||||
|
DATABASE_URL="file:./prod.db"
|
||||||
|
BUNKER_NSEC=nsec of the bunker (needed for create_account methods)
|
||||||
|
BUNKER_RELAY="wss://relay.nsec.app" - same as above
|
||||||
|
BUNKER_DOMAIN="nsec.app" - same as above
|
||||||
|
BUNKER_ORIGIN=where noauth is hosted
|
||||||
|
```
|
||||||
|
|
||||||
|
Then init the database and launch:
|
||||||
|
```
|
||||||
|
npx prisma migrate deploy
|
||||||
|
node -r dotenv/config src/index.js dotenv_config_path=.env
|
||||||
|
```
|
||||||
|
|
||||||
|
TODO
|
||||||
|
----
|
||||||
|
|
||||||
|
- Show details of requested operations
|
||||||
|
- Publish a profile for new sign ups
|
||||||
|
- Sync processed reqs across devices
|
||||||
|
- Sync connected apps and perms across devices
|
||||||
|
- Sync app activity across devices
|
||||||
|
- Group apps by domain
|
||||||
|
- Encrypt local nsec in Safari
|
||||||
|
- Add WebAuthn to the mix
|
||||||
|
- Add LN address to new profiles
|
||||||
|
- Confirm relay/contact list pruning requests
|
||||||
|
- Transfer/change nip05 name
|
||||||
|
- Better notifs with activity summaries
|
||||||
|
- How to send auth_url to new device if all other devices are down?
|
@ -3,37 +3,37 @@ const path = require('path')
|
|||||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
|
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
|
||||||
|
|
||||||
module.exports = function override(config) {
|
module.exports = function override(config) {
|
||||||
const fallback = config.resolve.fallback || {}
|
const fallback = config.resolve.fallback || {}
|
||||||
Object.assign(fallback, {
|
Object.assign(fallback, {
|
||||||
crypto: require.resolve('crypto-browserify'),
|
crypto: require.resolve('crypto-browserify'),
|
||||||
stream: require.resolve('stream-browserify'),
|
stream: require.resolve('stream-browserify'),
|
||||||
assert: require.resolve('assert'),
|
assert: require.resolve('assert'),
|
||||||
http: require.resolve('stream-http'),
|
http: require.resolve('stream-http'),
|
||||||
https: require.resolve('https-browserify'),
|
https: require.resolve('https-browserify'),
|
||||||
os: require.resolve('os-browserify'),
|
os: require.resolve('os-browserify'),
|
||||||
url: require.resolve('url'),
|
url: require.resolve('url'),
|
||||||
})
|
})
|
||||||
config.resolve.fallback = fallback
|
config.resolve.fallback = fallback
|
||||||
config.plugins = (config.plugins || []).concat([
|
config.plugins = (config.plugins || []).concat([
|
||||||
new webpack.ProvidePlugin({
|
new webpack.ProvidePlugin({
|
||||||
process: 'process/browser',
|
process: 'process/browser',
|
||||||
Buffer: ['buffer', 'Buffer'],
|
Buffer: ['buffer', 'Buffer'],
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
config.module.rules.unshift({
|
config.module.rules.unshift({
|
||||||
test: /\.m?js$/,
|
test: /\.m?js$/,
|
||||||
resolve: {
|
resolve: {
|
||||||
fullySpecified: false, // disable the behavior
|
fullySpecified: false, // disable the behavior
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
// turns off the plugin that forbids importing from node_modules for the above-mentioned stuff
|
// turns off the plugin that forbids importing from node_modules for the above-mentioned stuff
|
||||||
config.resolve.plugins = config.resolve.plugins.filter((plugin) => {
|
config.resolve.plugins = config.resolve.plugins.filter((plugin) => {
|
||||||
return !(plugin instanceof ModuleScopePlugin)
|
return !(plugin instanceof ModuleScopePlugin)
|
||||||
})
|
})
|
||||||
|
|
||||||
config.resolve.alias = {
|
config.resolve.alias = {
|
||||||
'@': path.resolve(__dirname, 'src'),
|
'@': path.resolve(__dirname, 'src'),
|
||||||
}
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
23
package-lock.json
generated
23
package-lock.json
generated
@ -41,6 +41,7 @@
|
|||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"use-debounce": "^10.0.0",
|
"use-debounce": "^10.0.0",
|
||||||
|
"usehooks-ts": "^2.14.0",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"workbox-background-sync": "^6.6.0",
|
"workbox-background-sync": "^6.6.0",
|
||||||
"workbox-broadcast-update": "^6.6.0",
|
"workbox-broadcast-update": "^6.6.0",
|
||||||
@ -17214,6 +17215,20 @@
|
|||||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/usehooks-ts": {
|
||||||
|
"version": "2.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.14.0.tgz",
|
||||||
|
"integrity": "sha512-jnhrjTRJoJS7cFxz63tRYc5mzTKf/h+Ii8P0PDHymT9qDe4ZA2/gzDRmDR4WGausg5X8wMIdghwi3BBCN9JKow==",
|
||||||
|
"dependencies": {
|
||||||
|
"lodash.debounce": "^4.0.8"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.15.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17 || ^18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/utf-8-validate": {
|
"node_modules/utf-8-validate": {
|
||||||
"version": "5.0.10",
|
"version": "5.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
|
||||||
@ -30591,6 +30606,14 @@
|
|||||||
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
|
"usehooks-ts": {
|
||||||
|
"version": "2.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.14.0.tgz",
|
||||||
|
"integrity": "sha512-jnhrjTRJoJS7cFxz63tRYc5mzTKf/h+Ii8P0PDHymT9qDe4ZA2/gzDRmDR4WGausg5X8wMIdghwi3BBCN9JKow==",
|
||||||
|
"requires": {
|
||||||
|
"lodash.debounce": "^4.0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"utf-8-validate": {
|
"utf-8-validate": {
|
||||||
"version": "5.0.10",
|
"version": "5.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
|
||||||
|
@ -36,6 +36,7 @@
|
|||||||
"redux-persist": "^6.0.0",
|
"redux-persist": "^6.0.0",
|
||||||
"typescript": "^5.3.2",
|
"typescript": "^5.3.2",
|
||||||
"use-debounce": "^10.0.0",
|
"use-debounce": "^10.0.0",
|
||||||
|
"usehooks-ts": "^2.14.0",
|
||||||
"web-vitals": "^2.1.4",
|
"web-vitals": "^2.1.4",
|
||||||
"workbox-background-sync": "^6.6.0",
|
"workbox-background-sync": "^6.6.0",
|
||||||
"workbox-broadcast-update": "^6.6.0",
|
"workbox-broadcast-update": "^6.6.0",
|
||||||
|
@ -10,12 +10,6 @@
|
|||||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png" />
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png" />
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<!--
|
<!--
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
Notice the use of %PUBLIC_URL% in the tags above.
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
It will be replaced with the URL of the `public` folder during the build.
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Nsec.app",
|
"name": "Nsec.app - Nostr key management tool",
|
||||||
"short_name": "Nsec.app - Nostr key management tool",
|
"short_name": "Nsec.app",
|
||||||
"start_url": ".",
|
"start_url": ".",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
|
32
src/App.tsx
32
src/App.tsx
@ -1,27 +1,28 @@
|
|||||||
import { DbKey, dbi } from './modules/db'
|
import { DbKey, dbi } from './modules/db'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { swicOnRender } from './modules/swic'
|
import { swicOnReload, swicOnRender } from './modules/swic'
|
||||||
import { useAppDispatch } from './store/hooks/redux'
|
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 AppRoutes from './routes/AppRoutes'
|
||||||
import { fetchProfile, ndk } from './modules/nostr'
|
import { fetchProfile, ndk } from './modules/nostr'
|
||||||
import { useModalSearchParams } from './hooks/useModalSearchParams'
|
|
||||||
import { MODAL_PARAMS_KEYS } from './types/modal'
|
|
||||||
import { ModalInitial } from './components/Modal/ModalInitial/ModalInitial'
|
import { ModalInitial } from './components/Modal/ModalInitial/ModalInitial'
|
||||||
import { ModalImportKeys } from './components/Modal/ModalImportKeys/ModalImportKeys'
|
import { ModalImportKeys } from './components/Modal/ModalImportKeys/ModalImportKeys'
|
||||||
import { ModalSignUp } from './components/Modal/ModalSignUp/ModalSignUp'
|
import { ModalSignUp } from './components/Modal/ModalSignUp/ModalSignUp'
|
||||||
import { ModalLogin } from './components/Modal/ModalLogin/ModalLogin'
|
import { ModalLogin } from './components/Modal/ModalLogin/ModalLogin'
|
||||||
|
import { useSessionStorage } from 'usehooks-ts'
|
||||||
|
import { RELOAD_STORAGE_KEY } from './utils/consts'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [render, setRender] = useState(0)
|
const [render, setRender] = useState(0)
|
||||||
const { handleOpen } = useModalSearchParams()
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const [_, setNeedReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
|
||||||
|
|
||||||
const [isConnected, setIsConnected] = useState(false)
|
const [isConnected, setIsConnected] = useState(false)
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
const keys: DbKey[] = await dbi.listKeys()
|
const keys: DbKey[] = await dbi.listKeys()
|
||||||
console.log(keys, 'keys')
|
|
||||||
|
|
||||||
dispatch(setKeys({ keys }))
|
dispatch(setKeys({ keys }))
|
||||||
const loadProfiles = async () => {
|
const loadProfiles = async () => {
|
||||||
@ -58,7 +59,6 @@ function App() {
|
|||||||
// rerender
|
// rerender
|
||||||
// setRender((r) => r + 1)
|
// setRender((r) => r + 1)
|
||||||
|
|
||||||
if (!keys.length) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ function App() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
ndk.connect().then(() => {
|
ndk.connect().then(() => {
|
||||||
console.log('NDK connected', { ndk })
|
console.log('NDK connected')
|
||||||
setIsConnected(true)
|
setIsConnected(true)
|
||||||
})
|
})
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@ -80,6 +80,24 @@ function App() {
|
|||||||
setRender((r) => r + 1)
|
setRender((r) => r + 1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// subscribe to service worker updates
|
||||||
|
swicOnReload(() => {
|
||||||
|
console.log('reload')
|
||||||
|
setNeedReload(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleBeforeUnload = () => {
|
||||||
|
setNeedReload(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', handleBeforeUnload)
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('beforeunload', handleBeforeUnload)
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
|
BIN
src/assets/fonts/Inter/Inter-Bold.ttf
Normal file
BIN
src/assets/fonts/Inter/Inter-Bold.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Inter/Inter-ExtraLight.ttf
Normal file
BIN
src/assets/fonts/Inter/Inter-ExtraLight.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Inter/Inter-Light.ttf
Normal file
BIN
src/assets/fonts/Inter/Inter-Light.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Inter/Inter-Medium.ttf
Normal file
BIN
src/assets/fonts/Inter/Inter-Medium.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Inter/Inter-Regular.ttf
Normal file
BIN
src/assets/fonts/Inter/Inter-Regular.ttf
Normal file
Binary file not shown.
BIN
src/assets/fonts/Inter/Inter-SemiBold.ttf
Normal file
BIN
src/assets/fonts/Inter/Inter-SemiBold.ttf
Normal file
Binary file not shown.
@ -3,7 +3,7 @@ import { Button } from '@/shared/Button/Button'
|
|||||||
import { Input } from '@/shared/Input/Input'
|
import { Input } from '@/shared/Input/Input'
|
||||||
import { Modal } from '@/shared/Modal/Modal'
|
import { Modal } from '@/shared/Modal/Modal'
|
||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { Autocomplete, CircularProgress, Stack, Typography } from '@mui/material'
|
import { Autocomplete, Stack, Typography } from '@mui/material'
|
||||||
import { StyledInput } from './styled'
|
import { StyledInput } from './styled'
|
||||||
import { FormEvent, useEffect, useState } from 'react'
|
import { FormEvent, useEffect, useState } from 'react'
|
||||||
import { isEmptyString } from '@/utils/helpers/helpers'
|
import { isEmptyString } from '@/utils/helpers/helpers'
|
||||||
@ -13,6 +13,7 @@ import { selectApps } from '@/store'
|
|||||||
import { dbi } from '@/modules/db'
|
import { dbi } from '@/modules/db'
|
||||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||||
import { setApps } from '@/store/reducers/content.slice'
|
import { setApps } from '@/store/reducers/content.slice'
|
||||||
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
|
||||||
export const ModalAppDetails = () => {
|
export const ModalAppDetails = () => {
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
@ -118,7 +119,7 @@ export const ModalAppDetails = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const isFormValid = !isEmptyString(url) && !isEmptyString(name) && !isEmptyString(icon)
|
const isFormValid = !isEmptyString(name)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||||
@ -129,6 +130,13 @@ export const ModalAppDetails = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Name"
|
||||||
|
fullWidth
|
||||||
|
placeholder="Enter app name"
|
||||||
|
onChange={handleInputChange('name')}
|
||||||
|
value={details.name}
|
||||||
|
/>
|
||||||
<Autocomplete
|
<Autocomplete
|
||||||
options={[]}
|
options={[]}
|
||||||
freeSolo
|
freeSolo
|
||||||
@ -149,13 +157,6 @@ export const ModalAppDetails = () => {
|
|||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Input
|
|
||||||
label="Name"
|
|
||||||
fullWidth
|
|
||||||
placeholder="Enter app name"
|
|
||||||
onChange={handleInputChange('name')}
|
|
||||||
value={details.name}
|
|
||||||
/>
|
|
||||||
<Input
|
<Input
|
||||||
label="Icon"
|
label="Icon"
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -165,7 +166,7 @@ export const ModalAppDetails = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Button varianttype="secondary" type="submit" fullWidth disabled={!isFormValid || isLoading}>
|
<Button varianttype="secondary" type="submit" fullWidth disabled={!isFormValid || isLoading}>
|
||||||
Save changes {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
Save changes {isLoading && <LoadingSpinner />}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AppInputProps, Input } from '@/shared/Input/Input'
|
import { Input } from '@/shared/Input/Input'
|
||||||
|
import { AppInputProps } from '@/shared/Input/types'
|
||||||
import { styled } from '@mui/material'
|
import { styled } from '@mui/material'
|
||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
import { Modal } from '@/shared/Modal/Modal'
|
import { Modal } from '@/shared/Modal/Modal'
|
||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { askNotificationPermission, call, getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/helpers'
|
import {
|
||||||
|
askNotificationPermission,
|
||||||
|
call,
|
||||||
|
getAppIconTitle,
|
||||||
|
getDomain,
|
||||||
|
getReferrerAppUrl,
|
||||||
|
getShortenNpub,
|
||||||
|
} from '@/utils/helpers/helpers'
|
||||||
import { Avatar, Box, Stack, Typography } from '@mui/material'
|
import { Avatar, Box, Stack, Typography } from '@mui/material'
|
||||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||||
import { useAppSelector } from '@/store/hooks/redux'
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
import { selectAppsByNpub, selectKeys, selectPendingsByNpub } from '@/store'
|
import { selectAppsByNpub, selectKeys, selectPendingsByNpub } from '@/store'
|
||||||
import { StyledButton, StyledToggleButtonsGroup } from './styled'
|
import { StyledButton, StyledToggleButtonsGroup } from './styled'
|
||||||
import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { swicCall } from '@/modules/swic'
|
import { swicCall, swicWaitStarted } from '@/modules/swic'
|
||||||
import { ACTION_TYPE } from '@/utils/consts'
|
import { ACTION_TYPE } from '@/utils/consts'
|
||||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||||
|
|
||||||
@ -28,6 +35,7 @@ export const ModalConfirmConnect = () => {
|
|||||||
const pending = useAppSelector((state) => selectPendingsByNpub(state, npub))
|
const pending = useAppSelector((state) => selectPendingsByNpub(state, npub))
|
||||||
|
|
||||||
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.BASIC)
|
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.BASIC)
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
|
|
||||||
const appNpub = searchParams.get('appNpub') || ''
|
const appNpub = searchParams.get('appNpub') || ''
|
||||||
const pendingReqId = searchParams.get('reqId') || ''
|
const pendingReqId = searchParams.get('reqId') || ''
|
||||||
@ -37,15 +45,7 @@ export const ModalConfirmConnect = () => {
|
|||||||
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
||||||
const { name, url = '', icon = '' } = triggerApp || {}
|
const { name, url = '', icon = '' } = triggerApp || {}
|
||||||
|
|
||||||
let appUrl = url || searchParams.get('appUrl') || ''
|
const appUrl = url || searchParams.get('appUrl') || getReferrerAppUrl()
|
||||||
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 appDomain = getDomain(appUrl)
|
||||||
const appName = name || appDomain || getShortenNpub(appNpub)
|
const appName = name || appDomain || getShortenNpub(appNpub)
|
||||||
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||||
@ -61,14 +61,45 @@ export const ModalConfirmConnect = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
// NOTE: when opened directly to this modal using authUrl,
|
||||||
// App doesn't exist yet!
|
// we might not have pending requests visible yet bcs we haven't
|
||||||
// const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
// loaded them yet, which means this modal will be closed with
|
||||||
const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId)
|
// the logic below. So now if it's popup then we wait for SW
|
||||||
console.log("pending", {isModalOpened, isPendingReqIdExists, isNpubExists, /*isAppNpubExists,*/ pendingReqId, pending});
|
// and then wait a little more to give it time to fetch
|
||||||
if (!isPopup && isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) {
|
// pending reqs from db. Same logic implemented in confirm-event.
|
||||||
closeModalAfterRequest()
|
|
||||||
return null
|
// FIXME move to a separate hook and reuse?
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isModalOpened) {
|
||||||
|
if (isPopup) {
|
||||||
|
console.log('waiting for sw')
|
||||||
|
// wait for SW to start
|
||||||
|
swicWaitStarted().then(() => {
|
||||||
|
// give it some time to load the pending reqs etc
|
||||||
|
console.log('waiting for sw done')
|
||||||
|
setTimeout(() => setIsLoaded(true), 500)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setIsLoaded(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsLoaded(false)
|
||||||
|
}
|
||||||
|
}, [isModalOpened, isPopup])
|
||||||
|
|
||||||
|
if (isLoaded) {
|
||||||
|
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||||
|
// NOTE: app doesn't exist yet!
|
||||||
|
// const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||||||
|
const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId)
|
||||||
|
// console.log("pending", {isModalOpened, isPendingReqIdExists, isNpubExists, /*isAppNpubExists,*/ pendingReqId, pending});
|
||||||
|
if (isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) {
|
||||||
|
// if (isPopup) window.close()
|
||||||
|
// else closeModalAfterRequest()
|
||||||
|
if (!isPopup) closeModalAfterRequest()
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
||||||
@ -101,7 +132,7 @@ export const ModalConfirmConnect = () => {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log('error', e)
|
console.log('error', e)
|
||||||
notify('Please enable Notifications in website settings!', 'error')
|
notify('Please enable Notifications in website settings!', 'error')
|
||||||
return
|
// keep going
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -139,7 +170,9 @@ export const ModalConfirmConnect = () => {
|
|||||||
if (isPopup) {
|
if (isPopup) {
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.visibilityState === 'hidden') {
|
if (document.visibilityState === 'hidden') {
|
||||||
disallow()
|
// FIXME it should be 'ignore once',
|
||||||
|
// not 'disallow & remember' - this is too strict
|
||||||
|
// disallow()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -164,11 +197,11 @@ export const ModalConfirmConnect = () => {
|
|||||||
>
|
>
|
||||||
{appAvatarTitle}
|
{appAvatarTitle}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Box>
|
<Box overflow={'auto'}>
|
||||||
<Typography variant="h5" fontWeight={600}>
|
<Typography variant="h5" fontWeight={600} noWrap>
|
||||||
{appName}
|
{appName}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" color={'GrayText'}>
|
<Typography variant="body2" color={'GrayText'} noWrap>
|
||||||
New app would like to connect
|
New app would like to connect
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
@ -187,7 +220,7 @@ export const ModalConfirmConnect = () => {
|
|||||||
</StyledToggleButtonsGroup>
|
</StyledToggleButtonsGroup>
|
||||||
<Stack direction={'row'} gap={'1rem'}>
|
<Stack direction={'row'} gap={'1rem'}>
|
||||||
<StyledButton onClick={disallow} varianttype="secondary">
|
<StyledButton onClick={disallow} varianttype="secondary">
|
||||||
Disallow
|
Ignore
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
<StyledButton fullWidth onClick={allow}>
|
<StyledButton fullWidth onClick={allow}>
|
||||||
Connect
|
Connect
|
||||||
|
@ -21,5 +21,8 @@ export const StyledToggleButtonsGroup = styled((props: ToggleButtonGroupProps) =
|
|||||||
border: 'initial',
|
border: 'initial',
|
||||||
borderRadius: '1rem',
|
borderRadius: '1rem',
|
||||||
},
|
},
|
||||||
|
'@media screen and (max-width: 320px)': {
|
||||||
|
marginBottom: '0.25rem',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
@ -4,22 +4,18 @@ import { StyledToggleButton } from './styled'
|
|||||||
|
|
||||||
type ActionToggleButtonProps = ToggleButtonProps & {
|
type ActionToggleButtonProps = ToggleButtonProps & {
|
||||||
description?: string
|
description?: string
|
||||||
hasinfo?: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({ hasinfo = false, ...props }) => {
|
export const ActionToggleButton: FC<ActionToggleButtonProps> = (props) => {
|
||||||
const { title, description = '' } = props
|
const { title, description = '' } = props
|
||||||
return (
|
return (
|
||||||
<StyledToggleButton {...props}>
|
<StyledToggleButton {...props}>
|
||||||
<Typography variant="body2">{title}</Typography>
|
<Typography variant="body2" noWrap className="title">
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
<Typography className="description" variant="caption" color={'GrayText'}>
|
<Typography className="description" variant="caption" color={'GrayText'}>
|
||||||
{description}
|
{description}
|
||||||
</Typography>
|
</Typography>
|
||||||
{hasinfo && (
|
|
||||||
<Typography className="info" color={'GrayText'}>
|
|
||||||
Info
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</StyledToggleButton>
|
</StyledToggleButton>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -29,4 +29,17 @@ export const StyledToggleButton = styled((props: ToggleButtonProps) => (
|
|||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
},
|
},
|
||||||
|
'@media screen and (max-width: 320px)': {
|
||||||
|
'& .title': {
|
||||||
|
fontSize: '14px',
|
||||||
|
},
|
||||||
|
'& .description': {
|
||||||
|
margin: '0.25rem 0',
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 3,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
@ -10,7 +10,7 @@ import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
|||||||
import { FC, useEffect, useMemo, useState } from 'react'
|
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 { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
import { swicCall } from '@/modules/swic'
|
import { swicCall, swicWaitStarted } from '@/modules/swic'
|
||||||
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
||||||
import { DbPending } from '@/modules/db'
|
import { DbPending } from '@/modules/db'
|
||||||
import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal'
|
import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal'
|
||||||
@ -47,6 +47,7 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
|||||||
|
|
||||||
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.ALWAYS)
|
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.ALWAYS)
|
||||||
const [pendingRequests, setPendingRequests] = useState<PendingRequest[]>([])
|
const [pendingRequests, setPendingRequests] = useState<PendingRequest[]>([])
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
|
|
||||||
const currentAppPendingReqs = useMemo(() => confirmEventReqs[appNpub]?.pending || [], [confirmEventReqs, appNpub])
|
const currentAppPendingReqs = useMemo(() => confirmEventReqs[appNpub]?.pending || [], [confirmEventReqs, appNpub])
|
||||||
|
|
||||||
@ -61,12 +62,33 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
useEffect(() => {
|
||||||
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
if (isModalOpened) {
|
||||||
|
if (isPopup) {
|
||||||
|
// wait for SW to start
|
||||||
|
swicWaitStarted().then(() => {
|
||||||
|
// give it some time to load the pending reqs etc
|
||||||
|
setTimeout(() => setIsLoaded(true), 500)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setIsLoaded(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setIsLoaded(false)
|
||||||
|
}
|
||||||
|
}, [isModalOpened, isPopup])
|
||||||
|
|
||||||
if (isModalOpened && (!isNpubExists || !isAppNpubExists)) {
|
if (isLoaded) {
|
||||||
closeModalAfterRequest()
|
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||||
return null
|
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||||||
|
// console.log("confirm event", { confirmEventReqs, isModalOpened, isNpubExists, isAppNpubExists });
|
||||||
|
if (isModalOpened && (!currentAppPendingReqs.length || !isNpubExists || !isAppNpubExists)) {
|
||||||
|
// if (isPopup) window.close()
|
||||||
|
// else closeModalAfterRequest()
|
||||||
|
if (!isPopup)
|
||||||
|
closeModalAfterRequest()
|
||||||
|
return null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
||||||
|
174
src/components/Modal/ModalEditName/ModalEditName.tsx
Normal file
174
src/components/Modal/ModalEditName/ModalEditName.tsx
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { CheckmarkIcon } from '@/assets'
|
||||||
|
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||||
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
|
import { swicCall } from '@/modules/swic'
|
||||||
|
import { Button } from '@/shared/Button/Button'
|
||||||
|
import { Input } from '@/shared/Input/Input'
|
||||||
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
import { Modal } from '@/shared/Modal/Modal'
|
||||||
|
import { selectKeys } from '@/store'
|
||||||
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
|
import { DOMAIN } from '@/utils/consts'
|
||||||
|
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||||
|
import { Stack, Typography, useTheme } from '@mui/material'
|
||||||
|
import { ChangeEvent, Fragment, useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { useDebounce } from 'use-debounce'
|
||||||
|
import { StyledSettingContainer } from './styled'
|
||||||
|
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
|
|
||||||
|
export const ModalEditName = () => {
|
||||||
|
const keys = useAppSelector(selectKeys)
|
||||||
|
const notify = useEnqueueSnackbar()
|
||||||
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
|
|
||||||
|
const key = keys.find((k) => k.npub === npub)
|
||||||
|
const name = key?.name || ''
|
||||||
|
|
||||||
|
const { palette } = useTheme()
|
||||||
|
|
||||||
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EDIT_NAME)
|
||||||
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.EDIT_NAME)
|
||||||
|
|
||||||
|
const [enteredName, setEnteredName] = useState('')
|
||||||
|
const [debouncedName] = useDebounce(enteredName, 300)
|
||||||
|
const isNameEqual = debouncedName === name
|
||||||
|
|
||||||
|
const [receiverNpub, setReceiverNpub] = useState('')
|
||||||
|
|
||||||
|
const [isAvailable, setIsAvailable] = useState(true)
|
||||||
|
const [isChecking, setIsChecking] = useState(false)
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [isTransferLoading, setIsTransferLoading] = useState(false)
|
||||||
|
|
||||||
|
const checkIsUsernameAvailable = useCallback(async () => {
|
||||||
|
if (!debouncedName.trim().length) return undefined
|
||||||
|
try {
|
||||||
|
setIsChecking(true)
|
||||||
|
const npubNip05 = await fetchNip05(`${debouncedName}@${DOMAIN}`)
|
||||||
|
setIsAvailable(!npubNip05 || npubNip05 === npub)
|
||||||
|
setIsChecking(false)
|
||||||
|
} catch (error) {
|
||||||
|
setIsAvailable(true)
|
||||||
|
setIsChecking(false)
|
||||||
|
}
|
||||||
|
}, [debouncedName, npub])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkIsUsernameAvailable()
|
||||||
|
}, [checkIsUsernameAvailable])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEnteredName(name)
|
||||||
|
return () => {
|
||||||
|
if (isModalOpened) {
|
||||||
|
setEnteredName('')
|
||||||
|
setReceiverNpub('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [isModalOpened])
|
||||||
|
|
||||||
|
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => setEnteredName(e.target.value)
|
||||||
|
|
||||||
|
const handleReceiverNpubChange = (e: ChangeEvent<HTMLInputElement>) => setReceiverNpub(e.target.value)
|
||||||
|
|
||||||
|
const getInputHelperText = () => {
|
||||||
|
if (!debouncedName.trim().length || isNameEqual) return ''
|
||||||
|
if (isChecking) return 'Loading...'
|
||||||
|
if (!isAvailable) return 'Already taken'
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<CheckmarkIcon /> Available
|
||||||
|
</Fragment>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const inputHelperText = getInputHelperText()
|
||||||
|
|
||||||
|
const getHelperTextColor = useCallback(() => {
|
||||||
|
if (!debouncedName || isChecking || isNameEqual) return palette.textSecondaryDecorate.main
|
||||||
|
return isAvailable ? palette.success.main : palette.error.main
|
||||||
|
// deps
|
||||||
|
}, [debouncedName, isAvailable, isChecking, isNameEqual, palette])
|
||||||
|
|
||||||
|
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||||
|
if (isModalOpened && !isNpubExists) {
|
||||||
|
handleCloseModal()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEditButtonDisabled = isNameEqual || !isAvailable || isChecking || isLoading || !enteredName.trim().length
|
||||||
|
const isTransferButtonDisabled = !name.length || !receiverNpub.trim().length || isTransferLoading
|
||||||
|
|
||||||
|
const handleEditName = async () => {
|
||||||
|
if (isEditButtonDisabled) return
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
await swicCall('editName', npub, enteredName)
|
||||||
|
notify('Username updated!', 'success')
|
||||||
|
setIsLoading(false)
|
||||||
|
} catch (error: any) {
|
||||||
|
setIsLoading(false)
|
||||||
|
notify(error?.message || 'Failed to edit username!', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTransferName = async () => {
|
||||||
|
if (isTransferButtonDisabled) return
|
||||||
|
try {
|
||||||
|
setIsTransferLoading(true)
|
||||||
|
await swicCall('transferName', npub, enteredName, receiverNpub)
|
||||||
|
notify('Username transferred!', 'success')
|
||||||
|
setIsTransferLoading(false)
|
||||||
|
setEnteredName('')
|
||||||
|
} catch (error: any) {
|
||||||
|
setIsTransferLoading(false)
|
||||||
|
notify(error?.message || 'Failed to transfer username!', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={isModalOpened} title="Username Settings" onClose={handleCloseModal}>
|
||||||
|
<Stack gap={'1rem'}>
|
||||||
|
<StyledSettingContainer>
|
||||||
|
<SectionTitle>Change name</SectionTitle>
|
||||||
|
<Input
|
||||||
|
label="User name"
|
||||||
|
fullWidth
|
||||||
|
placeholder="Enter a Username"
|
||||||
|
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||||
|
helperText={inputHelperText}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
value={enteredName}
|
||||||
|
helperTextProps={{
|
||||||
|
sx: {
|
||||||
|
'&.helper_text': {
|
||||||
|
color: getHelperTextColor(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Button fullWidth disabled={isEditButtonDisabled} onClick={handleEditName}>
|
||||||
|
Save name {isLoading && <LoadingSpinner />}
|
||||||
|
</Button>
|
||||||
|
</StyledSettingContainer>
|
||||||
|
<StyledSettingContainer>
|
||||||
|
<SectionTitle>Transfer name</SectionTitle>
|
||||||
|
<Input
|
||||||
|
label="Receiver npub"
|
||||||
|
fullWidth
|
||||||
|
placeholder="npub1..."
|
||||||
|
onChange={handleReceiverNpubChange}
|
||||||
|
value={receiverNpub}
|
||||||
|
/>
|
||||||
|
<Button fullWidth onClick={handleTransferName} disabled={isTransferButtonDisabled}>
|
||||||
|
Transfer name
|
||||||
|
</Button>
|
||||||
|
</StyledSettingContainer>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
10
src/components/Modal/ModalEditName/styled.tsx
Normal file
10
src/components/Modal/ModalEditName/styled.tsx
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { Stack, StackProps, styled } from '@mui/material'
|
||||||
|
|
||||||
|
export const StyledSettingContainer = styled((props: StackProps) => (
|
||||||
|
<Stack gap={'0.75rem'} component={'form'} {...props} />
|
||||||
|
))(({ theme }) => ({
|
||||||
|
padding: '1rem',
|
||||||
|
borderRadius: '1rem',
|
||||||
|
background: theme.palette.background.default,
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
}))
|
@ -5,8 +5,7 @@ import { Button } from '@/shared/Button/Button'
|
|||||||
import { Input } from '@/shared/Input/Input'
|
import { Input } from '@/shared/Input/Input'
|
||||||
import { Modal } from '@/shared/Modal/Modal'
|
import { Modal } from '@/shared/Modal/Modal'
|
||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { CircularProgress, Stack, Typography, useTheme } from '@mui/material'
|
import { Stack, Typography, useTheme } from '@mui/material'
|
||||||
import { StyledAppLogo } from './styled'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { FormInputType, schema } from './const'
|
import { FormInputType, schema } from './const'
|
||||||
@ -17,6 +16,9 @@ import { useDebounce } from 'use-debounce'
|
|||||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||||
import { DOMAIN } from '@/utils/consts'
|
import { DOMAIN } from '@/utils/consts'
|
||||||
import { CheckmarkIcon } from '@/assets'
|
import { CheckmarkIcon } from '@/assets'
|
||||||
|
import { getPublicKey, nip19 } from 'nostr-tools'
|
||||||
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
import { HeadingContainer } from './styled'
|
||||||
|
|
||||||
const FORM_DEFAULT_VALUES = {
|
const FORM_DEFAULT_VALUES = {
|
||||||
username: '',
|
username: '',
|
||||||
@ -42,35 +44,73 @@ export const ModalImportKeys = () => {
|
|||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
})
|
})
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [isAvailable, setIsAvailable] = useState(false)
|
const [nameNpub, setNameNpub] = useState('')
|
||||||
|
const [isTakenByNsec, setIsTakenByNsec] = useState(false)
|
||||||
|
const [isBadNsec, setIsBadNsec] = useState(false)
|
||||||
const enteredUsername = watch('username')
|
const enteredUsername = watch('username')
|
||||||
|
const enteredNsec = watch('nsec')
|
||||||
const [debouncedUsername] = useDebounce(enteredUsername, 100)
|
const [debouncedUsername] = useDebounce(enteredUsername, 100)
|
||||||
|
const [debouncedNsec] = useDebounce(enteredNsec, 100)
|
||||||
|
|
||||||
const checkIsUsernameAvailable = useCallback(async () => {
|
const checkIsUsernameAvailable = useCallback(async () => {
|
||||||
if (!debouncedUsername.trim().length) return undefined
|
if (!debouncedUsername.trim().length) return undefined
|
||||||
const npubNip05 = await fetchNip05(`${debouncedUsername}@${DOMAIN}`)
|
const npubNip05 = await fetchNip05(`${debouncedUsername}@${DOMAIN}`)
|
||||||
|
setNameNpub(npubNip05 || '')
|
||||||
setIsAvailable(!npubNip05)
|
|
||||||
}, [debouncedUsername])
|
}, [debouncedUsername])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
checkIsUsernameAvailable()
|
checkIsUsernameAvailable()
|
||||||
}, [checkIsUsernameAvailable])
|
}, [checkIsUsernameAvailable])
|
||||||
|
|
||||||
|
const checkNsecUsername = useCallback(async () => {
|
||||||
|
if (!debouncedNsec.trim().length) {
|
||||||
|
setIsTakenByNsec(false)
|
||||||
|
setIsBadNsec(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { type, data } = nip19.decode(debouncedNsec)
|
||||||
|
const ok = type === 'nsec'
|
||||||
|
setIsBadNsec(!ok)
|
||||||
|
if (ok) {
|
||||||
|
const npub = nip19.npubEncode(
|
||||||
|
// @ts-ignore
|
||||||
|
getPublicKey(data)
|
||||||
|
)
|
||||||
|
setIsTakenByNsec(!!nameNpub && nameNpub === npub)
|
||||||
|
} else {
|
||||||
|
setIsTakenByNsec(false)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setIsBadNsec(true)
|
||||||
|
setIsTakenByNsec(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [debouncedNsec])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkNsecUsername()
|
||||||
|
}, [checkNsecUsername])
|
||||||
|
|
||||||
const cleanUpStates = useCallback(() => {
|
const cleanUpStates = useCallback(() => {
|
||||||
hidePassword()
|
hidePassword()
|
||||||
reset()
|
reset()
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
setIsAvailable(false)
|
setNameNpub('')
|
||||||
|
setIsTakenByNsec(false)
|
||||||
|
setIsBadNsec(false)
|
||||||
}, [reset, hidePassword])
|
}, [reset, hidePassword])
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const submitHandler = async (values: FormInputType) => {
|
const submitHandler = async (values: FormInputType) => {
|
||||||
if (isLoading || !isAvailable) return undefined
|
if (isLoading) return undefined
|
||||||
try {
|
try {
|
||||||
const { nsec, username } = values
|
const { nsec, username } = values
|
||||||
|
if (!nsec || !username) throw new Error('Enter username and nsec')
|
||||||
|
if (nameNpub && !isTakenByNsec) throw new Error('Name taken')
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const k: any = await swicCall('importKey', username, nsec)
|
const k: any = await swicCall('importKey', username, nsec)
|
||||||
notify('Key imported!', 'success')
|
notify('Key imported!', 'success')
|
||||||
@ -88,9 +128,11 @@ export const ModalImportKeys = () => {
|
|||||||
}
|
}
|
||||||
}, [isModalOpened, cleanUpStates])
|
}, [isModalOpened, cleanUpStates])
|
||||||
|
|
||||||
const getInputHelperText = () => {
|
const getNameHelperText = () => {
|
||||||
if (!enteredUsername) return "Don't worry, username can be changed later."
|
if (!enteredUsername) return "Don't worry, username can be changed later."
|
||||||
if (!isAvailable) return 'Already taken'
|
if (isTakenByNsec) return 'Name matches your key'
|
||||||
|
if (isBadNsec) return 'Invalid nsec'
|
||||||
|
if (nameNpub) return 'Already taken'
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CheckmarkIcon /> Available
|
<CheckmarkIcon /> Available
|
||||||
@ -98,17 +140,25 @@ export const ModalImportKeys = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputHelperText = getInputHelperText()
|
const getNsecHelperText = () => {
|
||||||
|
if (isBadNsec) return 'Invalid nsec'
|
||||||
|
return 'Keys stay on your device.'
|
||||||
|
}
|
||||||
|
|
||||||
|
const nameHelperText = getNameHelperText()
|
||||||
|
const nsecHelperText = getNsecHelperText()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
|
||||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
||||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
<HeadingContainer>
|
||||||
<StyledAppLogo />
|
|
||||||
<Typography fontWeight={600} variant="h5">
|
<Typography fontWeight={600} variant="h5">
|
||||||
Import key
|
Import key
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
<Typography noWrap variant="body2" color={'GrayText'}>
|
||||||
|
Bring your existing Nostr keys to Nsec.app
|
||||||
|
</Typography>
|
||||||
|
</HeadingContainer>
|
||||||
<Input
|
<Input
|
||||||
label="Choose a username"
|
label="Choose a username"
|
||||||
fullWidth
|
fullWidth
|
||||||
@ -116,14 +166,14 @@ export const ModalImportKeys = () => {
|
|||||||
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||||
{...register('username')}
|
{...register('username')}
|
||||||
error={!!errors.username}
|
error={!!errors.username}
|
||||||
helperText={inputHelperText}
|
helperText={nameHelperText}
|
||||||
helperTextProps={{
|
helperTextProps={{
|
||||||
sx: {
|
sx: {
|
||||||
'&.helper_text': {
|
'&.helper_text': {
|
||||||
color:
|
color:
|
||||||
enteredUsername && isAvailable
|
enteredUsername && (isTakenByNsec || !nameNpub)
|
||||||
? theme.palette.success.main
|
? theme.palette.success.main
|
||||||
: enteredUsername && !isAvailable
|
: enteredUsername && nameNpub
|
||||||
? theme.palette.error.main
|
? theme.palette.error.main
|
||||||
: theme.palette.textSecondaryDecorate.main,
|
: theme.palette.textSecondaryDecorate.main,
|
||||||
},
|
},
|
||||||
@ -137,18 +187,18 @@ export const ModalImportKeys = () => {
|
|||||||
{...register('nsec')}
|
{...register('nsec')}
|
||||||
error={!!errors.nsec}
|
error={!!errors.nsec}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
helperText="Keys stay on your device."
|
helperText={nsecHelperText}
|
||||||
helperTextProps={{
|
helperTextProps={{
|
||||||
sx: {
|
sx: {
|
||||||
'&.helper_text': {
|
'&.helper_text': {
|
||||||
color: theme.palette.textSecondaryDecorate.main,
|
color: isBadNsec ? theme.palette.error.main : theme.palette.textSecondaryDecorate.main,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit" disabled={isLoading}>
|
<Button type="submit" disabled={isLoading}>
|
||||||
Import key {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
Import key {isLoading && <LoadingSpinner />}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { AppLogo } from '@/assets'
|
import { AppLogo } from '@/assets'
|
||||||
import { Box, styled } from '@mui/material'
|
import { Box, Stack, StackProps, styled } from '@mui/material'
|
||||||
|
|
||||||
export const StyledAppLogo = styled((props) => (
|
export const StyledAppLogo = styled((props) => (
|
||||||
<Box {...props}>
|
<Box {...props}>
|
||||||
@ -12,3 +12,14 @@ export const StyledAppLogo = styled((props) => (
|
|||||||
display: 'grid',
|
display: 'grid',
|
||||||
placeItems: 'center',
|
placeItems: 'center',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
export const HeadingContainer = styled((props: StackProps) => <Stack {...props} />)(() => ({
|
||||||
|
gap: '0.2rem',
|
||||||
|
padding: '0 1rem',
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
overflow: 'auto',
|
||||||
|
width: '100%',
|
||||||
|
'@media screen and (max-width: 320px)': {
|
||||||
|
padding: '0 0.75rem',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
@ -1,43 +1,36 @@
|
|||||||
import React, { useEffect, useState } from 'react'
|
// import { useEffect } from 'react'
|
||||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
import { Button } from '@/shared/Button/Button'
|
import { Button } from '@/shared/Button/Button'
|
||||||
import { Modal } from '@/shared/Modal/Modal'
|
import { Modal } from '@/shared/Modal/Modal'
|
||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { Fade, Stack } from '@mui/material'
|
import { Stack } from '@mui/material'
|
||||||
import { AppLink } from '@/shared/AppLink/AppLink'
|
// import { AppLink } from '@/shared/AppLink/AppLink'
|
||||||
|
|
||||||
export const ModalInitial = () => {
|
export const ModalInitial = () => {
|
||||||
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
|
|
||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.INITIAL)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
|
|
||||||
const [showAdvancedContent, setShowAdvancedContent] = useState(false)
|
// const [showAdvancedContent, setShowAdvancedContent] = useState(false)
|
||||||
|
|
||||||
const handleShowAdvanced = () => {
|
// const handleShowAdvanced = () => {
|
||||||
setShowAdvancedContent(true)
|
// setShowAdvancedContent(true)
|
||||||
}
|
// }
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
return () => {
|
// return () => {
|
||||||
if (isModalOpened) {
|
// if (isModalOpened) {
|
||||||
setShowAdvancedContent(false)
|
// setShowAdvancedContent(false)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}, [isModalOpened])
|
// }, [isModalOpened])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||||
<Stack paddingTop={'0.5rem'} gap={'1rem'}>
|
<Stack paddingTop={'0.5rem'} gap={'1rem'}>
|
||||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.SIGN_UP)}>Sign up</Button>
|
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.SIGN_UP)}>Sign up</Button>
|
||||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.LOGIN)}>Login</Button>
|
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.LOGIN)}>Login</Button>
|
||||||
<AppLink title="Advanced" alignSelf={'center'} onClick={handleShowAdvanced} />
|
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS)}>Import key</Button>
|
||||||
|
|
||||||
{showAdvancedContent && (
|
|
||||||
<Fade in>
|
|
||||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS)}>Import key</Button>
|
|
||||||
</Fade>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
@ -4,18 +4,18 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
|||||||
import { swicCall } from '@/modules/swic'
|
import { swicCall } from '@/modules/swic'
|
||||||
import { Modal } from '@/shared/Modal/Modal'
|
import { Modal } from '@/shared/Modal/Modal'
|
||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { CircularProgress, Stack, Typography } from '@mui/material'
|
import { Stack, Typography } from '@mui/material'
|
||||||
import { StyledAppLogo } from './styled'
|
|
||||||
import { Input } from '@/shared/Input/Input'
|
import { Input } from '@/shared/Input/Input'
|
||||||
import { Button } from '@/shared/Button/Button'
|
import { Button } from '@/shared/Button/Button'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import { useForm } from 'react-hook-form'
|
import { useForm } from 'react-hook-form'
|
||||||
import { FormInputType, schema } from './const'
|
import { FormInputType, schema } from './const'
|
||||||
import { yupResolver } from '@hookform/resolvers/yup'
|
import { yupResolver } from '@hookform/resolvers/yup'
|
||||||
import { DOMAIN } from '@/utils/consts'
|
import { DOMAIN } from '@/utils/consts'
|
||||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
import { fetchNip05, fetchNpubNames } from '@/utils/helpers/helpers'
|
||||||
import { usePassword } from '@/hooks/usePassword'
|
import { usePassword } from '@/hooks/usePassword'
|
||||||
import { dbi } from '@/modules/db'
|
import { dbi } from '@/modules/db'
|
||||||
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
|
||||||
const FORM_DEFAULT_VALUES = {
|
const FORM_DEFAULT_VALUES = {
|
||||||
username: '',
|
username: '',
|
||||||
@ -31,11 +31,14 @@ export const ModalLogin = () => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { hidePassword, inputProps } = usePassword()
|
const { hidePassword, inputProps } = usePassword()
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const isPopup = searchParams.get('popup') === 'true'
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
register,
|
register,
|
||||||
|
setValue,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<FormInputType>({
|
} = useForm<FormInputType>({
|
||||||
defaultValues: FORM_DEFAULT_VALUES,
|
defaultValues: FORM_DEFAULT_VALUES,
|
||||||
@ -78,7 +81,10 @@ export const ModalLogin = () => {
|
|||||||
notify(`Fetched ${k.npub}`, 'success')
|
notify(`Fetched ${k.npub}`, 'success')
|
||||||
dbi.addSynced(k.npub)
|
dbi.addSynced(k.npub)
|
||||||
cleanUpStates()
|
cleanUpStates()
|
||||||
navigate(`/key/${k.npub}`)
|
setTimeout(() => {
|
||||||
|
// give frontend time to read the new key first
|
||||||
|
navigate(`/key/${k.npub}${isPopup ? '?popup=true' : ''}`)
|
||||||
|
}, 300)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.log('error', error)
|
console.log('error', error)
|
||||||
notify(error?.message || 'Something went wrong!', 'error')
|
notify(error?.message || 'Something went wrong!', 'error')
|
||||||
@ -86,6 +92,22 @@ export const ModalLogin = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isModalOpened) {
|
||||||
|
const npub = searchParams.get('npub') || ''
|
||||||
|
const appNpub = searchParams.get('appNpub') || ''
|
||||||
|
if (isPopup && isModalOpened) {
|
||||||
|
swicCall('fetchPendingRequests', npub, appNpub)
|
||||||
|
|
||||||
|
fetchNpubNames(npub).then((names) => {
|
||||||
|
if (names.length) {
|
||||||
|
setValue('username', `${names[0]}@${DOMAIN}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [searchParams, isModalOpened, isPopup, setValue])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (isModalOpened) {
|
if (isModalOpened) {
|
||||||
@ -96,13 +118,15 @@ export const ModalLogin = () => {
|
|||||||
}, [isModalOpened, cleanUpStates])
|
}, [isModalOpened, cleanUpStates])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
|
||||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
||||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
<Stack gap={'0.2rem'} padding={'0 1rem'} alignSelf={'flex-start'}>
|
||||||
<StyledAppLogo />
|
|
||||||
<Typography fontWeight={600} variant="h5">
|
<Typography fontWeight={600} variant="h5">
|
||||||
Login
|
Login
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Typography noWrap variant="body2" color={'GrayText'}>
|
||||||
|
Sync keys from the cloud to this device
|
||||||
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Input
|
<Input
|
||||||
label="Username or nip05 or npub"
|
label="Username or nip05 or npub"
|
||||||
@ -118,10 +142,14 @@ export const ModalLogin = () => {
|
|||||||
{...register('password')}
|
{...register('password')}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
error={!!errors.password}
|
error={!!errors.password}
|
||||||
|
helperText={'Password you set in Cloud Sync settings'}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" fullWidth disabled={isLoading}>
|
|
||||||
Add account {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
<Stack gap={'0.5rem'}>
|
||||||
</Button>
|
<Button type="submit" fullWidth disabled={isLoading}>
|
||||||
|
Login {isLoading && <LoadingSpinner />}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
import { Button } from '@/shared/Button/Button'
|
|
||||||
import { Modal } from '@/shared/Modal/Modal'
|
import { Modal } from '@/shared/Modal/Modal'
|
||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { Box, CircularProgress, Stack, Typography } from '@mui/material'
|
import { Box, Stack, Typography } from '@mui/material'
|
||||||
import { StyledButton, StyledSettingContainer, StyledSynchedText } from './styled'
|
import { StyledButton, StyledSettingContainer, StyledSynchedText } from './styled'
|
||||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
import { CheckmarkIcon } from '@/assets'
|
import { CheckmarkIcon } from '@/assets'
|
||||||
@ -16,6 +15,8 @@ import { dbi } from '@/modules/db'
|
|||||||
import { usePassword } from '@/hooks/usePassword'
|
import { usePassword } from '@/hooks/usePassword'
|
||||||
import { useAppSelector } from '@/store/hooks/redux'
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
import { selectKeys } from '@/store'
|
import { selectKeys } from '@/store'
|
||||||
|
import { isValidPassphase, isWeakPassphase } from '@/modules/keys'
|
||||||
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
|
||||||
type ModalSettingsProps = {
|
type ModalSettingsProps = {
|
||||||
isSynced: boolean
|
isSynced: boolean
|
||||||
@ -58,8 +59,9 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setIsPasswordInvalid(false)
|
const password = e.target.value
|
||||||
setEnteredPassword(e.target.value)
|
setIsPasswordInvalid(!!password && !isValidPassphase(password))
|
||||||
|
setEnteredPassword(password)
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
@ -76,7 +78,7 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsPasswordInvalid(false)
|
setIsPasswordInvalid(false)
|
||||||
|
|
||||||
if (enteredPassword.trim().length < 6) {
|
if (!isValidPassphase(enteredPassword)) {
|
||||||
return setIsPasswordInvalid(true)
|
return setIsPasswordInvalid(true)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -114,18 +116,22 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
|||||||
{...inputProps}
|
{...inputProps}
|
||||||
onChange={handlePasswordChange}
|
onChange={handlePasswordChange}
|
||||||
value={enteredPassword}
|
value={enteredPassword}
|
||||||
helperText={isPasswordInvalid ? 'Invalid password' : ''}
|
|
||||||
placeholder="Enter a password"
|
placeholder="Enter a password"
|
||||||
helperTextProps={{
|
|
||||||
sx: {
|
|
||||||
'&.helper_text': {
|
|
||||||
color: 'red',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
disabled={!isChecked}
|
disabled={!isChecked}
|
||||||
/>
|
/>
|
||||||
{isSynced ? (
|
{isPasswordInvalid ? (
|
||||||
|
<Typography variant="body2" color={'red'}>
|
||||||
|
Password must include 6+ English letters, numbers or punctuation marks.
|
||||||
|
</Typography>
|
||||||
|
) : !!enteredPassword && isWeakPassphase(enteredPassword) ? (
|
||||||
|
<Typography variant="body2" color={'orange'}>
|
||||||
|
Weak password
|
||||||
|
</Typography>
|
||||||
|
) : !!enteredPassword && !isPasswordInvalid ? (
|
||||||
|
<Typography variant="body2" color={'green'}>
|
||||||
|
Good password
|
||||||
|
</Typography>
|
||||||
|
) : isSynced ? (
|
||||||
<Typography variant="body2" color={'GrayText'}>
|
<Typography variant="body2" color={'GrayText'}>
|
||||||
To change your password, type a new one and sync.
|
To change your password, type a new one and sync.
|
||||||
</Typography>
|
</Typography>
|
||||||
@ -136,10 +142,9 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
<StyledButton type="submit" fullWidth disabled={!isChecked}>
|
<StyledButton type="submit" fullWidth disabled={!isChecked}>
|
||||||
Sync {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
Sync {isLoading && <LoadingSpinner mode="secondary" />}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</StyledSettingContainer>
|
</StyledSettingContainer>
|
||||||
<Button onClick={onClose}>Done</Button>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
@ -2,9 +2,8 @@ import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
|||||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
import { Modal } from '@/shared/Modal/Modal'
|
import { Modal } from '@/shared/Modal/Modal'
|
||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { CircularProgress, Stack, Typography, useTheme } from '@mui/material'
|
import { Stack, Typography, useTheme } from '@mui/material'
|
||||||
import React, { ChangeEvent, useEffect, useState } from 'react'
|
import React, { ChangeEvent, useEffect, useState } from 'react'
|
||||||
import { StyledAppLogo } from './styled'
|
|
||||||
import { Input } from '@/shared/Input/Input'
|
import { Input } from '@/shared/Input/Input'
|
||||||
import { Button } from '@/shared/Button/Button'
|
import { Button } from '@/shared/Button/Button'
|
||||||
import { CheckmarkIcon } from '@/assets'
|
import { CheckmarkIcon } from '@/assets'
|
||||||
@ -12,6 +11,7 @@ import { swicCall } from '@/modules/swic'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { DOMAIN } from '@/utils/consts'
|
import { DOMAIN } from '@/utils/consts'
|
||||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||||
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
|
||||||
export const ModalSignUp = () => {
|
export const ModalSignUp = () => {
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
@ -60,9 +60,13 @@ export const ModalSignUp = () => {
|
|||||||
try {
|
try {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const k: any = await swicCall('generateKey', name)
|
const k: any = await swicCall('generateKey', name)
|
||||||
notify(`Account created for "${name}"`, 'success')
|
if (k.name) notify(`Account created for "${k.name}"`, 'success')
|
||||||
navigate(`/key/${k.npub}`)
|
else notify(`Failed to assign name "${name}", try again`, 'error')
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
|
setTimeout(() => {
|
||||||
|
// give frontend time to read the new key first
|
||||||
|
navigate(`/key/${k.npub}`)
|
||||||
|
}, 300)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error?.message || 'Something went wrong!', 'error')
|
notify(error?.message || 'Something went wrong!', 'error')
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@ -80,13 +84,15 @@ export const ModalSignUp = () => {
|
|||||||
}, [isModalOpened])
|
}, [isModalOpened])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
|
||||||
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
<Stack gap={'0.2rem'} padding={'0 1rem'} alignSelf={'flex-start'}>
|
||||||
<StyledAppLogo />
|
|
||||||
<Typography fontWeight={600} variant="h5">
|
<Typography fontWeight={600} variant="h5">
|
||||||
Sign up
|
Sign up
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<Typography noWrap variant="body2" color={'GrayText'}>
|
||||||
|
Generate new Nostr keys
|
||||||
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Input
|
<Input
|
||||||
label="Username"
|
label="Username"
|
||||||
@ -109,9 +115,11 @@ export const ModalSignUp = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button fullWidth type="submit" disabled={isLoading}>
|
<Stack gap={'0.5rem'}>
|
||||||
Create account {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
<Button fullWidth type="submit" disabled={isLoading}>
|
||||||
</Button>
|
Create account {isLoading && <LoadingSpinner />}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
33
src/components/ReloadBadge/ReloadBadge.tsx
Normal file
33
src/components/ReloadBadge/ReloadBadge.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { FC, memo, useCallback } from 'react'
|
||||||
|
import { Stack, Typography } from '@mui/material'
|
||||||
|
import { StyledAlert, StyledReloadButton } from './styled'
|
||||||
|
import { useSessionStorage } from 'usehooks-ts'
|
||||||
|
import { RELOAD_STORAGE_KEY } from '@/utils/consts'
|
||||||
|
|
||||||
|
type ReloadBadgeContentProps = {
|
||||||
|
onReload: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReloadBadgeContent: FC<ReloadBadgeContentProps> = memo(({ onReload }) => {
|
||||||
|
return (
|
||||||
|
<StyledAlert>
|
||||||
|
<Stack direction={'row'} className="content">
|
||||||
|
<Typography flex={1} className="title">
|
||||||
|
New version available!
|
||||||
|
</Typography>
|
||||||
|
<StyledReloadButton onClick={onReload}>Reload</StyledReloadButton>
|
||||||
|
</Stack>
|
||||||
|
</StyledAlert>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ReloadBadge = () => {
|
||||||
|
const [needReload, setNeedReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
|
||||||
|
|
||||||
|
const handleReload = useCallback(() => {
|
||||||
|
setNeedReload(false)
|
||||||
|
window.location.reload()
|
||||||
|
}, [setNeedReload])
|
||||||
|
|
||||||
|
return <>{needReload && <ReloadBadgeContent onReload={handleReload} />}</>
|
||||||
|
}
|
50
src/components/ReloadBadge/styled.tsx
Normal file
50
src/components/ReloadBadge/styled.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
||||||
|
import { Alert, AlertProps, styled } from '@mui/material'
|
||||||
|
import RefreshIcon from '@mui/icons-material/Refresh'
|
||||||
|
|
||||||
|
export const StyledAlert = styled((props: AlertProps) => (
|
||||||
|
<Alert {...props} variant="outlined" severity="info" classes={{ message: 'message' }} />
|
||||||
|
))(() => {
|
||||||
|
return {
|
||||||
|
height: 'auto',
|
||||||
|
marginTop: '0.5rem',
|
||||||
|
alignItems: 'center',
|
||||||
|
'& .message': {
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'initial',
|
||||||
|
},
|
||||||
|
'& .content': {
|
||||||
|
width: '100%',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '1rem',
|
||||||
|
},
|
||||||
|
'& .title': {
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 3,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
},
|
||||||
|
'@media screen and (max-width: 320px)': {
|
||||||
|
'& .title': {
|
||||||
|
fontSize: '14px',
|
||||||
|
WebkitLineClamp: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export const StyledReloadButton = styled((props: AppButtonProps) => <Button startIcon={<RefreshIcon />} {...props} />)(
|
||||||
|
({ theme }) => {
|
||||||
|
const isDarkTheme = theme.palette.mode === 'dark'
|
||||||
|
return {
|
||||||
|
padding: '0.25rem 1rem',
|
||||||
|
'&.button:is(:hover, :active, &)': {
|
||||||
|
background: isDarkTheme ? '#b8e7fb' : '#014361',
|
||||||
|
},
|
||||||
|
'@media screen and (max-width: 320px)': {
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
@ -1,27 +1,27 @@
|
|||||||
import { FC, ReactNode } from 'react'
|
import { FC, ReactNode } from 'react'
|
||||||
import { IconContainer, StyledContainer } from './styled'
|
import { IconContainer, StyledContainer } from './styled'
|
||||||
import { BoxProps, Stack, Typography } from '@mui/material'
|
import { BoxProps, Typography } from '@mui/material'
|
||||||
|
|
||||||
type WarningProps = {
|
type WarningProps = {
|
||||||
message?: string | ReactNode
|
message?: string | ReactNode
|
||||||
hint?: string | ReactNode
|
|
||||||
icon?: ReactNode
|
icon?: ReactNode
|
||||||
} & BoxProps
|
} & BoxProps
|
||||||
|
|
||||||
export const Warning: FC<WarningProps> = ({ hint, message, icon, ...restProps }) => {
|
export const Warning: FC<WarningProps> = ({ message, icon, ...restProps }) => {
|
||||||
|
const renderMessage = () => {
|
||||||
|
if (typeof message === 'string') {
|
||||||
|
return (
|
||||||
|
<Typography noWrap width={'100%'}>
|
||||||
|
{message}
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<StyledContainer {...restProps}>
|
<StyledContainer {...restProps}>
|
||||||
{icon && <IconContainer>{icon}</IconContainer>}
|
{icon && <IconContainer>{icon}</IconContainer>}
|
||||||
<Stack flex={1} direction={'column'} gap={'0.2rem'}>
|
{renderMessage()}
|
||||||
<Typography noWrap>
|
|
||||||
{message}
|
|
||||||
</Typography>
|
|
||||||
{hint && (
|
|
||||||
<Typography>
|
|
||||||
{hint}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Stack>
|
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -7,13 +7,14 @@ export const StyledContainer = styled((props: BoxProps) => <Box {...props} />)((
|
|||||||
padding: '0.5rem',
|
padding: '0.5rem',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '1rem',
|
gap: '0.5rem',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const IconContainer = styled((props: BoxProps) => <Box {...props} />)(() => ({
|
export const IconContainer = styled((props: BoxProps) => <Box {...props} />)(() => ({
|
||||||
width: '40px',
|
width: '40px',
|
||||||
|
minWidth: '40px',
|
||||||
height: '40px',
|
height: '40px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: 'grey',
|
background: 'grey',
|
||||||
|
@ -31,7 +31,7 @@ export const useModalSearchParams = () => {
|
|||||||
const enumKey = getEnumParam(modal)
|
const enumKey = getEnumParam(modal)
|
||||||
searchParams.delete(enumKey)
|
searchParams.delete(enumKey)
|
||||||
extraOptions?.onClose && extraOptions?.onClose(searchParams)
|
extraOptions?.onClose && extraOptions?.onClose(searchParams)
|
||||||
console.log({ searchParams })
|
// console.log({ searchParams })
|
||||||
setSearchParams(searchParams, { replace: !!extraOptions?.replace })
|
setSearchParams(searchParams, { replace: !!extraOptions?.replace })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ export const useProfile = (npub: string) => {
|
|||||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||||
const currentKey = useAppSelector((state) => selectKeyByNpub(state, npub))
|
const currentKey = useAppSelector((state) => selectKeyByNpub(state, npub))
|
||||||
|
|
||||||
const userName = getProfileUsername(profile) || currentKey?.name
|
const userName = currentKey?.name || getProfileUsername(profile)
|
||||||
const userAvatar = profile?.info?.picture || ''
|
const userAvatar = profile?.info?.picture || ''
|
||||||
const avatarTitle = getFirstLetter(userName)
|
const avatarTitle = getFirstLetter(userName)
|
||||||
|
|
||||||
|
@ -1,17 +1,63 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src:
|
||||||
|
local('Inter ExtraLight'),
|
||||||
|
local('Inter-ExtraLight'),
|
||||||
|
url('./assets/fonts/Inter/Inter-ExtraLight.ttf') format('truetype');
|
||||||
|
font-weight: 200;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src:
|
||||||
|
local('Inter Light'),
|
||||||
|
local('Inter-Light'),
|
||||||
|
url('./assets/fonts/Inter/Inter-Light.ttf') format('truetype');
|
||||||
|
font-weight: 300;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src:
|
||||||
|
local('Inter Regular'),
|
||||||
|
local('Inter-Regular'),
|
||||||
|
url('./assets/fonts/Inter/Inter-Regular.ttf') format('truetype');
|
||||||
|
font-weight: 400;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src:
|
||||||
|
local('Inter Medium'),
|
||||||
|
local('Inter-Medium'),
|
||||||
|
url('./assets/fonts/Inter/Inter-Medium.ttf') format('truetype');
|
||||||
|
font-weight: 500;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src:
|
||||||
|
local('Inter SemiBold'),
|
||||||
|
local('Inter-SemiBold'),
|
||||||
|
url('./assets/fonts/Inter/Inter-SemiBold.ttf') format('truetype');
|
||||||
|
font-weight: 600;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src:
|
||||||
|
local('Inter Bold'),
|
||||||
|
local('Inter-Bold'),
|
||||||
|
url('./assets/fonts/Inter/Inter-Bold.ttf') format('truetype');
|
||||||
|
font-weight: 700;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
font-family: 'Inter', sans-serif;
|
||||||
body {
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import './index.css'
|
import ThemeProvider from './modules/theme/ThemeProvider'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import './index.css'
|
||||||
import reportWebVitals from './reportWebVitals'
|
import reportWebVitals from './reportWebVitals'
|
||||||
import { swicRegister } from './modules/swic'
|
import { swicRegister } from './modules/swic'
|
||||||
import { BrowserRouter } from 'react-router-dom'
|
import { BrowserRouter } from 'react-router-dom'
|
||||||
import { Provider } from 'react-redux'
|
import { Provider } from 'react-redux'
|
||||||
import { persistor, store } from './store'
|
import { persistor, store } from './store'
|
||||||
import ThemeProvider from './modules/theme/ThemeProvider'
|
|
||||||
import { PersistGate } from 'redux-persist/integration/react'
|
import { PersistGate } from 'redux-persist/integration/react'
|
||||||
import { SnackbarProvider } from 'notistack'
|
import { SnackbarProvider } from 'notistack'
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Avatar, Stack, Toolbar, Typography } from '@mui/material'
|
import { Avatar, Stack, Toolbar, Typography, Divider, DividerProps, styled } from '@mui/material'
|
||||||
import { StyledAppBar, StyledAppLogo, StyledAppName, StyledProfileContainer, StyledThemeButton } from './styled'
|
import { StyledAppBar, StyledAppLogo, StyledAppName, StyledProfileContainer, StyledThemeButton } from './styled'
|
||||||
import { Menu } from './components/Menu'
|
import { Menu } from './components/Menu'
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
@ -8,11 +8,14 @@ import DarkModeIcon from '@mui/icons-material/DarkMode'
|
|||||||
import LightModeIcon from '@mui/icons-material/LightMode'
|
import LightModeIcon from '@mui/icons-material/LightMode'
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
||||||
import { setThemeMode } from '@/store/reducers/ui.slice'
|
import { setThemeMode } from '@/store/reducers/ui.slice'
|
||||||
|
import { useSessionStorage } from 'usehooks-ts'
|
||||||
|
import { RELOAD_STORAGE_KEY } from '@/utils/consts'
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const [needReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
|
||||||
|
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||||
@ -23,14 +26,14 @@ export const Header = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isDarkMode = themeMode === 'dark'
|
const isDarkMode = themeMode === 'dark'
|
||||||
const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#000" />
|
const themeIcon = isDarkMode ? <LightModeIcon htmlColor="#fff" /> : <DarkModeIcon htmlColor="#000" />
|
||||||
|
|
||||||
const handleChangeMode = () => {
|
const handleChangeMode = () => {
|
||||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledAppBar position="fixed">
|
<StyledAppBar position={needReload ? 'relative' : 'fixed'}>
|
||||||
<Toolbar sx={{ padding: '12px' }}>
|
<Toolbar sx={{ padding: '12px' }}>
|
||||||
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'} width={'100%'}>
|
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'} width={'100%'}>
|
||||||
{showProfile && (
|
{showProfile && (
|
||||||
@ -56,6 +59,15 @@ export const Header = () => {
|
|||||||
{showProfile ? <ProfileMenu /> : <Menu />}
|
{showProfile ? <ProfileMenu /> : <Menu />}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
<StyledDivider />
|
||||||
</StyledAppBar>
|
</StyledAppBar>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
width: '100%',
|
||||||
|
left: 0,
|
||||||
|
height: '2px',
|
||||||
|
})
|
||||||
|
@ -4,28 +4,21 @@ import { Avatar, ListItemIcon, MenuItem, Typography } from '@mui/material'
|
|||||||
import React, { FC } from 'react'
|
import React, { FC } from 'react'
|
||||||
|
|
||||||
type ListItemProfileProps = {
|
type ListItemProfileProps = {
|
||||||
onClickItem: () => void
|
onClickItem: () => void
|
||||||
} & DbKey
|
} & DbKey
|
||||||
|
|
||||||
export const ListItemProfile: FC<ListItemProfileProps> = ({
|
export const ListItemProfile: FC<ListItemProfileProps> = ({ onClickItem, npub }) => {
|
||||||
onClickItem,
|
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||||
npub,
|
return (
|
||||||
}) => {
|
<MenuItem sx={{ gap: '0.5rem' }} onClick={onClickItem}>
|
||||||
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
<ListItemIcon>
|
||||||
return (
|
<Avatar src={userAvatar} alt={userName} sx={{ width: 36, height: 36 }}>
|
||||||
<MenuItem sx={{ gap: '0.5rem' }} onClick={onClickItem}>
|
{avatarTitle}
|
||||||
<ListItemIcon>
|
</Avatar>
|
||||||
<Avatar
|
</ListItemIcon>
|
||||||
src={userAvatar}
|
<Typography variant="body2" noWrap>
|
||||||
alt={userName}
|
{userName}
|
||||||
sx={{ width: 36, height: 36 }}
|
</Typography>
|
||||||
>
|
</MenuItem>
|
||||||
{avatarTitle}
|
)
|
||||||
</Avatar>
|
|
||||||
</ListItemIcon>
|
|
||||||
<Typography variant='body2' noWrap>
|
|
||||||
{userName}
|
|
||||||
</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { Outlet } from 'react-router-dom'
|
import { Outlet } from 'react-router-dom'
|
||||||
import { Header } from './Header/Header'
|
import { Header } from './Header/Header'
|
||||||
import { Container, ContainerProps, Divider, DividerProps, styled } from '@mui/material'
|
import { Container, ContainerProps, styled } from '@mui/material'
|
||||||
|
import { ReloadBadge } from '@/components/ReloadBadge/ReloadBadge'
|
||||||
|
import { useSessionStorage } from 'usehooks-ts'
|
||||||
|
import { RELOAD_STORAGE_KEY } from '@/utils/consts'
|
||||||
|
|
||||||
export const Layout: FC = () => {
|
export const Layout: FC = () => {
|
||||||
|
const [needReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
|
||||||
|
const containerClassName = needReload ? 'reload' : ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer maxWidth="md">
|
<StyledContainer maxWidth="md" className={containerClassName}>
|
||||||
|
<ReloadBadge />
|
||||||
<Header />
|
<Header />
|
||||||
<StyledDivider />
|
|
||||||
<main>
|
<main>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
@ -24,14 +30,14 @@ const StyledContainer = styled((props: ContainerProps) => <Container maxWidth="s
|
|||||||
'& > main': {
|
'& > main': {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
maxHeight: '100%',
|
maxHeight: '100%',
|
||||||
|
},
|
||||||
|
'&:not(.reload) > main': {
|
||||||
paddingTop: 'calc(66px + 1rem)',
|
paddingTop: 'calc(66px + 1rem)',
|
||||||
},
|
},
|
||||||
})
|
'@media screen and (max-width: 320px)': {
|
||||||
|
marginBottom: '0.25rem',
|
||||||
const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({
|
paddingLeft: '0.75rem',
|
||||||
position: 'absolute',
|
paddingBottom: '0.75rem',
|
||||||
top: '66px',
|
paddingRight: '0.75rem',
|
||||||
width: '100%',
|
},
|
||||||
left: 0,
|
|
||||||
height: '2px',
|
|
||||||
})
|
})
|
||||||
|
@ -1,24 +1,34 @@
|
|||||||
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools'
|
import { Event, generatePrivateKey, getPublicKey, nip19, verifySignature } from 'nostr-tools'
|
||||||
import { DbApp, dbi, DbKey, DbPending, DbPerm } from './db'
|
import { DbApp, dbi, DbKey, DbPending, DbPerm } from './db'
|
||||||
import { Keys } from './keys'
|
import { Keys } from './keys'
|
||||||
import NDK, {
|
import NDK, {
|
||||||
IEventHandlingStrategy,
|
|
||||||
NDKEvent,
|
NDKEvent,
|
||||||
NDKNip46Backend,
|
NDKNip46Backend,
|
||||||
NDKPrivateKeySigner,
|
NDKPrivateKeySigner,
|
||||||
NDKSigner,
|
NDKSigner,
|
||||||
|
NDKSubscription,
|
||||||
|
NDKSubscriptionCacheUsage,
|
||||||
|
NDKUser,
|
||||||
} from '@nostr-dev-kit/ndk'
|
} from '@nostr-dev-kit/ndk'
|
||||||
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN } from '../utils/consts'
|
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN, REQ_TTL } from '../utils/consts'
|
||||||
import { Nip04 } from './nip04'
|
// import { Nip04 } from './nip04'
|
||||||
import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
|
import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
|
||||||
import { NostrPowEvent, minePow } from './pow'
|
import { NostrPowEvent, minePow } from './pow'
|
||||||
//import { PrivateKeySigner } from './signer'
|
//import { PrivateKeySigner } from './signer'
|
||||||
|
|
||||||
//const PERF_TEST = false
|
//const PERF_TEST = false
|
||||||
|
|
||||||
|
enum DECISION {
|
||||||
|
ASK = '',
|
||||||
|
ALLOW = 'allow',
|
||||||
|
DISALLOW = 'disallow',
|
||||||
|
IGNORE = 'ignore',
|
||||||
|
}
|
||||||
|
|
||||||
export interface KeyInfo {
|
export interface KeyInfo {
|
||||||
npub: string
|
npub: string
|
||||||
nip05?: string
|
nip05?: string
|
||||||
|
name?: string
|
||||||
locked: boolean
|
locked: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,16 +38,17 @@ interface Key {
|
|||||||
backoff: number
|
backoff: number
|
||||||
signer: NDKSigner
|
signer: NDKSigner
|
||||||
backend: NDKNip46Backend
|
backend: NDKNip46Backend
|
||||||
|
watcher: Watcher
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Pending {
|
interface Pending {
|
||||||
req: DbPending
|
req: DbPending
|
||||||
cb: (allow: boolean, remember: boolean, options?: any) => void
|
cb: (allow: DECISION, remember: boolean, options?: any) => void
|
||||||
notified?: boolean
|
notified?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAllowCallbackParams {
|
interface IAllowCallbackParams {
|
||||||
backend: NDKNip46Backend
|
backend: Nip46Backend
|
||||||
npub: string
|
npub: string
|
||||||
id: string
|
id: string
|
||||||
method: string
|
method: string
|
||||||
@ -46,86 +57,171 @@ interface IAllowCallbackParams {
|
|||||||
params?: any
|
params?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
class Nip04KeyHandlingStrategy implements IEventHandlingStrategy {
|
class Watcher {
|
||||||
private privkey: string
|
private ndk: NDK
|
||||||
private nip04 = new Nip04()
|
private signer: NDKSigner
|
||||||
|
private onReply: (id: string) => void
|
||||||
|
private sub?: NDKSubscription
|
||||||
|
|
||||||
constructor(privkey: string) {
|
constructor(ndk: NDK, signer: NDKSigner, onReply: (id: string) => void) {
|
||||||
this.privkey = privkey
|
this.ndk = ndk
|
||||||
|
this.signer = signer
|
||||||
|
this.onReply = onReply
|
||||||
}
|
}
|
||||||
|
|
||||||
private async getKey(backend: NDKNip46Backend, id: string, remotePubkey: string, recipientPubkey: string) {
|
async start() {
|
||||||
if (
|
this.sub = this.ndk.subscribe(
|
||||||
!(await backend.pubkeyAllowed({
|
{
|
||||||
id,
|
kinds: [KIND_RPC],
|
||||||
pubkey: remotePubkey,
|
authors: [(await this.signer.user()).pubkey],
|
||||||
// @ts-ignore
|
since: Math.floor(Date.now() / 1000 - 10),
|
||||||
method: 'get_nip04_key',
|
},
|
||||||
params: recipientPubkey,
|
{
|
||||||
}))
|
closeOnEose: false,
|
||||||
) {
|
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
|
||||||
backend.debug(`get_nip04_key request from ${remotePubkey} rejected`)
|
}
|
||||||
return undefined
|
)
|
||||||
|
this.sub.on('event', async (e: NDKEvent) => {
|
||||||
|
const peer = e.tags.find((t) => t.length >= 2 && t[0] === 'p')
|
||||||
|
console.log('watcher got event', { e, peer })
|
||||||
|
if (!peer) return
|
||||||
|
const decryptedContent = await this.signer.decrypt(new NDKUser({ pubkey: peer[1] }), e.content)
|
||||||
|
const parsedContent = JSON.parse(decryptedContent)
|
||||||
|
const { id, method, params, result, error } = parsedContent
|
||||||
|
console.log('watcher got', { peer, id, method, params, result, error })
|
||||||
|
if (method || result === 'auth_url') return
|
||||||
|
this.onReply(id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
this.sub!.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Nip46Backend extends NDKNip46Backend {
|
||||||
|
private allowCb: (params: IAllowCallbackParams) => Promise<DECISION>
|
||||||
|
private npub: string = ''
|
||||||
|
|
||||||
|
public constructor(ndk: NDK, signer: NDKSigner, allowCb: (params: IAllowCallbackParams) => Promise<DECISION>) {
|
||||||
|
super(ndk, signer, () => Promise.resolve(true))
|
||||||
|
this.allowCb = allowCb
|
||||||
|
signer.user().then((u) => (this.npub = nip19.npubEncode(u.pubkey)))
|
||||||
|
}
|
||||||
|
|
||||||
|
public async processEvent(event: NDKEvent) {
|
||||||
|
this.handleIncomingEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async handleIncomingEvent(event: NDKEvent) {
|
||||||
|
const { id, method, params } = (await this.rpc.parseEvent(event)) as any
|
||||||
|
const remotePubkey = event.pubkey
|
||||||
|
let response: string | undefined
|
||||||
|
|
||||||
|
this.debug('incoming event', { id, method, params })
|
||||||
|
|
||||||
|
// validate signature explicitly
|
||||||
|
if (!verifySignature(event.rawEvent() as Event)) {
|
||||||
|
this.debug('invalid signature', event.rawEvent())
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
return Buffer.from(this.nip04.createKey(this.privkey, recipientPubkey)).toString('hex')
|
const decision = await this.allowCb({
|
||||||
}
|
backend: this,
|
||||||
|
|
||||||
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>
|
|
||||||
) {
|
|
||||||
this.backend = backend
|
|
||||||
this.npub = npub
|
|
||||||
this.method = method
|
|
||||||
this.body = body
|
|
||||||
this.allowCb = allowCb
|
|
||||||
}
|
|
||||||
|
|
||||||
async handle(
|
|
||||||
backend: NDKNip46Backend,
|
|
||||||
id: string,
|
|
||||||
remotePubkey: string,
|
|
||||||
params: string[]
|
|
||||||
): Promise<string | undefined> {
|
|
||||||
console.log(Date.now(), 'handle', {
|
|
||||||
method: this.method,
|
|
||||||
id,
|
|
||||||
remotePubkey,
|
|
||||||
params,
|
|
||||||
})
|
|
||||||
const allow = await this.allowCb({
|
|
||||||
backend: this.backend,
|
|
||||||
npub: this.npub,
|
npub: this.npub,
|
||||||
id,
|
id,
|
||||||
method: this.method,
|
method,
|
||||||
remotePubkey,
|
remotePubkey,
|
||||||
params,
|
params,
|
||||||
})
|
})
|
||||||
if (!allow) return undefined
|
console.log(Date.now(), 'handle', { method, id, decision, remotePubkey, params })
|
||||||
return this.body.handle(backend, id, remotePubkey, params).then((r) => {
|
if (decision === DECISION.IGNORE) return
|
||||||
console.log(Date.now(), 'req', id, 'method', this.method, 'result', r)
|
|
||||||
return r
|
const allow = decision === DECISION.ALLOW
|
||||||
})
|
const strategy = this.handlers[method]
|
||||||
|
if (allow) {
|
||||||
|
if (strategy) {
|
||||||
|
try {
|
||||||
|
response = await strategy.handle(this, id, remotePubkey, params)
|
||||||
|
console.log(Date.now(), 'req', id, 'method', method, 'result', response)
|
||||||
|
} catch (e: any) {
|
||||||
|
this.debug('error handling event', e, { id, method, params })
|
||||||
|
this.rpc.sendResponse(id, remotePubkey, 'error', undefined, e.message)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.debug('unsupported method', { method, params })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
this.debug(`sending response to ${remotePubkey}`, response)
|
||||||
|
this.rpc.sendResponse(id, remotePubkey, response)
|
||||||
|
} else {
|
||||||
|
this.rpc.sendResponse(id, remotePubkey, 'error', undefined, 'Not authorized')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// class Nip04KeyHandlingStrategy implements IEventHandlingStrategy {
|
||||||
|
// private privkey: string
|
||||||
|
// private nip04 = new Nip04()
|
||||||
|
|
||||||
|
// constructor(privkey: string) {
|
||||||
|
// this.privkey = privkey
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private async getKey(backend: NDKNip46Backend, id: string, remotePubkey: string, recipientPubkey: string) {
|
||||||
|
// if (
|
||||||
|
// !(await backend.pubkeyAllowed({
|
||||||
|
// id,
|
||||||
|
// pubkey: remotePubkey,
|
||||||
|
// // @ts-ignore
|
||||||
|
// method: 'get_nip04_key',
|
||||||
|
// params: recipientPubkey,
|
||||||
|
// }))
|
||||||
|
// ) {
|
||||||
|
// backend.debug(`get_nip04_key request from ${remotePubkey} rejected`)
|
||||||
|
// return undefined
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return Buffer.from(this.nip04.createKey(this.privkey, recipientPubkey)).toString('hex')
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]) {
|
||||||
|
// const [recipientPubkey] = params
|
||||||
|
// return await this.getKey(backend, id, remotePubkey, recipientPubkey)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// FIXME why do we need it? Just to print
|
||||||
|
// class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
|
||||||
|
// readonly backend: NDKNip46Backend
|
||||||
|
// readonly method: string
|
||||||
|
// private body: IEventHandlingStrategy
|
||||||
|
|
||||||
|
// constructor(
|
||||||
|
// backend: NDKNip46Backend,
|
||||||
|
// method: string,
|
||||||
|
// body: IEventHandlingStrategy
|
||||||
|
// ) {
|
||||||
|
// this.backend = backend
|
||||||
|
// this.method = method
|
||||||
|
// this.body = body
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async handle(
|
||||||
|
// backend: NDKNip46Backend,
|
||||||
|
// id: string,
|
||||||
|
// remotePubkey: string,
|
||||||
|
// params: string[]
|
||||||
|
// ): Promise<string | undefined> {
|
||||||
|
// return this.body.handle(backend, id, remotePubkey, params).then((r) => {
|
||||||
|
// console.log(Date.now(), 'req', id, 'method', this.method, 'result', r)
|
||||||
|
// return r
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
export class NoauthBackend {
|
export class NoauthBackend {
|
||||||
readonly swg: ServiceWorkerGlobalScope
|
readonly swg: ServiceWorkerGlobalScope
|
||||||
private keysModule: Keys
|
private keysModule: Keys
|
||||||
@ -137,20 +233,21 @@ export class NoauthBackend {
|
|||||||
private confirmBuffer: Pending[] = []
|
private confirmBuffer: Pending[] = []
|
||||||
private accessBuffer: DbPending[] = []
|
private accessBuffer: DbPending[] = []
|
||||||
private notifCallback: (() => void) | null = null
|
private notifCallback: (() => void) | null = null
|
||||||
|
private pendingNpubEvents = new Map<string, NDKEvent[]>()
|
||||||
|
private ndk = new NDK({
|
||||||
|
explicitRelayUrls: NIP46_RELAYS,
|
||||||
|
enableOutboxModel: false,
|
||||||
|
})
|
||||||
|
|
||||||
public constructor(swg: ServiceWorkerGlobalScope) {
|
public constructor(swg: ServiceWorkerGlobalScope) {
|
||||||
this.swg = swg
|
this.swg = swg
|
||||||
this.keysModule = new Keys(swg.crypto.subtle)
|
this.keysModule = new Keys(swg.crypto.subtle)
|
||||||
|
this.ndk.connect()
|
||||||
|
|
||||||
const self = this
|
const self = this
|
||||||
swg.addEventListener('activate', (event) => {
|
swg.addEventListener('activate', (event) => {
|
||||||
console.log('activate')
|
console.log('activate new sw worker')
|
||||||
// swg.addEventListener('activate', event => event.waitUntil(swg.clients.claim()));
|
this.reloadUI()
|
||||||
})
|
|
||||||
|
|
||||||
swg.addEventListener('install', (event) => {
|
|
||||||
console.log('install')
|
|
||||||
// swg.addEventListener('install', event => event.waitUntil(swg.skipWaiting()));
|
|
||||||
})
|
})
|
||||||
|
|
||||||
swg.addEventListener('push', (event) => {
|
swg.addEventListener('push', (event) => {
|
||||||
@ -213,6 +310,13 @@ export class NoauthBackend {
|
|||||||
this.apps = await dbi.listApps()
|
this.apps = await dbi.listApps()
|
||||||
console.log('started apps', this.apps)
|
console.log('started apps', this.apps)
|
||||||
|
|
||||||
|
// drop old pending reqs
|
||||||
|
const pending = await dbi.listPending()
|
||||||
|
for (const p of pending) {
|
||||||
|
if (p.timestamp < Date.now() - REQ_TTL)
|
||||||
|
await dbi.removePending(p.id)
|
||||||
|
}
|
||||||
|
|
||||||
const sub = await this.swg.registration.pushManager.getSubscription()
|
const sub = await this.swg.registration.pushManager.getSubscription()
|
||||||
|
|
||||||
for (const k of this.enckeys) {
|
for (const k of this.enckeys) {
|
||||||
@ -265,7 +369,7 @@ export class NoauthBackend {
|
|||||||
if (r.status !== 200 && r.status !== 201) {
|
if (r.status !== 200 && r.status !== 201) {
|
||||||
console.log('Fetch error', url, method, r.status)
|
console.log('Fetch error', url, method, r.status)
|
||||||
const body = await r.json()
|
const body = await r.json()
|
||||||
throw new Error('Failed to fetch ' + url, { cause: body })
|
throw new Error('Failed to fetch ' + url, { cause: { body, status: r.status } })
|
||||||
}
|
}
|
||||||
|
|
||||||
return await r.json()
|
return await r.json()
|
||||||
@ -400,13 +504,48 @@ export class NoauthBackend {
|
|||||||
})
|
})
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.log('error', e.cause)
|
console.log('error', e.cause)
|
||||||
if (e.cause && e.cause.minPow > pow) pow = e.cause.minPow
|
if (e.cause && e.cause.body && e.cause.body.minPow > pow) pow = e.cause.body.minPow
|
||||||
else throw e
|
else throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Error('Too many requests, retry later')
|
throw new Error('Too many requests, retry later')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async sendDeleteNameToServer(npub: string, name: string) {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
npub,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
|
||||||
|
const method = 'DELETE'
|
||||||
|
const url = `${NOAUTHD_URL}/name`
|
||||||
|
|
||||||
|
return this.sendPostAuthd({
|
||||||
|
npub,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
private async sendTransferNameToServer(npub: string, name: string, newNpub: string) {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
npub,
|
||||||
|
name,
|
||||||
|
newNpub,
|
||||||
|
})
|
||||||
|
|
||||||
|
const method = 'PUT'
|
||||||
|
const url = `${NOAUTHD_URL}/name`
|
||||||
|
|
||||||
|
return this.sendPostAuthd({
|
||||||
|
npub,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private async sendTokenToServer(npub: string, token: string) {
|
private async sendTokenToServer(npub: string, token: string) {
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
npub,
|
npub,
|
||||||
@ -501,6 +640,7 @@ export class NoauthBackend {
|
|||||||
return {
|
return {
|
||||||
npub: k.npub,
|
npub: k.npub,
|
||||||
nip05: k.nip05,
|
nip05: k.nip05,
|
||||||
|
name: k.name,
|
||||||
locked: this.isLocked(k.npub),
|
locked: this.isLocked(k.npub),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -541,11 +681,16 @@ export class NoauthBackend {
|
|||||||
await this.startKey({ npub, sk })
|
await this.startKey({ npub, sk })
|
||||||
|
|
||||||
// assign nip05 before adding the key
|
// 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('@')) {
|
if (!existingName && name && !name.includes('@')) {
|
||||||
console.log('adding key', npub, name)
|
console.log('adding key', npub, name)
|
||||||
await this.sendNameToServer(npub, name)
|
try {
|
||||||
|
await this.sendNameToServer(npub, name)
|
||||||
|
} catch (e) {
|
||||||
|
console.log('create name failed', e)
|
||||||
|
// clear it
|
||||||
|
await dbi.editName(npub, '')
|
||||||
|
dbKey.name = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sub = await this.swg.registration.pushManager.getSubscription()
|
const sub = await this.swg.registration.pushManager.getSubscription()
|
||||||
@ -554,7 +699,9 @@ export class NoauthBackend {
|
|||||||
return this.keyInfo(dbKey)
|
return this.keyInfo(dbKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPerm(req: DbPending): string {
|
private getDecision(backend: Nip46Backend, req: DbPending): DECISION {
|
||||||
|
if (!(req.method in backend.handlers)) return DECISION.IGNORE
|
||||||
|
|
||||||
const reqPerm = getReqPerm(req)
|
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)
|
||||||
|
|
||||||
@ -563,27 +710,36 @@ export class NoauthBackend {
|
|||||||
// non-exact next
|
// non-exact next
|
||||||
if (!perm) perm = appPerms.find((p) => isPackagePerm(p.perm, reqPerm))
|
if (!perm) perm = appPerms.find((p) => isPackagePerm(p.perm, reqPerm))
|
||||||
|
|
||||||
console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms)
|
if (perm) {
|
||||||
return perm?.value || ''
|
console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms)
|
||||||
|
return perm.value === '1' ? DECISION.ALLOW : DECISION.DISALLOW
|
||||||
|
}
|
||||||
|
|
||||||
|
const conn = appPerms.find((p) => p.perm === 'connect')
|
||||||
|
if (conn && conn.value === '0') {
|
||||||
|
console.log('req', req, 'perm', reqPerm, 'ignore by connect disallow')
|
||||||
|
return DECISION.IGNORE
|
||||||
|
}
|
||||||
|
|
||||||
|
return DECISION.ASK
|
||||||
}
|
}
|
||||||
|
|
||||||
private async connectApp({
|
private async connectApp({
|
||||||
npub,
|
npub,
|
||||||
appNpub,
|
appNpub,
|
||||||
appUrl,
|
appUrl,
|
||||||
perms,
|
perms,
|
||||||
appName = '',
|
appName = '',
|
||||||
appIcon = ''
|
appIcon = '',
|
||||||
}: {
|
}: {
|
||||||
npub: string,
|
npub: string
|
||||||
appNpub: string,
|
appNpub: string
|
||||||
appUrl: string,
|
appUrl: string
|
||||||
appName?: string,
|
appName?: string
|
||||||
appIcon?: string,
|
appIcon?: string
|
||||||
perms: string[]
|
perms: string[]
|
||||||
}) {
|
}) {
|
||||||
|
await dbi.addApp({
|
||||||
await dbi.addApp({
|
|
||||||
appNpub: appNpub,
|
appNpub: appNpub,
|
||||||
npub: npub,
|
npub: npub,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@ -618,19 +774,19 @@ export class NoauthBackend {
|
|||||||
method,
|
method,
|
||||||
remotePubkey,
|
remotePubkey,
|
||||||
params,
|
params,
|
||||||
}: IAllowCallbackParams): Promise<boolean> {
|
}: IAllowCallbackParams): Promise<DECISION> {
|
||||||
// same reqs usually come on reconnects
|
// same reqs usually come on reconnects
|
||||||
if (this.doneReqIds.includes(id)) {
|
if (this.doneReqIds.includes(id)) {
|
||||||
console.log('request already done', id)
|
console.log('request already done', id)
|
||||||
// FIXME maybe repeat the reply, but without the Notification?
|
// FIXME maybe repeat the reply, but without the Notification?
|
||||||
return false
|
return DECISION.IGNORE
|
||||||
}
|
}
|
||||||
|
|
||||||
const appNpub = nip19.npubEncode(remotePubkey)
|
const appNpub = nip19.npubEncode(remotePubkey)
|
||||||
const connected = !!this.apps.find((a) => a.appNpub === appNpub)
|
const connected = !!this.apps.find((a) => a.appNpub === appNpub)
|
||||||
if (!connected && method !== 'connect') {
|
if (!connected && method !== 'connect') {
|
||||||
console.log('ignoring request before connect', method, id, appNpub, npub)
|
console.log('ignoring request before connect', method, id, appNpub, npub)
|
||||||
return false
|
return DECISION.IGNORE
|
||||||
}
|
}
|
||||||
|
|
||||||
const req: DbPending = {
|
const req: DbPending = {
|
||||||
@ -645,9 +801,21 @@ export class NoauthBackend {
|
|||||||
const self = this
|
const self = this
|
||||||
return new Promise(async (ok) => {
|
return new Promise(async (ok) => {
|
||||||
// called when it's decided whether to allow this or not
|
// 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, decision: DECISION, remember: boolean, options?: any) => {
|
||||||
// confirm
|
// confirm
|
||||||
console.log(Date.now(), allow ? 'allowed' : 'disallowed', npub, method, options, params)
|
console.log(Date.now(), decision, npub, method, options, params)
|
||||||
|
|
||||||
|
switch (decision) {
|
||||||
|
case DECISION.ASK:
|
||||||
|
throw new Error('Make a decision!')
|
||||||
|
case DECISION.IGNORE:
|
||||||
|
return // noop
|
||||||
|
case DECISION.ALLOW:
|
||||||
|
case DECISION.DISALLOW:
|
||||||
|
// fall through
|
||||||
|
}
|
||||||
|
|
||||||
|
const allow = decision === DECISION.ALLOW
|
||||||
|
|
||||||
if (manual) {
|
if (manual) {
|
||||||
await dbi.confirmPending(id, allow)
|
await dbi.confirmPending(id, allow)
|
||||||
@ -700,35 +868,40 @@ export class NoauthBackend {
|
|||||||
|
|
||||||
// reload
|
// reload
|
||||||
this.perms = await dbi.listPerms()
|
this.perms = await dbi.listPerms()
|
||||||
|
|
||||||
// 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) {
|
|
||||||
let perm = this.getPerm(r.req)
|
|
||||||
if (perm) {
|
|
||||||
r.cb(perm === '1', false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// release this promise to send reply
|
||||||
|
// to this req
|
||||||
|
ok(decision)
|
||||||
|
|
||||||
// notify UI that it was confirmed
|
// notify UI that it was confirmed
|
||||||
// if (!PERF_TEST)
|
// if (!PERF_TEST)
|
||||||
this.updateUI()
|
this.updateUI()
|
||||||
|
|
||||||
// return to let nip46 flow proceed
|
// after replying to this req check pending
|
||||||
ok(allow)
|
// reqs maybe they can be replied right away
|
||||||
|
if (remember) {
|
||||||
|
// 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 dec = this.getDecision(backend, r.req)
|
||||||
|
if (dec !== DECISION.ASK) {
|
||||||
|
r.cb(dec, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check perms
|
// check perms
|
||||||
const perm = this.getPerm(req)
|
const dec = this.getDecision(backend, req)
|
||||||
console.log(Date.now(), 'perm', req.id, perm)
|
console.log(Date.now(), 'decision', req.id, dec)
|
||||||
|
|
||||||
// have perm?
|
// have perm?
|
||||||
if (perm) {
|
if (dec !== DECISION.ASK) {
|
||||||
// reply immediately
|
// reply immediately
|
||||||
onAllow(false, perm === '1', false)
|
onAllow(false, dec, false)
|
||||||
} else {
|
} else {
|
||||||
// put pending req to db
|
// put pending req to db
|
||||||
await dbi.addPending(req)
|
await dbi.addPending(req)
|
||||||
@ -739,20 +912,30 @@ export class NoauthBackend {
|
|||||||
// put to a list of pending requests
|
// put to a list of pending requests
|
||||||
this.confirmBuffer.push({
|
this.confirmBuffer.push({
|
||||||
req,
|
req,
|
||||||
cb: (allow, remember, options) => onAllow(true, allow, remember, options),
|
cb: (decision, remember, options) => onAllow(true, decision, remember, options),
|
||||||
})
|
})
|
||||||
|
|
||||||
// OAuth flow
|
// OAuth flow
|
||||||
const confirmMethod = method === 'connect' ? 'confirm-connect' : 'confirm-event'
|
const isConnect = method === 'connect'
|
||||||
|
const confirmMethod = isConnect ? 'confirm-connect' : 'confirm-event'
|
||||||
const authUrl = `${self.swg.location.origin}/key/${npub}?${confirmMethod}=true&appNpub=${appNpub}&reqId=${id}&popup=true`
|
const authUrl = `${self.swg.location.origin}/key/${npub}?${confirmMethod}=true&appNpub=${appNpub}&reqId=${id}&popup=true`
|
||||||
// const authUrl = `${self.swg.location.origin}/key/${npub}?popup=true`
|
|
||||||
console.log('sending authUrl', authUrl, 'for', req)
|
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,
|
// NOTE: don't send auth_url immediately, wait some time
|
||||||
// which will cause SW (this code) to reload, to fetch
|
// to make sure other bunkers aren't replying
|
||||||
// the pending requests and to re-send this event,
|
setTimeout(() => {
|
||||||
// looping for 10 seconds (our request age threshold)
|
// request still there? (not dropped by the watcher)
|
||||||
backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl)
|
if (self.confirmBuffer.find((r) => r.req.id === id)) {
|
||||||
|
// 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)
|
||||||
|
} else {
|
||||||
|
console.log("skip sending auth_url")
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
|
||||||
// show notifs
|
// show notifs
|
||||||
// this.notify()
|
// this.notify()
|
||||||
@ -772,25 +955,30 @@ export class NoauthBackend {
|
|||||||
ndk.connect()
|
ndk.connect()
|
||||||
|
|
||||||
const signer = new NDKPrivateKeySigner(sk) // PrivateKeySigner
|
const signer = new NDKPrivateKeySigner(sk) // PrivateKeySigner
|
||||||
const backend = new NDKNip46Backend(ndk, signer, () => Promise.resolve(true))
|
const backend = new Nip46Backend(ndk, signer, this.allowPermitCallback.bind(this)) // , () => Promise.resolve(true)
|
||||||
this.keys.push({ npub, backend, signer, ndk, backoff })
|
const watcher = new Watcher(ndk, signer, (id) => {
|
||||||
|
// drop pending request
|
||||||
|
const index = self.confirmBuffer.findIndex((r) => r.req.id === id)
|
||||||
|
if (index >= 0) self.confirmBuffer.splice(index, 1)
|
||||||
|
dbi.removePending(id).then(() => this.updateUI())
|
||||||
|
})
|
||||||
|
this.keys.push({ npub, backend, signer, ndk, backoff, watcher })
|
||||||
|
|
||||||
// new method
|
// new method
|
||||||
backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk)
|
// backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk)
|
||||||
|
|
||||||
// assign our own permission callback
|
// // assign our own permission callback
|
||||||
for (const method in backend.handlers) {
|
// for (const method in backend.handlers) {
|
||||||
backend.handlers[method] = new EventHandlingStrategyWrapper(
|
// backend.handlers[method] = new EventHandlingStrategyWrapper(
|
||||||
backend,
|
// backend,
|
||||||
npub,
|
// method,
|
||||||
method,
|
// backend.handlers[method]
|
||||||
backend.handlers[method],
|
// )
|
||||||
this.allowPermitCallback.bind(this)
|
// }
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// start
|
// start
|
||||||
backend.start()
|
backend.start()
|
||||||
|
watcher.start()
|
||||||
console.log('started', npub)
|
console.log('started', npub)
|
||||||
|
|
||||||
// backoff reset on successfull connection
|
// backoff reset on successfull connection
|
||||||
@ -814,11 +1002,13 @@ export class NoauthBackend {
|
|||||||
const bo = self.keys.find((k) => k.npub === npub)?.backoff || 1000
|
const bo = self.keys.find((k) => k.npub === npub)?.backoff || 1000
|
||||||
setTimeout(() => {
|
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()
|
for (const r of ndk.pool.relays.values()) r.disconnect()
|
||||||
// make sure it no longer activates
|
// make sure it no longer activates
|
||||||
backend.handlers = {}
|
backend.handlers = {}
|
||||||
|
|
||||||
|
// stop watching
|
||||||
|
watcher.stop()
|
||||||
|
|
||||||
self.keys = self.keys.filter((k) => k.npub !== npub)
|
self.keys = self.keys.filter((k) => k.npub !== npub)
|
||||||
self.startKey({ npub, sk, backoff: Math.min(bo * 2, 60000) })
|
self.startKey({ npub, sk, backoff: Math.min(bo * 2, 60000) })
|
||||||
}, bo)
|
}, bo)
|
||||||
@ -829,6 +1019,27 @@ export class NoauthBackend {
|
|||||||
r.on('connect', onConnect)
|
r.on('connect', onConnect)
|
||||||
r.on('disconnect', onDisconnect)
|
r.on('disconnect', onDisconnect)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const pendingEvents = this.pendingNpubEvents.get(npub)
|
||||||
|
if (pendingEvents) {
|
||||||
|
this.pendingNpubEvents.delete(npub)
|
||||||
|
for (const e of pendingEvents) {
|
||||||
|
backend.processEvent(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchPendingRequests(npub: string, appNpub: string) {
|
||||||
|
const { data: pubkey } = nip19.decode(npub)
|
||||||
|
const { data: appPubkey } = nip19.decode(appNpub)
|
||||||
|
|
||||||
|
const events = await this.ndk.fetchEvents({
|
||||||
|
kinds: [KIND_RPC],
|
||||||
|
'#p': [pubkey as string],
|
||||||
|
authors: [appPubkey as string],
|
||||||
|
})
|
||||||
|
console.log('fetched pending for', npub, events.size)
|
||||||
|
this.pendingNpubEvents.set(npub, [...events.values()])
|
||||||
}
|
}
|
||||||
|
|
||||||
public async unlock(npub: string) {
|
public async unlock(npub: string) {
|
||||||
@ -945,7 +1156,7 @@ export class NoauthBackend {
|
|||||||
this.updateUI()
|
this.updateUI()
|
||||||
} else {
|
} else {
|
||||||
console.log('confirming req', id, allow, remember, options)
|
console.log('confirming req', id, allow, remember, options)
|
||||||
req.cb(allow, remember, options)
|
req.cb(allow ? DECISION.ALLOW : DECISION.DISALLOW, remember, options)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -963,6 +1174,36 @@ export class NoauthBackend {
|
|||||||
this.updateUI()
|
this.updateUI()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async editName(npub: string, name: string) {
|
||||||
|
const key = this.enckeys.find((k) => k.npub === npub)
|
||||||
|
if (!key) throw new Error('Npub not found')
|
||||||
|
if (key.name) {
|
||||||
|
try {
|
||||||
|
await this.sendDeleteNameToServer(npub, key.name)
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.cause && e.cause.status !== 404) throw e
|
||||||
|
console.log("Deleted name didn't exist")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (name) {
|
||||||
|
await this.sendNameToServer(npub, name)
|
||||||
|
}
|
||||||
|
await dbi.editName(npub, name)
|
||||||
|
key.name = name
|
||||||
|
this.updateUI()
|
||||||
|
}
|
||||||
|
|
||||||
|
private async transferName(npub: string, name: string, newNpub: string) {
|
||||||
|
const key = this.enckeys.find((k) => k.npub === npub)
|
||||||
|
if (!key) throw new Error('Npub not found')
|
||||||
|
if (!name) throw new Error('Empty name')
|
||||||
|
if (key.name !== name) throw new Error('Name changed, please reload')
|
||||||
|
await this.sendTransferNameToServer(npub, key.name, newNpub)
|
||||||
|
await dbi.editName(npub, '')
|
||||||
|
key.name = ''
|
||||||
|
this.updateUI()
|
||||||
|
}
|
||||||
|
|
||||||
private async enablePush(): Promise<boolean> {
|
private async enablePush(): Promise<boolean> {
|
||||||
const options = {
|
const options = {
|
||||||
userVisibleOnly: true,
|
userVisibleOnly: true,
|
||||||
@ -1009,8 +1250,14 @@ export class NoauthBackend {
|
|||||||
result = await this.deleteApp(args[0])
|
result = await this.deleteApp(args[0])
|
||||||
} else if (method === 'deletePerm') {
|
} else if (method === 'deletePerm') {
|
||||||
result = await this.deletePerm(args[0])
|
result = await this.deletePerm(args[0])
|
||||||
|
} else if (method === 'editName') {
|
||||||
|
result = await this.editName(args[0], args[1])
|
||||||
|
} else if (method === 'transferName') {
|
||||||
|
result = await this.transferName(args[0], args[1], args[2])
|
||||||
} else if (method === 'enablePush') {
|
} else if (method === 'enablePush') {
|
||||||
result = await this.enablePush()
|
result = await this.enablePush()
|
||||||
|
} else if (method === 'fetchPendingRequests') {
|
||||||
|
result = await this.fetchPendingRequests(args[0], args[1])
|
||||||
} else {
|
} else {
|
||||||
console.log('unknown method from UI ', method)
|
console.log('unknown method from UI ', method)
|
||||||
}
|
}
|
||||||
@ -1037,10 +1284,20 @@ export class NoauthBackend {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async reloadUI() {
|
||||||
|
const clients = await this.swg.clients.matchAll({
|
||||||
|
includeUncontrolled: true,
|
||||||
|
})
|
||||||
|
console.log('reloadUI clients', clients.length)
|
||||||
|
for (const client of clients) {
|
||||||
|
client.postMessage({ result: 'reload' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async onPush(event: any) {
|
public async onPush(event: any) {
|
||||||
console.log('push', { data: event.data })
|
console.log('push', { data: event.data })
|
||||||
// noop - we just need browser to launch this worker
|
// noop - we just need browser to launch this worker
|
||||||
// FIXME use event.waitUntil and and unblock after we
|
// FIXME use event.waitUntil and and unblock after we
|
||||||
// show a notification
|
// show a notification to avoid annoying the browser
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -89,6 +89,16 @@ export const dbi = {
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
editName: async (npub: string, name: string): Promise<void> => {
|
||||||
|
try {
|
||||||
|
await db.keys.where({ npub }).modify({
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`db editName error: ${error}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
},
|
||||||
getApp: async (appNpub: string) => {
|
getApp: async (appNpub: string) => {
|
||||||
try {
|
try {
|
||||||
return await db.apps.get(appNpub)
|
return await db.apps.get(appNpub)
|
||||||
|
@ -21,11 +21,31 @@ const ALGO = 'aes-256-cbc'
|
|||||||
const IV_SIZE = 16
|
const IV_SIZE = 16
|
||||||
|
|
||||||
// valid passwords are a limited ASCII only, see notes below
|
// valid passwords are a limited ASCII only, see notes below
|
||||||
const ASCII_REGEX = /^[A-Za-z0-9!@#$%^&*()]{4,}$/
|
const ASCII_REGEX = /^[A-Za-z0-9!@#$%^&*()\-_]{6,}$/
|
||||||
|
|
||||||
const ALGO_LOCAL = 'AES-CBC'
|
const ALGO_LOCAL = 'AES-CBC'
|
||||||
const KEY_SIZE_LOCAL = 256
|
const KEY_SIZE_LOCAL = 256
|
||||||
|
|
||||||
|
export function isValidPassphase(passphrase: string): boolean {
|
||||||
|
return ASCII_REGEX.test(passphrase)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWeakPassphase(passphrase: string): boolean {
|
||||||
|
const BIG_LETTER_REGEX = /[A-Z]+/
|
||||||
|
const SMALL_LETTER_REGEX = /[a-z]+/
|
||||||
|
const NUMBER_REGEX = /[0-9]+/
|
||||||
|
const PUNCT_REGEX = /[!@#$%^&*()\-_]+/
|
||||||
|
const big = BIG_LETTER_REGEX.test(passphrase) ? 1 : 0
|
||||||
|
const small = SMALL_LETTER_REGEX.test(passphrase) ? 1 : 0
|
||||||
|
const number = NUMBER_REGEX.test(passphrase) ? 1 : 0
|
||||||
|
const punct = PUNCT_REGEX.test(passphrase) ? 1 : 0
|
||||||
|
const base = big * 26 + small * 26 + number * 10 + punct * 12
|
||||||
|
const compl = Math.pow(base, passphrase.length)
|
||||||
|
const thresh = Math.pow(11, 14)
|
||||||
|
// console.log({ big, small, number, punct, base, compl, thresh });
|
||||||
|
return compl < thresh
|
||||||
|
}
|
||||||
|
|
||||||
export class Keys {
|
export class Keys {
|
||||||
subtle: any
|
subtle: any
|
||||||
|
|
||||||
@ -33,10 +53,6 @@ export class Keys {
|
|||||||
this.subtle = cryptoSubtle
|
this.subtle = cryptoSubtle
|
||||||
}
|
}
|
||||||
|
|
||||||
public isValidPassphase(passphrase: string): boolean {
|
|
||||||
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')
|
const salt = Buffer.from(pubkey, 'hex')
|
||||||
|
|
||||||
@ -45,7 +61,7 @@ export class Keys {
|
|||||||
// We could use string.normalize() to make sure all JS implementations
|
// We could use string.normalize() to make sure all JS implementations
|
||||||
// are compatible, but since we're looking to make this thing a standard
|
// 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
|
// 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 (!isValidPassphase(passphrase)) throw new Error('Password must be 4+ ASCII chars')
|
||||||
|
|
||||||
return new Promise((ok, fail) => {
|
return new Promise((ok, fail) => {
|
||||||
// NOTE: we should use Argon2 or scrypt later, for now
|
// NOTE: we should use Argon2 or scrypt later, for now
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
// service-worker client interface
|
// service-worker client interface,
|
||||||
|
// works on the frontend, not sw
|
||||||
import * as serviceWorkerRegistration from '../serviceWorkerRegistration'
|
import * as serviceWorkerRegistration from '../serviceWorkerRegistration'
|
||||||
|
|
||||||
export let swr: ServiceWorkerRegistration | null = null
|
export let swr: ServiceWorkerRegistration | null = null
|
||||||
const reqs = new Map<number, { ok: (r: any) => void; rej: (r: any) => void }>()
|
const reqs = new Map<number, { ok: (r: any) => void; rej: (r: any) => void }>()
|
||||||
let nextReqId = 1
|
let nextReqId = 1
|
||||||
let onRender: (() => void) | null = null
|
let onRender: (() => void) | null = null
|
||||||
|
let onReload: (() => void) | null = null
|
||||||
|
const queue: (() => Promise<void> | void)[] = []
|
||||||
|
|
||||||
export async function swicRegister() {
|
export async function swicRegister() {
|
||||||
serviceWorkerRegistration.register({
|
serviceWorkerRegistration.register({
|
||||||
@ -13,18 +16,24 @@ export async function swicRegister() {
|
|||||||
swr = registration
|
swr = registration
|
||||||
},
|
},
|
||||||
onError(e) {
|
onError(e) {
|
||||||
console.log(`error ${e}`)
|
console.log('sw error', e)
|
||||||
|
},
|
||||||
|
onUpdate() {
|
||||||
|
// tell new SW that it should activate immediately
|
||||||
|
swr?.waiting?.postMessage({ type: 'SKIP_WAITING' })
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
navigator.serviceWorker.ready.then((r) => {
|
navigator.serviceWorker.ready.then(async (r) => {
|
||||||
console.log('sw ready')
|
console.log('sw ready, queue', queue.length)
|
||||||
swr = r
|
swr = r
|
||||||
if (navigator.serviceWorker.controller) {
|
if (navigator.serviceWorker.controller) {
|
||||||
console.log(`This page is currently controlled by: ${navigator.serviceWorker.controller}`)
|
console.log(`This page is currently controlled by: ${navigator.serviceWorker.controller}`)
|
||||||
} else {
|
} else {
|
||||||
console.log('This page is not currently controlled by a service worker.')
|
console.log('This page is not currently controlled by a service worker.')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
while (queue.length) await queue.shift()!()
|
||||||
})
|
})
|
||||||
|
|
||||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||||
@ -32,12 +41,23 @@ export async function swicRegister() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function swicWaitStarted() {
|
||||||
|
return new Promise<void>((ok) => {
|
||||||
|
if (swr && swr.active) ok()
|
||||||
|
else queue.push(ok)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function onMessage(data: any) {
|
function onMessage(data: any) {
|
||||||
const { id, result, error } = data
|
const { id, result, error } = data
|
||||||
console.log('SW message', id, result, error)
|
console.log('SW message', id, result, error)
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
if (onRender) onRender()
|
if (result === 'reload') {
|
||||||
|
if (onReload) onReload()
|
||||||
|
} else {
|
||||||
|
if (onRender) onRender()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,22 +77,31 @@ export async function swicCall(method: string, ...args: any[]) {
|
|||||||
nextReqId++
|
nextReqId++
|
||||||
|
|
||||||
return new Promise((ok, rej) => {
|
return new Promise((ok, rej) => {
|
||||||
if (!swr || !swr.active) {
|
const call = async () => {
|
||||||
rej(new Error('No active service worker'))
|
if (!swr || !swr.active) {
|
||||||
return
|
rej(new Error('No active service worker'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reqs.set(id, { ok, rej })
|
||||||
|
const msg = {
|
||||||
|
id,
|
||||||
|
method,
|
||||||
|
args: [...args],
|
||||||
|
}
|
||||||
|
console.log('sending to SW', msg)
|
||||||
|
swr.active.postMessage(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
reqs.set(id, { ok, rej })
|
if (swr && swr.active) call()
|
||||||
const msg = {
|
else queue.push(call)
|
||||||
id,
|
|
||||||
method,
|
|
||||||
args: [...args],
|
|
||||||
}
|
|
||||||
console.log('sending to SW', msg)
|
|
||||||
swr.active.postMessage(msg)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function swicOnRender(cb: () => void) {
|
export function swicOnRender(cb: () => void) {
|
||||||
onRender = cb
|
onRender = cb
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function swicOnReload(cb: () => void) {
|
||||||
|
onReload = cb
|
||||||
|
}
|
||||||
|
@ -21,6 +21,7 @@ const commonTheme: Theme = createTheme({
|
|||||||
styleOverrides: {
|
styleOverrides: {
|
||||||
root: {
|
root: {
|
||||||
textTransform: 'initial',
|
textTransform: 'initial',
|
||||||
|
color: 'red',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -96,4 +97,6 @@ const darkTheme: Theme = createTheme({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.log(darkTheme)
|
||||||
|
|
||||||
export { lightTheme, darkTheme }
|
export { lightTheme, darkTheme }
|
||||||
|
@ -9,7 +9,6 @@ import { getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/help
|
|||||||
import { Button } from '@/shared/Button/Button'
|
import { Button } from '@/shared/Button/Button'
|
||||||
import { ACTION_TYPE } from '@/utils/consts'
|
import { ACTION_TYPE } from '@/utils/consts'
|
||||||
import { Permissions } from './components/Permissions/Permissions'
|
import { Permissions } from './components/Permissions/Permissions'
|
||||||
import { StyledAppIcon } from './styled'
|
|
||||||
import { useToggleConfirm } from '@/hooks/useToggleConfirm'
|
import { useToggleConfirm } from '@/hooks/useToggleConfirm'
|
||||||
import { ConfirmModal } from '@/shared/ConfirmModal/ConfirmModal'
|
import { ConfirmModal } from '@/shared/ConfirmModal/ConfirmModal'
|
||||||
import { swicCall } from '@/modules/swic'
|
import { swicCall } from '@/modules/swic'
|
||||||
@ -20,6 +19,8 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
|||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import MoreIcon from '@mui/icons-material/MoreVertRounded'
|
import MoreIcon from '@mui/icons-material/MoreVertRounded'
|
||||||
import { ModalAppDetails } from '@/components/Modal/ModalAppDetails/ModalAppDetails'
|
import { ModalAppDetails } from '@/components/Modal/ModalAppDetails/ModalAppDetails'
|
||||||
|
import { IconApp } from '@/shared/IconApp/IconApp'
|
||||||
|
import { HeadingContainer, AppInfoContainer, AppNameContainer } from './styled'
|
||||||
|
|
||||||
const AppPage = () => {
|
const AppPage = () => {
|
||||||
const keys = useAppSelector(selectKeys)
|
const keys = useAppSelector(selectKeys)
|
||||||
@ -43,8 +44,10 @@ const AppPage = () => {
|
|||||||
|
|
||||||
const { icon = '', name = '', url = '' } = currentApp || {}
|
const { icon = '', name = '', url = '' } = currentApp || {}
|
||||||
const appDomain = getDomain(url)
|
const appDomain = getDomain(url)
|
||||||
const appName = name || appDomain || getShortenNpub(appNpub)
|
const shortAppNpub = getShortenNpub(appNpub)
|
||||||
|
const appName = name || appDomain || shortAppNpub
|
||||||
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||||
|
const isAppNameExists = !!name || !!appDomain
|
||||||
|
|
||||||
const { timestamp } = connectPerm || {}
|
const { timestamp } = connectPerm || {}
|
||||||
const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected'
|
const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected'
|
||||||
@ -65,22 +68,33 @@ const AppPage = () => {
|
|||||||
<>
|
<>
|
||||||
<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}`)} />
|
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
|
||||||
<Stack marginBottom={'1rem'} direction={'row'} gap={'1rem'} width={'100%'}>
|
|
||||||
<StyledAppIcon src={icon}>{appAvatarTitle}</StyledAppIcon>
|
<HeadingContainer>
|
||||||
<Box flex={'1'} overflow={'hidden'}>
|
<IconApp size="big" picture={icon} alt={appAvatarTitle} />
|
||||||
<Stack direction={'row'} alignItems={'center'} gap={'0.5rem'}>
|
<Box flex={'1'} overflow={'auto'} alignSelf={'flex-start'} width={'100%'}>
|
||||||
<Typography variant="h4" noWrap flex={1}>
|
<AppInfoContainer>
|
||||||
{appName}
|
<AppNameContainer>
|
||||||
</Typography>
|
<Typography className="app_name" variant="h4" noWrap>
|
||||||
|
{appName}
|
||||||
|
</Typography>
|
||||||
|
{isAppNameExists && (
|
||||||
|
<Typography noWrap display={'block'} variant="body1" color={'GrayText'}>
|
||||||
|
{shortAppNpub}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</AppNameContainer>
|
||||||
|
|
||||||
<IconButton onClick={handleShowAppDetailsModal}>
|
<IconButton onClick={handleShowAppDetailsModal}>
|
||||||
<MoreIcon />
|
<MoreIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Stack>
|
</AppInfoContainer>
|
||||||
|
|
||||||
<Typography variant="body2" noWrap>
|
<Typography variant="body2" noWrap>
|
||||||
{connectedOn}
|
{connectedOn}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</Stack>
|
</HeadingContainer>
|
||||||
|
|
||||||
<Box marginBottom={'1rem'}>
|
<Box marginBottom={'1rem'}>
|
||||||
<SectionTitle marginBottom={'0.5rem'}>Disconnect</SectionTitle>
|
<SectionTitle marginBottom={'0.5rem'}>Disconnect</SectionTitle>
|
||||||
<Button fullWidth onClick={handleShow}>
|
<Button fullWidth onClick={handleShow}>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { DbHistory } from '@/modules/db'
|
import { DbHistory } from '@/modules/db'
|
||||||
import { Box, IconButton, Typography } from '@mui/material'
|
import { Box, IconButton, Typography } from '@mui/material'
|
||||||
import { StyledActivityItem } from './styled'
|
import { StyledActivityItem } from './styled'
|
||||||
@ -6,16 +6,17 @@ import { formatTimestampDate } from '@/utils/helpers/date'
|
|||||||
import ClearRoundedIcon from '@mui/icons-material/ClearRounded'
|
import ClearRoundedIcon from '@mui/icons-material/ClearRounded'
|
||||||
import DoneRoundedIcon from '@mui/icons-material/DoneRounded'
|
import DoneRoundedIcon from '@mui/icons-material/DoneRounded'
|
||||||
import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded'
|
import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded'
|
||||||
import { ACTIONS } from '@/utils/consts'
|
import { getReqActionName } from '@/utils/helpers/helpers'
|
||||||
|
|
||||||
type ItemActivityProps = DbHistory
|
type ItemActivityProps = DbHistory
|
||||||
|
|
||||||
export const ItemActivity: FC<ItemActivityProps> = ({ allowed, method, timestamp }) => {
|
export const ItemActivity: FC<ItemActivityProps> = (req) => {
|
||||||
|
const { allowed, timestamp } = req
|
||||||
return (
|
return (
|
||||||
<StyledActivityItem>
|
<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}>
|
<Typography flex={1} fontWeight={700}>
|
||||||
{ACTIONS[method] || method}
|
{getReqActionName(req)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
|
<Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,6 +1,32 @@
|
|||||||
import { Avatar, AvatarProps, styled } from '@mui/material'
|
import { Box, BoxProps, Stack, StackProps, styled } from '@mui/material'
|
||||||
|
|
||||||
export const StyledAppIcon = styled((props: AvatarProps) => <Avatar {...props} variant="rounded" />)(() => ({
|
export const HeadingContainer = styled((props: StackProps) => <Stack {...props} />)(() => ({
|
||||||
width: 70,
|
width: '100%',
|
||||||
height: 70,
|
marginBottom: '1rem',
|
||||||
|
flexDirection: 'row',
|
||||||
|
gap: '1rem',
|
||||||
|
alignItems: 'center',
|
||||||
|
'@media screen and (max-width: 320px)': {
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: '0.5rem',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const AppInfoContainer = styled((props: StackProps) => <Stack {...props} direction={'row'} />)(() => ({
|
||||||
|
width: '100%',
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'flex-start',
|
||||||
|
gap: '0.5rem',
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
overflow: 'hidden',
|
||||||
|
'@media screen and (max-width: 320px)': {
|
||||||
|
alignSelf: 'flex-start',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const AppNameContainer = styled((props: BoxProps) => <Box {...props} />)(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
}))
|
}))
|
||||||
|
@ -8,6 +8,8 @@ import { ModalConfirmConnect } from '@/components/Modal/ModalConfirmConnect/Moda
|
|||||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { getReferrerAppUrl } from '@/utils/helpers/helpers'
|
||||||
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
|
||||||
const CreatePage = () => {
|
const CreatePage = () => {
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
@ -16,6 +18,8 @@ const CreatePage = () => {
|
|||||||
|
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
const name = searchParams.get('name') || ''
|
const name = searchParams.get('name') || ''
|
||||||
const token = searchParams.get('token') || ''
|
const token = searchParams.get('token') || ''
|
||||||
const appNpub = searchParams.get('appNpub') || ''
|
const appNpub = searchParams.get('appNpub') || ''
|
||||||
@ -30,18 +34,14 @@ const CreatePage = () => {
|
|||||||
|
|
||||||
const handleClickAddAccount = async () => {
|
const handleClickAddAccount = async () => {
|
||||||
try {
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
const key: any = await swicCall('generateKey', name)
|
const key: any = await swicCall('generateKey', name)
|
||||||
|
|
||||||
let appUrl = ''
|
const appUrl = getReferrerAppUrl()
|
||||||
if (window.document.referrer) {
|
|
||||||
try {
|
|
||||||
const u = new URL(window.document.referrer)
|
|
||||||
appUrl = u.origin
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Created', key.npub, 'app', appUrl)
|
console.log('Created', key.npub, 'app', appUrl)
|
||||||
setCreated(true)
|
setCreated(true)
|
||||||
|
setIsLoading(false)
|
||||||
|
|
||||||
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||||
search: {
|
search: {
|
||||||
@ -58,6 +58,7 @@ const CreatePage = () => {
|
|||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error.message || error.toString(), 'error')
|
notify(error.message || error.toString(), 'error')
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,7 +94,9 @@ const CreatePage = () => {
|
|||||||
<Typography textAlign={'left'} variant="h6" paddingTop="0.5em">
|
<Typography textAlign={'left'} variant="h6" paddingTop="0.5em">
|
||||||
Chosen name: <b>{nip05}</b>
|
Chosen name: <b>{nip05}</b>
|
||||||
</Typography>
|
</Typography>
|
||||||
<GetStartedButton onClick={handleClickAddAccount}>Create account</GetStartedButton>
|
<GetStartedButton onClick={handleClickAddAccount}>
|
||||||
|
Create account {isLoading && <LoadingSpinner />}
|
||||||
|
</GetStartedButton>
|
||||||
|
|
||||||
<Typography textAlign={'left'} variant="h5" paddingTop="1em">
|
<Typography textAlign={'left'} variant="h5" paddingTop="1em">
|
||||||
What you need to know:
|
What you need to know:
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
|
import { useCallback, useState } from 'react'
|
||||||
import { useAppSelector } from '../../store/hooks/redux'
|
import { useAppSelector } from '../../store/hooks/redux'
|
||||||
import { Navigate, useParams } from 'react-router-dom'
|
import { Navigate, useParams, useSearchParams } from 'react-router-dom'
|
||||||
import { Stack } from '@mui/material'
|
import { Box, IconButton, Stack } from '@mui/material'
|
||||||
import { StyledIconButton } from './styled'
|
import { StyledIconButton } from './styled'
|
||||||
import { SettingsIcon, ShareIcon } from '@/assets'
|
import { SettingsIcon, ShareIcon } from '@/assets'
|
||||||
import { Apps } from './components/Apps'
|
import { Apps } from './components/Apps'
|
||||||
@ -18,13 +19,20 @@ import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
|
|||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
import { checkNpubSyncQuerier } from './utils'
|
import { checkNpubSyncQuerier } from './utils'
|
||||||
import { DOMAIN } from '@/utils/consts'
|
import { DOMAIN } from '@/utils/consts'
|
||||||
import { useCallback } from 'react'
|
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
||||||
|
import MoreHorizRoundedIcon from '@mui/icons-material/MoreHorizRounded'
|
||||||
|
import { ModalEditName } from '@/components/Modal/ModalEditName/ModalEditName'
|
||||||
|
|
||||||
const KeyPage = () => {
|
const KeyPage = () => {
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
const { keys, apps, pending, perms } = useAppSelector((state) => state.content)
|
const { keys, apps, pending, perms } = useAppSelector((state) => state.content)
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const [isCheckingSync, setIsChecking] = useState(true)
|
||||||
|
const handleStopChecking = () => setIsChecking(false)
|
||||||
|
|
||||||
|
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub, handleStopChecking), [npub], false)
|
||||||
|
|
||||||
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
|
|
||||||
const { handleOpen } = useModalSearchParams()
|
const { handleOpen } = useModalSearchParams()
|
||||||
const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning()
|
const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning()
|
||||||
|
|
||||||
@ -41,10 +49,21 @@ const KeyPage = () => {
|
|||||||
const { prepareEventPendings } = useTriggerConfirmModal(npub, pending, perms)
|
const { prepareEventPendings } = useTriggerConfirmModal(npub, pending, perms)
|
||||||
|
|
||||||
const isKeyExists = npub.trim().length && key
|
const isKeyExists = npub.trim().length && key
|
||||||
|
const isPopup = searchParams.get('popup') === 'true'
|
||||||
|
// console.log({ isKeyExists, isPopup })
|
||||||
|
|
||||||
|
if (isPopup && !isKeyExists) {
|
||||||
|
searchParams.set('login', 'true')
|
||||||
|
searchParams.set('npub', npub)
|
||||||
|
const url = `/home?${searchParams.toString()}`
|
||||||
|
return <Navigate to={url} />
|
||||||
|
}
|
||||||
|
|
||||||
if (!isKeyExists) return <Navigate to={`/home`} />
|
if (!isKeyExists) return <Navigate to={`/home`} />
|
||||||
|
|
||||||
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||||
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
||||||
|
const handleOpenEditNameModal = () => handleOpen(MODAL_PARAMS_KEYS.EDIT_NAME)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -55,13 +74,20 @@ const KeyPage = () => {
|
|||||||
<UserValueSection
|
<UserValueSection
|
||||||
title="Your login"
|
title="Your login"
|
||||||
value={username}
|
value={username}
|
||||||
copyValue={username}
|
endAdornment={
|
||||||
|
<Box display={'flex'} alignItems={'center'} gap={'0.25rem'}>
|
||||||
|
<IconButton onClick={handleOpenEditNameModal} color={username ? 'default' : 'error'}>
|
||||||
|
<MoreHorizRoundedIcon />
|
||||||
|
</IconButton>
|
||||||
|
<InputCopyButton value={username} />
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
explanationType={EXPLANATION_MODAL_KEYS.LOGIN}
|
explanationType={EXPLANATION_MODAL_KEYS.LOGIN}
|
||||||
/>
|
/>
|
||||||
<UserValueSection
|
<UserValueSection
|
||||||
title="Your NPUB"
|
title="Your NPUB"
|
||||||
value={npub}
|
value={npub}
|
||||||
copyValue={npub}
|
endAdornment={<InputCopyButton value={npub} />}
|
||||||
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -71,7 +97,11 @@ const KeyPage = () => {
|
|||||||
Connect app
|
Connect app
|
||||||
</StyledIconButton>
|
</StyledIconButton>
|
||||||
|
|
||||||
<StyledIconButton bgcolor_variant="secondary" onClick={handleOpenSettingsModal} withBadge={!isSynced}>
|
<StyledIconButton
|
||||||
|
bgcolor_variant="secondary"
|
||||||
|
onClick={handleOpenSettingsModal}
|
||||||
|
withBadge={!isCheckingSync && !isSynced}
|
||||||
|
>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
Settings
|
Settings
|
||||||
</StyledIconButton>
|
</StyledIconButton>
|
||||||
@ -79,11 +109,13 @@ const KeyPage = () => {
|
|||||||
|
|
||||||
<Apps apps={filteredApps} npub={npub} />
|
<Apps apps={filteredApps} npub={npub} />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<ModalConnectApp />
|
<ModalConnectApp />
|
||||||
<ModalSettings isSynced={isSynced} />
|
<ModalSettings isSynced={isSynced} />
|
||||||
<ModalExplanation />
|
<ModalExplanation />
|
||||||
<ModalConfirmConnect />
|
<ModalConfirmConnect />
|
||||||
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
|
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
|
||||||
|
<ModalEditName />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,7 @@ import { Box, Stack, Typography } from '@mui/material'
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { StyledEmptyAppsBox } from '../styled'
|
import { StyledEmptyAppsBox } from '../styled'
|
||||||
import { Button } from '@/shared/Button/Button'
|
import { Button } from '@/shared/Button/Button'
|
||||||
import { call } from '@/utils/helpers/helpers'
|
|
||||||
import { swicCall } from '@/modules/swic'
|
|
||||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
|
||||||
import { ItemApp } from './ItemApp'
|
import { ItemApp } from './ItemApp'
|
||||||
|
|
||||||
type AppsProps = {
|
type AppsProps = {
|
||||||
@ -16,22 +14,12 @@ type AppsProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Apps: FC<AppsProps> = ({ apps = [] }) => {
|
export const Apps: FC<AppsProps> = ({ apps = [] }) => {
|
||||||
const notify = useEnqueueSnackbar()
|
|
||||||
|
|
||||||
// eslint-disable-next-line
|
|
||||||
async function deletePerm(id: string) {
|
|
||||||
call(async () => {
|
|
||||||
await swicCall('deletePerm', id)
|
|
||||||
notify('Perm deleted!', 'success')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const openAppStore = () => {
|
const openAppStore = () => {
|
||||||
window.open('https://nostrapp.link', '_blank')
|
window.open('https://nostrapp.link', '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flex={1} marginBottom={'1rem'} display={'flex'} flexDirection={'column'} overflow={'auto'}>
|
<Box marginBottom={'1rem'} display={'flex'} flexDirection={'column'}>
|
||||||
<Stack direction={'row'} alignItems={'center'} justifyContent={'space-between'} marginBottom={'0.5rem'}>
|
<Stack direction={'row'} alignItems={'center'} justifyContent={'space-between'} marginBottom={'0.5rem'}>
|
||||||
<SectionTitle>Connected apps</SectionTitle>
|
<SectionTitle>Connected apps</SectionTitle>
|
||||||
<AppLink title="Discover Apps" onClick={openAppStore} />
|
<AppLink title="Discover Apps" onClick={openAppStore} />
|
||||||
@ -45,7 +33,7 @@ export const Apps: FC<AppsProps> = ({ apps = [] }) => {
|
|||||||
</StyledEmptyAppsBox>
|
</StyledEmptyAppsBox>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Stack gap={'0.5rem'} overflow={'auto'} flex={1}>
|
<Stack gap={'0.5rem'} overflow={'auto'} flex={1} paddingBottom={'0.75rem'}>
|
||||||
{apps.map((a) => (
|
{apps.map((a) => (
|
||||||
<ItemApp {...a} key={a.appNpub} />
|
<ItemApp {...a} key={a.appNpub} />
|
||||||
))}
|
))}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { Warning } from '@/components/Warning/Warning'
|
import { Warning } from '@/components/Warning/Warning'
|
||||||
import { CircularProgress, Stack, Typography } from '@mui/material'
|
import { CircularProgress, Stack, Typography, TypographyProps, styled } from '@mui/material'
|
||||||
import AutoModeOutlinedIcon from '@mui/icons-material/AutoModeOutlined'
|
import AutoModeOutlinedIcon from '@mui/icons-material/AutoModeOutlined'
|
||||||
|
|
||||||
type BackgroundSigningWarningProps = {
|
type BackgroundSigningWarningProps = {
|
||||||
@ -12,18 +12,29 @@ export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({ is
|
|||||||
return (
|
return (
|
||||||
<Warning
|
<Warning
|
||||||
message={
|
message={
|
||||||
<Stack direction={'row'} alignItems={'center'} gap={'1rem'}>
|
<Stack gap={'0.25rem'} overflow={'auto'} width={'100%'}>
|
||||||
Enable background service {isEnabling ? <CircularProgress size={'1.5rem'} /> : null}
|
<Typography variant="body1" noWrap fontWeight={'500'}>
|
||||||
|
Enable background service
|
||||||
|
</Typography>
|
||||||
|
<StyledHint>Please allow notifications for background operation.</StyledHint>
|
||||||
</Stack>
|
</Stack>
|
||||||
}
|
}
|
||||||
hint={
|
icon={
|
||||||
<Typography variant='body2'>
|
isEnabling ? (
|
||||||
Please allow notifications
|
<CircularProgress size={'1.5rem'} sx={{ color: '#fff' }} />
|
||||||
for background operation.
|
) : (
|
||||||
</Typography>
|
<AutoModeOutlinedIcon htmlColor="white" />
|
||||||
|
)
|
||||||
}
|
}
|
||||||
icon={<AutoModeOutlinedIcon htmlColor="white" />}
|
|
||||||
onClick={isEnabling ? undefined : onEnableBackSigning}
|
onClick={isEnabling ? undefined : onEnableBackSigning}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const StyledHint = styled((props: TypographyProps) => <Typography variant="body2" {...props} />)(() => ({
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: 2,
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
}))
|
||||||
|
@ -9,9 +9,12 @@ type ItemAppProps = DbApp
|
|||||||
|
|
||||||
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name, url }) => {
|
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name, url }) => {
|
||||||
const appDomain = getDomain(url)
|
const appDomain = getDomain(url)
|
||||||
const appName = name || appDomain || getShortenNpub(appNpub)
|
const shortAppNpub = getShortenNpub(appNpub)
|
||||||
|
const appName = name || appDomain || shortAppNpub
|
||||||
const appIcon = icon || `https://${appDomain}/favicon.ico`
|
const appIcon = icon || `https://${appDomain}/favicon.ico`
|
||||||
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||||
|
const isAppNameExists = !!name || !!appDomain
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledItemAppContainer
|
<StyledItemAppContainer
|
||||||
direction={'row'}
|
direction={'row'}
|
||||||
@ -21,18 +24,18 @@ export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name, url }) =>
|
|||||||
component={Link}
|
component={Link}
|
||||||
to={`/key/${npub}/app/${appNpub}`}
|
to={`/key/${npub}/app/${appNpub}`}
|
||||||
>
|
>
|
||||||
<Avatar
|
<Avatar variant="rounded" sx={{ width: 56, height: 56 }} src={appIcon} alt={appName}>
|
||||||
variant="rounded"
|
|
||||||
sx={{ width: 56, height: 56 }}
|
|
||||||
src={appIcon}
|
|
||||||
alt={appName}
|
|
||||||
>
|
|
||||||
{appAvatarTitle}
|
{appAvatarTitle}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Typography noWrap display={'block'} variant="body2">
|
<Typography noWrap display={'block'} variant="body1">
|
||||||
{appName}
|
{appName}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{isAppNameExists && (
|
||||||
|
<Typography noWrap display={'block'} variant="body2" color={'GrayText'}>
|
||||||
|
{shortAppNpub}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
<Typography noWrap display={'block'} variant="caption" color={'GrayText'}>
|
<Typography noWrap display={'block'} variant="caption" color={'GrayText'}>
|
||||||
Basic actions
|
Basic actions
|
||||||
</Typography>
|
</Typography>
|
||||||
|
@ -3,7 +3,6 @@ import { Box, Stack } from '@mui/material'
|
|||||||
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
import { AppLink } from '@/shared/AppLink/AppLink'
|
import { AppLink } from '@/shared/AppLink/AppLink'
|
||||||
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
|
||||||
import { StyledInput } from '../styled'
|
import { StyledInput } from '../styled'
|
||||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
|
|
||||||
@ -11,10 +10,10 @@ type UserValueSectionProps = {
|
|||||||
title: string
|
title: string
|
||||||
value: string
|
value: string
|
||||||
explanationType: EXPLANATION_MODAL_KEYS
|
explanationType: EXPLANATION_MODAL_KEYS
|
||||||
copyValue: string
|
endAdornment?: React.ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, copyValue }) => {
|
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, endAdornment }) => {
|
||||||
const { handleOpen } = useModalSearchParams()
|
const { handleOpen } = useModalSearchParams()
|
||||||
|
|
||||||
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
|
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
|
||||||
@ -30,7 +29,7 @@ const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanation
|
|||||||
<SectionTitle>{title}</SectionTitle>
|
<SectionTitle>{title}</SectionTitle>
|
||||||
<AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} />
|
<AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<StyledInput value={value} readOnly endAdornment={<InputCopyButton value={copyValue} />} />
|
<StyledInput value={value} readOnly endAdornment={endAdornment} />
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Input, AppInputProps } from '@/shared/Input/Input'
|
import { Input } from '@/shared/Input/Input'
|
||||||
|
import { AppInputProps } from '@/shared/Input/types'
|
||||||
import { Stack, StackProps, styled } from '@mui/material'
|
import { Stack, StackProps, styled } from '@mui/material'
|
||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
import { DbPending, DbPerm } from '@/modules/db'
|
import { DbPending, DbPerm } from '@/modules/db'
|
||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { ACTION_TYPE } from '@/utils/consts'
|
import { ACTION_TYPE, REQ_TTL } from '@/utils/consts'
|
||||||
import { useCallback, useEffect, useRef } from 'react'
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
|
||||||
export type IPendingsByAppNpub = {
|
export type IPendingsByAppNpub = {
|
||||||
[appNpub: string]: {
|
[appNpub: string]: {
|
||||||
@ -18,10 +19,13 @@ type IShownConfirmModals = {
|
|||||||
export const useTriggerConfirmModal = (npub: string, pending: DbPending[], perms: DbPerm[]) => {
|
export const useTriggerConfirmModal = (npub: string, pending: DbPending[], perms: DbPerm[]) => {
|
||||||
const { handleOpen, getModalOpened } = useModalSearchParams()
|
const { handleOpen, getModalOpened } = useModalSearchParams()
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const isPopup = searchParams.get('popup') === 'true'
|
||||||
|
|
||||||
const isConfirmConnectModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
const isConfirmConnectModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
||||||
const isConfirmEventModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
const isConfirmEventModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
||||||
|
|
||||||
const filteredPendingReqs = pending.filter((p) => p.npub === npub)
|
const filteredPendingReqs = pending.filter((p) => p.npub === npub && p.timestamp > Date.now() - REQ_TTL)
|
||||||
const filteredPerms = perms.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 npubConnectPerms = filteredPerms.filter((perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC)
|
||||||
@ -66,11 +70,19 @@ export const useTriggerConfirmModal = (npub: string, pending: DbPending[], perms
|
|||||||
search: {
|
search: {
|
||||||
appNpub: req.appNpub,
|
appNpub: req.appNpub,
|
||||||
reqId: req.id,
|
reqId: req.id,
|
||||||
|
popup: isPopup ? 'true' : '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, [connectPendings, filteredPendingReqs.length, handleOpen, isConfirmEventModalOpened, isConfirmConnectModalOpened])
|
}, [
|
||||||
|
connectPendings,
|
||||||
|
filteredPendingReqs.length,
|
||||||
|
handleOpen,
|
||||||
|
isConfirmEventModalOpened,
|
||||||
|
isConfirmConnectModalOpened,
|
||||||
|
isPopup,
|
||||||
|
])
|
||||||
|
|
||||||
const handleOpenConfirmEventModal = useCallback(() => {
|
const handleOpenConfirmEventModal = useCallback(() => {
|
||||||
if (!filteredPendingReqs.length || connectPendings.length) return undefined
|
if (!filteredPendingReqs.length || connectPendings.length) return undefined
|
||||||
@ -86,11 +98,12 @@ export const useTriggerConfirmModal = (npub: string, pending: DbPending[], perms
|
|||||||
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
||||||
search: {
|
search: {
|
||||||
appNpub,
|
appNpub,
|
||||||
|
popup: isPopup ? 'true' : '',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}, [connectPendings.length, filteredPendingReqs.length, handleOpen, prepareEventPendings])
|
}, [connectPendings.length, filteredPendingReqs.length, handleOpen, prepareEventPendings, isPopup])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleOpenConfirmEventModal()
|
handleOpenConfirmEventModal()
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Input, AppInputProps } from '@/shared/Input/Input'
|
import { Input } from '@/shared/Input/Input'
|
||||||
|
import { AppInputProps } from '@/shared/Input/types'
|
||||||
import { Box, Button, ButtonProps, styled, Badge } from '@mui/material'
|
import { Box, Button, ButtonProps, styled, Badge } from '@mui/material'
|
||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
@ -48,6 +49,7 @@ export const StyledEmptyAppsBox = styled(Box)(({ theme }) => {
|
|||||||
placeItems: 'center',
|
placeItems: 'center',
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
opacity: '0.6',
|
opacity: '0.6',
|
||||||
|
maxHeight: '100%',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { db } from '@/modules/db'
|
import { db } from '@/modules/db'
|
||||||
|
|
||||||
export const checkNpubSyncQuerier = (npub: string) => async () => {
|
export const checkNpubSyncQuerier = (npub: string, onResolve: () => void) => async () => {
|
||||||
const count = await db.syncHistory.where('npub').equals(npub).count()
|
const count = await db.syncHistory.where('npub').equals(npub).count()
|
||||||
|
if (!count) onResolve()
|
||||||
return count > 0
|
return count > 0
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
import { Suspense, lazy } from 'react'
|
import { Suspense, lazy } from 'react'
|
||||||
import { Route, Routes, Navigate } from 'react-router-dom'
|
import { Route, Routes, Navigate } from 'react-router-dom'
|
||||||
import HomePage from '../pages/HomePage/Home.Page'
|
|
||||||
import { Layout } from '../layout/Layout'
|
import { Layout } from '../layout/Layout'
|
||||||
import { CircularProgress, Stack } from '@mui/material'
|
import { CircularProgress, Stack } from '@mui/material'
|
||||||
import CreatePage from '@/pages/CreatePage/Create.Page'
|
|
||||||
|
|
||||||
const KeyPage = lazy(() => import('../pages/KeyPage/Key.Page'))
|
// Pages
|
||||||
const ConfirmPage = lazy(() => import('../pages/Confirm.Page'))
|
import CreatePage from '@/pages/CreatePage/Create.Page'
|
||||||
const AppPage = lazy(() => import('../pages/AppPage/App.Page'))
|
import HomePage from '../pages/HomePage/Home.Page'
|
||||||
|
import KeyPage from '../pages/KeyPage/Key.Page'
|
||||||
|
|
||||||
|
const ConfirmPage = lazy(() => import('@/pages/Confirm.Page'))
|
||||||
|
const AppPage = lazy(() => import('@/pages/AppPage/App.Page'))
|
||||||
|
|
||||||
const LoadingSpinner = () => (
|
const LoadingSpinner = () => (
|
||||||
<Stack height={'100%'} justifyContent={'center'} alignItems={'center'}>
|
<Stack height={'100%'} justifyContent={'center'} alignItems={'center'}>
|
||||||
@ -21,7 +23,6 @@ const AppRoutes = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Layout />}>
|
<Route path="/" element={<Layout />}>
|
||||||
<Route path="/" element={<Navigate to={'/home'} />} />
|
<Route path="/" element={<Navigate to={'/home'} />} />
|
||||||
{/* <Route path='/welcome' element={<WelcomePage />} /> */}
|
|
||||||
<Route path="/home" element={<HomePage />} />
|
<Route path="/home" element={<HomePage />} />
|
||||||
<Route path="/key/:npub" element={<KeyPage />} />
|
<Route path="/key/:npub" element={<KeyPage />} />
|
||||||
<Route path="/key/:npub/app/:appNpub" element={<AppPage />} />
|
<Route path="/key/:npub/app/:appNpub" element={<AppPage />} />
|
||||||
|
@ -19,6 +19,9 @@ const StyledButton = styled(
|
|||||||
const commonStyles = {
|
const commonStyles = {
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
borderRadius: '1rem',
|
borderRadius: '1rem',
|
||||||
|
'@media screen and (max-width: 320px)': {
|
||||||
|
padding: '0.25rem 0.75rem',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
if (varianttype === 'secondary') {
|
if (varianttype === 'secondary') {
|
||||||
return {
|
return {
|
||||||
@ -28,20 +31,20 @@ const StyledButton = styled(
|
|||||||
},
|
},
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
'&.disabled': {
|
'&.disabled': {
|
||||||
opacity: 0.5,
|
background: `${theme.palette.backgroundSecondary.default}50`,
|
||||||
cursor: 'not-allowed',
|
cursor: 'not-allowed',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...commonStyles,
|
...commonStyles,
|
||||||
'&.button:is(:hover, :active, &, .disabled)': {
|
'&.button:is(:hover, :active, &)': {
|
||||||
background: theme.palette.primary.main,
|
background: theme.palette.primary.main,
|
||||||
},
|
},
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
'&.disabled': {
|
'&.button.disabled': {
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
opacity: 0.5,
|
background: `${theme.palette.primary.main}75`,
|
||||||
cursor: 'not-allowed',
|
cursor: 'not-allowed',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { Input, AppInputProps } from '../Input/Input'
|
import { Input } from '../Input/Input'
|
||||||
|
import { AppInputProps } from '../Input/types'
|
||||||
|
|
||||||
export type DebounceProps = {
|
export type DebounceProps = {
|
||||||
handleDebounce: (value: string) => void
|
handleDebounce: (value: string) => void
|
||||||
|
@ -9,7 +9,8 @@ export const StyledButton = styled((props: ButtonProps) => (
|
|||||||
startIcon: 'icon',
|
startIcon: 'icon',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
))(() => ({
|
))(({ theme }) => ({
|
||||||
|
color: theme.palette.primary.main,
|
||||||
marginBottom: '0.5rem',
|
marginBottom: '0.5rem',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
'&:is(:hover,:active)': {
|
'&:is(:hover,:active)': {
|
||||||
@ -18,4 +19,7 @@ export const StyledButton = styled((props: ButtonProps) => (
|
|||||||
'& .icon': {
|
'& .icon': {
|
||||||
marginRight: '5px',
|
marginRight: '5px',
|
||||||
},
|
},
|
||||||
|
'@media screen and (max-width: 320px)': {
|
||||||
|
marginBottom: '0.25rem',
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import React, { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
|
import { StyledAppIcon, StyledAppImg } from './styled'
|
||||||
|
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'
|
||||||
|
import { IIconApp } from './types'
|
||||||
|
|
||||||
const failedCache = new Map<string, boolean>()
|
const failedCache = new Map<string, boolean>()
|
||||||
|
|
||||||
export const IconApp: FC<{ picture: string }> = ({ picture }) => {
|
export const IconApp: FC<IIconApp> = ({ picture = '', alt, isRounded, isSmall, onClick, size, ...rest }) => {
|
||||||
const c = failedCache.get(picture)
|
const c = failedCache.get(picture)
|
||||||
const [isFailed, setIsFailed] = useState(c !== undefined ? c : true)
|
const [isFailed, setIsFailed] = useState(c !== undefined ? c : true)
|
||||||
|
|
||||||
@ -26,5 +29,21 @@ export const IconApp: FC<{ picture: string }> = ({ picture }) => {
|
|||||||
}
|
}
|
||||||
}, [picture])
|
}, [picture])
|
||||||
|
|
||||||
return <div>IconApp</div>
|
return (
|
||||||
|
<StyledAppIcon isNotLoaded={isFailed} size={size} onClick={onClick} {...rest}>
|
||||||
|
{alt ? (
|
||||||
|
<StyledAppImg size={size} alt={alt} isSmall={isSmall} src={isFailed ? '' : picture}>
|
||||||
|
{isFailed && (
|
||||||
|
<div className="MuiAvatar-root MuiAvatar-square MuiAvatar-colorDefault">
|
||||||
|
{alt.substring(0, 1).toUpperCase()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</StyledAppImg>
|
||||||
|
) : (
|
||||||
|
<StyledAppImg size={size} alt={alt} isSmall={isSmall} src={isFailed ? '/' : picture}>
|
||||||
|
<ImageOutlinedIcon fontSize="inherit" />
|
||||||
|
</StyledAppImg>
|
||||||
|
)}
|
||||||
|
</StyledAppIcon>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
53
src/shared/IconApp/const.ts
Normal file
53
src/shared/IconApp/const.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { APP_NSEC_SIZE } from '@/utils/consts'
|
||||||
|
|
||||||
|
const SIZE_VALUE = {
|
||||||
|
[APP_NSEC_SIZE.BIG]: 70,
|
||||||
|
[APP_NSEC_SIZE.LARGE]: 56,
|
||||||
|
[APP_NSEC_SIZE.MEDIUM]: 40,
|
||||||
|
[APP_NSEC_SIZE.SMALL]: 36,
|
||||||
|
[APP_NSEC_SIZE.EXTRA_SMALL]: 30,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const APP_SIZE_VALUE = {
|
||||||
|
[APP_NSEC_SIZE.BIG]: {
|
||||||
|
height: SIZE_VALUE[APP_NSEC_SIZE.BIG],
|
||||||
|
minWidth: SIZE_VALUE[APP_NSEC_SIZE.BIG],
|
||||||
|
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.BIG],
|
||||||
|
},
|
||||||
|
[APP_NSEC_SIZE.LARGE]: {
|
||||||
|
height: SIZE_VALUE[APP_NSEC_SIZE.LARGE],
|
||||||
|
minWidth: SIZE_VALUE[APP_NSEC_SIZE.LARGE],
|
||||||
|
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.LARGE],
|
||||||
|
},
|
||||||
|
[APP_NSEC_SIZE.MEDIUM]: {
|
||||||
|
height: SIZE_VALUE[APP_NSEC_SIZE.MEDIUM],
|
||||||
|
minWidth: SIZE_VALUE[APP_NSEC_SIZE.MEDIUM],
|
||||||
|
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.MEDIUM],
|
||||||
|
},
|
||||||
|
[APP_NSEC_SIZE.SMALL]: {
|
||||||
|
height: SIZE_VALUE[APP_NSEC_SIZE.SMALL],
|
||||||
|
minWidth: SIZE_VALUE[APP_NSEC_SIZE.SMALL],
|
||||||
|
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.SMALL],
|
||||||
|
},
|
||||||
|
[APP_NSEC_SIZE.EXTRA_SMALL]: {
|
||||||
|
height: SIZE_VALUE[APP_NSEC_SIZE.EXTRA_SMALL],
|
||||||
|
minWidth: SIZE_VALUE[APP_NSEC_SIZE.EXTRA_SMALL],
|
||||||
|
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.EXTRA_SMALL],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const FONT_SIZE_VALUE = {
|
||||||
|
[APP_NSEC_SIZE.BIG]: 24,
|
||||||
|
[APP_NSEC_SIZE.LARGE]: 20,
|
||||||
|
[APP_NSEC_SIZE.MEDIUM]: 16,
|
||||||
|
[APP_NSEC_SIZE.SMALL]: 12,
|
||||||
|
[APP_NSEC_SIZE.EXTRA_SMALL]: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const APP_NAME_FONT_SIZE_VALUE = {
|
||||||
|
[APP_NSEC_SIZE.LARGE]: FONT_SIZE_VALUE[APP_NSEC_SIZE.LARGE],
|
||||||
|
[APP_NSEC_SIZE.BIG]: FONT_SIZE_VALUE[APP_NSEC_SIZE.BIG],
|
||||||
|
[APP_NSEC_SIZE.MEDIUM]: FONT_SIZE_VALUE[APP_NSEC_SIZE.MEDIUM],
|
||||||
|
[APP_NSEC_SIZE.SMALL]: FONT_SIZE_VALUE[APP_NSEC_SIZE.SMALL],
|
||||||
|
[APP_NSEC_SIZE.EXTRA_SMALL]: FONT_SIZE_VALUE[APP_NSEC_SIZE.EXTRA_SMALL],
|
||||||
|
}
|
54
src/shared/IconApp/styled.tsx
Normal file
54
src/shared/IconApp/styled.tsx
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { Avatar, Box, styled } from '@mui/material'
|
||||||
|
import { forwardRef } from 'react'
|
||||||
|
import { IAvatarStyled, IBoxStyled } from './types'
|
||||||
|
import { grey } from '@mui/material/colors'
|
||||||
|
import { AppNostrSize } from '@/types/app-nsec'
|
||||||
|
import { APP_NAME_FONT_SIZE_VALUE, APP_SIZE_VALUE } from './const'
|
||||||
|
import { APP_NSEC_SIZE } from '@/utils/consts'
|
||||||
|
|
||||||
|
const color = grey[500]
|
||||||
|
|
||||||
|
const getVariantApp = (isRounded: boolean, size: AppNostrSize) => {
|
||||||
|
if (isRounded) {
|
||||||
|
return {
|
||||||
|
height: 34,
|
||||||
|
minWidth: 34,
|
||||||
|
maxWidth: 34,
|
||||||
|
borderRadius: '7px',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return APP_SIZE_VALUE[size]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StyledAppIcon = styled(
|
||||||
|
forwardRef<HTMLAnchorElement, IBoxStyled>(function BoxDisplayName(props, ref) {
|
||||||
|
return <Box ref={ref} {...props} />
|
||||||
|
})
|
||||||
|
)(({ theme, isNotLoaded, isRounded = false, size = APP_NSEC_SIZE.MEDIUM }) => ({
|
||||||
|
position: 'relative',
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
overflow: 'hidden',
|
||||||
|
...getVariantApp(isRounded, size),
|
||||||
|
transition: theme.transitions.create(['border-color', 'transition']),
|
||||||
|
backgroundColor: isNotLoaded ? color : theme.palette.background.default,
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
':active': {
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.3)',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const StyledAppImg = styled(function BoxDisplayName(props: IAvatarStyled) {
|
||||||
|
return <Avatar variant="square" {...props} />
|
||||||
|
})(({ isSmall = false, size = APP_NSEC_SIZE.MEDIUM }) => ({
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
fontWeight: '500',
|
||||||
|
fontSize: APP_NAME_FONT_SIZE_VALUE[size],
|
||||||
|
'.MuiAvatar-img': {
|
||||||
|
objectFit: isSmall ? 'scale-down' : 'cover',
|
||||||
|
},
|
||||||
|
}))
|
24
src/shared/IconApp/types.ts
Normal file
24
src/shared/IconApp/types.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { AppNostrSize } from '@/types/app-nsec'
|
||||||
|
import { AvatarProps, BoxProps } from '@mui/material'
|
||||||
|
|
||||||
|
export type IconAppProps = {
|
||||||
|
size?: AppNostrSize
|
||||||
|
isRounded?: boolean
|
||||||
|
isSmall?: boolean
|
||||||
|
onClick?: () => void
|
||||||
|
isNotLoaded?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IconAppBase = {
|
||||||
|
picture: string
|
||||||
|
alt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IIconApp = Omit<IconAppProps, 'isNotLoaded'> & IconAppBase
|
||||||
|
|
||||||
|
export type IBoxStyled = IconAppProps & BoxProps
|
||||||
|
|
||||||
|
export type IAvatarStyled = {
|
||||||
|
size?: AppNostrSize
|
||||||
|
isSmall?: boolean
|
||||||
|
} & AvatarProps
|
@ -1,69 +1,34 @@
|
|||||||
import { ReactNode, forwardRef } from 'react'
|
import { ReactNode, forwardRef } from 'react'
|
||||||
import {
|
import { FormHelperText, FormLabel, InputBase } from '@mui/material'
|
||||||
Box,
|
import { StyledInputContainer } from './styled'
|
||||||
BoxProps,
|
import { AppInputProps } from './types'
|
||||||
FormHelperText,
|
|
||||||
FormHelperTextProps,
|
|
||||||
FormLabel,
|
|
||||||
InputBase,
|
|
||||||
InputBaseProps,
|
|
||||||
styled,
|
|
||||||
} from '@mui/material'
|
|
||||||
|
|
||||||
export type AppInputProps = InputBaseProps & {
|
const renderItem = <T,>(item: T, value: ReactNode) => (item ? value : null)
|
||||||
helperText?: string | ReactNode
|
|
||||||
helperTextProps?: FormHelperTextProps
|
|
||||||
containerProps?: BoxProps
|
|
||||||
label?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, AppInputProps>(
|
export const Input = forwardRef<HTMLInputElement, AppInputProps>(
|
||||||
({ helperText, containerProps, helperTextProps, label, ...props }, ref) => {
|
({ helperText, containerProps, helperTextProps, label, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<StyledInputContainer {...containerProps}>
|
<StyledInputContainer {...containerProps}>
|
||||||
{label ? (
|
{renderItem(
|
||||||
|
label,
|
||||||
<FormLabel className="label" htmlFor={props.id}>
|
<FormLabel className="label" htmlFor={props.id}>
|
||||||
{label}
|
{label}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
) : null}
|
)}
|
||||||
<InputBase autoComplete="off" className="input" {...props} classes={{ error: 'error' }} ref={ref} />
|
|
||||||
{helperText ? (
|
<InputBase
|
||||||
|
autoComplete="off"
|
||||||
|
{...props}
|
||||||
|
classes={{ error: 'error', root: 'input_root', input: 'input', disabled: 'disabled' }}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
{renderItem(
|
||||||
|
helperText,
|
||||||
<FormHelperText {...helperTextProps} className="helper_text">
|
<FormHelperText {...helperTextProps} className="helper_text">
|
||||||
{helperText}
|
{helperText}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
) : null}
|
)}
|
||||||
</StyledInputContainer>
|
</StyledInputContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({ theme }) => {
|
|
||||||
const isDark = theme.palette.mode === 'dark'
|
|
||||||
return {
|
|
||||||
width: '100%',
|
|
||||||
'& > .input': {
|
|
||||||
background: isDark ? '#000000A8' : '#000',
|
|
||||||
color: theme.palette.common.white,
|
|
||||||
padding: '0.75rem 1rem',
|
|
||||||
borderRadius: '1rem',
|
|
||||||
border: '0.3px solid #FFFFFF54',
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
'& input::placeholder': {
|
|
||||||
color: '#fff',
|
|
||||||
},
|
|
||||||
'&.error': {
|
|
||||||
border: '0.3px solid ' + theme.palette.error.main,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'& > .helper_text': {
|
|
||||||
margin: '0.5rem 1rem 0',
|
|
||||||
color: theme.palette.text.primary,
|
|
||||||
},
|
|
||||||
'& > .label': {
|
|
||||||
margin: '0 1rem 0.5rem',
|
|
||||||
display: 'block',
|
|
||||||
color: theme.palette.primary.main,
|
|
||||||
fontSize: '0.875rem',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
45
src/shared/Input/styled.tsx
Normal file
45
src/shared/Input/styled.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Box, BoxProps, styled } from '@mui/material'
|
||||||
|
|
||||||
|
export const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({ theme }) => {
|
||||||
|
const isDark = theme.palette.mode === 'dark'
|
||||||
|
return {
|
||||||
|
width: '100%',
|
||||||
|
'& > .input_root': {
|
||||||
|
background: isDark ? '#000000A8' : '#000',
|
||||||
|
color: theme.palette.common.white,
|
||||||
|
padding: '0.75rem 1rem',
|
||||||
|
borderRadius: '1rem',
|
||||||
|
border: '0.3px solid #FFFFFF54',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
'&.error': {
|
||||||
|
border: '0.3px solid ' + theme.palette.error.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'& .input:is(.disabled, &)': {
|
||||||
|
WebkitTextFillColor: '#ffffff80',
|
||||||
|
},
|
||||||
|
'& > .helper_text': {
|
||||||
|
margin: '0.5rem 0.5rem 0',
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
'& > .label': {
|
||||||
|
margin: '0 1rem 0.5rem',
|
||||||
|
display: 'block',
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
},
|
||||||
|
'@media screen and (max-width: 320px)': {
|
||||||
|
'& > .input_root': {
|
||||||
|
padding: '0.5rem 0.75rem',
|
||||||
|
borderRadius: '0.75rem',
|
||||||
|
},
|
||||||
|
'& > .label': {
|
||||||
|
margin: '0 0.25rem 0.25rem',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
},
|
||||||
|
'& > .helper_text': {
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
9
src/shared/Input/types.ts
Normal file
9
src/shared/Input/types.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { BoxProps, FormHelperTextProps, InputBaseProps } from '@mui/material'
|
||||||
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
|
export type AppInputProps = InputBaseProps & {
|
||||||
|
helperText?: string | ReactNode
|
||||||
|
helperTextProps?: FormHelperTextProps
|
||||||
|
containerProps?: BoxProps
|
||||||
|
label?: string
|
||||||
|
}
|
@ -1,8 +1,7 @@
|
|||||||
import { FC, useEffect, useState } from 'react'
|
import { FC, useEffect, useState } from 'react'
|
||||||
import { Fade, IconButton, Typography } from '@mui/material'
|
import { Fade, Typography } from '@mui/material'
|
||||||
import CopyToClipboard from 'react-copy-to-clipboard'
|
import CopyToClipboard from 'react-copy-to-clipboard'
|
||||||
import { CopyIcon } from '@/assets'
|
import { StyledContainer, StyledCopyButton } from './styled'
|
||||||
import { StyledContainer } from './styled'
|
|
||||||
|
|
||||||
type InputCopyButtonProps = {
|
type InputCopyButtonProps = {
|
||||||
value: string
|
value: string
|
||||||
@ -40,9 +39,7 @@ export const InputCopyButton: FC<InputCopyButtonProps> = ({ value, onCopy = () =
|
|||||||
</Fade>
|
</Fade>
|
||||||
)}
|
)}
|
||||||
<CopyToClipboard text={value} onCopy={handleCopy}>
|
<CopyToClipboard text={value} onCopy={handleCopy}>
|
||||||
<IconButton color="inherit">
|
<StyledCopyButton />
|
||||||
<CopyIcon />
|
|
||||||
</IconButton>
|
|
||||||
</CopyToClipboard>
|
</CopyToClipboard>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
)
|
)
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
import { Stack, StackProps, styled } from '@mui/material'
|
import { IconButton, Stack, StackProps, styled } from '@mui/material'
|
||||||
|
import { CopyIcon } from '@/assets'
|
||||||
|
|
||||||
export const StyledContainer = styled((props: StackProps & { copied: number }) => (
|
export const StyledContainer = styled((props: StackProps & { copied: number }) => (
|
||||||
<Stack {...props} direction={'row'} alignItems={'center'} />
|
<Stack {...props} direction={'row'} alignItems={'center'} />
|
||||||
))(({ theme, copied }) => ({
|
))(({ theme, copied }) => ({
|
||||||
color: copied ? theme.palette.success.main : theme.palette.textSecondaryDecorate.main,
|
color: copied ? theme.palette.success.main : theme.palette.textSecondaryDecorate.main,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
export const StyledCopyButton = styled((props) => (
|
||||||
|
<IconButton color="inherit" {...props}>
|
||||||
|
<CopyIcon />
|
||||||
|
</IconButton>
|
||||||
|
))(() => ({
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
}))
|
||||||
|
17
src/shared/LoadingSpinner/LoadingSpinner.tsx
Normal file
17
src/shared/LoadingSpinner/LoadingSpinner.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import { CircularProgress, CircularProgressProps, styled } from '@mui/material'
|
||||||
|
import { FC } from 'react'
|
||||||
|
|
||||||
|
type LoadingSpinnerProps = CircularProgressProps & {
|
||||||
|
mode?: 'default' | 'secondary'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LoadingSpinner: FC<LoadingSpinnerProps> = (props) => {
|
||||||
|
return <StyledCircularProgress {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StyledCircularProgress = styled((props: LoadingSpinnerProps) => (
|
||||||
|
<CircularProgress size={'1rem'} {...props} />
|
||||||
|
))(({ theme, mode = 'default' }) => ({
|
||||||
|
marginLeft: '0.5rem',
|
||||||
|
color: mode === 'default' ? theme.palette.text.secondary : theme.palette.text.primary,
|
||||||
|
}))
|
@ -2,16 +2,7 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'
|
|||||||
import { contentSlice } from './reducers/content.slice'
|
import { contentSlice } from './reducers/content.slice'
|
||||||
import { uiSlice } from './reducers/ui.slice'
|
import { uiSlice } from './reducers/ui.slice'
|
||||||
|
|
||||||
import {
|
import { persistStore, persistReducer } from 'redux-persist'
|
||||||
persistStore,
|
|
||||||
persistReducer,
|
|
||||||
// FLUSH,
|
|
||||||
// REGISTER,
|
|
||||||
// REHYDRATE,
|
|
||||||
// PAUSE,
|
|
||||||
// PERSIST,
|
|
||||||
// PURGE,
|
|
||||||
} from 'redux-persist'
|
|
||||||
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web
|
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web
|
||||||
import memoizeOne from 'memoize-one'
|
import memoizeOne from 'memoize-one'
|
||||||
import isDeepEqual from 'lodash.isequal'
|
import isDeepEqual from 'lodash.isequal'
|
||||||
|
6
src/types/app-nsec.ts
Normal file
6
src/types/app-nsec.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { APP_NSEC_SIZE } from '@/utils/consts'
|
||||||
|
import { OverridableStringUnion } from './utils'
|
||||||
|
|
||||||
|
export type AppNostrSizeUnion = (typeof APP_NSEC_SIZE)[keyof typeof APP_NSEC_SIZE]
|
||||||
|
|
||||||
|
export type AppNostrSize = OverridableStringUnion<AppNostrSizeUnion>
|
@ -10,6 +10,7 @@ export enum MODAL_PARAMS_KEYS {
|
|||||||
CONFIRM_EVENT = 'confirm-event',
|
CONFIRM_EVENT = 'confirm-event',
|
||||||
ACTIVITY = 'activity',
|
ACTIVITY = 'activity',
|
||||||
APP_DETAILS = 'app-details',
|
APP_DETAILS = 'app-details',
|
||||||
|
EDIT_NAME = 'edit-name',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EXPLANATION_MODAL_KEYS {
|
export enum EXPLANATION_MODAL_KEYS {
|
||||||
|
10
src/types/utils.ts
Normal file
10
src/types/utils.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export type OverridableStringUnion<T extends string> = GenerateStringUnion<DistributiveOmit<Record<T, true>>>
|
||||||
|
|
||||||
|
type GenerateStringUnion<T> = Extract<
|
||||||
|
{
|
||||||
|
[Key in keyof T]: true extends T[Key] ? Key : never
|
||||||
|
}[keyof T],
|
||||||
|
string
|
||||||
|
>
|
||||||
|
|
||||||
|
type DistributiveOmit<T> = T extends T ? T : never
|
@ -9,6 +9,10 @@ export const MAX_POW = 19
|
|||||||
|
|
||||||
export const KIND_RPC = 24133
|
export const KIND_RPC = 24133
|
||||||
|
|
||||||
|
export const RELOAD_STORAGE_KEY = 'reload'
|
||||||
|
|
||||||
|
export const REQ_TTL = 60000 // 1 min
|
||||||
|
|
||||||
export enum ACTION_TYPE {
|
export enum ACTION_TYPE {
|
||||||
BASIC = 'basic',
|
BASIC = 'basic',
|
||||||
ADVANCED = 'advanced',
|
ADVANCED = 'advanced',
|
||||||
@ -16,10 +20,18 @@ export enum ACTION_TYPE {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ACTIONS: { [type: string]: string } = {
|
export const ACTIONS: { [type: string]: string } = {
|
||||||
basic: 'Basic permissions',
|
basic: 'Basic permissions',
|
||||||
get_public_key: 'Get public key',
|
get_public_key: 'Get public key',
|
||||||
sign_event: 'Sign event',
|
sign_event: 'Sign event',
|
||||||
connect: 'Connect',
|
connect: 'Connect',
|
||||||
nip04_encrypt: 'Encrypt message',
|
nip04_encrypt: 'Encrypt message',
|
||||||
nip04_decrypt: 'Decrypt message',
|
nip04_decrypt: 'Decrypt message',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const APP_NSEC_SIZE = {
|
||||||
|
BIG: 'big',
|
||||||
|
LARGE: 'large',
|
||||||
|
MEDIUM: 'medium',
|
||||||
|
SMALL: 'small',
|
||||||
|
EXTRA_SMALL: 'extra-small',
|
||||||
|
} as const
|
||||||
|
@ -3,7 +3,7 @@ import { format } from 'date-fns'
|
|||||||
export const formatTimestampDate = (timestamp: number) => {
|
export const formatTimestampDate = (timestamp: number) => {
|
||||||
try {
|
try {
|
||||||
const date = new Date(timestamp)
|
const date = new Date(timestamp)
|
||||||
const formattedDate = format(date, "dd-MM-yyyy HH:mm")
|
const formattedDate = format(date, 'dd-MM-yyyy HH:mm')
|
||||||
return formattedDate
|
return formattedDate
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return ''
|
return ''
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { nip19 } from 'nostr-tools'
|
import { nip19 } from 'nostr-tools'
|
||||||
import { ACTIONS, ACTION_TYPE, NIP46_RELAYS } from '../consts'
|
import { ACTIONS, ACTION_TYPE, DOMAIN, NIP46_RELAYS, NOAUTHD_URL } from '../consts'
|
||||||
import { DbPending, DbPerm } from '@/modules/db'
|
import { DbHistory, DbPending, DbPerm } from '@/modules/db'
|
||||||
import { MetaEvent } from '@/types/meta-event'
|
import { MetaEvent } from '@/types/meta-event'
|
||||||
|
|
||||||
export async function call(cb: () => any) {
|
export async function call(cb: () => any) {
|
||||||
@ -16,7 +16,7 @@ export const getShortenNpub = (npub = '') => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getProfileUsername = (profile: MetaEvent | null) => {
|
export const getProfileUsername = (profile: MetaEvent | null) => {
|
||||||
if (!profile) return null
|
if (!profile) return undefined
|
||||||
return profile?.info?.name || profile?.info?.display_name
|
return profile?.info?.name || profile?.info?.display_name
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,6 +72,7 @@ export function isPackagePerm(perm: string, reqPerm: string) {
|
|||||||
case 'sign_event:10002':
|
case 'sign_event:10002':
|
||||||
case 'sign_event:30023':
|
case 'sign_event:30023':
|
||||||
case 'sign_event:10000':
|
case 'sign_event:10000':
|
||||||
|
case 'sign_event:27235':
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,6 +98,21 @@ export async function fetchNip05(value: string, origin?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function fetchNpubNames(npub: string) {
|
||||||
|
try {
|
||||||
|
const url = `${NOAUTHD_URL}/name?npub=${npub}`
|
||||||
|
const response = await fetch(url)
|
||||||
|
const names: {
|
||||||
|
names: string[]
|
||||||
|
} = await response.json()
|
||||||
|
|
||||||
|
return names.names
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Failed to fetch names for', npub, 'error: ' + e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const getDomain = (url: string) => {
|
export const getDomain = (url: string) => {
|
||||||
try {
|
try {
|
||||||
return new URL(url).hostname
|
return new URL(url).hostname
|
||||||
@ -105,11 +121,21 @@ export const getDomain = (url: string) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getReferrerAppUrl = () => {
|
||||||
|
// console.log('referrer', window.document.referrer)
|
||||||
|
if (!window.document.referrer) return ''
|
||||||
|
try {
|
||||||
|
const u = new URL(window.document.referrer.toLocaleLowerCase())
|
||||||
|
if (u.hostname !== DOMAIN && !u.hostname.endsWith('.' + DOMAIN)) return u.origin
|
||||||
|
} catch {}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
export const getAppIconTitle = (name: string | undefined, appNpub: string) => {
|
export const getAppIconTitle = (name: string | undefined, appNpub: string) => {
|
||||||
return name ? name[0].toLocaleUpperCase() : appNpub.substring(4, 7)
|
return name ? name[0].toLocaleUpperCase() : appNpub.substring(4, 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getReqActionName(req: DbPending) {
|
export function getReqActionName(req: DbPending | DbHistory) {
|
||||||
const action = ACTIONS[req.method]
|
const action = ACTIONS[req.method]
|
||||||
if (req.method === 'sign_event') {
|
if (req.method === 'sign_event') {
|
||||||
const kind = getSignReqKind(req)
|
const kind = getSignReqKind(req)
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user