Compare commits
6 Commits
feature/pa
...
refactor/e
Author | SHA1 | Date | |
---|---|---|---|
32c097c1ee | |||
8b349c0350 | |||
1a9dc0da82 | |||
676eaf6191 | |||
97c3bcc16d | |||
a5f7bf2a58 |
23
README
23
README
@ -1,23 +0,0 @@
|
||||
Noauth - Nostr key manager
|
||||
--------------------------
|
||||
|
||||
THIS IS BETA SOFTWARE, DON'T USE WITH REAL KEYS!
|
||||
|
||||
This is a web-based nostr signer app, it uses nip46 signer
|
||||
running inside a service worker, if SW is not running -
|
||||
a noauthd server sends a push message and wakes SW up. Also,
|
||||
keys can be saved to server and fetched later in an end-to-end
|
||||
encrypted way. Keys are encrypted with user-defined password,
|
||||
a good key derivation function is used to resist brute force.
|
||||
|
||||
This app works in Chrome on desktop and Android out of the box,
|
||||
try it with snort.social (use bunker:/... string as 'login string').
|
||||
|
||||
On iOS web push notifications are still experimental, eventually
|
||||
it will work on iOS out of the box too.
|
||||
|
||||
It works across devices, but that's unreliable, especially if
|
||||
signer is on mobile - if smartphone is locked then service worker might
|
||||
not wake up. Thanks to cloud sync/recovery of keys users can import
|
||||
their keys into this app on every device and then it works well.
|
||||
|
95
README.md
Normal file
95
README.md
Normal file
@ -0,0 +1,95 @@
|
||||
Noauth - Nostr key manager
|
||||
--------------------------
|
||||
|
||||
Nsec.app is a web app to store your Nostr keys
|
||||
and provide remote access to keys using nip46.
|
||||
|
||||
Features:
|
||||
- non-custodial store for your keys
|
||||
- can store many keys
|
||||
- provides nip46 access to apps
|
||||
- permission management for connected apps
|
||||
- works in any browser or platform
|
||||
- background operation even if app tab is closed
|
||||
- cloud e2ee sync for your keys
|
||||
- support for OAuth-like signin flow
|
||||
|
||||
|
||||
How it works
|
||||
------------
|
||||
|
||||
This is a web-based nostr signer app, it uses nip46 signer
|
||||
running inside a service worker, if SW is not running -
|
||||
a noauthd server sends a push message and wakes SW up. Also,
|
||||
keys can be saved to server and fetched later in an end-to-end
|
||||
encrypted way. Keys are encrypted with user-defined password,
|
||||
a good key derivation function is used to resist brute force.
|
||||
|
||||
It works across devices, but that's unreliable, especially if
|
||||
signer is on mobile - if your phone is locked then service worker might
|
||||
not wake up. Thanks to cloud sync/recovery of keys users can import
|
||||
their keys into this app on every device and then it works well.
|
||||
|
||||
How to self-host
|
||||
----------------
|
||||
|
||||
This app is non-custodial, so there isn't much need for
|
||||
self-hosting. However, if you'd like to run your own version of
|
||||
it, here is how to do it:
|
||||
|
||||
Create web push keys (https://github.com/web-push-libs/web-push):
|
||||
```
|
||||
npm install web-push;
|
||||
web-push generate-vapid-keys --json
|
||||
```
|
||||
|
||||
Edit .end in noauth:
|
||||
```
|
||||
REACT_APP_WEB_PUSH_PUBKEY=web push public key,
|
||||
REACT_APP_NOAUTHD_URL=address of the noauthd server (see below)
|
||||
REACT_APP_DOMAIN=domain name of your bunker (i.e. nsec.app)
|
||||
REACT_APP_RELAY=relay that you'll use, can use wss://relay.nsec.app - don't use public general-purpose relays, you'll hit rate limits very fast
|
||||
```
|
||||
|
||||
Then do:
|
||||
```
|
||||
npm install;
|
||||
npm run build;
|
||||
```
|
||||
The app is in the `build` folder.
|
||||
|
||||
To run the noauthd server (https://github.com/nostrband/noauthd),
|
||||
edit .env in noauthd:
|
||||
```
|
||||
PUSH_PUBKEY=web push public key, same as above
|
||||
PUSH_SECRET=web push private key that you generated above
|
||||
ORIGIN=address of the server itself, like http://localhost:8000
|
||||
DATABASE_URL="file:./prod.db"
|
||||
BUNKER_NSEC=nsec of the bunker (needed for create_account methods)
|
||||
BUNKER_RELAY="wss://relay.nsec.app" - same as above
|
||||
BUNKER_DOMAIN="nsec.app" - same as above
|
||||
BUNKER_ORIGIN=where noauth is hosted
|
||||
```
|
||||
|
||||
Then init the database and launch:
|
||||
```
|
||||
npx prisma migrate deploy
|
||||
node -r dotenv/config src/index.js dotenv_config_path=.env
|
||||
```
|
||||
|
||||
TODO
|
||||
----
|
||||
|
||||
- Show details of requested operations
|
||||
- Publish a profile for new sign ups
|
||||
- Sync processed reqs across devices
|
||||
- Sync connected apps and perms across devices
|
||||
- Sync app activity across devices
|
||||
- Group apps by domain
|
||||
- Encrypt local nsec in Safari
|
||||
- Add WebAuthn to the mix
|
||||
- Add LN address to new profiles
|
||||
- Confirm relay/contact list pruning requests
|
||||
- Transfer/change nip05 name
|
||||
- Better notifs with activity summaries
|
||||
- How to send auth_url to new device if all other devices are down?
|
@ -5,8 +5,6 @@ import { useAppDispatch } from './store/hooks/redux'
|
||||
import { setApps, setKeys, setPending, setPerms } from './store/reducers/content.slice'
|
||||
import AppRoutes from './routes/AppRoutes'
|
||||
import { fetchProfile, ndk } from './modules/nostr'
|
||||
import { useModalSearchParams } from './hooks/useModalSearchParams'
|
||||
import { MODAL_PARAMS_KEYS } from './types/modal'
|
||||
import { ModalInitial } from './components/Modal/ModalInitial/ModalInitial'
|
||||
import { ModalImportKeys } from './components/Modal/ModalImportKeys/ModalImportKeys'
|
||||
import { ModalSignUp } from './components/Modal/ModalSignUp/ModalSignUp'
|
||||
@ -14,7 +12,6 @@ import { ModalLogin } from './components/Modal/ModalLogin/ModalLogin'
|
||||
|
||||
function App() {
|
||||
const [render, setRender] = useState(0)
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const [isConnected, setIsConnected] = useState(false)
|
||||
|
@ -3,7 +3,7 @@ import { Button } from '@/shared/Button/Button'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Autocomplete, CircularProgress, Stack, Typography } from '@mui/material'
|
||||
import { Autocomplete, Stack, Typography } from '@mui/material'
|
||||
import { StyledInput } from './styled'
|
||||
import { FormEvent, useEffect, useState } from 'react'
|
||||
import { isEmptyString } from '@/utils/helpers/helpers'
|
||||
@ -13,6 +13,7 @@ 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()
|
||||
@ -118,7 +119,7 @@ export const ModalAppDetails = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const isFormValid = !isEmptyString(url) && !isEmptyString(name) && !isEmptyString(icon)
|
||||
const isFormValid = !isEmptyString(url) && !isEmptyString(name)
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
@ -165,7 +166,7 @@ export const ModalAppDetails = () => {
|
||||
/>
|
||||
|
||||
<Button varianttype="secondary" type="submit" fullWidth disabled={!isFormValid || isLoading}>
|
||||
Save changes {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
Save changes {isLoading && <LoadingSpinner />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
@ -57,7 +57,7 @@ export const ModalConfirmConnect = () => {
|
||||
// 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});
|
||||
// console.log("pending", {isModalOpened, isPendingReqIdExists, isNpubExists, /*isAppNpubExists,*/ pendingReqId, pending});
|
||||
if (!isPopup && isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) {
|
||||
closeModalAfterRequest()
|
||||
return null
|
||||
|
@ -61,10 +61,23 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
||||
},
|
||||
})
|
||||
|
||||
// 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)
|
||||
|
||||
if (isModalOpened && (!isNpubExists || !isAppNpubExists)) {
|
||||
// console.log("confirm event", { confirmEventReqs, isModalOpened, isNpubExists, isAppNpubExists });
|
||||
if (isModalOpened && (!currentAppPendingReqs.length || !isNpubExists || !isAppNpubExists)) {
|
||||
closeModalAfterRequest()
|
||||
return null
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ 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 { CircularProgress, Stack, Typography, useTheme } from '@mui/material'
|
||||
import { Stack, Typography, useTheme } from '@mui/material'
|
||||
import { StyledAppLogo } from './styled'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useForm } from 'react-hook-form'
|
||||
@ -18,6 +18,7 @@ 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: '',
|
||||
@ -69,12 +70,13 @@ export const ModalImportKeys = () => {
|
||||
}
|
||||
try {
|
||||
const { type, data } = nip19.decode(debouncedNsec)
|
||||
const ok = type === 'nsec';
|
||||
const ok = type === 'nsec'
|
||||
setIsBadNsec(!ok)
|
||||
if (ok) {
|
||||
const npub = nip19.npubEncode(
|
||||
// @ts-ignore
|
||||
getPublicKey(data))
|
||||
getPublicKey(data)
|
||||
)
|
||||
setIsTakenByNsec(!!nameNpub && nameNpub === npub)
|
||||
} else {
|
||||
setIsTakenByNsec(false)
|
||||
@ -84,7 +86,8 @@ export const ModalImportKeys = () => {
|
||||
setIsTakenByNsec(false)
|
||||
return
|
||||
}
|
||||
}, [debouncedNsec])
|
||||
// eslint-disable-next-line
|
||||
}, [debouncedNsec])
|
||||
|
||||
useEffect(() => {
|
||||
checkNsecUsername()
|
||||
@ -106,8 +109,8 @@ export const ModalImportKeys = () => {
|
||||
if (isLoading) return undefined
|
||||
try {
|
||||
const { nsec, username } = values
|
||||
if (!nsec || !username) throw new Error("Enter username and nsec")
|
||||
if (nameNpub && !isTakenByNsec) throw new Error("Name taken")
|
||||
if (!nsec || !username) throw new Error('Enter username and nsec')
|
||||
if (nameNpub && !isTakenByNsec) throw new Error('Name taken')
|
||||
setIsLoading(true)
|
||||
const k: any = await swicCall('importKey', username, nsec)
|
||||
notify('Key imported!', 'success')
|
||||
@ -186,16 +189,14 @@ export const ModalImportKeys = () => {
|
||||
helperTextProps={{
|
||||
sx: {
|
||||
'&.helper_text': {
|
||||
color: isBadNsec
|
||||
? theme.palette.error.main
|
||||
: theme.palette.textSecondaryDecorate.main,
|
||||
color: isBadNsec ? theme.palette.error.main : theme.palette.textSecondaryDecorate.main,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
Import key {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
Import key {isLoading && <LoadingSpinner />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
@ -4,7 +4,7 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { CircularProgress, Stack, Typography } from '@mui/material'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
import { StyledAppLogo } from './styled'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
@ -16,6 +16,7 @@ import { DOMAIN } from '@/utils/consts'
|
||||
import { fetchNip05 } 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: '',
|
||||
@ -120,7 +121,7 @@ export const ModalLogin = () => {
|
||||
error={!!errors.password}
|
||||
/>
|
||||
<Button type="submit" fullWidth disabled={isLoading}>
|
||||
Add account {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
Add account {isLoading && <LoadingSpinner />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Box, CircularProgress, Stack, Typography } from '@mui/material'
|
||||
import { Box, Stack, Typography } from '@mui/material'
|
||||
import { StyledButton, StyledSettingContainer, StyledSynchedText } from './styled'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { CheckmarkIcon } from '@/assets'
|
||||
@ -17,6 +16,7 @@ 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 = {
|
||||
isSynced: boolean
|
||||
@ -116,15 +116,7 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||
{...inputProps}
|
||||
onChange={handlePasswordChange}
|
||||
value={enteredPassword}
|
||||
// helperText={isPasswordInvalid ? 'Invalid password' : ''}
|
||||
placeholder="Enter a password"
|
||||
// helperTextProps={{
|
||||
// sx: {
|
||||
// '&.helper_text': {
|
||||
// color: 'red',
|
||||
// },
|
||||
// },
|
||||
// }}
|
||||
disabled={!isChecked}
|
||||
/>
|
||||
{isPasswordInvalid ? (
|
||||
@ -150,10 +142,9 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||
</Typography>
|
||||
)}
|
||||
<StyledButton type="submit" fullWidth disabled={!isChecked}>
|
||||
Sync {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
Sync {isLoading && <LoadingSpinner mode="secondary" />}
|
||||
</StyledButton>
|
||||
</StyledSettingContainer>
|
||||
{/* <Button onClick={onClose}>Done</Button> */}
|
||||
</Stack>
|
||||
</Modal>
|
||||
)
|
||||
|
@ -2,7 +2,7 @@ import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { CircularProgress, Stack, Typography, useTheme } from '@mui/material'
|
||||
import { Stack, Typography, useTheme } from '@mui/material'
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react'
|
||||
import { StyledAppLogo } from './styled'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
@ -12,6 +12,7 @@ import { swicCall } from '@/modules/swic'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||
|
||||
export const ModalSignUp = () => {
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
@ -110,7 +111,7 @@ export const ModalSignUp = () => {
|
||||
}}
|
||||
/>
|
||||
<Button fullWidth type="submit" disabled={isLoading}>
|
||||
Create account {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
Create account {isLoading && <LoadingSpinner />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
@ -23,7 +23,7 @@ export const Header = () => {
|
||||
}
|
||||
|
||||
const isDarkMode = themeMode === 'dark'
|
||||
const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#000" />
|
||||
const themeIcon = isDarkMode ? <LightModeIcon htmlColor="#fff" /> : <DarkModeIcon htmlColor="#000" />
|
||||
|
||||
const handleChangeMode = () => {
|
||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
||||
|
@ -743,9 +743,9 @@ export class NoauthBackend {
|
||||
})
|
||||
|
||||
// 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}?popup=true`
|
||||
console.log('sending authUrl', authUrl, 'for', req)
|
||||
// NOTE: if you set 'Update on reload' in the Chrome SW console
|
||||
// then this message will cause a new tab opened by the peer,
|
||||
|
@ -9,6 +9,7 @@ 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()
|
||||
@ -17,6 +18,8 @@ const CreatePage = () => {
|
||||
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const name = searchParams.get('name') || ''
|
||||
const token = searchParams.get('token') || ''
|
||||
const appNpub = searchParams.get('appNpub') || ''
|
||||
@ -31,12 +34,14 @@ const CreatePage = () => {
|
||||
|
||||
const handleClickAddAccount = async () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const key: any = await swicCall('generateKey', name)
|
||||
|
||||
const appUrl = getReferrerAppUrl();
|
||||
const appUrl = getReferrerAppUrl()
|
||||
|
||||
console.log('Created', key.npub, 'app', appUrl)
|
||||
setCreated(true)
|
||||
setIsLoading(false)
|
||||
|
||||
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||
search: {
|
||||
@ -53,6 +58,7 @@ const CreatePage = () => {
|
||||
})
|
||||
} catch (error: any) {
|
||||
notify(error.message || error.toString(), 'error')
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -88,7 +94,9 @@ const CreatePage = () => {
|
||||
<Typography textAlign={'left'} variant="h6" paddingTop="0.5em">
|
||||
Chosen name: <b>{nip05}</b>
|
||||
</Typography>
|
||||
<GetStartedButton onClick={handleClickAddAccount}>Create account</GetStartedButton>
|
||||
<GetStartedButton onClick={handleClickAddAccount}>
|
||||
Create account {isLoading && <LoadingSpinner />}
|
||||
</GetStartedButton>
|
||||
|
||||
<Typography textAlign={'left'} variant="h5" paddingTop="1em">
|
||||
What you need to know:
|
||||
|
@ -3,6 +3,7 @@ import { DbPending, DbPerm } from '@/modules/db'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { ACTION_TYPE } from '@/utils/consts'
|
||||
import { useCallback, useEffect, useRef } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
|
||||
export type IPendingsByAppNpub = {
|
||||
[appNpub: string]: {
|
||||
@ -18,6 +19,9 @@ type IShownConfirmModals = {
|
||||
export const useTriggerConfirmModal = (npub: string, pending: DbPending[], perms: DbPerm[]) => {
|
||||
const { handleOpen, getModalOpened } = useModalSearchParams()
|
||||
|
||||
const [searchParams] = useSearchParams()
|
||||
const isPopup = searchParams.get('popup') === 'true'
|
||||
|
||||
const isConfirmConnectModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
||||
const isConfirmEventModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
||||
|
||||
@ -66,11 +70,19 @@ export const useTriggerConfirmModal = (npub: string, pending: DbPending[], perms
|
||||
search: {
|
||||
appNpub: req.appNpub,
|
||||
reqId: req.id,
|
||||
popup: isPopup ? 'true' : '',
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}, [connectPendings, filteredPendingReqs.length, handleOpen, isConfirmEventModalOpened, isConfirmConnectModalOpened])
|
||||
}, [
|
||||
connectPendings,
|
||||
filteredPendingReqs.length,
|
||||
handleOpen,
|
||||
isConfirmEventModalOpened,
|
||||
isConfirmConnectModalOpened,
|
||||
isPopup,
|
||||
])
|
||||
|
||||
const handleOpenConfirmEventModal = useCallback(() => {
|
||||
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, {
|
||||
search: {
|
||||
appNpub,
|
||||
popup: isPopup ? 'true' : '',
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}, [connectPendings.length, filteredPendingReqs.length, handleOpen, prepareEventPendings])
|
||||
}, [connectPendings.length, filteredPendingReqs.length, handleOpen, prepareEventPendings, isPopup])
|
||||
|
||||
useEffect(() => {
|
||||
handleOpenConfirmEventModal()
|
||||
|
@ -48,6 +48,7 @@ export const StyledEmptyAppsBox = styled(Box)(({ theme }) => {
|
||||
placeItems: 'center',
|
||||
color: theme.palette.text.primary,
|
||||
opacity: '0.6',
|
||||
maxHeight: '100%',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -28,20 +28,20 @@ const StyledButton = styled(
|
||||
},
|
||||
color: theme.palette.text.primary,
|
||||
'&.disabled': {
|
||||
opacity: 0.5,
|
||||
background: `${theme.palette.backgroundSecondary.default}50`,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
...commonStyles,
|
||||
'&.button:is(:hover, :active, &, .disabled)': {
|
||||
'&.button:is(:hover, :active, &)': {
|
||||
background: theme.palette.primary.main,
|
||||
},
|
||||
color: theme.palette.text.secondary,
|
||||
'&.disabled': {
|
||||
color: theme.palette.text.secondary,
|
||||
opacity: 0.5,
|
||||
background: `${theme.palette.primary.main}50`,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
}
|
||||
|
17
src/shared/LoadingSpinner/LoadingSpinner.tsx
Normal file
17
src/shared/LoadingSpinner/LoadingSpinner.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { CircularProgress, CircularProgressProps, styled } from '@mui/material'
|
||||
import { FC } from 'react'
|
||||
|
||||
type LoadingSpinnerProps = CircularProgressProps & {
|
||||
mode?: 'default' | 'secondary'
|
||||
}
|
||||
|
||||
export const LoadingSpinner: FC<LoadingSpinnerProps> = (props) => {
|
||||
return <StyledCircularProgress {...props} />
|
||||
}
|
||||
|
||||
export const StyledCircularProgress = styled((props: LoadingSpinnerProps) => (
|
||||
<CircularProgress size={'1rem'} {...props} />
|
||||
))(({ theme, mode = 'default' }) => ({
|
||||
marginLeft: '0.5rem',
|
||||
color: mode === 'default' ? theme.palette.text.secondary : theme.palette.text.primary,
|
||||
}))
|
@ -110,8 +110,7 @@ export const getReferrerAppUrl = () => {
|
||||
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
|
||||
if (u.hostname !== DOMAIN && !u.hostname.endsWith('.' + DOMAIN)) return u.origin
|
||||
} catch {}
|
||||
return ''
|
||||
}
|
||||
|
Reference in New Issue
Block a user