Merge pull request #5 from nostrband/fix/modal-replace-notifs
Fix/modal replace notifs
This commit is contained in:
3
.env
3
.env
@@ -2,4 +2,5 @@
|
|||||||
# change if you're using a different noauthd server
|
# change if you're using a different noauthd server
|
||||||
REACT_APP_WEB_PUSH_PUBKEY=BNW_39YcKbV4KunFxFhvMW5JUs8AljfFnGUeZpaerO-gwCoWyQat5ol0xOGB8MLaqqCbz0iptd2Qv3SToSGynMk
|
REACT_APP_WEB_PUSH_PUBKEY=BNW_39YcKbV4KunFxFhvMW5JUs8AljfFnGUeZpaerO-gwCoWyQat5ol0xOGB8MLaqqCbz0iptd2Qv3SToSGynMk
|
||||||
#REACT_APP_NOAUTHD_URL=http://localhost:8000
|
#REACT_APP_NOAUTHD_URL=http://localhost:8000
|
||||||
REACT_APP_NOAUTHD_URL=https://noauthd.login.nostrapps.org
|
REACT_APP_NOAUTHD_URL=https://noauthd.login.nostrapps.org
|
||||||
|
REACT_APP_DOMAIN=nsec.app
|
14
src/App.tsx
14
src/App.tsx
@@ -1,4 +1,4 @@
|
|||||||
import { DbKey, DbPending, dbi } from './modules/db'
|
import { DbKey, dbi } from './modules/db'
|
||||||
import { useCallback, useEffect, useState } from 'react'
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
import { swicOnRender } from './modules/swic'
|
import { swicOnRender } from './modules/swic'
|
||||||
import { useAppDispatch } from './store/hooks/redux'
|
import { useAppDispatch } from './store/hooks/redux'
|
||||||
@@ -65,18 +65,14 @@ function App() {
|
|||||||
dispatch(setPending({ pending }))
|
dispatch(setPending({ pending }))
|
||||||
|
|
||||||
// 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
|
||||||
}, [dispatch])
|
}, [dispatch])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('NDK is connected', isConnected)
|
if (isConnected) load()
|
||||||
if (isConnected) {
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
}, [render, isConnected, load])
|
}, [render, isConnected, load])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@@ -14,7 +14,7 @@ import { ACTION_TYPE } from '@/utils/consts'
|
|||||||
|
|
||||||
|
|
||||||
export const ModalConfirmConnect = () => {
|
export const ModalConfirmConnect = () => {
|
||||||
const { getModalOpened, handleClose } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
||||||
|
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
@@ -37,20 +37,24 @@ export const ModalConfirmConnect = () => {
|
|||||||
return setSelectedActionType(value)
|
return setSelectedActionType(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCloseModal = handleClose(
|
const handleCloseModal = createHandleCloseReplace(
|
||||||
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
||||||
async (sp) => {
|
{
|
||||||
sp.delete('appNpub')
|
onClose: async (sp) => {
|
||||||
sp.delete('reqId')
|
sp.delete('appNpub')
|
||||||
await swicCall('confirm', pendingReqId, false, false)
|
sp.delete('reqId')
|
||||||
|
await swicCall('confirm', pendingReqId, false, false)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
const closeModalAfterRequest = handleClose(
|
const closeModalAfterRequest = createHandleCloseReplace(
|
||||||
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
||||||
(sp) => {
|
{
|
||||||
sp.delete('appNpub')
|
onClose: (sp) => {
|
||||||
sp.delete('reqId')
|
sp.delete('appNpub')
|
||||||
},
|
sp.delete('reqId')
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async function confirmPending(
|
async function confirmPending(
|
||||||
|
@@ -24,9 +24,10 @@ import {
|
|||||||
} from './styled'
|
} from './styled'
|
||||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
import { swicCall } from '@/modules/swic'
|
import { swicCall } from '@/modules/swic'
|
||||||
import { IPendingsByAppNpub } from '@/pages/KeyPage/Key.Page'
|
|
||||||
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'
|
||||||
|
|
||||||
enum ACTION_TYPE {
|
enum ACTION_TYPE {
|
||||||
ALWAYS = 'ALWAYS',
|
ALWAYS = 'ALWAYS',
|
||||||
@@ -44,20 +45,12 @@ type ModalConfirmEventProps = {
|
|||||||
confirmEventReqs: IPendingsByAppNpub
|
confirmEventReqs: IPendingsByAppNpub
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ACTIONS: { [type: string]: string } = {
|
|
||||||
get_public_key: 'Get public key',
|
|
||||||
sign_event: 'Sign event',
|
|
||||||
connect: 'Connect',
|
|
||||||
nip04_encrypt: 'Encrypt message',
|
|
||||||
nip04_decrypt: 'Decrypt message',
|
|
||||||
}
|
|
||||||
|
|
||||||
type PendingRequest = DbPending & { checked: boolean }
|
type PendingRequest = DbPending & { checked: boolean }
|
||||||
|
|
||||||
export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
||||||
confirmEventReqs,
|
confirmEventReqs,
|
||||||
}) => {
|
}) => {
|
||||||
const { getModalOpened, handleClose } = 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()
|
||||||
|
|
||||||
@@ -93,23 +86,27 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
|||||||
|
|
||||||
const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked)
|
const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked)
|
||||||
|
|
||||||
const handleCloseModal = handleClose(
|
const handleCloseModal = createHandleCloseReplace(
|
||||||
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
||||||
(sp) => {
|
{
|
||||||
sp.delete('appNpub')
|
onClose: (sp) => {
|
||||||
sp.delete('reqId')
|
sp.delete('appNpub')
|
||||||
selectedPendingRequests.forEach(
|
sp.delete('reqId')
|
||||||
async (req) => await swicCall('confirm', req.id, false, false),
|
selectedPendingRequests.forEach(
|
||||||
)
|
async (req) => await swicCall('confirm', req.id, false, false),
|
||||||
},
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const closeModalAfterRequest = handleClose(
|
const closeModalAfterRequest = createHandleCloseReplace(
|
||||||
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
||||||
(sp) => {
|
{
|
||||||
sp.delete('appNpub')
|
onClose: (sp) => {
|
||||||
sp.delete('reqId')
|
sp.delete('appNpub')
|
||||||
},
|
sp.delete('reqId')
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
async function confirmPending(allow: boolean) {
|
async function confirmPending(allow: boolean) {
|
||||||
@@ -173,7 +170,7 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
|||||||
<List>
|
<List>
|
||||||
{pendingRequests.map((req) => {
|
{pendingRequests.map((req) => {
|
||||||
return (
|
return (
|
||||||
<ListItem>
|
<ListItem key={req.id}>
|
||||||
<ListItemIcon>
|
<ListItemIcon>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={req.checked}
|
checked={req.checked}
|
||||||
|
@@ -12,13 +12,18 @@ import { useRef } from 'react'
|
|||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
|
|
||||||
export const ModalConnectApp = () => {
|
export const ModalConnectApp = () => {
|
||||||
const { getModalOpened, handleClose, handleOpen } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||||
const timerRef = useRef<NodeJS.Timeout>()
|
const timerRef = useRef<NodeJS.Timeout>()
|
||||||
|
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||||
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.CONNECT_APP, () => {
|
const handleCloseModal = createHandleCloseReplace(
|
||||||
clearTimeout(timerRef.current)
|
MODAL_PARAMS_KEYS.CONNECT_APP,
|
||||||
})
|
{
|
||||||
|
onClose: () => {
|
||||||
|
clearTimeout(timerRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
|
@@ -8,13 +8,15 @@ import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
|||||||
import { Stack, Typography } from '@mui/material'
|
import { Stack, Typography } from '@mui/material'
|
||||||
import React, { ChangeEvent, FormEvent, useState } from 'react'
|
import React, { ChangeEvent, FormEvent, useState } from 'react'
|
||||||
import { StyledAppLogo } from './styled'
|
import { StyledAppLogo } from './styled'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
export const ModalImportKeys = () => {
|
export const ModalImportKeys = () => {
|
||||||
const { getModalOpened, handleClose } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
||||||
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const [enteredNsec, setEnteredNsec] = useState('')
|
const [enteredNsec, setEnteredNsec] = useState('')
|
||||||
|
|
||||||
@@ -26,9 +28,9 @@ export const ModalImportKeys = () => {
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
try {
|
try {
|
||||||
if (!enteredNsec.trim().length) return
|
if (!enteredNsec.trim().length) return
|
||||||
await swicCall('importKey', enteredNsec)
|
const k: any = await swicCall('importKey', enteredNsec)
|
||||||
notify('Key imported!', 'success')
|
notify('Key imported!', 'success')
|
||||||
handleCloseModal()
|
navigate(`/key/${k.npub}`)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
notify(error.message, 'error')
|
notify(error.message, 'error')
|
||||||
}
|
}
|
||||||
@@ -36,12 +38,7 @@ export const ModalImportKeys = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||||
<Stack
|
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||||
paddingTop={'1rem'}
|
|
||||||
gap={'1rem'}
|
|
||||||
component={'form'}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
<Stack
|
<Stack
|
||||||
direction={'row'}
|
direction={'row'}
|
||||||
gap={'1rem'}
|
gap={'1rem'}
|
||||||
@@ -59,8 +56,9 @@ export const ModalImportKeys = () => {
|
|||||||
value={enteredNsec}
|
value={enteredNsec}
|
||||||
onChange={handleNsecChange}
|
onChange={handleNsecChange}
|
||||||
fullWidth
|
fullWidth
|
||||||
|
type='password'
|
||||||
/>
|
/>
|
||||||
<Button>Import nsec</Button>
|
<Button type='submit'>Import nsec</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
@@ -7,10 +7,10 @@ import { Fade, Stack } from '@mui/material'
|
|||||||
import { AppLink } from '@/shared/AppLink/AppLink'
|
import { AppLink } from '@/shared/AppLink/AppLink'
|
||||||
|
|
||||||
export const ModalInitial = () => {
|
export const ModalInitial = () => {
|
||||||
const { getModalOpened, handleClose, handleOpen } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
|
|
||||||
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.INITIAL)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
|
|
||||||
const [showAdvancedContent, setShowAdvancedContent] = useState(false)
|
const [showAdvancedContent, setShowAdvancedContent] = useState(false)
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ export const ModalInitial = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||||
<Stack paddingTop={'2.5rem'} gap={'1rem'}>
|
<Stack paddingTop={'0.5rem'} gap={'1rem'}>
|
||||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.SIGN_UP)}>
|
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.SIGN_UP)}>
|
||||||
Sign up
|
Sign up
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -14,9 +14,9 @@ import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
export const ModalLogin = () => {
|
export const ModalLogin = () => {
|
||||||
const { getModalOpened, handleClose } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.LOGIN)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.LOGIN)
|
||||||
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.LOGIN)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN)
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
@@ -37,8 +37,12 @@ export const ModalLogin = () => {
|
|||||||
const handlePasswordTypeChange = () =>
|
const handlePasswordTypeChange = () =>
|
||||||
setIsPasswordShown((prevState) => !prevState)
|
setIsPasswordShown((prevState) => !prevState)
|
||||||
|
|
||||||
|
const isFormValid =
|
||||||
|
enteredUsername.trim().length > 0 && enteredPassword.trim().length > 0
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
if (!isFormValid) return undefined
|
||||||
try {
|
try {
|
||||||
const [username, domain] = enteredUsername.split('@')
|
const [username, domain] = enteredUsername.split('@')
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
@@ -63,12 +67,7 @@ export const ModalLogin = () => {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||||
<Stack
|
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||||
paddingTop={'1rem'}
|
|
||||||
gap={'1rem'}
|
|
||||||
component={'form'}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
>
|
|
||||||
<Stack
|
<Stack
|
||||||
direction={'row'}
|
direction={'row'}
|
||||||
gap={'1rem'}
|
gap={'1rem'}
|
||||||
@@ -105,9 +104,9 @@ export const ModalLogin = () => {
|
|||||||
)}
|
)}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
type={isPasswordShown ? 'password' : 'text'}
|
type={isPasswordShown ? 'text' : 'password'}
|
||||||
/>
|
/>
|
||||||
<Button type='submit' fullWidth>
|
<Button type='submit' fullWidth disabled={!isFormValid}>
|
||||||
Login
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@@ -2,7 +2,13 @@ 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 { Box, IconButton, Stack, Typography } from '@mui/material'
|
import {
|
||||||
|
Box,
|
||||||
|
CircularProgress,
|
||||||
|
IconButton,
|
||||||
|
Stack,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material'
|
||||||
import {
|
import {
|
||||||
StyledButton,
|
StyledButton,
|
||||||
StyledSettingContainer,
|
StyledSettingContainer,
|
||||||
@@ -13,28 +19,37 @@ import { CheckmarkIcon } from '@/assets'
|
|||||||
import { Input } from '@/shared/Input/Input'
|
import { Input } from '@/shared/Input/Input'
|
||||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
|
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
|
||||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
||||||
import { ChangeEvent, 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'
|
||||||
|
|
||||||
export const ModalSettings = () => {
|
type ModalSettingsProps = {
|
||||||
const { getModalOpened, handleClose } = useModalSearchParams()
|
isSynced: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||||
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS)
|
||||||
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.SETTINGS)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SETTINGS)
|
||||||
|
|
||||||
const [enteredPassword, setEnteredPassword] = useState('')
|
const [enteredPassword, setEnteredPassword] = useState('')
|
||||||
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
||||||
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
|
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
|
||||||
const [isPasswordSynched, setIsPasswordSynched] = useState(false)
|
|
||||||
|
|
||||||
const [isChecked, setIsChecked] = useState(false)
|
const [isChecked, setIsChecked] = useState(false)
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => setIsChecked(isSynced), [isModalOpened, isSynced])
|
||||||
|
|
||||||
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setIsPasswordInvalid(false)
|
setIsPasswordInvalid(false)
|
||||||
setEnteredPassword(e.target.value)
|
setEnteredPassword(e.target.value)
|
||||||
@@ -47,7 +62,6 @@ export const ModalSettings = () => {
|
|||||||
handleCloseModal()
|
handleCloseModal()
|
||||||
setEnteredPassword('')
|
setEnteredPassword('')
|
||||||
setIsPasswordInvalid(false)
|
setIsPasswordInvalid(false)
|
||||||
setIsPasswordSynched(false)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChangeCheckbox = (e: unknown, checked: boolean) => {
|
const handleChangeCheckbox = (e: unknown, checked: boolean) => {
|
||||||
@@ -57,19 +71,21 @@ export const ModalSettings = () => {
|
|||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsPasswordInvalid(false)
|
setIsPasswordInvalid(false)
|
||||||
setIsPasswordSynched(false)
|
|
||||||
|
|
||||||
if (enteredPassword.trim().length < 6) {
|
if (enteredPassword.trim().length < 6) {
|
||||||
return setIsPasswordInvalid(true)
|
return setIsPasswordInvalid(true)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
setIsLoading(true)
|
||||||
await swicCall('saveKey', npub, enteredPassword)
|
await swicCall('saveKey', npub, enteredPassword)
|
||||||
notify('Key saved', 'success')
|
notify('Key saved', 'success')
|
||||||
|
dbi.addSynced(npub) // Sync npub
|
||||||
|
setEnteredPassword('')
|
||||||
setIsPasswordInvalid(false)
|
setIsPasswordInvalid(false)
|
||||||
setIsPasswordSynched(true)
|
setIsLoading(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setIsPasswordInvalid(false)
|
setIsPasswordInvalid(false)
|
||||||
setIsPasswordSynched(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +95,7 @@ export const ModalSettings = () => {
|
|||||||
<StyledSettingContainer onSubmit={handleSubmit}>
|
<StyledSettingContainer onSubmit={handleSubmit}>
|
||||||
<Stack direction={'row'} justifyContent={'space-between'}>
|
<Stack direction={'row'} justifyContent={'space-between'}>
|
||||||
<SectionTitle>Cloud sync</SectionTitle>
|
<SectionTitle>Cloud sync</SectionTitle>
|
||||||
{isPasswordSynched && (
|
{isSynced && (
|
||||||
<StyledSynchedText>
|
<StyledSynchedText>
|
||||||
<CheckmarkIcon /> Synched
|
<CheckmarkIcon /> Synched
|
||||||
</StyledSynchedText>
|
</StyledSynchedText>
|
||||||
@@ -91,7 +107,7 @@ export const ModalSettings = () => {
|
|||||||
checked={isChecked}
|
checked={isChecked}
|
||||||
/>
|
/>
|
||||||
<Typography variant='caption'>
|
<Typography variant='caption'>
|
||||||
Use this login on multiple devices
|
Use this key on multiple devices
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Input
|
<Input
|
||||||
@@ -111,7 +127,9 @@ export const ModalSettings = () => {
|
|||||||
type={isPasswordShown ? 'text' : 'password'}
|
type={isPasswordShown ? 'text' : 'password'}
|
||||||
onChange={handlePasswordChange}
|
onChange={handlePasswordChange}
|
||||||
value={enteredPassword}
|
value={enteredPassword}
|
||||||
helperText={isPasswordInvalid ? 'Invalid password' : ''}
|
helperText={
|
||||||
|
isPasswordInvalid ? 'Invalid password' : ''
|
||||||
|
}
|
||||||
placeholder='Enter a password'
|
placeholder='Enter a password'
|
||||||
helperTextProps={{
|
helperTextProps={{
|
||||||
sx: {
|
sx: {
|
||||||
@@ -122,8 +140,27 @@ export const ModalSettings = () => {
|
|||||||
}}
|
}}
|
||||||
disabled={!isChecked}
|
disabled={!isChecked}
|
||||||
/>
|
/>
|
||||||
<StyledButton type='submit' fullWidth disabled={!isChecked}>
|
{isSynced ? (
|
||||||
Sync
|
<Typography variant='body2' color={'GrayText'}>
|
||||||
|
To change your password, type a new one and sync.
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography variant='body2' color={'GrayText'}>
|
||||||
|
This key will be encrypted and stored on our server. You can use the password to download this key onto another device.
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
<StyledButton
|
||||||
|
type='submit'
|
||||||
|
fullWidth
|
||||||
|
disabled={!isChecked}
|
||||||
|
>
|
||||||
|
Sync{' '}
|
||||||
|
{isLoading && (
|
||||||
|
<CircularProgress
|
||||||
|
sx={{ marginLeft: '0.5rem' }}
|
||||||
|
size={'1rem'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</StyledButton>
|
</StyledButton>
|
||||||
</StyledSettingContainer>
|
</StyledSettingContainer>
|
||||||
<Button onClick={onClose}>Done</Button>
|
<Button onClick={onClose}>Done</Button>
|
||||||
|
@@ -8,9 +8,9 @@ import {
|
|||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
|
|
||||||
export const StyledSettingContainer = styled((props: StackProps) => (
|
export const StyledSettingContainer = styled((props: StackProps) => (
|
||||||
<Stack gap={'1rem'} component={'form'} {...props} />
|
<Stack gap={'0.75rem'} component={'form'} {...props} />
|
||||||
))(({ theme }) => ({
|
))(({ theme }) => ({
|
||||||
padding: '0.75rem',
|
padding: '1rem',
|
||||||
borderRadius: '1rem',
|
borderRadius: '1rem',
|
||||||
background: theme.palette.background.default,
|
background: theme.palette.background.default,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
@@ -22,6 +22,9 @@ export const StyledButton = styled(Button)(({ theme }) => {
|
|||||||
background: theme.palette.secondary.main,
|
background: theme.palette.secondary.main,
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
},
|
},
|
||||||
|
':disabled': {
|
||||||
|
cursor: 'not-allowed',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -12,9 +12,9 @@ import { swicCall } from '@/modules/swic'
|
|||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
export const ModalSignUp = () => {
|
export const ModalSignUp = () => {
|
||||||
const { getModalOpened, handleClose } = useModalSearchParams()
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SIGN_UP)
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SIGN_UP)
|
||||||
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.SIGN_UP)
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SIGN_UP)
|
||||||
const notify = useEnqueueSnackbar()
|
const notify = useEnqueueSnackbar()
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
|
|
||||||
|
23
src/hooks/useIsIOS.ts
Normal file
23
src/hooks/useIsIOS.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom hook to detect if the platform is iOS or not.
|
||||||
|
* @returns {boolean} True if the platform is iOS, false otherwise.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const iOSRegex = /iPad|iPhone|iPod/
|
||||||
|
|
||||||
|
function useIsIOS() {
|
||||||
|
const [isIOS, setIsIOS] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const isIOSUserAgent =
|
||||||
|
iOSRegex.test(navigator.userAgent) ||
|
||||||
|
(navigator.userAgent.includes('Mac') && 'ontouchend' in document)
|
||||||
|
setIsIOS(isIOSUserAgent)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return isIOS
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useIsIOS
|
@@ -17,6 +17,11 @@ export type IExtraOptions = {
|
|||||||
append?: boolean
|
append?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type IExtraCloseOptions = {
|
||||||
|
replace?: boolean
|
||||||
|
onClose?: (s: URLSearchParams) => void
|
||||||
|
}
|
||||||
|
|
||||||
export const useModalSearchParams = () => {
|
export const useModalSearchParams = () => {
|
||||||
const [searchParams, setSearchParams] = useSearchParams()
|
const [searchParams, setSearchParams] = useSearchParams()
|
||||||
|
|
||||||
@@ -29,13 +34,20 @@ export const useModalSearchParams = () => {
|
|||||||
]
|
]
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleClose =
|
const createHandleClose =
|
||||||
(modal: MODAL_PARAMS_KEYS, onClose?: (s: URLSearchParams) => void) =>
|
(modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraCloseOptions) =>
|
||||||
() => {
|
() => {
|
||||||
const enumKey = getEnumParam(modal)
|
const enumKey = getEnumParam(modal)
|
||||||
searchParams.delete(enumKey)
|
searchParams.delete(enumKey)
|
||||||
onClose && onClose(searchParams)
|
extraOptions?.onClose && extraOptions?.onClose(searchParams)
|
||||||
setSearchParams(searchParams)
|
console.log({ searchParams })
|
||||||
|
setSearchParams(searchParams, { replace: !!extraOptions?.replace })
|
||||||
|
}
|
||||||
|
|
||||||
|
const createHandleCloseReplace =
|
||||||
|
(modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraCloseOptions) =>
|
||||||
|
() => {
|
||||||
|
return createHandleClose(modal, { ...extraOptions, replace: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpen = useCallback(
|
const handleOpen = useCallback(
|
||||||
@@ -61,7 +73,7 @@ export const useModalSearchParams = () => {
|
|||||||
pathname: location.pathname,
|
pathname: location.pathname,
|
||||||
search: searchString,
|
search: searchString,
|
||||||
},
|
},
|
||||||
{ replace: extraOptions?.replace || true },
|
{ replace: !!extraOptions?.replace },
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[location, navigate, getEnumParam],
|
[location, navigate, getEnumParam],
|
||||||
@@ -78,7 +90,8 @@ export const useModalSearchParams = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
getModalOpened,
|
getModalOpened,
|
||||||
handleClose,
|
createHandleClose,
|
||||||
|
createHandleCloseReplace,
|
||||||
handleOpen,
|
handleOpen,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
src/hooks/useToggleConfirm.ts
Normal file
15
src/hooks/useToggleConfirm.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
|
||||||
|
export const useToggleConfirm = () => {
|
||||||
|
const [showConfirm, setShowConfirm] = useState(false)
|
||||||
|
|
||||||
|
const handleShow = useCallback(() => setShowConfirm(true), [])
|
||||||
|
|
||||||
|
const handleClose = useCallback(() => setShowConfirm(false), [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
open: showConfirm,
|
||||||
|
handleShow,
|
||||||
|
handleClose,
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools'
|
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools'
|
||||||
import { dbi, DbKey, DbPending, DbPerm } from './db'
|
import { DbApp, dbi, DbKey, DbPending, DbPerm } from './db'
|
||||||
import { Keys } from './keys'
|
import { Keys } from './keys'
|
||||||
import NDK, {
|
import NDK, {
|
||||||
IEventHandlingStrategy,
|
IEventHandlingStrategy,
|
||||||
@@ -10,7 +10,7 @@ import NDK, {
|
|||||||
} from '@nostr-dev-kit/ndk'
|
} from '@nostr-dev-kit/ndk'
|
||||||
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS } from '../utils/consts'
|
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS } from '../utils/consts'
|
||||||
import { Nip04 } from './nip04'
|
import { Nip04 } from './nip04'
|
||||||
import { getReqPerm, isPackagePerm } from '@/utils/helpers/helpers'
|
import { getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
|
||||||
//import { PrivateKeySigner } from './signer'
|
//import { PrivateKeySigner } from './signer'
|
||||||
|
|
||||||
//const PERF_TEST = false
|
//const PERF_TEST = false
|
||||||
@@ -32,6 +32,7 @@ interface Key {
|
|||||||
interface Pending {
|
interface Pending {
|
||||||
req: DbPending
|
req: DbPending
|
||||||
cb: (allow: boolean, remember: boolean, options?: any) => void
|
cb: (allow: boolean, remember: boolean, options?: any) => void
|
||||||
|
notified?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAllowCallbackParams {
|
interface IAllowCallbackParams {
|
||||||
@@ -145,6 +146,7 @@ export class NoauthBackend {
|
|||||||
private enckeys: DbKey[] = []
|
private enckeys: DbKey[] = []
|
||||||
private keys: Key[] = []
|
private keys: Key[] = []
|
||||||
private perms: DbPerm[] = []
|
private perms: DbPerm[] = []
|
||||||
|
private apps: DbApp[] = []
|
||||||
private doneReqIds: string[] = []
|
private doneReqIds: string[] = []
|
||||||
private confirmBuffer: Pending[] = []
|
private confirmBuffer: Pending[] = []
|
||||||
private accessBuffer: DbPending[] = []
|
private accessBuffer: DbPending[] = []
|
||||||
@@ -193,16 +195,25 @@ export class NoauthBackend {
|
|||||||
.matchAll({ type: 'window' })
|
.matchAll({ type: 'window' })
|
||||||
.then((clientList) => {
|
.then((clientList) => {
|
||||||
console.log('clients', clientList.length)
|
console.log('clients', clientList.length)
|
||||||
|
// FIXME find a client that has our
|
||||||
|
// key page
|
||||||
for (const client of clientList) {
|
for (const client of clientList) {
|
||||||
console.log('client', client.url)
|
console.log('client', client.url)
|
||||||
if (
|
if (
|
||||||
new URL(client.url).pathname === '/' &&
|
new URL(client.url).pathname === '/' &&
|
||||||
'focus' in client
|
'focus' in client
|
||||||
)
|
) {
|
||||||
return client.focus()
|
client.focus()
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// if (self.swg.clients.openWindow)
|
|
||||||
// return self.swg.clients.openWindow("/");
|
// confirm screen url
|
||||||
|
const req = event.notification.data.req
|
||||||
|
console.log("req", req)
|
||||||
|
// const url = `${self.swg.location.origin}/key/${req.npub}?confirm-connect=true&appNpub=${req.appNpub}&reqId=${req.id}`
|
||||||
|
const url = `${self.swg.location.origin}/key/${req.npub}`
|
||||||
|
self.swg.clients.openWindow(url)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -216,6 +227,8 @@ export class NoauthBackend {
|
|||||||
console.log('started encKeys', this.listKeys())
|
console.log('started encKeys', this.listKeys())
|
||||||
this.perms = await dbi.listPerms()
|
this.perms = await dbi.listPerms()
|
||||||
console.log('started perms', this.perms)
|
console.log('started perms', this.perms)
|
||||||
|
this.apps = await dbi.listApps()
|
||||||
|
console.log('started apps', this.apps)
|
||||||
|
|
||||||
const sub = await this.swg.registration.pushManager.getSubscription()
|
const sub = await this.swg.registration.pushManager.getSubscription()
|
||||||
|
|
||||||
@@ -381,21 +394,69 @@ export class NoauthBackend {
|
|||||||
// and update the notifications
|
// and update the notifications
|
||||||
|
|
||||||
for (const r of this.confirmBuffer) {
|
for (const r of this.confirmBuffer) {
|
||||||
const text = `Confirm "${r.req.method}" by "${r.req.appNpub}"`
|
|
||||||
this.swg.registration.showNotification('Signer access', {
|
if (r.notified) continue
|
||||||
body: text,
|
|
||||||
tag: 'confirm-' + r.req.appNpub,
|
const key = this.keys.find(k => k.npub === r.req.npub)
|
||||||
actions: [
|
if (!key) continue
|
||||||
{
|
|
||||||
action: 'allow:' + r.req.id,
|
const app = this.apps.find(a => a.appNpub === r.req.appNpub)
|
||||||
title: 'Yes',
|
if (r.req.method !== 'connect' && !app) continue
|
||||||
},
|
|
||||||
{
|
// FIXME use Nsec.app icon!
|
||||||
action: 'disallow:' + r.req.id,
|
const icon = 'https://nostr.band/android-chrome-192x192.png'
|
||||||
title: 'No',
|
|
||||||
},
|
const appName = app?.name || getShortenNpub(r.req.appNpub)
|
||||||
],
|
// FIXME load profile?
|
||||||
})
|
const keyName = getShortenNpub(r.req.npub)
|
||||||
|
|
||||||
|
const tag = 'confirm-' + r.req.appNpub
|
||||||
|
const allowAction = 'allow:' + r.req.id
|
||||||
|
const disallowAction = 'disallow:' + r.req.id
|
||||||
|
const data = { req: r.req }
|
||||||
|
|
||||||
|
if (r.req.method === 'connect') {
|
||||||
|
const title = `Connect with new app`
|
||||||
|
const body = `Allow app "${appName}" to connect to key "${keyName}"`
|
||||||
|
this.swg.registration.showNotification(title, {
|
||||||
|
body,
|
||||||
|
tag,
|
||||||
|
icon,
|
||||||
|
data,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: allowAction,
|
||||||
|
title: 'Connect',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: disallowAction,
|
||||||
|
title: 'Ignore',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
const title = `Permission request`
|
||||||
|
const body = `Allow "${r.req.method}" by "${appName}" to "${keyName}"`
|
||||||
|
this.swg.registration.showNotification(title, {
|
||||||
|
body,
|
||||||
|
tag,
|
||||||
|
icon,
|
||||||
|
data,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: allowAction,
|
||||||
|
title: 'Yes',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: disallowAction,
|
||||||
|
title: 'No',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark
|
||||||
|
r.notified = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.notifCallback) this.notifCallback()
|
if (this.notifCallback) this.notifCallback()
|
||||||
@@ -509,6 +570,9 @@ export class NoauthBackend {
|
|||||||
icon: '',
|
icon: '',
|
||||||
url: '',
|
url: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// reload
|
||||||
|
self.apps = await dbi.listApps()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -771,6 +835,7 @@ export class NoauthBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async deleteApp(appNpub: string) {
|
private async deleteApp(appNpub: string) {
|
||||||
|
this.apps = this.apps.filter((a) => a.appNpub !== appNpub)
|
||||||
this.perms = this.perms.filter((p) => p.appNpub !== appNpub)
|
this.perms = this.perms.filter((p) => p.appNpub !== appNpub)
|
||||||
await dbi.removeApp(appNpub)
|
await dbi.removeApp(appNpub)
|
||||||
await dbi.removeAppPerms(appNpub)
|
await dbi.removeAppPerms(appNpub)
|
||||||
|
@@ -48,23 +48,29 @@ export interface DbHistory {
|
|||||||
allowed: boolean
|
allowed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DbSyncHistory {
|
||||||
|
npub: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface DbSchema extends Dexie {
|
export interface DbSchema extends Dexie {
|
||||||
keys: Dexie.Table<DbKey, string>
|
keys: Dexie.Table<DbKey, string>
|
||||||
apps: Dexie.Table<DbApp, string>
|
apps: Dexie.Table<DbApp, string>
|
||||||
perms: Dexie.Table<DbPerm, string>
|
perms: Dexie.Table<DbPerm, string>
|
||||||
pending: Dexie.Table<DbPending, string>
|
pending: Dexie.Table<DbPending, string>
|
||||||
history: Dexie.Table<DbHistory, string>
|
history: Dexie.Table<DbHistory, string>
|
||||||
|
syncHistory: Dexie.Table<DbSyncHistory, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const db = new Dexie('noauthdb') as DbSchema
|
export const db = new Dexie('noauthdb') as DbSchema
|
||||||
|
|
||||||
db.version(7).stores({
|
db.version(8).stores({
|
||||||
keys: 'npub',
|
keys: 'npub',
|
||||||
apps: 'appNpub,npub,name,timestamp',
|
apps: 'appNpub,npub,name,timestamp',
|
||||||
perms: 'id,npub,appNpub,perm,value,timestamp',
|
perms: 'id,npub,appNpub,perm,value,timestamp',
|
||||||
pending: 'id,npub,appNpub,timestamp,method',
|
pending: 'id,npub,appNpub,timestamp,method',
|
||||||
history: 'id,npub,appNpub,timestamp,method,allowed',
|
history: 'id,npub,appNpub,timestamp,method,allowed',
|
||||||
requestHistory: 'id',
|
requestHistory: 'id',
|
||||||
|
syncHistory: 'npub',
|
||||||
})
|
})
|
||||||
|
|
||||||
export const dbi = {
|
export const dbi = {
|
||||||
@@ -201,4 +207,12 @@ export const dbi = {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
addSynced: async (npub: string) => {
|
||||||
|
try {
|
||||||
|
await db.syncHistory.add({ npub })
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`db addSynced error: ${error}`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
@@ -1,40 +1,42 @@
|
|||||||
import { useLiveQuery } from 'dexie-react-hooks'
|
|
||||||
import { DbHistory, db } from '@/modules/db'
|
|
||||||
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, selectPermsByNpubAndAppNpub } from '@/store'
|
||||||
import { Navigate } from 'react-router-dom'
|
import { Navigate, useNavigate } from 'react-router-dom'
|
||||||
import { formatTimestampDate } from '@/utils/helpers/date'
|
import { formatTimestampDate } from '@/utils/helpers/date'
|
||||||
import { Avatar, Box, Stack, Typography } from '@mui/material'
|
import { Box, Stack, Typography } from '@mui/material'
|
||||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
import { getShortenNpub } from '@/utils/helpers/helpers'
|
||||||
import { PermissionMenuButton } from './styled'
|
import { Button } from '@/shared/Button/Button'
|
||||||
import { PermissionsMenu } from './components/PermissionsMenu'
|
import { ACTION_TYPE } from '@/utils/consts'
|
||||||
import { useOpenMenu } from '@/hooks/useOpenMenu'
|
import { Permissions } from './components/Permissions/Permissions'
|
||||||
import { ActivityList } from './components/ActivityList'
|
import { StyledAppIcon } from './styled'
|
||||||
|
import { useToggleConfirm } from '@/hooks/useToggleConfirm'
|
||||||
const getAppHistoryQuery = (appNpub: string) =>
|
import { ConfirmModal } from '@/shared/ConfirmModal/ConfirmModal'
|
||||||
db.history.where('appNpub').equals(appNpub).toArray()
|
import { swicCall } from '@/modules/swic'
|
||||||
|
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||||
|
import { IOSBackButton } from '@/shared/IOSBackButton/IOSBackButton'
|
||||||
|
import { ModalActivities } from './components/Activities/ModalActivities'
|
||||||
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
|
|
||||||
const AppPage = () => {
|
const AppPage = () => {
|
||||||
const { appNpub = '', npub = '' } = useParams()
|
const { appNpub = '', npub = '' } = useParams()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
const perms = useAppSelector((state) =>
|
const perms = useAppSelector((state) =>
|
||||||
selectPermsByNpubAndAppNpub(state, npub, appNpub),
|
selectPermsByNpubAndAppNpub(state, npub, appNpub),
|
||||||
)
|
)
|
||||||
const currentApp = useAppSelector((state) =>
|
const currentApp = useAppSelector((state) =>
|
||||||
selectAppByAppNpub(state, appNpub),
|
selectAppByAppNpub(state, appNpub),
|
||||||
)
|
)
|
||||||
const history = useLiveQuery(
|
|
||||||
() => {
|
|
||||||
if (!appNpub.trim().length) return []
|
|
||||||
return getAppHistoryQuery(appNpub)
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
[] as DbHistory[],
|
|
||||||
)
|
|
||||||
|
|
||||||
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()
|
const { open, handleClose, handleShow } = useToggleConfirm()
|
||||||
const connectPerm = perms.find((perm) => perm.perm === 'connect')
|
const { handleOpen: handleOpenModal } = useModalSearchParams()
|
||||||
|
|
||||||
|
const connectPerm = perms.find(
|
||||||
|
(perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC,
|
||||||
|
)
|
||||||
|
|
||||||
if (!currentApp) {
|
if (!currentApp) {
|
||||||
return <Navigate to={`/key/${npub}`} />
|
return <Navigate to={`/key/${npub}`} />
|
||||||
@@ -43,52 +45,77 @@ const AppPage = () => {
|
|||||||
const { icon = '', name = '' } = currentApp || {}
|
const { icon = '', name = '' } = currentApp || {}
|
||||||
const appName = name || getShortenNpub(appNpub)
|
const appName = name || getShortenNpub(appNpub)
|
||||||
const { timestamp } = connectPerm || {}
|
const { timestamp } = connectPerm || {}
|
||||||
|
|
||||||
const connectedOn =
|
const connectedOn =
|
||||||
connectPerm && timestamp
|
connectPerm && timestamp
|
||||||
? `Connected at ${formatTimestampDate(timestamp)}`
|
? `Connected at ${formatTimestampDate(timestamp)}`
|
||||||
: 'Not connected'
|
: 'Not connected'
|
||||||
|
|
||||||
|
const handleDeleteApp = async () => {
|
||||||
|
try {
|
||||||
|
await swicCall('deleteApp', appNpub)
|
||||||
|
notify(`App: «${appName}» successfully deleted!`, 'success')
|
||||||
|
navigate(`key/${npub}`)
|
||||||
|
} catch (error: any) {
|
||||||
|
notify(error?.message || 'Failed to delete app', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack maxHeight={'100%'} overflow={'auto'}>
|
<>
|
||||||
<Stack
|
<Stack
|
||||||
marginBottom={'1rem'}
|
maxHeight={'100%'}
|
||||||
direction={'row'}
|
overflow={'auto'}
|
||||||
gap={'1rem'}
|
alignItems={'flex-start'}
|
||||||
width={'100%'}
|
height={'100%'}
|
||||||
>
|
>
|
||||||
<Avatar
|
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
|
||||||
src={icon}
|
<Stack
|
||||||
sx={{
|
marginBottom={'1rem'}
|
||||||
width: 70,
|
direction={'row'}
|
||||||
height: 70,
|
gap={'1rem'}
|
||||||
}}
|
width={'100%'}
|
||||||
variant='rounded'
|
>
|
||||||
/>
|
<StyledAppIcon src={icon} />
|
||||||
<Box flex={'1'} overflow={'hidden'}>
|
<Box flex={'1'} overflow={'hidden'}>
|
||||||
<Typography variant='h4' noWrap>
|
<Typography variant='h4' noWrap>
|
||||||
{appName}
|
{appName}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body2' noWrap>
|
<Typography variant='body2' noWrap>
|
||||||
{connectedOn}
|
{connectedOn}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<Box marginBottom={'1rem'}>
|
||||||
|
<SectionTitle marginBottom={'0.5rem'}>
|
||||||
|
Disconnect
|
||||||
|
</SectionTitle>
|
||||||
|
<Button fullWidth onClick={handleShow}>
|
||||||
|
Delete app
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
<Permissions perms={perms} />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
fullWidth
|
||||||
|
onClick={() =>
|
||||||
|
handleOpenModal(MODAL_PARAMS_KEYS.ACTIVITY)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Activity
|
||||||
|
</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Box marginBottom={'1rem'}>
|
<ConfirmModal
|
||||||
<SectionTitle marginBottom={'0.5rem'}>Permissions</SectionTitle>
|
open={open}
|
||||||
<PermissionMenuButton onClick={handleOpen}>
|
headingText='Delete app'
|
||||||
Basic/Advanced/Custom {perms.length}
|
description='Are you sure you want to delete this app?'
|
||||||
</PermissionMenuButton>
|
onCancel={handleClose}
|
||||||
<PermissionsMenu
|
onConfirm={handleDeleteApp}
|
||||||
open={open}
|
onClose={handleClose}
|
||||||
anchorEl={anchorEl}
|
/>
|
||||||
perms={perms}
|
<ModalActivities appNpub={appNpub} />
|
||||||
onClose={handleClose}
|
</>
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<ActivityList history={history} />
|
|
||||||
</Stack>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
45
src/pages/AppPage/components/Activities/ItemActivity.tsx
Normal file
45
src/pages/AppPage/components/Activities/ItemActivity.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import { DbHistory } from '@/modules/db'
|
||||||
|
import { Box, IconButton, Typography } from '@mui/material'
|
||||||
|
import { StyledActivityItem } from './styled'
|
||||||
|
import { formatTimestampDate } from '@/utils/helpers/date'
|
||||||
|
import ClearRoundedIcon from '@mui/icons-material/ClearRounded'
|
||||||
|
import DoneRoundedIcon from '@mui/icons-material/DoneRounded'
|
||||||
|
import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded'
|
||||||
|
import { ACTIONS } from '@/utils/consts'
|
||||||
|
|
||||||
|
type ItemActivityProps = DbHistory
|
||||||
|
|
||||||
|
export const ItemActivity: FC<ItemActivityProps> = ({
|
||||||
|
allowed,
|
||||||
|
method,
|
||||||
|
timestamp,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<StyledActivityItem>
|
||||||
|
<Box
|
||||||
|
display={'flex'}
|
||||||
|
flexDirection={'column'}
|
||||||
|
gap={'0.5rem'}
|
||||||
|
flex={1}
|
||||||
|
>
|
||||||
|
<Typography flex={1} fontWeight={700}>
|
||||||
|
{ACTIONS[method] || method}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2'>
|
||||||
|
{formatTimestampDate(timestamp)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
{allowed ? (
|
||||||
|
<DoneRoundedIcon htmlColor='green' />
|
||||||
|
) : (
|
||||||
|
<ClearRoundedIcon htmlColor='red' />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<IconButton>
|
||||||
|
<MoreVertRoundedIcon />
|
||||||
|
</IconButton>
|
||||||
|
</StyledActivityItem>
|
||||||
|
)
|
||||||
|
}
|
39
src/pages/AppPage/components/Activities/ModalActivities.tsx
Normal file
39
src/pages/AppPage/components/Activities/ModalActivities.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import { Modal } from '@/shared/Modal/Modal'
|
||||||
|
import { Box } from '@mui/material'
|
||||||
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
|
import { HistoryDefaultValue, getActivityHistoryQuerier } from '../../utils'
|
||||||
|
import { ItemActivity } from './ItemActivity'
|
||||||
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
|
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
|
|
||||||
|
type ModalActivitiesProps = {
|
||||||
|
appNpub: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModalActivities: FC<ModalActivitiesProps> = ({ appNpub }) => {
|
||||||
|
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||||
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.ACTIVITY)
|
||||||
|
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.ACTIVITY)
|
||||||
|
|
||||||
|
const history = useLiveQuery(
|
||||||
|
getActivityHistoryQuerier(appNpub),
|
||||||
|
[],
|
||||||
|
HistoryDefaultValue,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={isModalOpened}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
fixedHeight='calc(100% - 5rem)'
|
||||||
|
title='Activity history'
|
||||||
|
>
|
||||||
|
<Box overflow={'auto'}>
|
||||||
|
{history.map((item) => {
|
||||||
|
return <ItemActivity {...item} key={item.id} />
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
12
src/pages/AppPage/components/Activities/styled.tsx
Normal file
12
src/pages/AppPage/components/Activities/styled.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { Box, BoxProps } from '@mui/material'
|
||||||
|
|
||||||
|
export const StyledActivityItem = styled((props: BoxProps) => (
|
||||||
|
<Box {...props} />
|
||||||
|
))(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.5rem',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '0.25rem',
|
||||||
|
}))
|
@@ -1,53 +0,0 @@
|
|||||||
import React, { FC } from 'react'
|
|
||||||
import { DbHistory } from '@/modules/db'
|
|
||||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
|
||||||
import { Box, IconButton, Stack, Typography } from '@mui/material'
|
|
||||||
import MoreIcon from '@mui/icons-material/MoreVert'
|
|
||||||
import { ACTIONS } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent'
|
|
||||||
import { formatTimestampDate } from '@/utils/helpers/date'
|
|
||||||
import { StyledButton } from './styled'
|
|
||||||
|
|
||||||
type ActivityListProps = {
|
|
||||||
history: DbHistory[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ActivityList: FC<ActivityListProps> = ({ history = [] }) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SectionTitle marginBottom={'0.5rem'}>Activity</SectionTitle>
|
|
||||||
<Box
|
|
||||||
flex={1}
|
|
||||||
overflow={'auto'}
|
|
||||||
display={'flex'}
|
|
||||||
flexDirection={'column'}
|
|
||||||
gap={'0.5rem'}
|
|
||||||
>
|
|
||||||
{history.map((h) => {
|
|
||||||
return (
|
|
||||||
<Stack>
|
|
||||||
<Box
|
|
||||||
width={'100%'}
|
|
||||||
display={'flex'}
|
|
||||||
gap={'0.5rem'}
|
|
||||||
alignItems={'center'}
|
|
||||||
>
|
|
||||||
<Typography flex={1} fontWeight={700}>
|
|
||||||
{ACTIONS[h.method] || h.method}
|
|
||||||
</Typography>
|
|
||||||
<StyledButton>
|
|
||||||
{h.allowed ? 'allow' : 'disallow'}
|
|
||||||
</StyledButton>
|
|
||||||
<IconButton>
|
|
||||||
<MoreIcon />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
<Typography variant='caption'>
|
|
||||||
{formatTimestampDate(h.timestamp)}
|
|
||||||
</Typography>
|
|
||||||
</Stack>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
@@ -1,37 +0,0 @@
|
|||||||
import { FC } from 'react'
|
|
||||||
import { Box, Typography } from '@mui/material'
|
|
||||||
import { DbPerm } from '@/modules/db'
|
|
||||||
import { ACTIONS } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent'
|
|
||||||
import { StyledMenuItem } from './styled'
|
|
||||||
import { formatTimestampDate } from '@/utils/helpers/date'
|
|
||||||
|
|
||||||
type ItemPermissionMenuProps = {
|
|
||||||
permission: DbPerm
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ItemPermissionMenu: FC<ItemPermissionMenuProps> = ({
|
|
||||||
permission,
|
|
||||||
}) => {
|
|
||||||
const { perm, value, timestamp } = permission || {}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<StyledMenuItem>
|
|
||||||
<Box
|
|
||||||
width={'100%'}
|
|
||||||
display={'flex'}
|
|
||||||
gap={'0.5rem'}
|
|
||||||
alignItems={'center'}
|
|
||||||
>
|
|
||||||
<Typography flex={1} fontWeight={700}>
|
|
||||||
{ACTIONS[perm] || perm}
|
|
||||||
</Typography>
|
|
||||||
<Typography textTransform={'capitalize'} variant='body2'>
|
|
||||||
{value === '1' ? 'allow' : 'disallow'}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Typography variant='body2'>
|
|
||||||
{formatTimestampDate(timestamp)}
|
|
||||||
</Typography>
|
|
||||||
</StyledMenuItem>
|
|
||||||
)
|
|
||||||
}
|
|
59
src/pages/AppPage/components/Permissions/ItemPermission.tsx
Normal file
59
src/pages/AppPage/components/Permissions/ItemPermission.tsx
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { Box, IconButton, Typography } from '@mui/material'
|
||||||
|
import { DbPerm } from '@/modules/db'
|
||||||
|
import { formatTimestampDate } from '@/utils/helpers/date'
|
||||||
|
import { ACTIONS } from '@/utils/consts'
|
||||||
|
import { StyledPermissionItem } from './styled'
|
||||||
|
import ClearRoundedIcon from '@mui/icons-material/ClearRounded'
|
||||||
|
import DoneRoundedIcon from '@mui/icons-material/DoneRounded'
|
||||||
|
import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded'
|
||||||
|
import { ItemPermissionMenu } from './ItemPermissionMenu'
|
||||||
|
import { useOpenMenu } from '@/hooks/useOpenMenu'
|
||||||
|
|
||||||
|
type ItemPermissionProps = {
|
||||||
|
permission: DbPerm
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => {
|
||||||
|
const { perm, value, timestamp, id } = permission || {}
|
||||||
|
|
||||||
|
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()
|
||||||
|
|
||||||
|
const isAllowed = value === '1'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<StyledPermissionItem>
|
||||||
|
<Box
|
||||||
|
display={'flex'}
|
||||||
|
flexDirection={'column'}
|
||||||
|
gap={'0.5rem'}
|
||||||
|
flex={1}
|
||||||
|
>
|
||||||
|
<Typography flex={1} fontWeight={700}>
|
||||||
|
{ACTIONS[perm] || perm}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant='body2'>
|
||||||
|
{formatTimestampDate(timestamp)}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
{isAllowed ? (
|
||||||
|
<DoneRoundedIcon htmlColor='green' />
|
||||||
|
) : (
|
||||||
|
<ClearRoundedIcon htmlColor='red' />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<IconButton onClick={handleOpen}>
|
||||||
|
<MoreVertRoundedIcon />
|
||||||
|
</IconButton>
|
||||||
|
</StyledPermissionItem>
|
||||||
|
<ItemPermissionMenu
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
open={open}
|
||||||
|
handleClose={handleClose}
|
||||||
|
permId={id}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@@ -0,0 +1,62 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { Menu, MenuItem, MenuProps } from '@mui/material'
|
||||||
|
import { ConfirmModal } from '@/shared/ConfirmModal/ConfirmModal'
|
||||||
|
import { swicCall } from '@/modules/swic'
|
||||||
|
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||||
|
|
||||||
|
type ItemPermissionMenuProps = {
|
||||||
|
permId: string
|
||||||
|
handleClose: () => void
|
||||||
|
} & MenuProps
|
||||||
|
|
||||||
|
export const ItemPermissionMenu: FC<ItemPermissionMenuProps> = ({
|
||||||
|
open,
|
||||||
|
anchorEl,
|
||||||
|
handleClose,
|
||||||
|
permId,
|
||||||
|
}) => {
|
||||||
|
const [showConfirm, setShowConfirm] = useState(false)
|
||||||
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
|
const handleShowConfirm = () => {
|
||||||
|
setShowConfirm(true)
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
const handleCloseConfirm = () => setShowConfirm(false)
|
||||||
|
|
||||||
|
const handleDeletePerm = async () => {
|
||||||
|
try {
|
||||||
|
await swicCall('deletePerm', permId)
|
||||||
|
notify('Permission successfully deleted!', 'success')
|
||||||
|
handleCloseConfirm()
|
||||||
|
} catch (error: any) {
|
||||||
|
notify(error?.message || 'Failed to delete permission', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Menu
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClose}
|
||||||
|
anchorOrigin={{
|
||||||
|
horizontal: 'left',
|
||||||
|
vertical: 'bottom',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem onClick={handleShowConfirm}>
|
||||||
|
Delete permission
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
<ConfirmModal
|
||||||
|
open={showConfirm}
|
||||||
|
onClose={handleCloseConfirm}
|
||||||
|
onCancel={handleCloseConfirm}
|
||||||
|
headingText='Delete permission'
|
||||||
|
description='Are you sure you want to delete this permission?'
|
||||||
|
onConfirm={handleDeletePerm}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
28
src/pages/AppPage/components/Permissions/Permissions.tsx
Normal file
28
src/pages/AppPage/components/Permissions/Permissions.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { FC } from 'react'
|
||||||
|
import { DbPerm } from '@/modules/db'
|
||||||
|
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
|
import { Box } from '@mui/material'
|
||||||
|
import { ItemPermission } from './ItemPermission'
|
||||||
|
|
||||||
|
type PermissionsProps = {
|
||||||
|
perms: DbPerm[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Permissions: FC<PermissionsProps> = ({ perms }) => {
|
||||||
|
return (
|
||||||
|
<Box width={'100%'} marginBottom={'1rem'} flex={1} overflow={'auto'}>
|
||||||
|
<SectionTitle marginBottom={'0.5rem'}>Permissions</SectionTitle>
|
||||||
|
<Box
|
||||||
|
flex={1}
|
||||||
|
overflow={'auto'}
|
||||||
|
display={'flex'}
|
||||||
|
flexDirection={'column'}
|
||||||
|
gap={'0.5rem'}
|
||||||
|
>
|
||||||
|
{perms.map((perm) => {
|
||||||
|
return <ItemPermission key={perm.id} permission={perm} />
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
11
src/pages/AppPage/components/Permissions/styled.tsx
Normal file
11
src/pages/AppPage/components/Permissions/styled.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Box, BoxProps, styled } from '@mui/material'
|
||||||
|
|
||||||
|
export const StyledPermissionItem = styled((props: BoxProps) => (
|
||||||
|
<Box {...props} />
|
||||||
|
))(() => ({
|
||||||
|
display: 'flex',
|
||||||
|
gap: '0.5rem',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '0.5rem',
|
||||||
|
}))
|
@@ -1,26 +0,0 @@
|
|||||||
import { DbPerm } from '@/modules/db'
|
|
||||||
import { Menu, MenuItem, MenuProps } from '@mui/material'
|
|
||||||
import { FC } from 'react'
|
|
||||||
import { ItemPermissionMenu } from './ItemPermissionMenu'
|
|
||||||
|
|
||||||
type PermissionsMenuProps = {
|
|
||||||
perms: DbPerm[]
|
|
||||||
} & MenuProps
|
|
||||||
|
|
||||||
export const PermissionsMenu: FC<PermissionsMenuProps> = ({
|
|
||||||
perms,
|
|
||||||
open,
|
|
||||||
anchorEl,
|
|
||||||
onClose,
|
|
||||||
}) => {
|
|
||||||
const isNoPerms = perms.length === 0
|
|
||||||
return (
|
|
||||||
<Menu open={open} anchorEl={anchorEl} onClose={onClose}>
|
|
||||||
{isNoPerms && <MenuItem>No permissions</MenuItem>}
|
|
||||||
{!isNoPerms &&
|
|
||||||
perms.map((perm) => (
|
|
||||||
<ItemPermissionMenu permission={perm} key={perm.id} />
|
|
||||||
))}
|
|
||||||
</Menu>
|
|
||||||
)
|
|
||||||
}
|
|
@@ -1,17 +1,5 @@
|
|||||||
import { Button } from '@/shared/Button/Button'
|
import { Button } from '@/shared/Button/Button'
|
||||||
import { MenuItem, MenuItemProps, styled } from '@mui/material'
|
import { styled } from '@mui/material'
|
||||||
|
|
||||||
export const StyledMenuItem = styled((props: MenuItemProps) => (
|
|
||||||
<MenuItem {...props} />
|
|
||||||
))(({ theme }) => ({
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
gap: '0.5rem',
|
|
||||||
'&:not(:first-of-type)': {
|
|
||||||
borderTop: '1px solid ' + theme.palette.secondary.main,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const StyledButton = styled(Button)({
|
export const StyledButton = styled(Button)({
|
||||||
textTransform: 'capitalize',
|
textTransform: 'capitalize',
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
import { Avatar, AvatarProps, styled } from '@mui/material'
|
||||||
import { styled } from '@mui/material'
|
|
||||||
|
|
||||||
export const PermissionMenuButton = styled((props: AppButtonProps) => (
|
export const StyledAppIcon = styled((props: AvatarProps) => (
|
||||||
<Button {...props} variant='outlined' fullWidth />
|
<Avatar {...props} variant='rounded' />
|
||||||
))(() => ({}))
|
))(() => ({
|
||||||
|
width: 70,
|
||||||
|
height: 70,
|
||||||
|
}))
|
||||||
|
18
src/pages/AppPage/utils.ts
Normal file
18
src/pages/AppPage/utils.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { DbHistory, db } from '@/modules/db'
|
||||||
|
|
||||||
|
export const getActivityHistoryQuerier = (appNpub: string) => () => {
|
||||||
|
if (!appNpub.trim().length) return []
|
||||||
|
|
||||||
|
const result = db.history
|
||||||
|
.where('appNpub')
|
||||||
|
.equals(appNpub)
|
||||||
|
.reverse()
|
||||||
|
.sortBy('timestamp')
|
||||||
|
.then(a => a.slice(0, 30))
|
||||||
|
// .limit(30)
|
||||||
|
// .toArray()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HistoryDefaultValue: DbHistory[] = []
|
@@ -1,12 +1,13 @@
|
|||||||
import { Fragment } from 'react'
|
import { Fragment } from 'react'
|
||||||
import { ItemKey } from './components/ItemKey'
|
import { ItemKey } from './components/ItemKey'
|
||||||
import { Box, Stack, Typography } from '@mui/material'
|
import { Box, Stack, Typography } from '@mui/material'
|
||||||
import { AddAccountButton } from './styled'
|
import { AddAccountButton, GetStartedButton, LearnMoreButton } from './styled'
|
||||||
import { useAppSelector } from '@/store/hooks/redux'
|
import { useAppSelector } from '@/store/hooks/redux'
|
||||||
import { selectKeys } from '@/store'
|
import { selectKeys } from '@/store'
|
||||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
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 { DOMAIN } from '@/utils/consts'
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
const keys = useAppSelector(selectKeys)
|
const keys = useAppSelector(selectKeys)
|
||||||
@@ -15,20 +16,43 @@ const HomePage = () => {
|
|||||||
const { handleOpen } = useModalSearchParams()
|
const { handleOpen } = useModalSearchParams()
|
||||||
const handleClickAddAccount = () => handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
const handleClickAddAccount = () => handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||||
|
|
||||||
|
const handleLearnMore = () => {
|
||||||
|
// @ts-ignore
|
||||||
|
window.open(`https://info.${DOMAIN}`, '_blank').focus();
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack maxHeight={'100%'} overflow={'auto'}>
|
<Stack maxHeight={'100%'} overflow={'auto'}>
|
||||||
<SectionTitle marginBottom={'0.5rem'}>
|
<SectionTitle marginBottom={'0.5rem'}>
|
||||||
{isNoKeys ? 'Welcome!' : 'Keys:'}
|
{isNoKeys ? 'Welcome' : 'Keys:'}
|
||||||
</SectionTitle>
|
</SectionTitle>
|
||||||
<Stack gap={'0.5rem'} overflow={'auto'}>
|
<Stack gap={'0.5rem'} overflow={'auto'}>
|
||||||
{isNoKeys && (
|
{isNoKeys && (
|
||||||
<Typography textAlign={'center'} variant='h5'>
|
<>
|
||||||
Hello, this is a key storage app for Nostr
|
<Typography textAlign={'left'} variant='h6' paddingTop='1em'>
|
||||||
</Typography>
|
Nsec.app is a novel key storage app for Nostr.
|
||||||
|
</Typography>
|
||||||
|
<GetStartedButton onClick={handleClickAddAccount}>
|
||||||
|
Get started
|
||||||
|
</GetStartedButton>
|
||||||
|
<Typography textAlign={'left'} variant='h6' paddingTop='2em'>
|
||||||
|
Your keys are stored in your browser and
|
||||||
|
can be used in many Nostr apps without the
|
||||||
|
need for a browser extension.
|
||||||
|
</Typography>
|
||||||
|
<LearnMoreButton onClick={handleLearnMore}>
|
||||||
|
Learn more
|
||||||
|
</LearnMoreButton>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{!isNoKeys && (
|
{!isNoKeys && (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<Box flex={1} overflow={'auto'} borderRadius={'8px'}>
|
<Box
|
||||||
|
flex={1}
|
||||||
|
overflow={'auto'}
|
||||||
|
borderRadius={'8px'}
|
||||||
|
padding={'0.25rem'}
|
||||||
|
>
|
||||||
{keys.map((key) => (
|
{keys.map((key) => (
|
||||||
<ItemKey {...key} key={key.npub} />
|
<ItemKey {...key} key={key.npub} />
|
||||||
))}
|
))}
|
||||||
|
@@ -40,8 +40,8 @@ const StyledKeyContainer = styled((props: StackProps) => (
|
|||||||
return {
|
return {
|
||||||
boxShadow:
|
boxShadow:
|
||||||
theme.palette.mode === 'dark'
|
theme.palette.mode === 'dark'
|
||||||
? '2px 2px 8px 0px rgba(92, 92, 92, 0.2)'
|
? '0px 1px 6px 0px rgba(92, 92, 92, 0.2)'
|
||||||
: '2px 2px 8px 0px rgba(0, 0, 0, 0.2)',
|
: '0px 1px 6px 0px rgba(0, 0, 0, 0.2)',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
padding: '0.5rem 1rem',
|
padding: '0.5rem 1rem',
|
||||||
background: theme.palette.background.paper,
|
background: theme.palette.background.paper,
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
||||||
import { styled } from '@mui/material'
|
import { styled } from '@mui/material'
|
||||||
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
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) => (
|
export const AddAccountButton = styled((props: AppButtonProps) => (
|
||||||
<Button {...props} startIcon={<PersonAddAltRoundedIcon />} />
|
<Button {...props} startIcon={<PersonAddAltRoundedIcon />} />
|
||||||
@@ -8,3 +10,17 @@ export const AddAccountButton = styled((props: AppButtonProps) => (
|
|||||||
alignSelf: 'center',
|
alignSelf: 'center',
|
||||||
padding: '0.35rem 1rem',
|
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',
|
||||||
|
}))
|
||||||
|
@@ -1,327 +1,89 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
||||||
import { SectionTitle } from '../../shared/SectionTitle/SectionTitle'
|
|
||||||
import { useAppSelector } from '../../store/hooks/redux'
|
import { useAppSelector } from '../../store/hooks/redux'
|
||||||
import {
|
|
||||||
askNotificationPermission,
|
|
||||||
getShortenNpub,
|
|
||||||
} from '../../utils/helpers/helpers'
|
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { fetchProfile } from '../../modules/nostr'
|
import { Stack } from '@mui/material'
|
||||||
import { Badge, Box, CircularProgress, Stack } from '@mui/material'
|
|
||||||
import { StyledIconButton } from './styled'
|
import { StyledIconButton } from './styled'
|
||||||
import { SettingsIcon, ShareIcon } from '@/assets'
|
import { SettingsIcon, ShareIcon } from '@/assets'
|
||||||
import { AppLink } from '@/shared/AppLink/AppLink'
|
|
||||||
import { MetaEvent } from '@/types/meta-event'
|
|
||||||
import { Apps } from './components/Apps'
|
import { Apps } from './components/Apps'
|
||||||
import { ModalConnectApp } from '@/components/Modal/ModalConnectApp/ModalConnectApp'
|
import { ModalConnectApp } from '@/components/Modal/ModalConnectApp/ModalConnectApp'
|
||||||
import { StyledInput } from './components/styled'
|
|
||||||
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
|
||||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
import { ModalSettings } from '@/components/Modal/ModalSettings/ModalSettings'
|
import { ModalSettings } from '@/components/Modal/ModalSettings/ModalSettings'
|
||||||
import { ModalExplanation } from '@/components/Modal/ModalExplanation/ModalExplanation'
|
import { ModalExplanation } from '@/components/Modal/ModalExplanation/ModalExplanation'
|
||||||
import { Warning } from '@/components/Warning/Warning'
|
|
||||||
import GppMaybeIcon from '@mui/icons-material/GppMaybe'
|
|
||||||
import { swicCall, swr } from '@/modules/swic'
|
|
||||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
|
||||||
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 { DbPending } from '@/modules/db'
|
import { useProfile } from './hooks/useProfile'
|
||||||
import { ACTION_TYPE } from '@/utils/consts'
|
import { useBackgroundSigning } from './hooks/useBackgroundSigning'
|
||||||
|
import { BackgroundSigningWarning } from './components/BackgroundSigningWarning'
|
||||||
export type IPendingsByAppNpub = {
|
import UserValueSection from './components/UserValueSection'
|
||||||
[appNpub: string]: {
|
import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
|
||||||
pending: DbPending[]
|
import { useLiveQuery } from 'dexie-react-hooks'
|
||||||
isConnected: boolean
|
import { checkNpubSyncQuerier } from './utils'
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type IShownConfirmModals = {
|
|
||||||
[reqId: string]: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const KeyPage = () => {
|
const KeyPage = () => {
|
||||||
const { apps, pending, perms } = useAppSelector((state) => state.content)
|
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
|
const { apps, pending, perms } = useAppSelector((state) => state.content)
|
||||||
|
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
|
||||||
|
|
||||||
const { handleOpen, getModalOpened } = useModalSearchParams()
|
const { handleOpen } = useModalSearchParams()
|
||||||
const isConfirmConnectModalOpened = getModalOpened(
|
|
||||||
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
|
||||||
)
|
|
||||||
const isConfirmEventModalOpened = getModalOpened(
|
|
||||||
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
|
||||||
)
|
|
||||||
|
|
||||||
const notify = useEnqueueSnackbar()
|
const { userNameWithPrefix } = useProfile(npub)
|
||||||
|
const { handleEnableBackground, showWarning, isEnabling } =
|
||||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
useBackgroundSigning()
|
||||||
const userName =
|
|
||||||
profile?.info?.name ||
|
|
||||||
profile?.info?.display_name ||
|
|
||||||
getShortenNpub(npub)
|
|
||||||
const userNameWithPrefix = userName + '@nsec.app'
|
|
||||||
|
|
||||||
const [showWarning, setShowWarning] = useState(false)
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
|
|
||||||
const filteredApps = apps.filter((a) => a.npub === npub)
|
const filteredApps = apps.filter((a) => a.npub === npub)
|
||||||
const filteredPendingReqs = pending.filter((p) => p.npub === npub)
|
const { prepareEventPendings } = useTriggerConfirmModal(
|
||||||
const filteredPerms = perms.filter((p) => p.npub === npub)
|
npub,
|
||||||
|
pending,
|
||||||
const npubConnectPerms = filteredPerms.filter(
|
perms,
|
||||||
(perm) => perm.perm === 'connect'
|
|
||||||
|| perm.perm === ACTION_TYPE.BASIC,
|
|
||||||
)
|
)
|
||||||
const excludeConnectPendings = filteredPendingReqs.filter(
|
|
||||||
(pr) => pr.method !== 'connect',
|
|
||||||
)
|
|
||||||
const connectPendings = filteredPendingReqs.filter(
|
|
||||||
(pr) => pr.method === 'connect',
|
|
||||||
)
|
|
||||||
|
|
||||||
const prepareEventPendings =
|
|
||||||
excludeConnectPendings.reduce<IPendingsByAppNpub>((acc, current) => {
|
|
||||||
const isConnected = npubConnectPerms.some(
|
|
||||||
(cp) => cp.appNpub === current.appNpub,
|
|
||||||
)
|
|
||||||
if (!acc[current.appNpub]) {
|
|
||||||
acc[current.appNpub] = {
|
|
||||||
pending: [current],
|
|
||||||
isConnected,
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}
|
|
||||||
acc[current.appNpub].pending.push(current)
|
|
||||||
acc[current.appNpub].isConnected = isConnected
|
|
||||||
return acc
|
|
||||||
}, {})
|
|
||||||
// console.log({
|
|
||||||
// pending,
|
|
||||||
// filteredPerms,
|
|
||||||
// npubConnectPerms,
|
|
||||||
// excludeConnectPendings,
|
|
||||||
// connectPendings,
|
|
||||||
// prepareEventPendings
|
|
||||||
// });
|
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetchProfile(npub)
|
|
||||||
setProfile(response as any)
|
|
||||||
} catch (e) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line
|
|
||||||
}, [npub])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load()
|
|
||||||
}, [load])
|
|
||||||
|
|
||||||
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
|
|
||||||
handleOpen(MODAL_PARAMS_KEYS.EXPLANATION, {
|
|
||||||
search: {
|
|
||||||
type,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleOpenConnectAppModal = () =>
|
const handleOpenConnectAppModal = () =>
|
||||||
handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||||
|
|
||||||
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
||||||
|
|
||||||
const checkBackgroundSigning = useCallback(async () => {
|
|
||||||
if (swr) {
|
|
||||||
const isBackgroundEnable = await swr.pushManager.getSubscription()
|
|
||||||
if (!isBackgroundEnable) setShowWarning(true)
|
|
||||||
else setShowWarning(false)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
checkBackgroundSigning()
|
|
||||||
}, [checkBackgroundSigning])
|
|
||||||
|
|
||||||
const handleEnableBackground = async () => {
|
|
||||||
try {
|
|
||||||
setIsLoading(true)
|
|
||||||
await askNotificationPermission()
|
|
||||||
const r = await swicCall('enablePush')
|
|
||||||
if (!r) return notify(`Failed to enable push subscription`, 'error')
|
|
||||||
notify('Enabled!', 'success')
|
|
||||||
checkBackgroundSigning()
|
|
||||||
setIsLoading(false)
|
|
||||||
} catch (e) {
|
|
||||||
notify(`Failed to enable push subscription`, 'error')
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const shownConnectModals = useRef<IShownConfirmModals>({})
|
|
||||||
|
|
||||||
const shownConfirmEventModals = useRef<IShownConfirmModals>({})
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
shownConnectModals.current = {}
|
|
||||||
shownConfirmEventModals.current = {}
|
|
||||||
}
|
|
||||||
}, [npub, pending.length])
|
|
||||||
|
|
||||||
const handleOpenConfirmConnectModal = useCallback(() => {
|
|
||||||
if (
|
|
||||||
!filteredPendingReqs.length ||
|
|
||||||
isConfirmEventModalOpened ||
|
|
||||||
isConfirmConnectModalOpened
|
|
||||||
)
|
|
||||||
return undefined
|
|
||||||
|
|
||||||
for (let i = 0; i < connectPendings.length; i++) {
|
|
||||||
const req = connectPendings[i]
|
|
||||||
if (shownConnectModals.current[req.id]) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
shownConnectModals.current[req.id] = true
|
|
||||||
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
|
||||||
search: {
|
|
||||||
appNpub: req.appNpub,
|
|
||||||
reqId: req.id,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
connectPendings,
|
|
||||||
filteredPendingReqs.length,
|
|
||||||
handleOpen,
|
|
||||||
isConfirmEventModalOpened,
|
|
||||||
isConfirmConnectModalOpened,
|
|
||||||
])
|
|
||||||
|
|
||||||
const handleOpenConfirmEventModal = useCallback(() => {
|
|
||||||
if (!filteredPendingReqs.length || connectPendings.length)
|
|
||||||
return undefined
|
|
||||||
|
|
||||||
for (let i = 0; i < Object.keys(prepareEventPendings).length; i++) {
|
|
||||||
const appNpub = Object.keys(prepareEventPendings)[i]
|
|
||||||
|
|
||||||
if (
|
|
||||||
shownConfirmEventModals.current[appNpub] ||
|
|
||||||
!prepareEventPendings[appNpub].isConnected
|
|
||||||
) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
shownConfirmEventModals.current[appNpub] = true
|
|
||||||
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
|
||||||
search: {
|
|
||||||
appNpub,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
connectPendings.length,
|
|
||||||
filteredPendingReqs.length,
|
|
||||||
handleOpen,
|
|
||||||
prepareEventPendings,
|
|
||||||
])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleOpenConfirmEventModal()
|
|
||||||
}, [handleOpenConfirmEventModal])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleOpenConfirmConnectModal()
|
|
||||||
}, [handleOpenConfirmConnectModal])
|
|
||||||
|
|
||||||
const renderUserValueSection = (
|
|
||||||
title: string,
|
|
||||||
value: string,
|
|
||||||
explanationType: EXPLANATION_MODAL_KEYS,
|
|
||||||
copyValue: string,
|
|
||||||
) => {
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Stack
|
|
||||||
direction={'row'}
|
|
||||||
alignItems={'center'}
|
|
||||||
justifyContent={'space-between'}
|
|
||||||
marginBottom={'0.5rem'}
|
|
||||||
>
|
|
||||||
<SectionTitle>{title}</SectionTitle>
|
|
||||||
<AppLink
|
|
||||||
title='What is this?'
|
|
||||||
onClick={() =>
|
|
||||||
handleOpenExplanationModal(explanationType)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
<StyledInput
|
|
||||||
value={value}
|
|
||||||
readOnly
|
|
||||||
endAdornment={<InputCopyButton value={copyValue} />}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack gap={'1rem'} height={'100%'}>
|
<Stack gap={'1rem'} height={'100%'}>
|
||||||
{showWarning && (
|
{showWarning && (
|
||||||
<Warning
|
<BackgroundSigningWarning
|
||||||
message={
|
isEnabling={isEnabling}
|
||||||
<Stack
|
onEnableBackSigning={handleEnableBackground}
|
||||||
direction={'row'}
|
|
||||||
alignItems={'center'}
|
|
||||||
gap={'1rem'}
|
|
||||||
>
|
|
||||||
Please enable push notifications{' '}
|
|
||||||
{isLoading ? (
|
|
||||||
<CircularProgress size={'1.5rem'} />
|
|
||||||
) : null}
|
|
||||||
</Stack>
|
|
||||||
}
|
|
||||||
Icon={<GppMaybeIcon htmlColor='white' />}
|
|
||||||
onClick={isLoading ? undefined : handleEnableBackground}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{renderUserValueSection(
|
<UserValueSection
|
||||||
'Your login',
|
title='Your login'
|
||||||
userNameWithPrefix,
|
value={userNameWithPrefix}
|
||||||
EXPLANATION_MODAL_KEYS.NPUB,
|
copyValue={npub + '@nsec.app'}
|
||||||
npub + '@nsec.app',
|
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||||
)}
|
/>
|
||||||
{renderUserValueSection(
|
<UserValueSection
|
||||||
'Your NPUB',
|
title='Your NPUB'
|
||||||
npub,
|
value={npub}
|
||||||
EXPLANATION_MODAL_KEYS.NPUB,
|
copyValue={npub}
|
||||||
npub,
|
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||||
)}
|
/>
|
||||||
|
|
||||||
<Stack direction={'row'} gap={'0.75rem'}>
|
<Stack direction={'row'} gap={'0.75rem'}>
|
||||||
<StyledIconButton onClick={handleOpenConnectAppModal}>
|
<StyledIconButton onClick={handleOpenConnectAppModal}>
|
||||||
<ShareIcon />
|
<ShareIcon />
|
||||||
Connect app
|
Connect app
|
||||||
</StyledIconButton>
|
</StyledIconButton>
|
||||||
<Badge sx={{ flex: 1 }} badgeContent={''} color='error'>
|
|
||||||
<StyledIconButton
|
<StyledIconButton
|
||||||
bgcolor_variant='secondary'
|
bgcolor_variant='secondary'
|
||||||
onClick={handleOpenSettingsModal}
|
onClick={handleOpenSettingsModal}
|
||||||
>
|
withBadge={!isSynced}
|
||||||
<SettingsIcon />
|
>
|
||||||
Settings
|
<SettingsIcon />
|
||||||
</StyledIconButton>
|
Settings
|
||||||
</Badge>
|
</StyledIconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Apps apps={filteredApps} npub={npub} />
|
<Apps apps={filteredApps} npub={npub} />
|
||||||
</Stack>
|
</Stack>
|
||||||
<ModalConnectApp />
|
<ModalConnectApp />
|
||||||
<ModalSettings />
|
<ModalSettings isSynced={isSynced} />
|
||||||
<ModalExplanation />
|
<ModalExplanation />
|
||||||
<ModalConfirmConnect />
|
<ModalConfirmConnect />
|
||||||
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
|
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
|
||||||
|
27
src/pages/KeyPage/components/BackgroundSigningWarning.tsx
Normal file
27
src/pages/KeyPage/components/BackgroundSigningWarning.tsx
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import { Warning } from '@/components/Warning/Warning'
|
||||||
|
import { CircularProgress, Stack } from '@mui/material'
|
||||||
|
import GppMaybeIcon from '@mui/icons-material/GppMaybe'
|
||||||
|
|
||||||
|
type BackgroundSigningWarningProps = {
|
||||||
|
isEnabling: boolean
|
||||||
|
onEnableBackSigning: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({
|
||||||
|
isEnabling,
|
||||||
|
onEnableBackSigning,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Warning
|
||||||
|
message={
|
||||||
|
<Stack direction={'row'} alignItems={'center'} gap={'1rem'}>
|
||||||
|
Please enable push notifications{' '}
|
||||||
|
{isEnabling ? <CircularProgress size={'1.5rem'} /> : null}
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
Icon={<GppMaybeIcon htmlColor='white' />}
|
||||||
|
onClick={isEnabling ? undefined : onEnableBackSigning}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
55
src/pages/KeyPage/components/UserValueSection.tsx
Normal file
55
src/pages/KeyPage/components/UserValueSection.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import { Box, Stack } from '@mui/material'
|
||||||
|
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||||
|
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||||
|
import { AppLink } from '@/shared/AppLink/AppLink'
|
||||||
|
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
||||||
|
import { StyledInput } from '../styled'
|
||||||
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
|
|
||||||
|
type UserValueSectionProps = {
|
||||||
|
title: string
|
||||||
|
value: string
|
||||||
|
explanationType: EXPLANATION_MODAL_KEYS
|
||||||
|
copyValue: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserValueSection: FC<UserValueSectionProps> = ({
|
||||||
|
title,
|
||||||
|
value,
|
||||||
|
explanationType,
|
||||||
|
copyValue,
|
||||||
|
}) => {
|
||||||
|
const { handleOpen } = useModalSearchParams()
|
||||||
|
|
||||||
|
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
|
||||||
|
handleOpen(MODAL_PARAMS_KEYS.EXPLANATION, {
|
||||||
|
search: {
|
||||||
|
type,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Stack
|
||||||
|
direction={'row'}
|
||||||
|
alignItems={'center'}
|
||||||
|
justifyContent={'space-between'}
|
||||||
|
marginBottom={'0.5rem'}
|
||||||
|
>
|
||||||
|
<SectionTitle>{title}</SectionTitle>
|
||||||
|
<AppLink
|
||||||
|
title='What is this?'
|
||||||
|
onClick={() => handleOpenExplanationModal(explanationType)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<StyledInput
|
||||||
|
value={value}
|
||||||
|
readOnly
|
||||||
|
endAdornment={<InputCopyButton value={copyValue} />}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserValueSection
|
@@ -31,4 +31,10 @@ export const StyledItemAppContainer = styled(
|
|||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
boxShadow: 'none',
|
boxShadow: 'none',
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
|
background: theme.palette.backgroundSecondary.default,
|
||||||
|
borderRadius: '12px',
|
||||||
|
padding: '0.5rem 1rem',
|
||||||
|
':hover': {
|
||||||
|
background: `${theme.palette.backgroundSecondary.default}95`,
|
||||||
|
},
|
||||||
}))
|
}))
|
||||||
|
40
src/pages/KeyPage/hooks/useBackgroundSigning.ts
Normal file
40
src/pages/KeyPage/hooks/useBackgroundSigning.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||||
|
import { swicCall, swr } from '@/modules/swic'
|
||||||
|
import { askNotificationPermission } from '@/utils/helpers/helpers'
|
||||||
|
import { useState, useEffect, useCallback } from 'react'
|
||||||
|
|
||||||
|
export const useBackgroundSigning = () => {
|
||||||
|
const [showWarning, setShowWarning] = useState(false)
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const notify = useEnqueueSnackbar()
|
||||||
|
|
||||||
|
const checkBackgroundSigning = useCallback(async () => {
|
||||||
|
if (!swr) return undefined
|
||||||
|
const isBackgroundEnable = await swr.pushManager.getSubscription()
|
||||||
|
setShowWarning(!isBackgroundEnable)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleEnableBackground = useCallback(async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
try {
|
||||||
|
await askNotificationPermission()
|
||||||
|
const result = await swicCall('enablePush')
|
||||||
|
if (!result) throw new Error('Failed to activate the push subscription')
|
||||||
|
notify('Push notifications enabled!', 'success')
|
||||||
|
setShowWarning(false)
|
||||||
|
} catch (error: any) {
|
||||||
|
notify(
|
||||||
|
`Failed to enable push subscription: ${error}`,
|
||||||
|
'error',
|
||||||
|
)
|
||||||
|
}
|
||||||
|
setIsLoading(false)
|
||||||
|
checkBackgroundSigning()
|
||||||
|
}, [notify, checkBackgroundSigning])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
checkBackgroundSigning()
|
||||||
|
}, [checkBackgroundSigning])
|
||||||
|
|
||||||
|
return { showWarning, isEnabling: isLoading, handleEnableBackground }
|
||||||
|
}
|
29
src/pages/KeyPage/hooks/useProfile.ts
Normal file
29
src/pages/KeyPage/hooks/useProfile.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { fetchProfile } from '@/modules/nostr'
|
||||||
|
import { MetaEvent } from '@/types/meta-event'
|
||||||
|
import { getProfileUsername } from '@/utils/helpers/helpers'
|
||||||
|
|
||||||
|
export const useProfile = (npub: string) => {
|
||||||
|
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||||
|
|
||||||
|
const userName = getProfileUsername(profile, npub)
|
||||||
|
const userNameWithPrefix = userName + '@nsec.app'
|
||||||
|
|
||||||
|
const loadProfile = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetchProfile(npub)
|
||||||
|
setProfile(response)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch profile:', error)
|
||||||
|
}
|
||||||
|
}, [npub])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadProfile()
|
||||||
|
}, [loadProfile])
|
||||||
|
|
||||||
|
return {
|
||||||
|
profile,
|
||||||
|
userNameWithPrefix,
|
||||||
|
}
|
||||||
|
}
|
143
src/pages/KeyPage/hooks/useTriggerConfirmModal.ts
Normal file
143
src/pages/KeyPage/hooks/useTriggerConfirmModal.ts
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||||
|
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'
|
||||||
|
|
||||||
|
export type IPendingsByAppNpub = {
|
||||||
|
[appNpub: string]: {
|
||||||
|
pending: DbPending[]
|
||||||
|
isConnected: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type IShownConfirmModals = {
|
||||||
|
[reqId: string]: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useTriggerConfirmModal = (
|
||||||
|
npub: string,
|
||||||
|
pending: DbPending[],
|
||||||
|
perms: DbPerm[],
|
||||||
|
) => {
|
||||||
|
const { handleOpen, getModalOpened } = useModalSearchParams()
|
||||||
|
|
||||||
|
const isConfirmConnectModalOpened = getModalOpened(
|
||||||
|
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
||||||
|
)
|
||||||
|
const isConfirmEventModalOpened = getModalOpened(
|
||||||
|
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
const filteredPendingReqs = pending.filter((p) => p.npub === npub)
|
||||||
|
const filteredPerms = perms.filter((p) => p.npub === npub)
|
||||||
|
|
||||||
|
const npubConnectPerms = filteredPerms.filter(
|
||||||
|
(perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC,
|
||||||
|
)
|
||||||
|
const excludeConnectPendings = filteredPendingReqs.filter(
|
||||||
|
(pr) => pr.method !== 'connect',
|
||||||
|
)
|
||||||
|
const connectPendings = filteredPendingReqs.filter(
|
||||||
|
(pr) => pr.method === 'connect',
|
||||||
|
)
|
||||||
|
|
||||||
|
const prepareEventPendings =
|
||||||
|
excludeConnectPendings.reduce<IPendingsByAppNpub>((acc, current) => {
|
||||||
|
const isConnected = npubConnectPerms.some(
|
||||||
|
(cp) => cp.appNpub === current.appNpub,
|
||||||
|
)
|
||||||
|
if (!acc[current.appNpub]) {
|
||||||
|
acc[current.appNpub] = {
|
||||||
|
pending: [current],
|
||||||
|
isConnected,
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}
|
||||||
|
acc[current.appNpub].pending.push(current)
|
||||||
|
acc[current.appNpub].isConnected = isConnected
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
const shownConnectModals = useRef<IShownConfirmModals>({})
|
||||||
|
const shownConfirmEventModals = useRef<IShownConfirmModals>({})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
shownConnectModals.current = {}
|
||||||
|
shownConfirmEventModals.current = {}
|
||||||
|
}
|
||||||
|
}, [npub, pending.length])
|
||||||
|
|
||||||
|
const handleOpenConfirmConnectModal = useCallback(() => {
|
||||||
|
if (
|
||||||
|
!filteredPendingReqs.length ||
|
||||||
|
isConfirmEventModalOpened ||
|
||||||
|
isConfirmConnectModalOpened
|
||||||
|
)
|
||||||
|
return undefined
|
||||||
|
|
||||||
|
for (let i = 0; i < connectPendings.length; i++) {
|
||||||
|
const req = connectPendings[i]
|
||||||
|
if (shownConnectModals.current[req.id]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
shownConnectModals.current[req.id] = true
|
||||||
|
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||||
|
search: {
|
||||||
|
appNpub: req.appNpub,
|
||||||
|
reqId: req.id,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
connectPendings,
|
||||||
|
filteredPendingReqs.length,
|
||||||
|
handleOpen,
|
||||||
|
isConfirmEventModalOpened,
|
||||||
|
isConfirmConnectModalOpened,
|
||||||
|
])
|
||||||
|
|
||||||
|
const handleOpenConfirmEventModal = useCallback(() => {
|
||||||
|
if (!filteredPendingReqs.length || connectPendings.length)
|
||||||
|
return undefined
|
||||||
|
|
||||||
|
for (let i = 0; i < Object.keys(prepareEventPendings).length; i++) {
|
||||||
|
const appNpub = Object.keys(prepareEventPendings)[i]
|
||||||
|
|
||||||
|
if (
|
||||||
|
shownConfirmEventModals.current[appNpub] ||
|
||||||
|
!prepareEventPendings[appNpub].isConnected
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
shownConfirmEventModals.current[appNpub] = true
|
||||||
|
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
||||||
|
search: {
|
||||||
|
appNpub,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
connectPendings.length,
|
||||||
|
filteredPendingReqs.length,
|
||||||
|
handleOpen,
|
||||||
|
prepareEventPendings,
|
||||||
|
])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleOpenConfirmEventModal()
|
||||||
|
}, [handleOpenConfirmEventModal])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
handleOpenConfirmConnectModal()
|
||||||
|
}, [handleOpenConfirmConnectModal])
|
||||||
|
|
||||||
|
return {
|
||||||
|
prepareEventPendings,
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,23 @@
|
|||||||
import { Input, InputProps } from '@/shared/Input/Input'
|
import { Input, InputProps } from '@/shared/Input/Input'
|
||||||
import { Box, Button, ButtonProps, styled } from '@mui/material'
|
import { Box, Button, ButtonProps, styled, Badge } from '@mui/material'
|
||||||
|
|
||||||
type StyledIconButtonProps = ButtonProps & {
|
type StyledIconButtonProps = ButtonProps & {
|
||||||
bgcolor_variant?: 'primary' | 'secondary'
|
bgcolor_variant?: 'primary' | 'secondary'
|
||||||
|
withBadge?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StyledIconButton = styled((props: StyledIconButtonProps) => (
|
export const StyledIconButton = styled(
|
||||||
<Button {...props} />
|
({ withBadge, ...props }: StyledIconButtonProps) => {
|
||||||
))(({ bgcolor_variant = 'primary', theme }) => {
|
if (withBadge) {
|
||||||
|
return (
|
||||||
|
<Badge sx={{ flex: 1 }} badgeContent={''} color='error'>
|
||||||
|
<Button {...props} />
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <Button {...props} />
|
||||||
|
},
|
||||||
|
)(({ bgcolor_variant = 'primary', theme }) => {
|
||||||
const isPrimary = bgcolor_variant === 'primary'
|
const isPrimary = bgcolor_variant === 'primary'
|
||||||
return {
|
return {
|
||||||
flex: '1',
|
flex: '1',
|
||||||
|
6
src/pages/KeyPage/utils.ts
Normal file
6
src/pages/KeyPage/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { db } from '@/modules/db'
|
||||||
|
|
||||||
|
export const checkNpubSyncQuerier = (npub: string) => async () => {
|
||||||
|
const count = await db.syncHistory.where('npub').equals(npub).count()
|
||||||
|
return count > 0
|
||||||
|
}
|
@@ -15,7 +15,7 @@ const WelcomePage = () => {
|
|||||||
const npubInputRef = useRef<HTMLInputElement | null>(null)
|
const npubInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
const passwordInputRef = useRef<HTMLInputElement | null>(null)
|
const passwordInputRef = useRef<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
// if (isKeysExists) return <Navigate to={'/home'} />
|
if (isKeysExists) return <Navigate to={'/home'} />
|
||||||
|
|
||||||
async function generateKey() {
|
async function generateKey() {
|
||||||
try {
|
try {
|
||||||
|
@@ -21,7 +21,7 @@ const AppRoutes = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/' element={<Layout />}>
|
<Route path='/' element={<Layout />}>
|
||||||
<Route path='/' element={<Navigate to={'/home'} />} />
|
<Route path='/' element={<Navigate to={'/home'} />} />
|
||||||
<Route path='/welcome' element={<WelcomePage />} />
|
{/* <Route path='/welcome' element={<WelcomePage />} /> */}
|
||||||
<Route path='/home' element={<HomePage />} />
|
<Route path='/home' element={<HomePage />} />
|
||||||
<Route path='/key/:npub' element={<KeyPage />} />
|
<Route path='/key/:npub' element={<KeyPage />} />
|
||||||
<Route
|
<Route
|
||||||
|
@@ -43,5 +43,11 @@ const StyledButton = styled(
|
|||||||
background: theme.palette.primary.main,
|
background: theme.palette.primary.main,
|
||||||
},
|
},
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
|
':disabled': {
|
||||||
|
'&.button:is(:hover, :active, &)': {
|
||||||
|
background: theme.palette.backgroundSecondary.default,
|
||||||
|
},
|
||||||
|
color: theme.palette.backgroundSecondary.paper,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@@ -34,4 +34,5 @@ const StyledCheckbox = styled(
|
|||||||
),
|
),
|
||||||
)(() => ({
|
)(() => ({
|
||||||
'& .MuiSvgIcon-root': { fontSize: '1.5rem' },
|
'& .MuiSvgIcon-root': { fontSize: '1.5rem' },
|
||||||
|
marginLeft: '-10px',
|
||||||
}))
|
}))
|
||||||
|
65
src/shared/ConfirmModal/ConfirmModal.tsx
Normal file
65
src/shared/ConfirmModal/ConfirmModal.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogProps,
|
||||||
|
DialogTitle,
|
||||||
|
Slide,
|
||||||
|
} from '@mui/material'
|
||||||
|
import { Button } from '../Button/Button'
|
||||||
|
import { TransitionProps } from '@mui/material/transitions'
|
||||||
|
import { StyledDialogContentText } from './styled'
|
||||||
|
|
||||||
|
const Transition = React.forwardRef(function Transition(
|
||||||
|
props: TransitionProps & {
|
||||||
|
children: React.ReactElement<any, any>
|
||||||
|
},
|
||||||
|
ref: React.Ref<unknown>,
|
||||||
|
) {
|
||||||
|
return <Slide direction='up' ref={ref} {...props} />
|
||||||
|
})
|
||||||
|
|
||||||
|
type ConfirmModalProps = {
|
||||||
|
onConfirm: () => void
|
||||||
|
onCancel: () => void
|
||||||
|
headingText: string
|
||||||
|
description?: string
|
||||||
|
} & DialogProps
|
||||||
|
|
||||||
|
export const ConfirmModal: FC<ConfirmModalProps> = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
onConfirm,
|
||||||
|
onCancel,
|
||||||
|
headingText = 'Confirm',
|
||||||
|
description,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
TransitionComponent={Transition}
|
||||||
|
keepMounted
|
||||||
|
onClose={onClose}
|
||||||
|
sx={{ zIndex: 1302 }}
|
||||||
|
PaperProps={{
|
||||||
|
sx: {
|
||||||
|
borderRadius: '10px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogTitle fontWeight={600} fontSize={'1.5rem'}>
|
||||||
|
{headingText}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<StyledDialogContentText>{description}</StyledDialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button varianttype='secondary' onClick={onCancel}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onConfirm}>Confirm</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
11
src/shared/ConfirmModal/styled.tsx
Normal file
11
src/shared/ConfirmModal/styled.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import {
|
||||||
|
DialogContentText,
|
||||||
|
DialogContentTextProps,
|
||||||
|
styled,
|
||||||
|
} from '@mui/material'
|
||||||
|
|
||||||
|
export const StyledDialogContentText = styled(
|
||||||
|
(props: DialogContentTextProps) => <DialogContentText {...props} />,
|
||||||
|
)(({ theme }) => ({
|
||||||
|
color: theme.palette.primary.main,
|
||||||
|
}))
|
25
src/shared/IOSBackButton/IOSBackButton.tsx
Normal file
25
src/shared/IOSBackButton/IOSBackButton.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import React, { FC } from 'react'
|
||||||
|
import { ButtonProps } from '@mui/material'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
import useIsIOS from '@/hooks/useIsIOS'
|
||||||
|
import { StyledButton } from './styled'
|
||||||
|
|
||||||
|
type IOSBackButtonProps = ButtonProps & {
|
||||||
|
onNavigate?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IOSBackButton: FC<IOSBackButtonProps> = ({ onNavigate }) => {
|
||||||
|
const isIOS = useIsIOS()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleNavigateBack = () => {
|
||||||
|
if (onNavigate && typeof onNavigate === 'function') {
|
||||||
|
return onNavigate()
|
||||||
|
}
|
||||||
|
navigate(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isIOS) return null
|
||||||
|
|
||||||
|
return <StyledButton onClick={handleNavigateBack}>Back</StyledButton>
|
||||||
|
}
|
21
src/shared/IOSBackButton/styled.tsx
Normal file
21
src/shared/IOSBackButton/styled.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Button, ButtonProps, styled } from '@mui/material'
|
||||||
|
import GoBackIcon from '@mui/icons-material/KeyboardBackspaceRounded'
|
||||||
|
|
||||||
|
export const StyledButton = styled((props: ButtonProps) => (
|
||||||
|
<Button
|
||||||
|
{...props}
|
||||||
|
startIcon={<GoBackIcon />}
|
||||||
|
classes={{
|
||||||
|
startIcon: 'icon',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))(() => ({
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
borderRadius: '8px',
|
||||||
|
'&:is(:hover,:active)': {
|
||||||
|
textDecoration: 'underline',
|
||||||
|
},
|
||||||
|
'& .icon': {
|
||||||
|
marginRight: '5px',
|
||||||
|
},
|
||||||
|
}))
|
@@ -11,6 +11,7 @@ import CloseRoundedIcon from '@mui/icons-material/CloseRounded'
|
|||||||
|
|
||||||
type ModalProps = DialogProps & {
|
type ModalProps = DialogProps & {
|
||||||
withCloseButton?: boolean
|
withCloseButton?: boolean
|
||||||
|
fixedHeight?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const Transition = forwardRef(function Transition(
|
const Transition = forwardRef(function Transition(
|
||||||
@@ -27,10 +28,12 @@ export const Modal: FC<ModalProps> = ({
|
|||||||
title,
|
title,
|
||||||
onClose,
|
onClose,
|
||||||
withCloseButton = true,
|
withCloseButton = true,
|
||||||
|
fixedHeight,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<StyledDialog
|
<StyledDialog
|
||||||
|
fixedHeight={fixedHeight}
|
||||||
{...props}
|
{...props}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
TransitionComponent={Transition}
|
TransitionComponent={Transition}
|
||||||
@@ -46,7 +49,13 @@ export const Modal: FC<ModalProps> = ({
|
|||||||
</StyledCloseButtonWrapper>
|
</StyledCloseButtonWrapper>
|
||||||
)}
|
)}
|
||||||
{title && <StyledDialogTitle>{title}</StyledDialogTitle>}
|
{title && <StyledDialogTitle>{title}</StyledDialogTitle>}
|
||||||
<StyledDialogContent>{children}</StyledDialogContent>
|
<StyledDialogContent
|
||||||
|
sx={{
|
||||||
|
paddingTop: withCloseButton ? '1.5rem' : '0',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</StyledDialogContent>
|
||||||
</StyledDialog>
|
</StyledDialog>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -10,37 +10,43 @@ import {
|
|||||||
styled,
|
styled,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
|
|
||||||
export const StyledDialog = styled((props: DialogProps) => (
|
export const StyledDialog = styled(
|
||||||
<Dialog
|
(props: DialogProps & { fixedHeight?: string }) => (
|
||||||
{...props}
|
<Dialog
|
||||||
classes={{
|
{...props}
|
||||||
container: 'container',
|
classes={{
|
||||||
paper: 'paper',
|
container: 'container',
|
||||||
}}
|
paper: 'paper',
|
||||||
slotProps={{
|
}}
|
||||||
backdrop: {
|
slotProps={{
|
||||||
sx: {
|
backdrop: {
|
||||||
backdropFilter: 'blur(2px)',
|
sx: {
|
||||||
|
backdropFilter: 'blur(2px)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}}
|
||||||
}}
|
fullWidth
|
||||||
fullWidth
|
/>
|
||||||
/>
|
),
|
||||||
))(({ theme }) => ({
|
)(({ theme, fixedHeight = '' }) => {
|
||||||
'& .container': {
|
const fixedHeightStyles = fixedHeight ? { height: fixedHeight } : {}
|
||||||
alignItems: 'flex-end',
|
return {
|
||||||
},
|
'& .container': {
|
||||||
'& .paper': {
|
alignItems: 'flex-end',
|
||||||
margin: '0',
|
},
|
||||||
width: '100%',
|
'& .paper': {
|
||||||
borderTopLeftRadius: '2rem',
|
margin: '0',
|
||||||
borderTopRightRadius: '2rem',
|
width: '100%',
|
||||||
background:
|
borderTopLeftRadius: '2rem',
|
||||||
theme.palette.mode === 'light'
|
borderTopRightRadius: '2rem',
|
||||||
? '#fff'
|
background:
|
||||||
: theme.palette.secondary.main,
|
theme.palette.mode === 'light'
|
||||||
},
|
? '#fff'
|
||||||
}))
|
: theme.palette.secondary.main,
|
||||||
|
...fixedHeightStyles,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
export const StyledDialogTitle = styled((props: DialogTitleProps) => (
|
export const StyledDialogTitle = styled((props: DialogTitleProps) => (
|
||||||
<DialogTitle {...props} variant='h5' />
|
<DialogTitle {...props} variant='h5' />
|
||||||
@@ -56,6 +62,8 @@ export const StyledDialogContent = styled((props: DialogContentProps) => (
|
|||||||
))(() => {
|
))(() => {
|
||||||
return {
|
return {
|
||||||
padding: '0 1rem 1rem',
|
padding: '0 1rem 1rem',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@@ -8,6 +8,7 @@ export enum MODAL_PARAMS_KEYS {
|
|||||||
SIGN_UP = 'sign-up',
|
SIGN_UP = 'sign-up',
|
||||||
CONFIRM_CONNECT = 'confirm-connect',
|
CONFIRM_CONNECT = 'confirm-connect',
|
||||||
CONFIRM_EVENT = 'confirm-event',
|
CONFIRM_EVENT = 'confirm-event',
|
||||||
|
ACTIVITY = 'activity',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum EXPLANATION_MODAL_KEYS {
|
export enum EXPLANATION_MODAL_KEYS {
|
||||||
|
@@ -1,9 +1,18 @@
|
|||||||
export const NIP46_RELAYS = ['wss://relay.login.nostrapps.org']
|
export const NIP46_RELAYS = ['wss://relay.login.nostrapps.org']
|
||||||
export const NOAUTHD_URL = process.env.REACT_APP_NOAUTHD_URL
|
export const NOAUTHD_URL = process.env.REACT_APP_NOAUTHD_URL
|
||||||
export const WEB_PUSH_PUBKEY = process.env.REACT_APP_WEB_PUSH_PUBKEY
|
export const WEB_PUSH_PUBKEY = process.env.REACT_APP_WEB_PUSH_PUBKEY
|
||||||
|
export const DOMAIN = process.env.REACT_APP_DOMAIN
|
||||||
|
|
||||||
export enum ACTION_TYPE {
|
export enum ACTION_TYPE {
|
||||||
BASIC = 'basic',
|
BASIC = 'basic',
|
||||||
ADVANCED = 'advanced',
|
ADVANCED = 'advanced',
|
||||||
CUSTOM = 'custom',
|
CUSTOM = 'custom',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const ACTIONS: { [type: string]: string } = {
|
||||||
|
get_public_key: 'Get public key',
|
||||||
|
sign_event: 'Sign event',
|
||||||
|
connect: 'Connect',
|
||||||
|
nip04_encrypt: 'Encrypt message',
|
||||||
|
nip04_decrypt: 'Decrypt message',
|
||||||
|
}
|
||||||
|
@@ -1,11 +1,7 @@
|
|||||||
import { nip19 } from 'nostr-tools'
|
import { nip19 } from 'nostr-tools'
|
||||||
import { ACTION_TYPE, NIP46_RELAYS } from '../consts'
|
import { ACTION_TYPE, NIP46_RELAYS } from '../consts'
|
||||||
import { DbPending } from '@/modules/db'
|
import { DbPending } from '@/modules/db'
|
||||||
|
import { MetaEvent } from '@/types/meta-event'
|
||||||
export async function log(s: string) {
|
|
||||||
const log = document.getElementById('log')
|
|
||||||
if (log) log.innerHTML = s
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function call(cb: () => any) {
|
export async function call(cb: () => any) {
|
||||||
try {
|
try {
|
||||||
@@ -19,6 +15,14 @@ export const getShortenNpub = (npub = '') => {
|
|||||||
return npub.substring(0, 10) + '...' + npub.slice(-4)
|
return npub.substring(0, 10) + '...' + npub.slice(-4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = '') => {
|
||||||
if (!npub) return ''
|
if (!npub) return ''
|
||||||
const { data: pubkey } = nip19.decode(npub)
|
const { data: pubkey } = nip19.decode(npub)
|
||||||
@@ -29,11 +33,9 @@ export async function askNotificationPermission() {
|
|||||||
return new Promise<void>((ok, rej) => {
|
return new Promise<void>((ok, rej) => {
|
||||||
// Let's check if the browser supports notifications
|
// Let's check if the browser supports notifications
|
||||||
if (!('Notification' in window)) {
|
if (!('Notification' in window)) {
|
||||||
log('This browser does not support notifications.')
|
rej('This browser does not support notifications.')
|
||||||
rej()
|
|
||||||
} else {
|
} else {
|
||||||
Notification.requestPermission().then(() => {
|
Notification.requestPermission().then(() => {
|
||||||
log('notifications perm' + Notification.permission)
|
|
||||||
if (Notification.permission === 'granted') ok()
|
if (Notification.permission === 'granted') ok()
|
||||||
else rej()
|
else rej()
|
||||||
})
|
})
|
||||||
|
Reference in New Issue
Block a user