Compare commits
65 Commits
better-con
...
refactor/d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6186f3dd3d | ||
|
|
04c425c32c | ||
|
|
aac537c7a2 | ||
|
|
2058b900ac | ||
|
|
32c097c1ee | ||
|
|
43e375efe9 | ||
|
|
8b349c0350 | ||
|
|
0be2159efb | ||
|
|
1a9dc0da82 | ||
|
|
676eaf6191 | ||
|
|
97c3bcc16d | ||
|
|
a5f7bf2a58 | ||
|
|
0b56813ece | ||
|
|
8d205d9d93 | ||
|
|
9a18e79862 | ||
|
|
ab2df05d50 | ||
|
|
163de16a84 | ||
|
|
e9b290db30 | ||
|
|
544ac18b59 | ||
|
|
2551022d5e | ||
|
|
45c39ca904 | ||
|
|
041b84eb0b | ||
|
|
69166ff501 | ||
|
|
043e159e53 | ||
|
|
13d0a62fec | ||
|
|
d11cccec35 | ||
|
|
81b8624bd1 | ||
|
|
f45300583c | ||
|
|
0cf042e5d9 | ||
|
|
ec544a0592 | ||
|
|
72d561f8c9 | ||
|
|
f408fd1b38 | ||
|
|
977a4b5c93 | ||
|
|
8ccdc06f49 | ||
|
|
6589a98d52 | ||
|
|
fed1ece2d4 | ||
|
|
2b6a1e1e5d | ||
|
|
104404b04c | ||
|
|
e4fdb7794a | ||
|
|
e7e3b871e4 | ||
|
|
1566592683 | ||
|
|
063213cb89 | ||
|
|
52b119b424 | ||
|
|
0bf6fafb3e | ||
|
|
12afbaa76b | ||
|
|
14a83ec721 | ||
|
|
4aa4f7f175 | ||
|
|
7aaea89f21 | ||
|
|
dfb8889b9d | ||
|
|
89fc5b0ae0 | ||
|
|
48c07ad1c0 | ||
|
|
b24e3d31b0 | ||
|
|
caf8f9a82b | ||
|
|
b27fb5ec07 | ||
|
|
449bdb79ce | ||
|
|
d16c3cd9b0 | ||
|
|
fe4705afc8 | ||
|
|
9d565ddbde | ||
|
|
c5c5843cb8 | ||
|
|
34bf3f7c12 | ||
|
|
d3ab9174e1 | ||
|
|
8faccc383b | ||
|
|
1305af6896 | ||
|
|
593fafd9f8 | ||
|
|
2ba1eaef65 |
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
@@ -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?
|
||||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 502 B After Width: | Height: | Size: 536 B |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -1,31 +1,14 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta
|
<meta name="description" content="Web site created using create-react-app" />
|
||||||
name="description"
|
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png" />
|
||||||
content="Web site created using create-react-app"
|
<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="apple-touch-icon"
|
|
||||||
sizes="180x180"
|
|
||||||
href="%PUBLIC_URL%/apple-touch-icon.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="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.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
@@ -56,5 +39,6 @@
|
|||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
To begin the development, run `npm start` or `yarn start`.
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
To create a production bundle, use `npm run build` or `yarn build`.
|
||||||
--></body>
|
-->
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "Noauth",
|
"name": "Nsec.app",
|
||||||
"short_name": "Noauth Nostr key manager",
|
"short_name": "Nsec.app - Nostr key management tool",
|
||||||
|
"start_url": ".",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/android-chrome-192x192.png",
|
"src": "/android-chrome-192x192.png",
|
||||||
@@ -13,7 +14,6 @@
|
|||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": ".",
|
|
||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
"display": "standalone"
|
"display": "standalone"
|
||||||
|
|||||||
11
src/App.tsx
@@ -5,8 +5,6 @@ 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'
|
||||||
@@ -14,7 +12,6 @@ import { ModalLogin } from './components/Modal/ModalLogin/ModalLogin'
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [render, setRender] = useState(0)
|
const [render, setRender] = useState(0)
|
||||||
const { handleOpen } = useModalSearchParams()
|
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const [isConnected, setIsConnected] = useState(false)
|
const [isConnected, setIsConnected] = useState(false)
|
||||||
@@ -45,11 +42,7 @@ function App() {
|
|||||||
const apps = await dbi.listApps()
|
const apps = await dbi.listApps()
|
||||||
dispatch(
|
dispatch(
|
||||||
setApps({
|
setApps({
|
||||||
apps: apps.map((app) => ({
|
apps,
|
||||||
...app,
|
|
||||||
// MOCK IMAGE
|
|
||||||
icon: 'https://nostr.band/android-chrome-192x192.png',
|
|
||||||
})),
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,7 +55,7 @@ function App() {
|
|||||||
// rerender
|
// rerender
|
||||||
// setRender((r) => r + 1)
|
// setRender((r) => r + 1)
|
||||||
|
|
||||||
if (!keys.length) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
// if (!keys.length) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="32" height="36" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path d="M25.5479 20.2413C24.4588 19.0649 22.6437 18.9473 21.3126 19.7708L18.8925 17.8885C19.8606 16.2416 19.9816 14.2417 19.0135 12.5948L21.4337 10.8302C23.0067 12.1242 25.3059 12.0066 26.7579 10.5949C28.331 9.06555 28.452 6.59511 26.8789 5.06579C25.3059 3.53647 22.7647 3.41883 21.1916 4.94815C19.9816 6.12455 19.6186 7.88915 20.3446 9.41847L18.0455 11.1831C16.1094 9.41847 13.0842 9.18319 11.0271 10.5949L8.72796 8.35971C10.059 6.59511 9.93803 4.00703 8.24394 2.36007C6.42884 0.477835 3.28267 0.477835 1.46757 2.24243C-0.468534 4.00703 -0.468534 7.06567 1.34656 8.83027C3.04066 10.4772 5.58179 10.7125 7.5179 9.53611L9.81702 11.7713C8.36494 13.7712 8.36495 16.4769 9.93803 18.3591L7.27589 21.1825C6.06582 20.5943 4.61374 20.8295 3.64569 21.7707C2.43562 22.9471 2.31462 24.9469 3.52468 26.1233C4.73475 27.2997 6.79186 27.4174 8.00192 26.241C9.09098 25.1822 9.21199 23.6529 8.48595 22.4765L11.0271 19.6531C13.0842 20.9472 15.8674 20.8295 17.6824 19.1826L20.1026 21.0648C19.6186 22.2412 19.7396 23.6529 20.7076 24.594C21.9177 25.8881 24.0958 25.8881 25.3059 24.7117C26.7579 23.5353 26.7579 21.5354 25.5479 20.2413Z" fill="currentColor"/>
|
<path d="M28.2711 21.2958C27.1084 21.2958 26.0749 21.8372 25.4037 22.6797L21.8702 20.6405C22.2577 19.8106 22.4755 18.8846 22.4755 17.908C22.4755 16.9314 22.2577 16.0053 21.8702 15.1755L25.3404 13.1742C26.0091 14.0648 27.0704 14.6442 28.2711 14.6442C30.2949 14.6442 31.9363 13.0047 31.9363 10.9831C31.9363 8.96158 30.2949 7.32208 28.2711 7.32208C26.2472 7.32208 24.6058 8.96158 24.6058 10.9831C24.6058 11.4006 24.6793 11.8003 24.8084 12.1748L21.3028 14.1963C20.2338 12.6732 18.5241 11.6333 16.5635 11.4638V7.274C18.3189 7.00076 19.6639 5.49029 19.6639 3.66104C19.6639 1.6395 18.0225 0 15.9987 0C13.9748 0 12.3334 1.6395 12.3334 3.66104C12.3334 5.49029 13.6784 7.00329 15.4338 7.274V11.4638C13.4733 11.6333 11.7635 12.6732 10.6946 14.1963L7.1889 12.1748C7.31808 11.8003 7.39154 11.4006 7.39154 10.9831C7.39154 8.96158 5.75015 7.32208 3.72629 7.32208C1.70242 7.32208 0.0610352 8.96158 0.0610352 10.9831C0.0610352 13.0047 1.70242 14.6442 3.72629 14.6442C4.92693 14.6442 5.98825 14.0648 6.65697 13.1742L10.1272 15.1755C9.73963 16.0053 9.52179 16.9314 9.52179 17.908C9.52179 18.8846 9.73963 19.8106 10.1272 20.643L6.59364 22.6822C5.9224 21.8397 4.88893 21.2983 3.72629 21.2983C1.70242 21.2983 0.0610352 22.9378 0.0610352 24.9593C0.0610352 26.9809 1.70242 28.6204 3.72629 28.6204C5.75015 28.6204 7.39154 26.9809 7.39154 24.9593C7.39154 24.5039 7.30542 24.0687 7.1509 23.6639L10.6946 21.6196C11.3329 22.5279 12.1992 23.2667 13.2098 23.7499V32.3497C13.2098 32.9721 13.5569 33.8551 13.9799 34.3131L15.2286 35.6565C15.6516 36.1145 16.3457 36.1145 16.7712 35.6565L18.02 34.3131C18.443 33.8551 18.79 32.9721 18.79 32.3497V23.7499C19.8007 23.2667 20.667 22.5304 21.3053 21.6196L24.849 23.6639C24.697 24.0662 24.6083 24.5014 24.6083 24.9593C24.6083 26.9809 26.2497 28.6204 28.2736 28.6204C30.2975 28.6204 31.9388 26.9809 31.9388 24.9593C31.9388 22.9378 30.2975 21.2983 28.2736 21.2983L28.2711 21.2958Z" fill="white"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 2.0 KiB |
175
src/components/Modal/ModalAppDetails/ModalAppDetails.tsx
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
|
import { Button } from '@/shared/Button/Button'
|
||||||
|
import { Input } from '@/shared/Input/Input'
|
||||||
|
import { Modal } from '@/shared/Modal/Modal'
|
||||||
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
|
import { Autocomplete, Stack, Typography } from '@mui/material'
|
||||||
|
import { StyledInput } from './styled'
|
||||||
|
import { FormEvent, useEffect, useState } from 'react'
|
||||||
|
import { isEmptyString } from '@/utils/helpers/helpers'
|
||||||
|
import { useParams } from 'react-router-dom'
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
||||||
|
import { selectApps } from '@/store'
|
||||||
|
import { dbi } from '@/modules/db'
|
||||||
|
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||||
|
import { setApps } from '@/store/reducers/content.slice'
|
||||||
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
|
||||||
|
export const ModalAppDetails = () => {
|
||||||
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.APP_DETAILS)
|
||||||
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.APP_DETAILS)
|
||||||
|
|
||||||
|
const { appNpub = '' } = useParams()
|
||||||
|
const apps = useAppSelector(selectApps)
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
|
const [details, setDetails] = useState({
|
||||||
|
url: '',
|
||||||
|
name: '',
|
||||||
|
icon: '',
|
||||||
|
})
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentApp = apps.find((app) => app.appNpub === appNpub)
|
||||||
|
if (!currentApp) return
|
||||||
|
|
||||||
|
setDetails({
|
||||||
|
icon: currentApp.icon || '',
|
||||||
|
name: currentApp.name || '',
|
||||||
|
url: currentApp.url || '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
}, [appNpub, isModalOpened])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (isModalOpened) {
|
||||||
|
// modal closed
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isModalOpened])
|
||||||
|
|
||||||
|
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||||||
|
|
||||||
|
if (isModalOpened && !isAppNpubExists) {
|
||||||
|
handleCloseModal()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { icon, name, url } = details
|
||||||
|
|
||||||
|
const handleInputBlur = () => {
|
||||||
|
if (isEmptyString(url)) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const u = new URL(url)
|
||||||
|
|
||||||
|
if (isEmptyString(name)) setDetails((prev) => ({ ...prev, name: u.hostname }))
|
||||||
|
if (isEmptyString(icon)) {
|
||||||
|
const iconUrl = `https://${u.hostname}/favicon.ico`
|
||||||
|
setDetails((prev) => ({ ...prev, icon: iconUrl }))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* empty */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleInputChange = (key: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setDetails((prevState) => {
|
||||||
|
return { ...prevState, [key]: e.target.value }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAutocompletInputChange = (e: unknown, value: string) => {
|
||||||
|
setDetails((prevState) => {
|
||||||
|
return { ...prevState, url: value }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitHandler = async (e: FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (isLoading) return undefined
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const updatedApp = {
|
||||||
|
url,
|
||||||
|
name,
|
||||||
|
icon,
|
||||||
|
appNpub,
|
||||||
|
}
|
||||||
|
await dbi.updateApp(updatedApp)
|
||||||
|
const apps = await dbi.listApps()
|
||||||
|
dispatch(
|
||||||
|
setApps({
|
||||||
|
apps,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
notify(`App successfully updated!`, 'success')
|
||||||
|
setIsLoading(false)
|
||||||
|
handleCloseModal()
|
||||||
|
} catch (error: any) {
|
||||||
|
setIsLoading(false)
|
||||||
|
notify(error?.message || 'Something went wrong!', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFormValid = !isEmptyString(name)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||||
|
<Stack gap={'1rem'} component={'form'} onSubmit={submitHandler}>
|
||||||
|
<Stack alignItems={'center'}>
|
||||||
|
<Typography fontWeight={600} variant="h5">
|
||||||
|
App details
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Name"
|
||||||
|
fullWidth
|
||||||
|
placeholder="Enter app name"
|
||||||
|
onChange={handleInputChange('name')}
|
||||||
|
value={details.name}
|
||||||
|
/>
|
||||||
|
<Autocomplete
|
||||||
|
options={[]}
|
||||||
|
freeSolo
|
||||||
|
onBlur={handleInputBlur}
|
||||||
|
onInputChange={handleAutocompletInputChange}
|
||||||
|
inputValue={details.url}
|
||||||
|
renderInput={({ inputProps, disabled, id, InputProps }) => {
|
||||||
|
return (
|
||||||
|
<StyledInput
|
||||||
|
{...InputProps}
|
||||||
|
className="input"
|
||||||
|
inputProps={inputProps}
|
||||||
|
disabled={disabled}
|
||||||
|
label="URL"
|
||||||
|
fullWidth
|
||||||
|
placeholder="Enter URL"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
label="Icon"
|
||||||
|
fullWidth
|
||||||
|
placeholder="Enter app icon url"
|
||||||
|
onChange={handleInputChange('icon')}
|
||||||
|
value={details.icon}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button varianttype="secondary" type="submit" fullWidth disabled={!isFormValid || isLoading}>
|
||||||
|
Save changes {isLoading && <LoadingSpinner />}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
11
src/components/Modal/ModalAppDetails/styled.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { AppInputProps, Input } from '@/shared/Input/Input'
|
||||||
|
import { styled } from '@mui/material'
|
||||||
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
|
export const StyledInput = styled(
|
||||||
|
forwardRef<HTMLInputElement, AppInputProps>((props, ref) => <Input {...props} ref={ref} />)
|
||||||
|
)(() => ({
|
||||||
|
'& .MuiAutocomplete-endAdornment': {
|
||||||
|
right: '1rem',
|
||||||
|
},
|
||||||
|
}))
|
||||||
@@ -1,55 +1,73 @@
|
|||||||
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 { call, getAppIconTitle, 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 { 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 } 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 { useState } from 'react'
|
||||||
import { swicCall } from '@/modules/swic'
|
import { swicCall } from '@/modules/swic'
|
||||||
import { ACTION_TYPE } from '@/utils/consts'
|
import { ACTION_TYPE } from '@/utils/consts'
|
||||||
|
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||||
|
|
||||||
export const ModalConfirmConnect = () => {
|
export const ModalConfirmConnect = () => {
|
||||||
|
const keys = useAppSelector(selectKeys)
|
||||||
|
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
|
const notify = useEnqueueSnackbar()
|
||||||
|
const navigate = useNavigate()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
||||||
|
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const [searchParams] = useSearchParams()
|
||||||
|
const paramNpub = searchParams.get('npub') || ''
|
||||||
|
const { npub = paramNpub } = useParams<{ npub: string }>()
|
||||||
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
|
const apps = useAppSelector((state) => selectAppsByNpub(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 [searchParams] = useSearchParams()
|
|
||||||
const appNpub = searchParams.get('appNpub') || ''
|
const appNpub = searchParams.get('appNpub') || ''
|
||||||
const pendingReqId = searchParams.get('reqId') || ''
|
const pendingReqId = searchParams.get('reqId') || ''
|
||||||
const isPopup = searchParams.get('popup') === 'true'
|
const isPopup = searchParams.get('popup') === 'true'
|
||||||
|
const token = searchParams.get('token') || ''
|
||||||
|
|
||||||
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
||||||
const { name, icon = '' } = triggerApp || {}
|
const { name, url = '', icon = '' } = triggerApp || {}
|
||||||
const appName = name || getShortenNpub(appNpub)
|
|
||||||
const appAvatarTitle = getAppIconTitle(name, appNpub)
|
const appUrl = url || searchParams.get('appUrl') || getReferrerAppUrl();
|
||||||
|
const appDomain = getDomain(appUrl)
|
||||||
|
const appName = name || appDomain || getShortenNpub(appNpub)
|
||||||
|
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||||
|
const appIcon = icon || (appDomain ? `https://${appDomain}/favicon.ico` : '')
|
||||||
|
|
||||||
|
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||||
|
onClose: (sp) => {
|
||||||
|
sp.delete('appNpub')
|
||||||
|
sp.delete('reqId')
|
||||||
|
sp.delete('popup')
|
||||||
|
sp.delete('npub')
|
||||||
|
sp.delete('appUrl')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||||
|
// 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 (!isPopup && isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) {
|
||||||
|
closeModalAfterRequest()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
||||||
if (!value) return undefined
|
if (!value) return undefined
|
||||||
return setSelectedActionType(value)
|
return setSelectedActionType(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
// const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
|
||||||
// onClose: async (sp) => {
|
|
||||||
// sp.delete('appNpub')
|
|
||||||
// sp.delete('reqId')
|
|
||||||
// await swicCall('confirm', pendingReqId, false, false)
|
|
||||||
// },
|
|
||||||
// })
|
|
||||||
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
|
||||||
onClose: (sp) => {
|
|
||||||
sp.delete('appNpub')
|
|
||||||
sp.delete('reqId')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
async function confirmPending(id: string, allow: boolean, remember: boolean, options?: any) {
|
async function confirmPending(id: string, allow: boolean, remember: boolean, options?: any) {
|
||||||
call(async () => {
|
call(async () => {
|
||||||
await swicCall('confirm', id, allow, remember, options)
|
await swicCall('confirm', id, allow, remember, options)
|
||||||
@@ -59,16 +77,55 @@ export const ModalConfirmConnect = () => {
|
|||||||
if (isPopup) window.close()
|
if (isPopup) window.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
const allow = () => {
|
const allow = async () => {
|
||||||
const options: any = {}
|
let perms = ['connect', 'get_public_key']
|
||||||
if (selectedActionType === ACTION_TYPE.BASIC) options.perms = [ACTION_TYPE.BASIC]
|
if (selectedActionType === ACTION_TYPE.BASIC) perms = [ACTION_TYPE.BASIC]
|
||||||
// else
|
|
||||||
// options.perms = ['connect','get_public_key'];
|
if (pendingReqId) {
|
||||||
confirmPending(pendingReqId, true, true, options)
|
const options = { perms, appUrl }
|
||||||
|
await confirmPending(pendingReqId, true, true, options)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
await askNotificationPermission()
|
||||||
|
const result = await swicCall('enablePush')
|
||||||
|
if (!result) throw new Error('Failed to activate the push subscription')
|
||||||
|
console.log('enablePush done')
|
||||||
|
} catch (e: any) {
|
||||||
|
console.log('error', e)
|
||||||
|
notify('Please enable Notifications in website settings!', 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await swicCall('connectApp', { npub, appNpub, appUrl, perms })
|
||||||
|
console.log('connectApp done', npub, appNpub, appUrl, perms)
|
||||||
|
} catch (e: any) {
|
||||||
|
notify(e.toString(), 'error')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
try {
|
||||||
|
await swicCall('redeemToken', npub, token)
|
||||||
|
console.log('redeemToken done')
|
||||||
|
} catch (e) {
|
||||||
|
console.log('error', e)
|
||||||
|
notify('App did not reply. Please try to log in now.', 'error')
|
||||||
|
navigate(`/key/${npub}`, { replace: true })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
notify('App connected! Closing...', 'success')
|
||||||
|
|
||||||
|
if (isPopup) setTimeout(() => window.close(), 3000)
|
||||||
|
else navigate(`/key/${npub}`, { replace: true })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const disallow = () => {
|
const disallow = () => {
|
||||||
confirmPending(pendingReqId, false, true)
|
if (pendingReqId) confirmPending(pendingReqId, false, true)
|
||||||
|
else closeModalAfterRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPopup) {
|
if (isPopup) {
|
||||||
@@ -80,10 +137,14 @@ export const ModalConfirmConnect = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title='Connection request' open={isModalOpened} withCloseButton={false}
|
<Modal title="Connection request" open={isModalOpened} withCloseButton={false}>
|
||||||
// withCloseButton={!isPopup} onClose={!isPopup ? handleCloseModal : undefined}
|
|
||||||
>
|
|
||||||
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
||||||
|
{!pendingReqId && (
|
||||||
|
<Typography variant="body1" color={'GrayText'}>
|
||||||
|
You will be asked to <b>enable notifications</b>, this is needed for a reliable communication with Nostr
|
||||||
|
apps.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
|
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
|
||||||
<Avatar
|
<Avatar
|
||||||
variant="rounded"
|
variant="rounded"
|
||||||
@@ -91,7 +152,7 @@ export const ModalConfirmConnect = () => {
|
|||||||
width: 56,
|
width: 56,
|
||||||
height: 56,
|
height: 56,
|
||||||
}}
|
}}
|
||||||
src={icon}
|
src={appIcon}
|
||||||
>
|
>
|
||||||
{appAvatarTitle}
|
{appAvatarTitle}
|
||||||
</Avatar>
|
</Avatar>
|
||||||
@@ -109,14 +170,7 @@ export const ModalConfirmConnect = () => {
|
|||||||
value={ACTION_TYPE.BASIC}
|
value={ACTION_TYPE.BASIC}
|
||||||
title="Basic permissions"
|
title="Basic permissions"
|
||||||
description="Read your public key, sign notes, reactions, zaps, etc"
|
description="Read your public key, sign notes, reactions, zaps, etc"
|
||||||
// hasinfo
|
|
||||||
/>
|
/>
|
||||||
{/* <ActionToggleButton
|
|
||||||
value={ACTION_TYPE.ADVANCED}
|
|
||||||
title='Advanced'
|
|
||||||
description='Use for trusted apps only'
|
|
||||||
hasinfo
|
|
||||||
/> */}
|
|
||||||
<ActionToggleButton
|
<ActionToggleButton
|
||||||
value={ACTION_TYPE.CUSTOM}
|
value={ACTION_TYPE.CUSTOM}
|
||||||
title="On demand"
|
title="On demand"
|
||||||
@@ -128,7 +182,6 @@ export const ModalConfirmConnect = () => {
|
|||||||
Disallow
|
Disallow
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
<StyledButton fullWidth onClick={allow}>
|
<StyledButton fullWidth onClick={allow}>
|
||||||
{/* Allow {selectedActionType} actions */}
|
|
||||||
Connect
|
Connect
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
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 { call, getAppIconTitle, getShortenNpub, getSignReqKind } from '@/utils/helpers/helpers'
|
import { call, getAppIconTitle, getReqActionName, getShortenNpub } from '@/utils/helpers/helpers'
|
||||||
import { Avatar, Box, List, ListItem, ListItemIcon, ListItemText, Stack, Typography } from '@mui/material'
|
import { Avatar, Box, List, ListItem, ListItemIcon, ListItemText, Stack, Typography } from '@mui/material'
|
||||||
import { useParams, useSearchParams } from 'react-router-dom'
|
import { useParams, useSearchParams } from 'react-router-dom'
|
||||||
import { useAppSelector } from '@/store/hooks/redux'
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
import { selectAppsByNpub } from '@/store'
|
import { selectAppsByNpub, selectKeys } from '@/store'
|
||||||
import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
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'
|
||||||
@@ -13,7 +13,6 @@ import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
|||||||
import { swicCall } from '@/modules/swic'
|
import { swicCall } 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 { ACTIONS } from '@/utils/consts'
|
|
||||||
import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal'
|
import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal'
|
||||||
|
|
||||||
enum ACTION_TYPE {
|
enum ACTION_TYPE {
|
||||||
@@ -35,13 +34,14 @@ type ModalConfirmEventProps = {
|
|||||||
type PendingRequest = DbPending & { checked: boolean }
|
type PendingRequest = DbPending & { checked: boolean }
|
||||||
|
|
||||||
export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs }) => {
|
export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs }) => {
|
||||||
|
const keys = useAppSelector(selectKeys)
|
||||||
|
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
const appNpub = searchParams.get('appNpub') || ''
|
const appNpub = searchParams.get('appNpub') || ''
|
||||||
const isPopup = searchParams.get('popup') === 'true'
|
const isPopup = searchParams.get('popup') === 'true'
|
||||||
|
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
|
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
|
||||||
|
|
||||||
@@ -54,6 +54,34 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
|||||||
setPendingRequests(currentAppPendingReqs.map((pr) => ({ ...pr, checked: true })))
|
setPendingRequests(currentAppPendingReqs.map((pr) => ({ ...pr, checked: true })))
|
||||||
}, [currentAppPendingReqs])
|
}, [currentAppPendingReqs])
|
||||||
|
|
||||||
|
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
||||||
|
onClose: (sp) => {
|
||||||
|
sp.delete('appNpub')
|
||||||
|
sp.delete('reqId')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// FIXME: when opened directly to this modal using authUrl,
|
||||||
|
// we might not have pending requests visible yet bcs we haven't
|
||||||
|
// loaded them yet, which means this modal will be closed with
|
||||||
|
// the login below. It's fine if only one app has sent pending
|
||||||
|
// requests atm, bcs the modal would re-appear as soon as we load
|
||||||
|
// the requests. But if there are several pending reqs from other
|
||||||
|
// apps then popup might show a different one! Which is very
|
||||||
|
// contrary to what user expects. So:
|
||||||
|
// - if isPopup - dont close the modal with logic below
|
||||||
|
// - show some 'loading' indicator until we've got some requests
|
||||||
|
// for the specified appNpub
|
||||||
|
// FIXME is the same logic valid for Connect modal?
|
||||||
|
|
||||||
|
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||||
|
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)) {
|
||||||
|
closeModalAfterRequest()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
||||||
const { name, icon = '' } = triggerApp || {}
|
const { name, icon = '' } = triggerApp || {}
|
||||||
const appName = name || getShortenNpub(appNpub)
|
const appName = name || getShortenNpub(appNpub)
|
||||||
@@ -66,21 +94,6 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
|||||||
|
|
||||||
const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked)
|
const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked)
|
||||||
|
|
||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
|
||||||
onClose: (sp) => {
|
|
||||||
sp.delete('appNpub')
|
|
||||||
sp.delete('reqId')
|
|
||||||
selectedPendingRequests.forEach(async (req) => await swicCall('confirm', req.id, false, false))
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
|
||||||
onClose: (sp) => {
|
|
||||||
sp.delete('appNpub')
|
|
||||||
sp.delete('reqId')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
async function confirmPending(allow: boolean) {
|
async function confirmPending(allow: boolean) {
|
||||||
selectedPendingRequests.forEach((req) => {
|
selectedPendingRequests.forEach((req) => {
|
||||||
call(async () => {
|
call(async () => {
|
||||||
@@ -101,15 +114,6 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
|||||||
setPendingRequests(newPendingRequests)
|
setPendingRequests(newPendingRequests)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getAction = (req: PendingRequest) => {
|
|
||||||
const action = ACTIONS[req.method]
|
|
||||||
if (req.method === 'sign_event') {
|
|
||||||
const kind = getSignReqKind(req)
|
|
||||||
if (kind !== undefined) return `${action} of kind ${kind}`
|
|
||||||
}
|
|
||||||
return action
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isPopup) {
|
if (isPopup) {
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.visibilityState === 'hidden') {
|
if (document.visibilityState === 'hidden') {
|
||||||
@@ -119,9 +123,7 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal title='Permission request' open={isModalOpened} withCloseButton={false}
|
<Modal title="Permission request" open={isModalOpened} withCloseButton={false}>
|
||||||
// withCloseButton={!isPopup} onClose={!isPopup ? handleCloseModal : undefined}
|
|
||||||
>
|
|
||||||
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
||||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
|
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
|
||||||
<Avatar
|
<Avatar
|
||||||
@@ -154,7 +156,7 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
|||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Checkbox checked={req.checked} onChange={handleChangeCheckbox(req.id)} />
|
<Checkbox checked={req.checked} onChange={handleChangeCheckbox(req.id)} />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText>{getAction(req)}</ListItemText>
|
<ListItemText>{getReqActionName(req)}</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -163,11 +165,6 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
|||||||
<StyledToggleButtonsGroup value={selectedActionType} onChange={handleActionTypeChange} exclusive>
|
<StyledToggleButtonsGroup value={selectedActionType} onChange={handleActionTypeChange} exclusive>
|
||||||
<ActionToggleButton value={ACTION_TYPE.ALWAYS} title="Always" />
|
<ActionToggleButton value={ACTION_TYPE.ALWAYS} title="Always" />
|
||||||
<ActionToggleButton value={ACTION_TYPE.ONCE} title="Just once" />
|
<ActionToggleButton value={ACTION_TYPE.ONCE} title="Just once" />
|
||||||
{/* <ActionToggleButton
|
|
||||||
value={ACTION_TYPE.ALLOW_ALL}
|
|
||||||
title='Allow All Advanced Actions'
|
|
||||||
hasinfo
|
|
||||||
/> */}
|
|
||||||
</StyledToggleButtonsGroup>
|
</StyledToggleButtonsGroup>
|
||||||
|
|
||||||
<Stack direction={'row'} gap={'1rem'}>
|
<Stack direction={'row'} gap={'1rem'}>
|
||||||
|
|||||||
@@ -5,16 +5,23 @@ import { Button } from '@/shared/Button/Button'
|
|||||||
import { Input } from '@/shared/Input/Input'
|
import { Input } from '@/shared/Input/Input'
|
||||||
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
||||||
import { Modal } from '@/shared/Modal/Modal'
|
import { Modal } from '@/shared/Modal/Modal'
|
||||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { selectKeys } from '@/store'
|
||||||
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
|
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { getBunkerLink } from '@/utils/helpers/helpers'
|
import { getBunkerLink } from '@/utils/helpers/helpers'
|
||||||
import { Stack, Typography } from '@mui/material'
|
import { Stack, Typography } from '@mui/material'
|
||||||
import { useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
export const ModalConnectApp = () => {
|
export const ModalConnectApp = () => {
|
||||||
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
const keys = useAppSelector(selectKeys)
|
||||||
const timerRef = useRef<NodeJS.Timeout>()
|
|
||||||
|
|
||||||
|
const timerRef = useRef<NodeJS.Timeout>()
|
||||||
|
const notify = useEnqueueSnackbar()
|
||||||
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
|
const bunkerStr = getBunkerLink(npub)
|
||||||
|
|
||||||
|
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONNECT_APP, {
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONNECT_APP, {
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
@@ -22,11 +29,11 @@ export const ModalConnectApp = () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||||
|
if (isModalOpened && !isNpubExists) {
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
handleCloseModal()
|
||||||
|
return null
|
||||||
const bunkerStr = getBunkerLink(npub)
|
}
|
||||||
|
|
||||||
const handleShareBunker = async () => {
|
const handleShareBunker = async () => {
|
||||||
const shareData = {
|
const shareData = {
|
||||||
@@ -62,7 +69,10 @@ export const ModalConnectApp = () => {
|
|||||||
value={bunkerStr}
|
value={bunkerStr}
|
||||||
endAdornment={<InputCopyButton value={bunkerStr} onCopy={handleCopy} />}
|
endAdornment={<InputCopyButton value={bunkerStr} onCopy={handleCopy} />}
|
||||||
/>
|
/>
|
||||||
<AppLink title="What is this?" onClick={() => handleOpen(MODAL_PARAMS_KEYS.EXPLANATION)} />
|
<AppLink
|
||||||
|
title="What is this?"
|
||||||
|
onClick={() => handleOpen(MODAL_PARAMS_KEYS.EXPLANATION, { search: { type: EXPLANATION_MODAL_KEYS.BUNKER } })}
|
||||||
|
/>
|
||||||
<Button fullWidth onClick={handleShareBunker}>
|
<Button fullWidth onClick={handleShareBunker}>
|
||||||
Share it
|
Share it
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
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 { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { Stack, Typography } from '@mui/material'
|
import { Stack, Typography } from '@mui/material'
|
||||||
import { Button } from '@/shared/Button/Button'
|
import { Button } from '@/shared/Button/Button'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
@@ -10,7 +10,7 @@ type ModalExplanationProps = {
|
|||||||
explanationText?: string
|
explanationText?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModalExplanation: FC<ModalExplanationProps> = ({ explanationText = '' }) => {
|
export const ModalExplanation: FC<ModalExplanationProps> = () => {
|
||||||
const { getModalOpened } = useModalSearchParams()
|
const { getModalOpened } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EXPLANATION)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EXPLANATION)
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
@@ -18,21 +18,76 @@ export const ModalExplanation: FC<ModalExplanationProps> = ({ explanationText =
|
|||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
searchParams.delete('type')
|
searchParams.delete('type')
|
||||||
searchParams.delete(MODAL_PARAMS_KEYS.EXPLANATION)
|
searchParams.delete(MODAL_PARAMS_KEYS.EXPLANATION)
|
||||||
setSearchParams(searchParams)
|
setSearchParams(searchParams, { replace: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const type = searchParams.get('type')
|
||||||
|
|
||||||
|
let title = ''
|
||||||
|
let explanationText
|
||||||
|
switch (type) {
|
||||||
|
case EXPLANATION_MODAL_KEYS.NPUB: {
|
||||||
|
title = 'What is NPUB?'
|
||||||
|
explanationText = (
|
||||||
|
<>
|
||||||
|
NPUB is your Nostr PUBlic key.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
It is your global unique identifier on the Nostr network, and is derived from your private key.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
You can share your NPUB with other people so that they could unambiguously find you on the network.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case EXPLANATION_MODAL_KEYS.LOGIN: {
|
||||||
|
title = 'What is Login?'
|
||||||
|
explanationText = (
|
||||||
|
<>
|
||||||
|
Login (username) is your human-readable name on the Nostr network.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
Unlike your NPUB, which is a long string of random symbols, your login is a meaningful name tied to a website
|
||||||
|
address (like name@nsec.app).
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
Use your username to log in to Nostr apps.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
You can have many usernames all pointing to your NPUB. People also refer to these names as nostr-addresses or
|
||||||
|
NIP05 names.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case EXPLANATION_MODAL_KEYS.BUNKER: {
|
||||||
|
title = 'What is Bunker URL?'
|
||||||
|
explanationText = (
|
||||||
|
<>
|
||||||
|
Bunker URL is a string used to connect to Nostr apps.
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
Some apps require bunker URL to connect to your keys. Paste it to the app and then confirm a connection
|
||||||
|
request.
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title="What is this?"
|
title={title}
|
||||||
open={isModalOpened}
|
open={isModalOpened}
|
||||||
onClose={handleCloseModal}
|
onClose={handleCloseModal}
|
||||||
|
withCloseButton={false}
|
||||||
PaperProps={{
|
PaperProps={{
|
||||||
sx: {
|
sx: {
|
||||||
minHeight: '60%',
|
minHeight: '60%',
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Stack height={'100%'}>
|
<Stack height={'100%'} gap={2}>
|
||||||
<Typography flex={1}>{explanationText}</Typography>
|
<Typography flex={1}>{explanationText}</Typography>
|
||||||
<Button fullWidth onClick={handleCloseModal}>
|
<Button fullWidth onClick={handleCloseModal}>
|
||||||
Got it!
|
Got it!
|
||||||
|
|||||||
@@ -5,56 +5,199 @@ 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 { Stack, Typography } from '@mui/material'
|
import { Stack, Typography, useTheme } from '@mui/material'
|
||||||
import React, { ChangeEvent, FormEvent, useState } from 'react'
|
|
||||||
import { StyledAppLogo } from './styled'
|
import { StyledAppLogo } from './styled'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useForm } from 'react-hook-form'
|
||||||
|
import { FormInputType, schema } from './const'
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup'
|
||||||
|
import { usePassword } from '@/hooks/usePassword'
|
||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { useDebounce } from 'use-debounce'
|
||||||
|
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||||
|
import { DOMAIN } from '@/utils/consts'
|
||||||
|
import { CheckmarkIcon } from '@/assets'
|
||||||
|
import { getPublicKey, nip19 } from 'nostr-tools'
|
||||||
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
|
||||||
|
const FORM_DEFAULT_VALUES = {
|
||||||
|
username: '',
|
||||||
|
nsec: '',
|
||||||
|
}
|
||||||
|
|
||||||
export const ModalImportKeys = () => {
|
export const ModalImportKeys = () => {
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
||||||
|
const { hidePassword, inputProps } = usePassword()
|
||||||
|
const theme = useTheme()
|
||||||
|
|
||||||
|
const {
|
||||||
|
handleSubmit,
|
||||||
|
reset,
|
||||||
|
register,
|
||||||
|
formState: { errors },
|
||||||
|
watch,
|
||||||
|
} = useForm<FormInputType>({
|
||||||
|
defaultValues: FORM_DEFAULT_VALUES,
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
mode: 'onSubmit',
|
||||||
|
})
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [nameNpub, setNameNpub] = useState('')
|
||||||
|
const [isTakenByNsec, setIsTakenByNsec] = useState(false)
|
||||||
|
const [isBadNsec, setIsBadNsec] = useState(false)
|
||||||
|
const enteredUsername = watch('username')
|
||||||
|
const enteredNsec = watch('nsec')
|
||||||
|
const [debouncedUsername] = useDebounce(enteredUsername, 100)
|
||||||
|
const [debouncedNsec] = useDebounce(enteredNsec, 100)
|
||||||
|
|
||||||
|
const checkIsUsernameAvailable = useCallback(async () => {
|
||||||
|
if (!debouncedUsername.trim().length) return undefined
|
||||||
|
const npubNip05 = await fetchNip05(`${debouncedUsername}@${DOMAIN}`)
|
||||||
|
setNameNpub(npubNip05 || '')
|
||||||
|
}, [debouncedUsername])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
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(() => {
|
||||||
|
hidePassword()
|
||||||
|
reset()
|
||||||
|
setIsLoading(false)
|
||||||
|
setNameNpub('')
|
||||||
|
setIsTakenByNsec(false)
|
||||||
|
setIsBadNsec(false)
|
||||||
|
}, [reset, hidePassword])
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const [enteredNsec, setEnteredNsec] = useState('')
|
const submitHandler = async (values: FormInputType) => {
|
||||||
|
if (isLoading) return undefined
|
||||||
const handleNsecChange = (e: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setEnteredNsec(e.target.value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent) => {
|
|
||||||
e.preventDefault()
|
|
||||||
try {
|
try {
|
||||||
if (!enteredNsec.trim().length) return
|
const { nsec, username } = values
|
||||||
const enteredName = '' // FIXME get from input
|
if (!nsec || !username) throw new Error('Enter username and nsec')
|
||||||
const k: any = await swicCall('importKey', enteredName, enteredNsec)
|
if (nameNpub && !isTakenByNsec) throw new Error('Name taken')
|
||||||
|
setIsLoading(true)
|
||||||
|
const k: any = await swicCall('importKey', username, nsec)
|
||||||
notify('Key imported!', 'success')
|
notify('Key imported!', 'success')
|
||||||
navigate(`/key/${k.npub}`)
|
navigate(`/key/${k.npub}`)
|
||||||
|
cleanUpStates()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error.message, 'error')
|
notify(error?.message || 'Something went wrong!', 'error')
|
||||||
|
cleanUpStates()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
isModalOpened && cleanUpStates()
|
||||||
|
}
|
||||||
|
}, [isModalOpened, cleanUpStates])
|
||||||
|
|
||||||
|
const getNameHelperText = () => {
|
||||||
|
if (!enteredUsername) return "Don't worry, username can be changed later."
|
||||||
|
if (isTakenByNsec) return 'Name matches your key'
|
||||||
|
if (isBadNsec) return 'Invalid nsec'
|
||||||
|
if (nameNpub) return 'Already taken'
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<CheckmarkIcon /> Available
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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}>
|
||||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
||||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
||||||
<StyledAppLogo />
|
<StyledAppLogo />
|
||||||
<Typography fontWeight={600} variant="h5">
|
<Typography fontWeight={600} variant="h5">
|
||||||
Import keys
|
Import key
|
||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Input
|
<Input
|
||||||
label="Enter a NSEC"
|
label="Choose a username"
|
||||||
placeholder="Your NSEC"
|
|
||||||
value={enteredNsec}
|
|
||||||
onChange={handleNsecChange}
|
|
||||||
fullWidth
|
fullWidth
|
||||||
type="password"
|
placeholder="Enter a Username"
|
||||||
|
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||||
|
{...register('username')}
|
||||||
|
error={!!errors.username}
|
||||||
|
helperText={nameHelperText}
|
||||||
|
helperTextProps={{
|
||||||
|
sx: {
|
||||||
|
'&.helper_text': {
|
||||||
|
color:
|
||||||
|
enteredUsername && (isTakenByNsec || !nameNpub)
|
||||||
|
? theme.palette.success.main
|
||||||
|
: enteredUsername && nameNpub
|
||||||
|
? theme.palette.error.main
|
||||||
|
: theme.palette.textSecondaryDecorate.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<Button type="submit">Import nsec</Button>
|
<Input
|
||||||
|
label="Paste your private key"
|
||||||
|
placeholder="nsec1..."
|
||||||
|
fullWidth
|
||||||
|
{...register('nsec')}
|
||||||
|
error={!!errors.nsec}
|
||||||
|
{...inputProps}
|
||||||
|
helperText={nsecHelperText}
|
||||||
|
helperTextProps={{
|
||||||
|
sx: {
|
||||||
|
'&.helper_text': {
|
||||||
|
color: isBadNsec ? theme.palette.error.main : theme.palette.textSecondaryDecorate.main,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button type="submit" disabled={isLoading}>
|
||||||
|
Import key {isLoading && <LoadingSpinner />}
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
8
src/components/Modal/ModalImportKeys/const.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import * as yup from 'yup'
|
||||||
|
|
||||||
|
export const schema = yup.object().shape({
|
||||||
|
username: yup.string().required(),
|
||||||
|
nsec: yup.string().required(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type FormInputType = yup.InferType<typeof schema>
|
||||||
@@ -5,10 +5,10 @@ export const StyledAppLogo = styled((props) => (
|
|||||||
<Box {...props}>
|
<Box {...props}>
|
||||||
<AppLogo />
|
<AppLogo />
|
||||||
</Box>
|
</Box>
|
||||||
))({
|
))(() => ({
|
||||||
background: '#00000054',
|
background: '#0d0d0d',
|
||||||
padding: '0.75rem',
|
padding: '0.75rem',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
placeItems: 'center',
|
placeItems: 'center',
|
||||||
})
|
}))
|
||||||
|
|||||||
@@ -1,43 +1,20 @@
|
|||||||
import React, { useEffect, useState } 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'
|
|
||||||
|
|
||||||
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 handleShowAdvanced = () => {
|
|
||||||
setShowAdvancedContent(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
if (isModalOpened) {
|
|
||||||
setShowAdvancedContent(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [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 keys</Button>
|
|
||||||
</Fade>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,18 +4,24 @@ 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 { IconButton, Stack, Typography } from '@mui/material'
|
import { Stack, Typography } from '@mui/material'
|
||||||
import { StyledAppLogo } from './styled'
|
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 VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
|
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
|
||||||
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'
|
||||||
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 { dbi } from '@/modules/db'
|
||||||
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
|
||||||
|
const FORM_DEFAULT_VALUES = {
|
||||||
|
username: '',
|
||||||
|
password: '',
|
||||||
|
}
|
||||||
|
|
||||||
export const ModalLogin = () => {
|
export const ModalLogin = () => {
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
@@ -23,36 +29,38 @@ export const ModalLogin = () => {
|
|||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN)
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { hidePassword, inputProps } = usePassword()
|
||||||
|
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: {
|
defaultValues: FORM_DEFAULT_VALUES,
|
||||||
username: '',
|
|
||||||
password: '',
|
|
||||||
},
|
|
||||||
resolver: yupResolver(schema),
|
resolver: yupResolver(schema),
|
||||||
mode: 'onSubmit',
|
mode: 'onSubmit',
|
||||||
})
|
})
|
||||||
|
|
||||||
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
|
||||||
|
|
||||||
const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState)
|
|
||||||
|
|
||||||
const cleanUpStates = useCallback(() => {
|
const cleanUpStates = useCallback(() => {
|
||||||
setIsPasswordShown(false)
|
hidePassword()
|
||||||
reset()
|
reset()
|
||||||
}, [reset])
|
setIsLoading(false)
|
||||||
|
}, [reset, hidePassword])
|
||||||
|
|
||||||
const submitHandler = async (values: FormInputType) => {
|
const submitHandler = async (values: FormInputType) => {
|
||||||
|
if (isLoading) return undefined
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
let npub = values.username
|
let npub = values.username
|
||||||
let name = ''
|
let name = ''
|
||||||
|
|
||||||
if (!npub.startsWith('npub1')) {
|
if (!npub.startsWith('npub1')) {
|
||||||
name = npub
|
name = npub
|
||||||
if (!npub.includes('@')) {
|
if (!npub.includes('@')) {
|
||||||
@@ -72,14 +80,35 @@ export const ModalLogin = () => {
|
|||||||
console.log('fetch', npub, name)
|
console.log('fetch', npub, name)
|
||||||
const k: any = await swicCall('fetchKey', npub, passphrase, name)
|
const k: any = await swicCall('fetchKey', npub, passphrase, name)
|
||||||
notify(`Fetched ${k.npub}`, 'success')
|
notify(`Fetched ${k.npub}`, 'success')
|
||||||
|
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')
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, setValue])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
if (isModalOpened) {
|
if (isModalOpened) {
|
||||||
@@ -110,16 +139,11 @@ export const ModalLogin = () => {
|
|||||||
fullWidth
|
fullWidth
|
||||||
placeholder="Your password"
|
placeholder="Your password"
|
||||||
{...register('password')}
|
{...register('password')}
|
||||||
endAdornment={
|
{...inputProps}
|
||||||
<IconButton size="small" onClick={handlePasswordTypeChange}>
|
|
||||||
{isPasswordShown ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlinedIcon />}
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
type={isPasswordShown ? 'text' : 'password'}
|
|
||||||
error={!!errors.password}
|
error={!!errors.password}
|
||||||
/>
|
/>
|
||||||
<Button type="submit" fullWidth>
|
<Button type="submit" fullWidth disabled={isLoading}>
|
||||||
Add account
|
Add account {isLoading && <LoadingSpinner />}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
import * as yup from 'yup'
|
import * as yup from 'yup'
|
||||||
|
|
||||||
export const schema = yup.object().shape({
|
export const schema = yup.object().shape({
|
||||||
username: yup
|
username: yup.string().required(),
|
||||||
.string()
|
|
||||||
.test('Domain validation', 'The domain is required!', function (value) {
|
|
||||||
if (!value || !value.trim().length) return false
|
|
||||||
|
|
||||||
const USERNAME_WITH_DOMAIN_REGEXP = new RegExp(/^[\w-.]+@([\w-]+\.)+[\w-]{2,8}$/g)
|
|
||||||
return USERNAME_WITH_DOMAIN_REGEXP.test(value)
|
|
||||||
})
|
|
||||||
.required(),
|
|
||||||
password: yup.string().required().min(4),
|
password: yup.string().required().min(4),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const StyledAppLogo = styled((props) => (
|
|||||||
<AppLogo />
|
<AppLogo />
|
||||||
</Box>
|
</Box>
|
||||||
))({
|
))({
|
||||||
background: '#00000054',
|
background: '#0d0d0d',
|
||||||
padding: '0.75rem',
|
padding: '0.75rem',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
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, IconButton, 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'
|
||||||
import { Input } from '@/shared/Input/Input'
|
import { Input } from '@/shared/Input/Input'
|
||||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
|
|
||||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
|
||||||
import { ChangeEvent, FC, useEffect, useState } from 'react'
|
import { ChangeEvent, FC, useEffect, useState } from 'react'
|
||||||
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
||||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||||
import { swicCall } from '@/modules/swic'
|
import { swicCall } from '@/modules/swic'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { dbi } from '@/modules/db'
|
import { dbi } from '@/modules/db'
|
||||||
|
import { usePassword } from '@/hooks/usePassword'
|
||||||
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
|
import { selectKeys } from '@/store'
|
||||||
|
import { isValidPassphase, isWeakPassphase } from '@/modules/keys'
|
||||||
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
|
||||||
type ModalSettingsProps = {
|
type ModalSettingsProps = {
|
||||||
isSynced: boolean
|
isSynced: boolean
|
||||||
@@ -23,28 +25,44 @@ type ModalSettingsProps = {
|
|||||||
export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
|
const keys = useAppSelector(selectKeys)
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS)
|
||||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SETTINGS)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SETTINGS)
|
||||||
|
|
||||||
|
const { hidePassword, inputProps } = usePassword()
|
||||||
|
|
||||||
const [enteredPassword, setEnteredPassword] = useState('')
|
const [enteredPassword, setEnteredPassword] = useState('')
|
||||||
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
|
||||||
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
|
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
|
||||||
|
|
||||||
const [isChecked, setIsChecked] = useState(false)
|
const [isChecked, setIsChecked] = useState(false)
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
useEffect(() => setIsChecked(isSynced), [isModalOpened, isSynced])
|
useEffect(() => setIsChecked(isSynced), [isModalOpened, isSynced])
|
||||||
|
|
||||||
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
useEffect(() => {
|
||||||
setIsPasswordInvalid(false)
|
return () => {
|
||||||
setEnteredPassword(e.target.value)
|
if (isModalOpened) {
|
||||||
|
// modal closed
|
||||||
|
hidePassword()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [hidePassword, isModalOpened])
|
||||||
|
|
||||||
|
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||||
|
|
||||||
|
if (isModalOpened && !isNpubExists) {
|
||||||
|
handleCloseModal()
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState)
|
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const password = e.target.value
|
||||||
|
setIsPasswordInvalid(!!password && !isValidPassphase(password))
|
||||||
|
setEnteredPassword(password)
|
||||||
|
}
|
||||||
|
|
||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
handleCloseModal()
|
handleCloseModal()
|
||||||
@@ -60,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 {
|
||||||
@@ -95,30 +113,25 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
|||||||
</Box>
|
</Box>
|
||||||
<Input
|
<Input
|
||||||
fullWidth
|
fullWidth
|
||||||
endAdornment={
|
{...inputProps}
|
||||||
<IconButton size="small" onClick={handlePasswordTypeChange}>
|
|
||||||
{isPasswordShown ? (
|
|
||||||
<VisibilityOffOutlinedIcon htmlColor="#6b6b6b" />
|
|
||||||
) : (
|
|
||||||
<VisibilityOutlinedIcon htmlColor="#6b6b6b" />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
type={isPasswordShown ? 'text' : 'password'}
|
|
||||||
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>
|
||||||
@@ -129,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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ 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 { Stack, Typography, useTheme } from '@mui/material'
|
import { Stack, Typography, useTheme } from '@mui/material'
|
||||||
import React, { ChangeEvent, useState } from 'react'
|
import React, { ChangeEvent, useEffect, useState } from 'react'
|
||||||
import { StyledAppLogo } from './styled'
|
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'
|
||||||
import { swicCall } from '@/modules/swic'
|
import { swicCall } from '@/modules/swic'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { DOMAIN, NOAUTHD_URL } 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()
|
||||||
@@ -25,6 +26,8 @@ export const ModalSignUp = () => {
|
|||||||
const [enteredValue, setEnteredValue] = useState('')
|
const [enteredValue, setEnteredValue] = useState('')
|
||||||
const [isAvailable, setIsAvailable] = useState(false)
|
const [isAvailable, setIsAvailable] = useState(false)
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setEnteredValue(e.target.value)
|
setEnteredValue(e.target.value)
|
||||||
const name = e.target.value.trim()
|
const name = e.target.value.trim()
|
||||||
@@ -36,31 +39,50 @@ export const ModalSignUp = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputHelperText = enteredValue ? (
|
const getInputHelperText = () => {
|
||||||
isAvailable ? (
|
if (!enteredValue) return "Don't worry, username can be changed later."
|
||||||
|
if (!isAvailable) return 'Already taken'
|
||||||
|
return (
|
||||||
<>
|
<>
|
||||||
<CheckmarkIcon /> Available
|
<CheckmarkIcon /> Available
|
||||||
</>
|
</>
|
||||||
) : (
|
|
||||||
<>Already taken</>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
"Don't worry, username can be changed later."
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputHelperText = getInputHelperText()
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (isLoading || !isAvailable) return undefined
|
||||||
|
|
||||||
const name = enteredValue.trim()
|
const name = enteredValue.trim()
|
||||||
if (!name.length) return
|
if (!name.length) return
|
||||||
e.preventDefault()
|
|
||||||
try {
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
const k: any = await swicCall('generateKey', name)
|
const k: any = await swicCall('generateKey', name)
|
||||||
notify(`Account created for "${name}"`, 'success')
|
notify(`Account created for "${name}"`, 'success')
|
||||||
|
setIsLoading(false)
|
||||||
|
setTimeout(() => {
|
||||||
|
// give frontend time to read the new key first
|
||||||
navigate(`/key/${k.npub}`)
|
navigate(`/key/${k.npub}`)
|
||||||
|
}, 300)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error.message, 'error')
|
notify(error?.message || 'Something went wrong!', 'error')
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (isModalOpened) {
|
||||||
|
// modal closed
|
||||||
|
setIsLoading(false)
|
||||||
|
setIsAvailable(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isModalOpened])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||||
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||||
@@ -71,9 +93,9 @@ export const ModalSignUp = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Input
|
<Input
|
||||||
label="Enter a Username"
|
label="Username"
|
||||||
fullWidth
|
fullWidth
|
||||||
placeholder="Username"
|
placeholder="Enter a Username"
|
||||||
helperText={inputHelperText}
|
helperText={inputHelperText}
|
||||||
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
@@ -91,8 +113,8 @@ export const ModalSignUp = () => {
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button fullWidth type="submit">
|
<Button fullWidth type="submit" disabled={isLoading}>
|
||||||
Create account
|
Create account {isLoading && <LoadingSpinner />}
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const StyledAppLogo = styled((props) => (
|
|||||||
<AppLogo />
|
<AppLogo />
|
||||||
</Box>
|
</Box>
|
||||||
))({
|
))({
|
||||||
background: '#00000054',
|
background: '#0d0d0d',
|
||||||
padding: '0.75rem',
|
padding: '0.75rem',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
|
|||||||
30
src/hooks/usePassword.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { useCallback, useMemo, useState } from 'react'
|
||||||
|
import { IconButton } from '@mui/material'
|
||||||
|
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
|
||||||
|
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
||||||
|
|
||||||
|
export const usePassword = () => {
|
||||||
|
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
||||||
|
|
||||||
|
const handlePasswordTypeChange = useCallback(() => setIsPasswordShown((prevState) => !prevState), [])
|
||||||
|
|
||||||
|
const hidePassword = useCallback(() => setIsPasswordShown(false), [])
|
||||||
|
|
||||||
|
const inputProps = useMemo(
|
||||||
|
() => ({
|
||||||
|
endAdornment: (
|
||||||
|
<IconButton size="small" onClick={handlePasswordTypeChange}>
|
||||||
|
{isPasswordShown ? (
|
||||||
|
<VisibilityOffOutlinedIcon htmlColor="#6b6b6b" />
|
||||||
|
) : (
|
||||||
|
<VisibilityOutlinedIcon htmlColor="#6b6b6b" />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
),
|
||||||
|
type: isPasswordShown ? 'text' : 'password',
|
||||||
|
}),
|
||||||
|
[handlePasswordTypeChange, isPasswordShown]
|
||||||
|
)
|
||||||
|
|
||||||
|
return { inputProps, hidePassword }
|
||||||
|
}
|
||||||
41
src/hooks/useProfile.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { fetchProfile } from '@/modules/nostr'
|
||||||
|
import { MetaEvent } from '@/types/meta-event'
|
||||||
|
import { getProfileUsername, getShortenNpub } from '@/utils/helpers/helpers'
|
||||||
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
|
import { selectKeyByNpub } from '@/store'
|
||||||
|
|
||||||
|
const getFirstLetter = (text: string | undefined): string | null => {
|
||||||
|
if (!text || text.trim().length === 0) return null
|
||||||
|
return text.substring(0, 1).toUpperCase()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useProfile = (npub: string) => {
|
||||||
|
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||||
|
const currentKey = useAppSelector((state) => selectKeyByNpub(state, npub))
|
||||||
|
|
||||||
|
const userName = getProfileUsername(profile) || currentKey?.name
|
||||||
|
const userAvatar = profile?.info?.picture || ''
|
||||||
|
const avatarTitle = getFirstLetter(userName)
|
||||||
|
|
||||||
|
const loadProfile = useCallback(async () => {
|
||||||
|
if (!npub) return undefined
|
||||||
|
try {
|
||||||
|
const response = await fetchProfile(npub)
|
||||||
|
setProfile(response)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch profile:', error)
|
||||||
|
}
|
||||||
|
}, [npub])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadProfile()
|
||||||
|
}, [loadProfile])
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile,
|
||||||
|
userName: userName || getShortenNpub(npub),
|
||||||
|
userAvatar,
|
||||||
|
avatarTitle,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,53 +1,58 @@
|
|||||||
import { Avatar, Stack, Toolbar, Typography } from '@mui/material'
|
import { Avatar, Stack, Toolbar, Typography } from '@mui/material'
|
||||||
import { AppLogo } from '../../assets'
|
import { StyledAppBar, StyledAppLogo, StyledAppName, StyledProfileContainer, StyledThemeButton } from './styled'
|
||||||
import { StyledAppBar, StyledAppName } from './styled'
|
|
||||||
import { Menu } from './components/Menu'
|
import { Menu } from './components/Menu'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
|
||||||
import { MetaEvent } from '@/types/meta-event'
|
|
||||||
import { fetchProfile } from '@/modules/nostr'
|
|
||||||
import { ProfileMenu } from './components/ProfileMenu'
|
import { ProfileMenu } from './components/ProfileMenu'
|
||||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
import { useProfile } from '@/hooks/useProfile'
|
||||||
|
import DarkModeIcon from '@mui/icons-material/DarkMode'
|
||||||
|
import LightModeIcon from '@mui/icons-material/LightMode'
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
||||||
|
import { setThemeMode } from '@/store/reducers/ui.slice'
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
|
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||||
|
const showProfile = Boolean(npub)
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const handleNavigate = () => {
|
||||||
if (!npub) return setProfile(null)
|
navigate(`/key/${npub}`)
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetchProfile(npub)
|
|
||||||
setProfile(response as any)
|
|
||||||
} catch (e) {
|
|
||||||
return setProfile(null)
|
|
||||||
}
|
}
|
||||||
}, [npub])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const isDarkMode = themeMode === 'dark'
|
||||||
load()
|
const themeIcon = isDarkMode ? <LightModeIcon htmlColor="#fff" /> : <DarkModeIcon htmlColor="#000" />
|
||||||
}, [load])
|
|
||||||
|
|
||||||
const showProfile = Boolean(npub || profile)
|
const handleChangeMode = () => {
|
||||||
const userName = profile?.info?.name || getShortenNpub(npub)
|
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
||||||
const userAvatar = profile?.info?.picture || ''
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledAppBar position="fixed">
|
<StyledAppBar position="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 && (
|
||||||
<Stack gap={'1rem'} direction={'row'} alignItems={'center'} flex={1}>
|
<StyledProfileContainer>
|
||||||
<Avatar src={userAvatar} alt={userName} />
|
<Avatar src={userAvatar} alt={userName} onClick={handleNavigate} className="avatar">
|
||||||
<Typography fontWeight={600}>{userName}</Typography>
|
{avatarTitle}
|
||||||
</Stack>
|
</Avatar>
|
||||||
) : (
|
<Typography fontWeight={600} onClick={handleNavigate} className="username">
|
||||||
|
{userName}
|
||||||
|
</Typography>
|
||||||
|
</StyledProfileContainer>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!showProfile && (
|
||||||
<StyledAppName>
|
<StyledAppName>
|
||||||
<AppLogo />
|
<StyledAppLogo />
|
||||||
<span>Nsec.app</span>
|
<span>Nsec.app</span>
|
||||||
</StyledAppName>
|
</StyledAppName>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<StyledThemeButton onClick={handleChangeMode}>{themeIcon}</StyledThemeButton>
|
||||||
|
|
||||||
{showProfile ? <ProfileMenu /> : <Menu />}
|
{showProfile ? <ProfileMenu /> : <Menu />}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
|||||||
31
src/layout/Header/components/ListItemProfile.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { useProfile } from '@/hooks/useProfile'
|
||||||
|
import { DbKey } from '@/modules/db'
|
||||||
|
import { Avatar, ListItemIcon, MenuItem, Typography } from '@mui/material'
|
||||||
|
import React, { FC } from 'react'
|
||||||
|
|
||||||
|
type ListItemProfileProps = {
|
||||||
|
onClickItem: () => void
|
||||||
|
} & DbKey
|
||||||
|
|
||||||
|
export const ListItemProfile: FC<ListItemProfileProps> = ({
|
||||||
|
onClickItem,
|
||||||
|
npub,
|
||||||
|
}) => {
|
||||||
|
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||||
|
return (
|
||||||
|
<MenuItem sx={{ gap: '0.5rem' }} onClick={onClickItem}>
|
||||||
|
<ListItemIcon>
|
||||||
|
<Avatar
|
||||||
|
src={userAvatar}
|
||||||
|
alt={userName}
|
||||||
|
sx={{ width: 36, height: 36 }}
|
||||||
|
>
|
||||||
|
{avatarTitle}
|
||||||
|
</Avatar>
|
||||||
|
</ListItemIcon>
|
||||||
|
<Typography variant='body2' noWrap>
|
||||||
|
{userName}
|
||||||
|
</Typography>
|
||||||
|
</MenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DbKey } from '@/modules/db'
|
import { DbKey } from '@/modules/db'
|
||||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
import { Stack } from '@mui/material'
|
||||||
import { Avatar, ListItemIcon, MenuItem, Stack, Typography } from '@mui/material'
|
import { FC } from 'react'
|
||||||
import React, { FC } from 'react'
|
import { ListItemProfile } from './ListItemProfile'
|
||||||
|
|
||||||
type ListProfilesProps = {
|
type ListProfilesProps = {
|
||||||
keys: DbKey[]
|
keys: DbKey[]
|
||||||
@@ -12,18 +12,7 @@ export const ListProfiles: FC<ListProfilesProps> = ({ keys = [], onClickItem })
|
|||||||
return (
|
return (
|
||||||
<Stack maxHeight={'10rem'} overflow={'auto'}>
|
<Stack maxHeight={'10rem'} overflow={'auto'}>
|
||||||
{keys.map((key) => {
|
{keys.map((key) => {
|
||||||
const userName = key?.profile?.info?.name || getShortenNpub(key.npub)
|
return <ListItemProfile {...key} key={key.npub} onClickItem={() => onClickItem(key)} />
|
||||||
const userAvatar = key?.profile?.info?.picture || ''
|
|
||||||
return (
|
|
||||||
<MenuItem sx={{ gap: '0.5rem' }} onClick={() => onClickItem(key)} key={key.npub}>
|
|
||||||
<ListItemIcon>
|
|
||||||
<Avatar src={userAvatar} alt={userName} sx={{ width: 36, height: 36 }} />
|
|
||||||
</ListItemIcon>
|
|
||||||
<Typography variant="body2" noWrap>
|
|
||||||
{userName}
|
|
||||||
</Typography>
|
|
||||||
</MenuItem>
|
|
||||||
)
|
|
||||||
})}
|
})}
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { Menu as MuiMenu } from '@mui/material'
|
import { Menu as MuiMenu } from '@mui/material'
|
||||||
import DarkModeIcon from '@mui/icons-material/DarkMode'
|
|
||||||
import LightModeIcon from '@mui/icons-material/LightMode'
|
|
||||||
import LoginIcon from '@mui/icons-material/Login'
|
import LoginIcon from '@mui/icons-material/Login'
|
||||||
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
||||||
import { setThemeMode } from '@/store/reducers/ui.slice'
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
|
||||||
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 { MenuButton } from './styled'
|
import { MenuButton } from './styled'
|
||||||
@@ -14,26 +11,17 @@ import MenuRoundedIcon from '@mui/icons-material/MenuRounded'
|
|||||||
import { selectKeys } from '@/store'
|
import { selectKeys } from '@/store'
|
||||||
|
|
||||||
export const Menu = () => {
|
export const Menu = () => {
|
||||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
|
||||||
const keys = useAppSelector(selectKeys)
|
const keys = useAppSelector(selectKeys)
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const { handleOpen } = useModalSearchParams()
|
const { handleOpen } = useModalSearchParams()
|
||||||
|
|
||||||
const isDarkMode = themeMode === 'dark'
|
|
||||||
const isNoKeys = !keys || keys.length === 0
|
|
||||||
|
|
||||||
const { anchorEl, handleClose, handleOpen: handleOpenMenu, open } = useOpenMenu()
|
const { anchorEl, handleClose, handleOpen: handleOpenMenu, open } = useOpenMenu()
|
||||||
|
|
||||||
const handleChangeMode = () => {
|
const isNoKeys = !keys || keys.length === 0
|
||||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
|
||||||
}
|
|
||||||
const handleNavigateToAuth = () => {
|
const handleNavigateToAuth = () => {
|
||||||
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
handleClose()
|
handleClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#feb94a" />
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuButton onClick={handleOpenMenu}>
|
<MenuButton onClick={handleOpenMenu}>
|
||||||
@@ -52,7 +40,6 @@ export const Menu = () => {
|
|||||||
onClick={handleNavigateToAuth}
|
onClick={handleNavigateToAuth}
|
||||||
title={isNoKeys ? 'Sign up' : 'Add account'}
|
title={isNoKeys ? 'Sign up' : 'Add account'}
|
||||||
/>
|
/>
|
||||||
<MenuItem Icon={themeIcon} onClick={handleChangeMode} title="Change theme" />
|
|
||||||
</MuiMenu>
|
</MuiMenu>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,11 +9,9 @@ import LoginIcon from '@mui/icons-material/Login'
|
|||||||
import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded'
|
import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded'
|
||||||
import HomeRoundedIcon from '@mui/icons-material/HomeRounded'
|
import HomeRoundedIcon from '@mui/icons-material/HomeRounded'
|
||||||
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
||||||
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
import { selectKeys } from '@/store'
|
import { selectKeys } from '@/store'
|
||||||
import { setThemeMode } from '@/store/reducers/ui.slice'
|
|
||||||
import DarkModeIcon from '@mui/icons-material/DarkMode'
|
|
||||||
import LightModeIcon from '@mui/icons-material/LightMode'
|
|
||||||
import { ListProfiles } from './ListProfiles'
|
import { ListProfiles } from './ListProfiles'
|
||||||
import { DbKey } from '@/modules/db'
|
import { DbKey } from '@/modules/db'
|
||||||
|
|
||||||
@@ -23,10 +21,7 @@ export const ProfileMenu = () => {
|
|||||||
|
|
||||||
const keys = useAppSelector(selectKeys)
|
const keys = useAppSelector(selectKeys)
|
||||||
const isNoKeys = !keys || keys.length === 0
|
const isNoKeys = !keys || keys.length === 0
|
||||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
|
||||||
const isDarkMode = themeMode === 'dark'
|
|
||||||
|
|
||||||
const dispatch = useAppDispatch()
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const handleNavigateToAuth = () => {
|
const handleNavigateToAuth = () => {
|
||||||
@@ -39,17 +34,11 @@ export const ProfileMenu = () => {
|
|||||||
handleClose()
|
handleClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChangeMode = () => {
|
|
||||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleNavigateToKeyInnerPage = (key: DbKey) => {
|
const handleNavigateToKeyInnerPage = (key: DbKey) => {
|
||||||
navigate('/key/' + key.npub)
|
navigate('/key/' + key.npub)
|
||||||
handleClose()
|
handleClose()
|
||||||
}
|
}
|
||||||
|
|
||||||
const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#feb94a" />
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<MenuButton onClick={handleOpenMenu}>
|
<MenuButton onClick={handleOpenMenu}>
|
||||||
@@ -71,7 +60,6 @@ export const ProfileMenu = () => {
|
|||||||
onClick={handleNavigateToAuth}
|
onClick={handleNavigateToAuth}
|
||||||
title={isNoKeys ? 'Sign up' : 'Add account'}
|
title={isNoKeys ? 'Sign up' : 'Add account'}
|
||||||
/>
|
/>
|
||||||
<MenuItem Icon={themeIcon} onClick={handleChangeMode} title="Change theme" />
|
|
||||||
</Menu>
|
</Menu>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AppBar, Typography, TypographyProps, styled } from '@mui/material'
|
import { AppLogo } from '@/assets'
|
||||||
|
import { AppBar, IconButton, Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
export const StyledAppBar = styled(AppBar)(({ theme }) => {
|
export const StyledAppBar = styled(AppBar)(({ theme }) => {
|
||||||
@@ -11,6 +12,7 @@ export const StyledAppBar = styled(AppBar)(({ theme }) => {
|
|||||||
maxWidth: 'inherit',
|
maxWidth: 'inherit',
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: 'translateX(-50%)',
|
transform: 'translateX(-50%)',
|
||||||
|
borderRadius: '8px',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -29,3 +31,26 @@ export const StyledAppName = styled((props: TypographyProps) => (
|
|||||||
lineHeight: '22.4px',
|
lineHeight: '22.4px',
|
||||||
marginLeft: '0.5rem',
|
marginLeft: '0.5rem',
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
export const StyledProfileContainer = styled((props: StackProps) => <Stack {...props} />)(() => ({
|
||||||
|
gap: '1rem',
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
flex: 1,
|
||||||
|
'& .avatar': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
'& .username': {
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const StyledThemeButton = styled(IconButton)({
|
||||||
|
margin: '0 0.5rem',
|
||||||
|
})
|
||||||
|
|
||||||
|
export const StyledAppLogo = styled(AppLogo)(({ theme }) => ({
|
||||||
|
'& path': {
|
||||||
|
fill: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|||||||
@@ -46,6 +46,12 @@ interface IAllowCallbackParams {
|
|||||||
params?: any
|
params?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class Nip46Backend extends NDKNip46Backend {
|
||||||
|
public async processEvent(event: NDKEvent) {
|
||||||
|
this.handleIncomingEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class Nip04KeyHandlingStrategy implements IEventHandlingStrategy {
|
class Nip04KeyHandlingStrategy implements IEventHandlingStrategy {
|
||||||
private privkey: string
|
private privkey: string
|
||||||
private nip04 = new Nip04()
|
private nip04 = new Nip04()
|
||||||
@@ -137,10 +143,16 @@ 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) => {
|
||||||
@@ -250,7 +262,7 @@ export class NoauthBackend {
|
|||||||
const url = `${NOAUTHD_URL}/name?npub=${npub}`
|
const url = `${NOAUTHD_URL}/name?npub=${npub}`
|
||||||
const r = await fetch(url)
|
const r = await fetch(url)
|
||||||
const d = await r.json()
|
const d = await r.json()
|
||||||
return d?.names?.length ? d.names[0] as string : ''
|
return d?.names?.length ? (d.names[0] as string) : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
private async sendPost({ url, method, headers, body }: { url: string; method: string; headers: any; body: string }) {
|
private async sendPost({ url, method, headers, body }: { url: string; method: string; headers: any; body: string }) {
|
||||||
@@ -407,6 +419,23 @@ export class NoauthBackend {
|
|||||||
throw new Error('Too many requests, retry later')
|
throw new Error('Too many requests, retry later')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async sendTokenToServer(npub: string, token: string) {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
npub,
|
||||||
|
token,
|
||||||
|
})
|
||||||
|
|
||||||
|
const method = 'POST'
|
||||||
|
const url = `${NOAUTHD_URL}/created`
|
||||||
|
|
||||||
|
return this.sendPostAuthd({
|
||||||
|
npub,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
private notify() {
|
private notify() {
|
||||||
// FIXME collect info from accessBuffer and confirmBuffer
|
// FIXME collect info from accessBuffer and confirmBuffer
|
||||||
// and update the notifications
|
// and update the notifications
|
||||||
@@ -420,8 +449,8 @@ export class NoauthBackend {
|
|||||||
const app = this.apps.find((a) => a.appNpub === r.req.appNpub)
|
const app = this.apps.find((a) => a.appNpub === r.req.appNpub)
|
||||||
if (r.req.method !== 'connect' && !app) continue
|
if (r.req.method !== 'connect' && !app) continue
|
||||||
|
|
||||||
// FIXME use Nsec.app icon!
|
// FIXME check
|
||||||
const icon = 'https://nostr.band/android-chrome-192x192.png'
|
const icon = 'assets/icons/logo.svg'
|
||||||
|
|
||||||
const appName = app?.name || getShortenNpub(r.req.appNpub)
|
const appName = app?.name || getShortenNpub(r.req.appNpub)
|
||||||
// FIXME load profile?
|
// FIXME load profile?
|
||||||
@@ -550,6 +579,49 @@ export class NoauthBackend {
|
|||||||
return perm?.value || ''
|
return perm?.value || ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async connectApp({
|
||||||
|
npub,
|
||||||
|
appNpub,
|
||||||
|
appUrl,
|
||||||
|
perms,
|
||||||
|
appName = '',
|
||||||
|
appIcon = '',
|
||||||
|
}: {
|
||||||
|
npub: string
|
||||||
|
appNpub: string
|
||||||
|
appUrl: string
|
||||||
|
appName?: string
|
||||||
|
appIcon?: string
|
||||||
|
perms: string[]
|
||||||
|
}) {
|
||||||
|
await dbi.addApp({
|
||||||
|
appNpub: appNpub,
|
||||||
|
npub: npub,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
name: appName,
|
||||||
|
icon: appIcon,
|
||||||
|
url: appUrl,
|
||||||
|
})
|
||||||
|
|
||||||
|
// reload
|
||||||
|
this.apps = await dbi.listApps()
|
||||||
|
|
||||||
|
// write new perms confirmed by user
|
||||||
|
for (const p of perms) {
|
||||||
|
await dbi.addPerm({
|
||||||
|
id: Math.random().toString(36).substring(7),
|
||||||
|
npub: npub,
|
||||||
|
appNpub: appNpub,
|
||||||
|
perm: p,
|
||||||
|
value: '1',
|
||||||
|
timestamp: Date.now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// reload
|
||||||
|
this.perms = await dbi.listPerms()
|
||||||
|
}
|
||||||
|
|
||||||
private async allowPermitCallback({
|
private async allowPermitCallback({
|
||||||
backend,
|
backend,
|
||||||
npub,
|
npub,
|
||||||
@@ -566,7 +638,7 @@ export class NoauthBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 false
|
||||||
@@ -600,7 +672,7 @@ export class NoauthBackend {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
name: '',
|
name: '',
|
||||||
icon: '',
|
icon: '',
|
||||||
url: '',
|
url: options.appUrl || '',
|
||||||
})
|
})
|
||||||
|
|
||||||
// reload
|
// reload
|
||||||
@@ -682,7 +754,8 @@ export class NoauthBackend {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// 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`
|
||||||
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
|
// NOTE: if you set 'Update on reload' in the Chrome SW console
|
||||||
@@ -710,7 +783,7 @@ 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, () => Promise.resolve(true))
|
||||||
this.keys.push({ npub, backend, signer, ndk, backoff })
|
this.keys.push({ npub, backend, signer, ndk, backoff })
|
||||||
|
|
||||||
// new method
|
// new method
|
||||||
@@ -767,6 +840,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) {
|
||||||
@@ -790,6 +884,11 @@ export class NoauthBackend {
|
|||||||
return k
|
return k
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async redeemToken(npub: string, token: string) {
|
||||||
|
console.log('redeeming token', npub, token)
|
||||||
|
await this.sendTokenToServer(npub, token)
|
||||||
|
}
|
||||||
|
|
||||||
private async importKey(name: string, nsec: string) {
|
private async importKey(name: string, nsec: string) {
|
||||||
const k = await this.addKey({ name, nsec })
|
const k = await this.addKey({ name, nsec })
|
||||||
this.updateUI()
|
this.updateUI()
|
||||||
@@ -829,7 +928,7 @@ export class NoauthBackend {
|
|||||||
const npubName = await this.fetchNpubName(npub)
|
const npubName = await this.fetchNpubName(npub)
|
||||||
if (npubName) {
|
if (npubName) {
|
||||||
// already have name for this npub
|
// already have name for this npub
|
||||||
console.log("existing npub name", npub, npubName)
|
console.log('existing npub name', npub, npubName)
|
||||||
name = npubName
|
name = npubName
|
||||||
} else if (nip05.includes('@')) {
|
} else if (nip05.includes('@')) {
|
||||||
// no name for them?
|
// no name for them?
|
||||||
@@ -837,7 +936,7 @@ export class NoauthBackend {
|
|||||||
if (domain === DOMAIN) {
|
if (domain === DOMAIN) {
|
||||||
// wtf? how did we learn their npub if
|
// wtf? how did we learn their npub if
|
||||||
// it's the name on our server but we can't fetch it?
|
// it's the name on our server but we can't fetch it?
|
||||||
console.log("existing name", nip05name)
|
console.log('existing name', nip05name)
|
||||||
name = nip05name
|
name = nip05name
|
||||||
} else {
|
} else {
|
||||||
// try to take same name on our domain
|
// try to take same name on our domain
|
||||||
@@ -850,13 +949,13 @@ export class NoauthBackend {
|
|||||||
takenName = await fetchNip05(`${name}@${DOMAIN}`)
|
takenName = await fetchNip05(`${name}@${DOMAIN}`)
|
||||||
}
|
}
|
||||||
if (takenName) {
|
if (takenName) {
|
||||||
console.log("All names taken, leave without a name?")
|
console.log('All names taken, leave without a name?')
|
||||||
name = ''
|
name = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("fetch", { name, existingName })
|
console.log('fetch', { name, existingName })
|
||||||
|
|
||||||
// add new key
|
// add new key
|
||||||
const nsec = await this.keysModule.decryptKeyPass({
|
const nsec = await this.keysModule.decryptKeyPass({
|
||||||
@@ -926,6 +1025,8 @@ export class NoauthBackend {
|
|||||||
let result = undefined
|
let result = undefined
|
||||||
if (method === 'generateKey') {
|
if (method === 'generateKey') {
|
||||||
result = await this.generateKey(args[0])
|
result = await this.generateKey(args[0])
|
||||||
|
} else if (method === 'redeemToken') {
|
||||||
|
result = await this.redeemToken(args[0], args[1])
|
||||||
} else if (method === 'importKey') {
|
} else if (method === 'importKey') {
|
||||||
result = await this.importKey(args[0], args[1])
|
result = await this.importKey(args[0], args[1])
|
||||||
} else if (method === 'saveKey') {
|
} else if (method === 'saveKey') {
|
||||||
@@ -934,12 +1035,16 @@ export class NoauthBackend {
|
|||||||
result = await this.fetchKey(args[0], args[1], args[2])
|
result = await this.fetchKey(args[0], args[1], args[2])
|
||||||
} else if (method === 'confirm') {
|
} else if (method === 'confirm') {
|
||||||
result = await this.confirm(args[0], args[1], args[2], args[3])
|
result = await this.confirm(args[0], args[1], args[2], args[3])
|
||||||
|
} else if (method === 'connectApp') {
|
||||||
|
result = await this.connectApp(args[0])
|
||||||
} else if (method === 'deleteApp') {
|
} else if (method === 'deleteApp') {
|
||||||
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 === '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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,17 @@ export const dbi = {
|
|||||||
console.log(`db addApp error: ${error}`)
|
console.log(`db addApp error: ${error}`)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
updateApp: async (app: Omit<DbApp, 'npub' | 'timestamp'>) => {
|
||||||
|
try {
|
||||||
|
await db.apps.where({ appNpub: app.appNpub }).modify({
|
||||||
|
name: app.name,
|
||||||
|
icon: app.icon,
|
||||||
|
url: app.url,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`db updateApp error: ${error}`)
|
||||||
|
}
|
||||||
|
},
|
||||||
listApps: async (): Promise<DbApp[]> => {
|
listApps: async (): Promise<DbApp[]> => {
|
||||||
try {
|
try {
|
||||||
return await db.apps.toArray()
|
return await db.apps.toArray()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ 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
|
||||||
|
const queue: (() => Promise<void>)[] = []
|
||||||
|
|
||||||
export async function swicRegister() {
|
export async function swicRegister() {
|
||||||
serviceWorkerRegistration.register({
|
serviceWorkerRegistration.register({
|
||||||
@@ -17,14 +18,17 @@ export async function swicRegister() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
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) => {
|
||||||
@@ -57,6 +61,8 @@ export async function swicCall(method: string, ...args: any[]) {
|
|||||||
nextReqId++
|
nextReqId++
|
||||||
|
|
||||||
return new Promise((ok, rej) => {
|
return new Promise((ok, rej) => {
|
||||||
|
|
||||||
|
const call = async () => {
|
||||||
if (!swr || !swr.active) {
|
if (!swr || !swr.active) {
|
||||||
rej(new Error('No active service worker'))
|
rej(new Error('No active service worker'))
|
||||||
return
|
return
|
||||||
@@ -70,6 +76,10 @@ export async function swicCall(method: string, ...args: any[]) {
|
|||||||
}
|
}
|
||||||
console.log('sending to SW', msg)
|
console.log('sending to SW', msg)
|
||||||
swr.active.postMessage(msg)
|
swr.active.postMessage(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (swr && swr.active) call()
|
||||||
|
else queue.push(call)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useParams } from 'react-router'
|
import { useParams } from 'react-router'
|
||||||
import { useAppSelector } from '@/store/hooks/redux'
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
import { selectAppByAppNpub, selectPermsByNpubAndAppNpub } from '@/store'
|
import { selectAppByAppNpub, selectKeys, selectPermsByNpubAndAppNpub } from '@/store'
|
||||||
import { Navigate, useNavigate } from 'react-router-dom'
|
import { Navigate, useNavigate } from 'react-router-dom'
|
||||||
import { formatTimestampDate } from '@/utils/helpers/date'
|
import { formatTimestampDate } from '@/utils/helpers/date'
|
||||||
import { Box, Stack, Typography } from '@mui/material'
|
import { Box, IconButton, Stack, Typography } from '@mui/material'
|
||||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
import { getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/helpers'
|
||||||
import { Button } from '@/shared/Button/Button'
|
import { 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'
|
||||||
@@ -18,28 +18,37 @@ import { IOSBackButton } from '@/shared/IOSBackButton/IOSBackButton'
|
|||||||
import { ModalActivities } from './components/Activities/ModalActivities'
|
import { ModalActivities } from './components/Activities/ModalActivities'
|
||||||
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 MoreIcon from '@mui/icons-material/MoreVertRounded'
|
||||||
|
import { ModalAppDetails } from '@/components/Modal/ModalAppDetails/ModalAppDetails'
|
||||||
|
|
||||||
const AppPage = () => {
|
const AppPage = () => {
|
||||||
|
const keys = useAppSelector(selectKeys)
|
||||||
|
|
||||||
const { appNpub = '', npub = '' } = useParams()
|
const { appNpub = '', npub = '' } = useParams()
|
||||||
|
const currentApp = useAppSelector((state) => selectAppByAppNpub(state, appNpub))
|
||||||
|
const perms = useAppSelector((state) => selectPermsByNpubAndAppNpub(state, npub, appNpub))
|
||||||
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
const perms = useAppSelector((state) => selectPermsByNpubAndAppNpub(state, npub, appNpub))
|
|
||||||
const currentApp = useAppSelector((state) => selectAppByAppNpub(state, appNpub))
|
|
||||||
|
|
||||||
const { open, handleClose, handleShow } = useToggleConfirm()
|
const { open, handleClose, handleShow } = useToggleConfirm()
|
||||||
const { handleOpen: handleOpenModal } = useModalSearchParams()
|
const { handleOpen: handleOpenModal } = useModalSearchParams()
|
||||||
|
|
||||||
const connectPerm = perms.find((perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC)
|
const connectPerm = perms.find((perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC)
|
||||||
|
|
||||||
if (!currentApp) {
|
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||||
|
|
||||||
|
if (!isNpubExists || !currentApp) {
|
||||||
return <Navigate to={`/key/${npub}`} />
|
return <Navigate to={`/key/${npub}`} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const { icon = '', name = '' } = currentApp || {}
|
const { icon = '', name = '', url = '' } = currentApp || {}
|
||||||
const appName = name || getShortenNpub(appNpub)
|
const appDomain = getDomain(url)
|
||||||
const { timestamp } = connectPerm || {}
|
const shortAppNpub = getShortenNpub(appNpub)
|
||||||
|
const appName = name || appDomain || shortAppNpub
|
||||||
|
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||||
|
const isAppNameExists = !!name
|
||||||
|
|
||||||
|
const { timestamp } = connectPerm || {}
|
||||||
const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected'
|
const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected'
|
||||||
|
|
||||||
const handleDeleteApp = async () => {
|
const handleDeleteApp = async () => {
|
||||||
@@ -52,16 +61,30 @@ const AppPage = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleShowAppDetailsModal = () => handleOpenModal(MODAL_PARAMS_KEYS.APP_DETAILS)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack maxHeight={'100%'} overflow={'auto'} alignItems={'flex-start'} height={'100%'}>
|
<Stack maxHeight={'100%'} overflow={'auto'} alignItems={'flex-start'} height={'100%'}>
|
||||||
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
|
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
|
||||||
<Stack marginBottom={'1rem'} direction={'row'} gap={'1rem'} width={'100%'}>
|
<Stack marginBottom={'1rem'} direction={'row'} gap={'1rem'} width={'100%'} alignItems={'center'}>
|
||||||
<StyledAppIcon src={icon} />
|
<StyledAppIcon src={icon}>{appAvatarTitle}</StyledAppIcon>
|
||||||
<Box flex={'1'} overflow={'hidden'}>
|
<Box flex={'1'} overflow={'hidden'}>
|
||||||
|
<Stack direction={'row'} alignItems={'flex-start'} gap={'0.5rem'} marginBottom={'0.5rem'}>
|
||||||
|
<Box display={'flex'} flexDirection={'column'} flex={1}>
|
||||||
<Typography variant="h4" noWrap>
|
<Typography variant="h4" noWrap>
|
||||||
{appName}
|
{appName}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
{isAppNameExists && (
|
||||||
|
<Typography noWrap display={'block'} variant="body1" color={'GrayText'}>
|
||||||
|
{shortAppNpub}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<IconButton onClick={handleShowAppDetailsModal}>
|
||||||
|
<MoreIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
<Typography variant="body2" noWrap>
|
<Typography variant="body2" noWrap>
|
||||||
{connectedOn}
|
{connectedOn}
|
||||||
</Typography>
|
</Typography>
|
||||||
@@ -89,6 +112,7 @@ const AppPage = () => {
|
|||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
/>
|
/>
|
||||||
<ModalActivities appNpub={appNpub} />
|
<ModalActivities appNpub={appNpub} />
|
||||||
|
<ModalAppDetails />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -7,15 +7,17 @@ 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 { 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>
|
||||||
|
|||||||
@@ -2,20 +2,20 @@ import { FC } from 'react'
|
|||||||
import { Box, IconButton, Typography } from '@mui/material'
|
import { Box, IconButton, Typography } from '@mui/material'
|
||||||
import { DbPerm } from '@/modules/db'
|
import { DbPerm } from '@/modules/db'
|
||||||
import { formatTimestampDate } from '@/utils/helpers/date'
|
import { formatTimestampDate } from '@/utils/helpers/date'
|
||||||
import { ACTIONS } from '@/utils/consts'
|
|
||||||
import { StyledPermissionItem } from './styled'
|
import { StyledPermissionItem } from './styled'
|
||||||
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 { ItemPermissionMenu } from './ItemPermissionMenu'
|
import { ItemPermissionMenu } from './ItemPermissionMenu'
|
||||||
import { useOpenMenu } from '@/hooks/useOpenMenu'
|
import { useOpenMenu } from '@/hooks/useOpenMenu'
|
||||||
|
import { getPermActionName } from '@/utils/helpers/helpers'
|
||||||
|
|
||||||
type ItemPermissionProps = {
|
type ItemPermissionProps = {
|
||||||
permission: DbPerm
|
permission: DbPerm
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => {
|
export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => {
|
||||||
const { perm, value, timestamp, id } = permission || {}
|
const { value, timestamp, id } = permission || {}
|
||||||
|
|
||||||
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()
|
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => {
|
|||||||
<StyledPermissionItem>
|
<StyledPermissionItem>
|
||||||
<Box display={'flex'} flexDirection={'column'} gap={'0.5rem'} flex={1}>
|
<Box display={'flex'} flexDirection={'column'} gap={'0.5rem'} flex={1}>
|
||||||
<Typography flex={1} fontWeight={700}>
|
<Typography flex={1} fontWeight={700}>
|
||||||
{ACTIONS[perm] || perm}
|
{getPermActionName(permission)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
|
<Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const StyledAppLogo = styled((props) => (
|
|||||||
<AppLogo />
|
<AppLogo />
|
||||||
</Box>
|
</Box>
|
||||||
))({
|
))({
|
||||||
background: '#00000054',
|
background: '#0d0d0d',
|
||||||
padding: '0.75rem',
|
padding: '0.75rem',
|
||||||
borderRadius: '16px',
|
borderRadius: '16px',
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
|
|||||||
122
src/pages/CreatePage/Create.Page.tsx
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { Stack, Typography } from '@mui/material'
|
||||||
|
import { GetStartedButton, LearnMoreButton } from './styled'
|
||||||
|
import { DOMAIN } from '@/utils/consts'
|
||||||
|
import { useSearchParams } from 'react-router-dom'
|
||||||
|
import { swicCall } from '@/modules/swic'
|
||||||
|
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||||
|
import { ModalConfirmConnect } from '@/components/Modal/ModalConfirmConnect/ModalConfirmConnect'
|
||||||
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { getReferrerAppUrl } from '@/utils/helpers/helpers'
|
||||||
|
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||||
|
|
||||||
|
const CreatePage = () => {
|
||||||
|
const notify = useEnqueueSnackbar()
|
||||||
|
const { handleOpen } = useModalSearchParams()
|
||||||
|
const [created, setCreated] = useState(false)
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
const name = searchParams.get('name') || ''
|
||||||
|
const token = searchParams.get('token') || ''
|
||||||
|
const appNpub = searchParams.get('appNpub') || ''
|
||||||
|
const isValid = name && token && appNpub
|
||||||
|
|
||||||
|
const nip05 = `${name}@${DOMAIN}`
|
||||||
|
|
||||||
|
const handleLearnMore = () => {
|
||||||
|
// @ts-ignore
|
||||||
|
window.open(`https://${DOMAIN}`, '_blank').focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClickAddAccount = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
|
const key: any = await swicCall('generateKey', name)
|
||||||
|
|
||||||
|
const appUrl = getReferrerAppUrl()
|
||||||
|
|
||||||
|
console.log('Created', key.npub, 'app', appUrl)
|
||||||
|
setCreated(true)
|
||||||
|
setIsLoading(false)
|
||||||
|
|
||||||
|
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||||
|
search: {
|
||||||
|
npub: key.npub,
|
||||||
|
appNpub,
|
||||||
|
appUrl,
|
||||||
|
token,
|
||||||
|
// needed for this screen itself
|
||||||
|
name,
|
||||||
|
// will close after all done
|
||||||
|
popup: 'true',
|
||||||
|
},
|
||||||
|
replace: true,
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
notify(error.message || error.toString(), 'error')
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return (
|
||||||
|
<Stack maxHeight={'100%'} overflow={'auto'}>
|
||||||
|
<Typography textAlign={'center'} variant="h6" paddingTop="1em">
|
||||||
|
Bad parameters.
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack maxHeight={'100%'} overflow={'auto'}>
|
||||||
|
{created && (
|
||||||
|
<>
|
||||||
|
<Typography textAlign={'center'} variant="h4" paddingTop="0.5em">
|
||||||
|
Account created!
|
||||||
|
</Typography>
|
||||||
|
<Typography textAlign={'center'} variant="body1" paddingTop="0.5em">
|
||||||
|
User name: <b>{nip05}</b>
|
||||||
|
</Typography>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!created && (
|
||||||
|
<>
|
||||||
|
<Typography textAlign={'center'} variant="h4" paddingTop="0.5em">
|
||||||
|
Welcome to Nostr!
|
||||||
|
</Typography>
|
||||||
|
<Stack gap={'0.5rem'} overflow={'auto'}>
|
||||||
|
<Typography textAlign={'left'} variant="h6" paddingTop="0.5em">
|
||||||
|
Chosen name: <b>{nip05}</b>
|
||||||
|
</Typography>
|
||||||
|
<GetStartedButton onClick={handleClickAddAccount}>
|
||||||
|
Create account {isLoading && <LoadingSpinner />}
|
||||||
|
</GetStartedButton>
|
||||||
|
|
||||||
|
<Typography textAlign={'left'} variant="h5" paddingTop="1em">
|
||||||
|
What you need to know:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<ol style={{ marginLeft: '1em' }}>
|
||||||
|
<li>Nostr accounts are based on cryptographic keys.</li>
|
||||||
|
<li>All your actions on Nostr will be signed by your keys.</li>
|
||||||
|
<li>Nsec.app is one of many services to manage Nostr keys.</li>
|
||||||
|
<li>When you create an account, a new key will be created.</li>
|
||||||
|
<li>This key can later be used with other Nostr websites.</li>
|
||||||
|
</ol>
|
||||||
|
<LearnMoreButton onClick={handleLearnMore}>Learn more</LearnMoreButton>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<ModalConfirmConnect />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CreatePage
|
||||||
26
src/pages/CreatePage/styled.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
||||||
|
import { styled } from '@mui/material'
|
||||||
|
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
||||||
|
import PlayArrowOutlinedIcon from '@mui/icons-material/PlayArrowOutlined'
|
||||||
|
import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'
|
||||||
|
|
||||||
|
export const AddAccountButton = styled((props: AppButtonProps) => (
|
||||||
|
<Button {...props} startIcon={<PersonAddAltRoundedIcon />} />
|
||||||
|
))(() => ({
|
||||||
|
alignSelf: 'center',
|
||||||
|
padding: '0.35rem 1rem',
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const GetStartedButton = styled((props: AppButtonProps) => (
|
||||||
|
<Button {...props} startIcon={<PlayArrowOutlinedIcon />} />
|
||||||
|
))(() => ({
|
||||||
|
alignSelf: 'left',
|
||||||
|
padding: '0.35rem 1rem',
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const LearnMoreButton = styled((props: AppButtonProps) => (
|
||||||
|
<Button {...props} startIcon={<HelpOutlineOutlinedIcon />} />
|
||||||
|
))(() => ({
|
||||||
|
alignSelf: 'left',
|
||||||
|
padding: '0.35rem 1rem',
|
||||||
|
}))
|
||||||
@@ -18,7 +18,7 @@ const HomePage = () => {
|
|||||||
|
|
||||||
const handleLearnMore = () => {
|
const handleLearnMore = () => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.open(`https://info.${DOMAIN}`, '_blank').focus()
|
window.open(`https://${DOMAIN}`, '_blank').focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { DbKey } from '../../../modules/db'
|
import { DbKey } from '../../../modules/db'
|
||||||
import { Avatar, Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material'
|
import { Avatar, Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material'
|
||||||
import { getShortenNpub } from '../../../utils/helpers/helpers'
|
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import { useProfile } from '@/hooks/useProfile'
|
||||||
|
|
||||||
type ItemKeyProps = DbKey
|
type ItemKeyProps = DbKey
|
||||||
|
|
||||||
export const ItemKey: FC<ItemKeyProps> = (props) => {
|
export const ItemKey: FC<ItemKeyProps> = (props) => {
|
||||||
const { npub, profile } = props
|
const { npub } = props
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||||
|
|
||||||
const handleNavigate = () => {
|
const handleNavigate = () => {
|
||||||
navigate('/key/' + npub)
|
navigate('/key/' + npub)
|
||||||
}
|
}
|
||||||
const { name = '', picture = '' } = profile?.info || {}
|
|
||||||
const userName = name || getShortenNpub(npub)
|
|
||||||
const userAvatar = picture || ''
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledKeyContainer onClick={handleNavigate}>
|
<StyledKeyContainer onClick={handleNavigate}>
|
||||||
<Stack direction={'row'} alignItems={'center'} gap="1rem">
|
<Stack direction={'row'} alignItems={'center'} gap="1rem">
|
||||||
<Avatar src={userAvatar} alt={userName} />
|
<Avatar src={userAvatar} alt={userName}>
|
||||||
|
{avatarTitle}
|
||||||
|
</Avatar>
|
||||||
<StyledText variant="body1">{userName}</StyledText>
|
<StyledText variant="body1">{userName}</StyledText>
|
||||||
</Stack>
|
</Stack>
|
||||||
</StyledKeyContainer>
|
</StyledKeyContainer>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useAppSelector } from '../../store/hooks/redux'
|
import { useAppSelector } from '../../store/hooks/redux'
|
||||||
import { useParams } from 'react-router-dom'
|
import { Navigate, useParams, useSearchParams } from 'react-router-dom'
|
||||||
import { Stack } from '@mui/material'
|
import { Stack } from '@mui/material'
|
||||||
import { StyledIconButton } from './styled'
|
import { StyledIconButton } from './styled'
|
||||||
import { SettingsIcon, ShareIcon } from '@/assets'
|
import { SettingsIcon, ShareIcon } from '@/assets'
|
||||||
@@ -11,7 +11,6 @@ import { ModalSettings } from '@/components/Modal/ModalSettings/ModalSettings'
|
|||||||
import { ModalExplanation } from '@/components/Modal/ModalExplanation/ModalExplanation'
|
import { ModalExplanation } from '@/components/Modal/ModalExplanation/ModalExplanation'
|
||||||
import { ModalConfirmConnect } from '@/components/Modal/ModalConfirmConnect/ModalConfirmConnect'
|
import { ModalConfirmConnect } from '@/components/Modal/ModalConfirmConnect/ModalConfirmConnect'
|
||||||
import { ModalConfirmEvent } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent'
|
import { ModalConfirmEvent } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent'
|
||||||
import { useProfile } from './hooks/useProfile'
|
|
||||||
import { useBackgroundSigning } from './hooks/useBackgroundSigning'
|
import { useBackgroundSigning } from './hooks/useBackgroundSigning'
|
||||||
import { BackgroundSigningWarning } from './components/BackgroundSigningWarning'
|
import { BackgroundSigningWarning } from './components/BackgroundSigningWarning'
|
||||||
import UserValueSection from './components/UserValueSection'
|
import UserValueSection from './components/UserValueSection'
|
||||||
@@ -19,29 +18,47 @@ 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, useState } from 'react'
|
||||||
|
|
||||||
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 isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
|
const [searchParams] = useSearchParams()
|
||||||
|
|
||||||
|
const [isCheckingSync, setIsChecking] = useState(true)
|
||||||
|
const handleStopChecking = () => setIsChecking(false)
|
||||||
|
|
||||||
|
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub, handleStopChecking), [npub], false)
|
||||||
|
|
||||||
const { handleOpen } = useModalSearchParams()
|
const { handleOpen } = useModalSearchParams()
|
||||||
|
|
||||||
// const { userNameWithPrefix } = useProfile(npub)
|
|
||||||
const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning()
|
const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning()
|
||||||
|
|
||||||
const key = keys.find((k) => k.npub === npub)
|
const key = keys.find((k) => k.npub === npub)
|
||||||
let username = ''
|
|
||||||
if (key?.name) {
|
const getUsername = useCallback(() => {
|
||||||
if (key.name.includes('@')) username = key.name
|
if (!key || !key?.name) return ''
|
||||||
else username = `${key?.name}@${DOMAIN}`
|
if (key.name.includes('@')) return key.name
|
||||||
}
|
return `${key?.name}@${DOMAIN}`
|
||||||
|
}, [key])
|
||||||
|
const username = getUsername()
|
||||||
|
|
||||||
const filteredApps = apps.filter((a) => a.npub === npub)
|
const filteredApps = apps.filter((a) => a.npub === npub)
|
||||||
const { prepareEventPendings } = useTriggerConfirmModal(npub, pending, perms)
|
const { prepareEventPendings } = useTriggerConfirmModal(npub, pending, perms)
|
||||||
|
|
||||||
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
const isKeyExists = npub.trim().length && key
|
||||||
|
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`} />
|
||||||
|
|
||||||
|
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||||
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -54,7 +71,7 @@ const KeyPage = () => {
|
|||||||
title="Your login"
|
title="Your login"
|
||||||
value={username}
|
value={username}
|
||||||
copyValue={username}
|
copyValue={username}
|
||||||
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
explanationType={EXPLANATION_MODAL_KEYS.LOGIN}
|
||||||
/>
|
/>
|
||||||
<UserValueSection
|
<UserValueSection
|
||||||
title="Your NPUB"
|
title="Your NPUB"
|
||||||
@@ -69,7 +86,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>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ type AppsProps = {
|
|||||||
npub: string
|
npub: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Apps: FC<AppsProps> = ({ apps = [], npub = '' }) => {
|
export const Apps: FC<AppsProps> = ({ apps = [] }) => {
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@@ -26,18 +26,22 @@ export const Apps: FC<AppsProps> = ({ apps = [], npub = '' }) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const openAppStore = () => {
|
||||||
|
window.open('https://nostrapp.link', '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flex={1} marginBottom={'1rem'} display={'flex'} flexDirection={'column'} overflow={'auto'}>
|
<Box flex={1} marginBottom={'1rem'} display={'flex'} flexDirection={'column'} overflow={'auto'}>
|
||||||
<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" />
|
<AppLink title="Discover Apps" onClick={openAppStore} />
|
||||||
</Stack>
|
</Stack>
|
||||||
{!apps.length && (
|
{!apps.length && (
|
||||||
<StyledEmptyAppsBox>
|
<StyledEmptyAppsBox>
|
||||||
<Typography className="message" variant="h5" fontWeight={600} textAlign={'center'}>
|
<Typography className="message" variant="h5" fontWeight={600} textAlign={'center'}>
|
||||||
No connected apps
|
No connected apps
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button>Discover Nostr Apps</Button>
|
<Button onClick={openAppStore}>Discover Nostr Apps</Button>
|
||||||
</StyledEmptyAppsBox>
|
</StyledEmptyAppsBox>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,19 @@ import { DbApp } from '@/modules/db'
|
|||||||
import { Avatar, Stack, Typography } from '@mui/material'
|
import { Avatar, Stack, Typography } from '@mui/material'
|
||||||
import { FC } from 'react'
|
import { FC } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
// import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'
|
import { getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/helpers'
|
||||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
|
||||||
import { StyledItemAppContainer } from './styled'
|
import { StyledItemAppContainer } from './styled'
|
||||||
|
|
||||||
type ItemAppProps = DbApp
|
type ItemAppProps = DbApp
|
||||||
|
|
||||||
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => {
|
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name, url }) => {
|
||||||
const appName = name || getShortenNpub(appNpub)
|
const appDomain = getDomain(url)
|
||||||
|
const shortAppNpub = getShortenNpub(appNpub)
|
||||||
|
const appName = name || appDomain || shortAppNpub
|
||||||
|
const appIcon = icon || `https://${appDomain}/favicon.ico`
|
||||||
|
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||||
|
const isAppNameExists = !!name
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledItemAppContainer
|
<StyledItemAppContainer
|
||||||
direction={'row'}
|
direction={'row'}
|
||||||
@@ -19,11 +24,18 @@ export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => {
|
|||||||
component={Link}
|
component={Link}
|
||||||
to={`/key/${npub}/app/${appNpub}`}
|
to={`/key/${npub}/app/${appNpub}`}
|
||||||
>
|
>
|
||||||
<Avatar variant="square" sx={{ width: 56, height: 56 }} src={icon} alt={name} />
|
<Avatar variant="rounded" sx={{ width: 56, height: 56 }} src={appIcon} alt={appName}>
|
||||||
|
{appAvatarTitle}
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Input, InputProps } from '@/shared/Input/Input'
|
import { Input, AppInputProps } from '@/shared/Input/Input'
|
||||||
import { Stack, StackProps, styled } from '@mui/material'
|
import { Stack, StackProps, styled } from '@mui/material'
|
||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
|
|
||||||
export const StyledInput = styled(
|
export const StyledInput = styled(
|
||||||
forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
|
forwardRef<HTMLInputElement, AppInputProps>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react'
|
|
||||||
import { fetchProfile } from '@/modules/nostr'
|
|
||||||
import { MetaEvent } from '@/types/meta-event'
|
|
||||||
import { getProfileUsername } from '@/utils/helpers/helpers'
|
|
||||||
import { DOMAIN } from '@/utils/consts'
|
|
||||||
|
|
||||||
export const useProfile = (npub: string) => {
|
|
||||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
|
||||||
|
|
||||||
const userName = getProfileUsername(profile, npub)
|
|
||||||
// FIXME use nip05?
|
|
||||||
const userNameWithPrefix = userName + '@' + DOMAIN
|
|
||||||
|
|
||||||
const loadProfile = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetchProfile(npub)
|
|
||||||
setProfile(response)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to fetch profile:', error)
|
|
||||||
}
|
|
||||||
}, [npub])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadProfile()
|
|
||||||
}, [loadProfile])
|
|
||||||
|
|
||||||
return {
|
|
||||||
profile,
|
|
||||||
userNameWithPrefix,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@ 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 } 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,6 +19,9 @@ 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)
|
||||||
|
|
||||||
@@ -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,4 @@
|
|||||||
import { Input, InputProps } from '@/shared/Input/Input'
|
import { Input, AppInputProps } from '@/shared/Input/Input'
|
||||||
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,12 +48,13 @@ 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%',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
export const StyledInput = styled(
|
export const StyledInput = styled(
|
||||||
forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
|
forwardRef<HTMLInputElement, AppInputProps>(({ className, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<Input
|
<Input
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -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,9 +1,9 @@
|
|||||||
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 HomePage from '../pages/HomePage/Home.Page'
|
||||||
import WelcomePage from '../pages/Welcome.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'))
|
const KeyPage = lazy(() => import('../pages/KeyPage/Key.Page'))
|
||||||
const ConfirmPage = lazy(() => import('../pages/Confirm.Page'))
|
const ConfirmPage = lazy(() => import('../pages/Confirm.Page'))
|
||||||
@@ -26,6 +26,7 @@ const AppRoutes = () => {
|
|||||||
<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 />} />
|
||||||
<Route path="/key/:npub/:req_id" element={<ConfirmPage />} />
|
<Route path="/key/:npub/:req_id" element={<ConfirmPage />} />
|
||||||
|
<Route path="/create" element={<CreatePage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<Navigate to={'/home'} />} />
|
<Route path="*" element={<Navigate to={'/home'} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ export type AppButtonProps = MuiButtonProps & {
|
|||||||
|
|
||||||
export const Button = forwardRef<HTMLButtonElement, AppButtonProps>(({ children, ...restProps }, ref) => {
|
export const Button = forwardRef<HTMLButtonElement, AppButtonProps>(({ children, ...restProps }, ref) => {
|
||||||
return (
|
return (
|
||||||
<StyledButton classes={{ root: 'button' }} {...restProps} ref={ref}>
|
<StyledButton classes={{ root: 'button', disabled: 'disabled' }} {...restProps} ref={ref}>
|
||||||
{children}
|
{children}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
)
|
)
|
||||||
@@ -27,6 +27,10 @@ const StyledButton = styled(
|
|||||||
background: theme.palette.backgroundSecondary.default,
|
background: theme.palette.backgroundSecondary.default,
|
||||||
},
|
},
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
|
'&.disabled': {
|
||||||
|
background: `${theme.palette.backgroundSecondary.default}50`,
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
@@ -35,11 +39,10 @@ const StyledButton = styled(
|
|||||||
background: theme.palette.primary.main,
|
background: theme.palette.primary.main,
|
||||||
},
|
},
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
':disabled': {
|
'&.disabled': {
|
||||||
'&.button:is(:hover, :active, &)': {
|
color: theme.palette.text.secondary,
|
||||||
background: theme.palette.backgroundSecondary.default,
|
background: `${theme.palette.primary.main}50`,
|
||||||
},
|
cursor: 'not-allowed',
|
||||||
color: theme.palette.backgroundSecondary.paper,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { forwardRef, useRef } from 'react'
|
import { useRef } from 'react'
|
||||||
import { Input, InputProps } from '../Input/Input'
|
import { Input, AppInputProps } from '../Input/Input'
|
||||||
|
|
||||||
export type DebounceProps = {
|
export type DebounceProps = {
|
||||||
handleDebounce: (value: string) => void
|
handleDebounce: (value: string) => void
|
||||||
debounceTimeout: number
|
debounceTimeout: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DebounceInput = (props: InputProps & DebounceProps) => {
|
export const DebounceInput = (props: AppInputProps & DebounceProps) => {
|
||||||
const { handleDebounce, debounceTimeout, ...rest } = props
|
const { handleDebounce, debounceTimeout, ...rest } = props
|
||||||
|
|
||||||
const timerRef = useRef<number>()
|
const timerRef = useRef<number>()
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import {
|
|||||||
styled,
|
styled,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
|
|
||||||
export type InputProps = InputBaseProps & {
|
export type AppInputProps = InputBaseProps & {
|
||||||
helperText?: string | ReactNode
|
helperText?: string | ReactNode
|
||||||
helperTextProps?: FormHelperTextProps
|
helperTextProps?: FormHelperTextProps
|
||||||
containerProps?: BoxProps
|
containerProps?: BoxProps
|
||||||
label?: string
|
label?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
export const Input = forwardRef<HTMLInputElement, AppInputProps>(
|
||||||
({ helperText, containerProps, helperTextProps, label, ...props }, ref) => {
|
({ helperText, containerProps, helperTextProps, label, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<StyledInputContainer {...containerProps}>
|
<StyledInputContainer {...containerProps}>
|
||||||
@@ -26,7 +26,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
{label}
|
{label}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
) : null}
|
) : null}
|
||||||
<InputBase className="input" {...props} classes={{ error: 'error' }} inputRef={ref} />
|
<InputBase autoComplete="off" className="input" {...props} classes={{ error: 'error' }} ref={ref} />
|
||||||
{helperText ? (
|
{helperText ? (
|
||||||
<FormHelperText {...helperTextProps} className="helper_text">
|
<FormHelperText {...helperTextProps} className="helper_text">
|
||||||
{helperText}
|
{helperText}
|
||||||
|
|||||||
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,
|
||||||
|
}))
|
||||||
@@ -20,7 +20,7 @@ const Transition = forwardRef(function Transition(
|
|||||||
|
|
||||||
export const Modal: FC<ModalProps> = ({ children, title, onClose, withCloseButton = true, fixedHeight, ...props }) => {
|
export const Modal: FC<ModalProps> = ({ children, title, onClose, withCloseButton = true, fixedHeight, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<StyledDialog fixedHeight={fixedHeight} {...props} onClose={onClose} TransitionComponent={Transition}>
|
<StyledDialog fixedheight={fixedHeight} {...props} onClose={onClose} TransitionComponent={Transition}>
|
||||||
{withCloseButton && (
|
{withCloseButton && (
|
||||||
<StyledCloseButtonWrapper>
|
<StyledCloseButtonWrapper>
|
||||||
<IconButton onClick={() => onClose && onClose({}, 'backdropClick')} className="close_btn">
|
<IconButton onClick={() => onClose && onClose({}, 'backdropClick')} className="close_btn">
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
styled,
|
styled,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
|
|
||||||
export const StyledDialog = styled((props: DialogProps & { fixedHeight?: string }) => (
|
export const StyledDialog = styled((props: DialogProps & { fixedheight?: string }) => (
|
||||||
<Dialog
|
<Dialog
|
||||||
{...props}
|
{...props}
|
||||||
classes={{
|
classes={{
|
||||||
@@ -26,8 +26,8 @@ export const StyledDialog = styled((props: DialogProps & { fixedHeight?: string
|
|||||||
}}
|
}}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
))(({ theme, fixedHeight = '' }) => {
|
))(({ theme, fixedheight = '' }) => {
|
||||||
const fixedHeightStyles = fixedHeight ? { height: fixedHeight } : {}
|
const fixedHeightStyles = fixedheight ? { height: fixedheight } : {}
|
||||||
return {
|
return {
|
||||||
'& .container': {
|
'& .container': {
|
||||||
alignItems: 'flex-end',
|
alignItems: 'flex-end',
|
||||||
|
|||||||
@@ -43,6 +43,11 @@ export type RootState = ReturnType<typeof store.getState>
|
|||||||
export type AppDispatch = typeof store.dispatch
|
export type AppDispatch = typeof store.dispatch
|
||||||
|
|
||||||
export const selectKeys = (state: RootState) => state.content.keys
|
export const selectKeys = (state: RootState) => state.content.keys
|
||||||
|
export const selectApps = (state: RootState) => state.content.apps
|
||||||
|
|
||||||
|
export const selectKeyByNpub = (state: RootState, npub: string) => {
|
||||||
|
return state.content.keys.find((key) => key.npub === npub)
|
||||||
|
}
|
||||||
|
|
||||||
export const selectAppsByNpub = memoizeOne((state: RootState, npub: string) => {
|
export const selectAppsByNpub = memoizeOne((state: RootState, npub: string) => {
|
||||||
return state.content.apps.filter((app) => app.npub === npub)
|
return state.content.apps.filter((app) => app.npub === npub)
|
||||||
|
|||||||
@@ -9,9 +9,11 @@ export enum MODAL_PARAMS_KEYS {
|
|||||||
CONFIRM_CONNECT = 'confirm-connect',
|
CONFIRM_CONNECT = 'confirm-connect',
|
||||||
CONFIRM_EVENT = 'confirm-event',
|
CONFIRM_EVENT = 'confirm-event',
|
||||||
ACTIVITY = 'activity',
|
ACTIVITY = 'activity',
|
||||||
|
APP_DETAILS = 'app-details',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EXPLANATION_MODAL_KEYS {
|
export enum EXPLANATION_MODAL_KEYS {
|
||||||
BUNKER = 'bunker',
|
BUNKER = 'bunker',
|
||||||
NPUB = 'npub',
|
NPUB = 'npub',
|
||||||
|
LOGIN = 'login',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export enum ACTION_TYPE {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ACTIONS: { [type: string]: string } = {
|
export const ACTIONS: { [type: string]: string } = {
|
||||||
|
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',
|
||||||
|
|||||||
@@ -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, "HH:mm',' dd-MM-yyyy")
|
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 { ACTION_TYPE, NIP46_RELAYS } from '../consts'
|
import { ACTIONS, ACTION_TYPE, DOMAIN, NIP46_RELAYS, NOAUTHD_URL } from '../consts'
|
||||||
import { DbPending } 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) {
|
||||||
@@ -15,14 +15,9 @@ export const getShortenNpub = (npub = '') => {
|
|||||||
return npub.substring(0, 10) + '...' + npub.slice(-4)
|
return npub.substring(0, 10) + '...' + npub.slice(-4)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAppIconTitle = (name: string | undefined, appNpub: string) => {
|
export const getProfileUsername = (profile: MetaEvent | null) => {
|
||||||
return name
|
if (!profile) return null
|
||||||
? name[0].toLocaleUpperCase()
|
return profile?.info?.name || profile?.info?.display_name
|
||||||
: appNpub.substring(4, 7);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getProfileUsername = (profile: MetaEvent | null, npub: string) => {
|
|
||||||
return profile?.info?.name || profile?.info?.display_name || getShortenNpub(npub)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBunkerLink = (npub = '') => {
|
export const getBunkerLink = (npub = '') => {
|
||||||
@@ -85,7 +80,7 @@ export function isPackagePerm(perm: string, reqPerm: string) {
|
|||||||
|
|
||||||
export async function fetchNip05(value: string, origin?: string) {
|
export async function fetchNip05(value: string, origin?: string) {
|
||||||
try {
|
try {
|
||||||
const [username, domain] = value.split('@')
|
const [username, domain] = value.toLocaleLowerCase().split('@')
|
||||||
if (!origin) origin = `https://${domain}`
|
if (!origin) origin = `https://${domain}`
|
||||||
const response = await fetch(`${origin}/.well-known/nostr.json?name=${username}`)
|
const response = await fetch(`${origin}/.well-known/nostr.json?name=${username}`)
|
||||||
const getNpub: {
|
const getNpub: {
|
||||||
@@ -101,3 +96,63 @@ export async function fetchNip05(value: string, origin?: string) {
|
|||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
try {
|
||||||
|
return new URL(url).hostname
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
return name ? name[0].toLocaleUpperCase() : appNpub.substring(4, 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReqActionName(req: DbPending | DbHistory) {
|
||||||
|
const action = ACTIONS[req.method]
|
||||||
|
if (req.method === 'sign_event') {
|
||||||
|
const kind = getSignReqKind(req)
|
||||||
|
if (kind !== undefined) return `${action} of kind ${kind}`
|
||||||
|
}
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPermActionName(req: DbPerm) {
|
||||||
|
const method = req.perm.split(':')[0]
|
||||||
|
const action = ACTIONS[method]
|
||||||
|
if (method === 'sign_event') {
|
||||||
|
const kind = req.perm.split(':')[1]
|
||||||
|
if (kind !== undefined) return `${action} of kind ${kind}`
|
||||||
|
}
|
||||||
|
return action
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isEmptyString = (str = '') => {
|
||||||
|
return str.trim().length === 0
|
||||||
|
}
|
||||||
|
|||||||
@@ -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": ".",
|
||||||
|
|||||||