Merge pull request #5 from nostrband/fix/modal-replace-notifs
Fix/modal replace notifs
This commit is contained in:
commit
878bae6c2f
3
.env
3
.env
@ -2,4 +2,5 @@
|
||||
# change if you're using a different noauthd server
|
||||
REACT_APP_WEB_PUSH_PUBKEY=BNW_39YcKbV4KunFxFhvMW5JUs8AljfFnGUeZpaerO-gwCoWyQat5ol0xOGB8MLaqqCbz0iptd2Qv3SToSGynMk
|
||||
#REACT_APP_NOAUTHD_URL=http://localhost:8000
|
||||
REACT_APP_NOAUTHD_URL=https://noauthd.login.nostrapps.org
|
||||
REACT_APP_NOAUTHD_URL=https://noauthd.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 { swicOnRender } from './modules/swic'
|
||||
import { useAppDispatch } from './store/hooks/redux'
|
||||
@ -65,18 +65,14 @@ function App() {
|
||||
dispatch(setPending({ pending }))
|
||||
|
||||
// rerender
|
||||
// setRender((r) => r + 1)
|
||||
|
||||
if (!keys.length)
|
||||
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||
// setRender((r) => r + 1)
|
||||
|
||||
if (!keys.length) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||
// eslint-disable-next-line
|
||||
}, [dispatch])
|
||||
|
||||
useEffect(() => {
|
||||
console.log('NDK is connected', isConnected)
|
||||
if (isConnected) {
|
||||
load()
|
||||
}
|
||||
if (isConnected) load()
|
||||
}, [render, isConnected, load])
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -14,7 +14,7 @@ import { ACTION_TYPE } from '@/utils/consts'
|
||||
|
||||
|
||||
export const ModalConfirmConnect = () => {
|
||||
const { getModalOpened, handleClose } = useModalSearchParams()
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
||||
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
@ -37,20 +37,24 @@ export const ModalConfirmConnect = () => {
|
||||
return setSelectedActionType(value)
|
||||
}
|
||||
|
||||
const handleCloseModal = handleClose(
|
||||
const handleCloseModal = createHandleCloseReplace(
|
||||
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
||||
async (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
await swicCall('confirm', pendingReqId, false, false)
|
||||
{
|
||||
onClose: async (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
await swicCall('confirm', pendingReqId, false, false)
|
||||
}
|
||||
},
|
||||
)
|
||||
const closeModalAfterRequest = handleClose(
|
||||
const closeModalAfterRequest = createHandleCloseReplace(
|
||||
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
||||
(sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
},
|
||||
{
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
async function confirmPending(
|
||||
|
@ -24,9 +24,10 @@ import {
|
||||
} from './styled'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { IPendingsByAppNpub } from '@/pages/KeyPage/Key.Page'
|
||||
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
||||
import { DbPending } from '@/modules/db'
|
||||
import { ACTIONS } from '@/utils/consts'
|
||||
import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal'
|
||||
|
||||
enum ACTION_TYPE {
|
||||
ALWAYS = 'ALWAYS',
|
||||
@ -44,20 +45,12 @@ type ModalConfirmEventProps = {
|
||||
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 }
|
||||
|
||||
export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
||||
confirmEventReqs,
|
||||
}) => {
|
||||
const { getModalOpened, handleClose } = useModalSearchParams()
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
@ -93,23 +86,27 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
||||
|
||||
const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked)
|
||||
|
||||
const handleCloseModal = handleClose(
|
||||
const handleCloseModal = createHandleCloseReplace(
|
||||
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
||||
(sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
selectedPendingRequests.forEach(
|
||||
async (req) => await swicCall('confirm', req.id, false, false),
|
||||
)
|
||||
},
|
||||
{
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
selectedPendingRequests.forEach(
|
||||
async (req) => await swicCall('confirm', req.id, false, false),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const closeModalAfterRequest = handleClose(
|
||||
const closeModalAfterRequest = createHandleCloseReplace(
|
||||
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
||||
(sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
},
|
||||
{
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
async function confirmPending(allow: boolean) {
|
||||
@ -173,7 +170,7 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({
|
||||
<List>
|
||||
{pendingRequests.map((req) => {
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItem key={req.id}>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
checked={req.checked}
|
||||
|
@ -12,13 +12,18 @@ import { useRef } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
export const ModalConnectApp = () => {
|
||||
const { getModalOpened, handleClose, handleOpen } = useModalSearchParams()
|
||||
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||
const timerRef = useRef<NodeJS.Timeout>()
|
||||
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.CONNECT_APP, () => {
|
||||
clearTimeout(timerRef.current)
|
||||
})
|
||||
const handleCloseModal = createHandleCloseReplace(
|
||||
MODAL_PARAMS_KEYS.CONNECT_APP,
|
||||
{
|
||||
onClose: () => {
|
||||
clearTimeout(timerRef.current)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
|
@ -8,13 +8,15 @@ import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
import React, { ChangeEvent, FormEvent, useState } from 'react'
|
||||
import { StyledAppLogo } from './styled'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
export const ModalImportKeys = () => {
|
||||
const { getModalOpened, handleClose } = useModalSearchParams()
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
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 navigate = useNavigate()
|
||||
|
||||
const [enteredNsec, setEnteredNsec] = useState('')
|
||||
|
||||
@ -26,9 +28,9 @@ export const ModalImportKeys = () => {
|
||||
e.preventDefault()
|
||||
try {
|
||||
if (!enteredNsec.trim().length) return
|
||||
await swicCall('importKey', enteredNsec)
|
||||
const k: any = await swicCall('importKey', enteredNsec)
|
||||
notify('Key imported!', 'success')
|
||||
handleCloseModal()
|
||||
navigate(`/key/${k.npub}`)
|
||||
} catch (error: any) {
|
||||
notify(error.message, 'error')
|
||||
}
|
||||
@ -36,12 +38,7 @@ export const ModalImportKeys = () => {
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack
|
||||
paddingTop={'1rem'}
|
||||
gap={'1rem'}
|
||||
component={'form'}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
gap={'1rem'}
|
||||
@ -59,8 +56,9 @@ export const ModalImportKeys = () => {
|
||||
value={enteredNsec}
|
||||
onChange={handleNsecChange}
|
||||
fullWidth
|
||||
type='password'
|
||||
/>
|
||||
<Button>Import nsec</Button>
|
||||
<Button type='submit'>Import nsec</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
)
|
||||
|
@ -7,10 +7,10 @@ import { Fade, Stack } from '@mui/material'
|
||||
import { AppLink } from '@/shared/AppLink/AppLink'
|
||||
|
||||
export const ModalInitial = () => {
|
||||
const { getModalOpened, handleClose, handleOpen } = useModalSearchParams()
|
||||
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||
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)
|
||||
|
||||
@ -28,7 +28,7 @@ export const ModalInitial = () => {
|
||||
|
||||
return (
|
||||
<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)}>
|
||||
Sign up
|
||||
</Button>
|
||||
|
@ -14,9 +14,9 @@ import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
export const ModalLogin = () => {
|
||||
const { getModalOpened, handleClose } = useModalSearchParams()
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.LOGIN)
|
||||
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.LOGIN)
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN)
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
@ -37,8 +37,12 @@ export const ModalLogin = () => {
|
||||
const handlePasswordTypeChange = () =>
|
||||
setIsPasswordShown((prevState) => !prevState)
|
||||
|
||||
const isFormValid =
|
||||
enteredUsername.trim().length > 0 && enteredPassword.trim().length > 0
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!isFormValid) return undefined
|
||||
try {
|
||||
const [username, domain] = enteredUsername.split('@')
|
||||
const response = await fetch(
|
||||
@ -63,12 +67,7 @@ export const ModalLogin = () => {
|
||||
}
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack
|
||||
paddingTop={'1rem'}
|
||||
gap={'1rem'}
|
||||
component={'form'}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||
<Stack
|
||||
direction={'row'}
|
||||
gap={'1rem'}
|
||||
@ -105,9 +104,9 @@ export const ModalLogin = () => {
|
||||
)}
|
||||
</IconButton>
|
||||
}
|
||||
type={isPasswordShown ? 'password' : 'text'}
|
||||
type={isPasswordShown ? 'text' : 'password'}
|
||||
/>
|
||||
<Button type='submit' fullWidth>
|
||||
<Button type='submit' fullWidth disabled={!isFormValid}>
|
||||
Login
|
||||
</Button>
|
||||
</Stack>
|
||||
|
@ -2,7 +2,13 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Box, IconButton, Stack, Typography } from '@mui/material'
|
||||
import {
|
||||
Box,
|
||||
CircularProgress,
|
||||
IconButton,
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material'
|
||||
import {
|
||||
StyledButton,
|
||||
StyledSettingContainer,
|
||||
@ -13,28 +19,37 @@ import { CheckmarkIcon } from '@/assets'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
||||
import { ChangeEvent, useState } from 'react'
|
||||
import { ChangeEvent, FC, useEffect, useState } from 'react'
|
||||
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { dbi } from '@/modules/db'
|
||||
|
||||
export const ModalSettings = () => {
|
||||
const { getModalOpened, handleClose } = useModalSearchParams()
|
||||
type ModalSettingsProps = {
|
||||
isSynced: boolean
|
||||
}
|
||||
|
||||
export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
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 [isPasswordShown, setIsPasswordShown] = useState(false)
|
||||
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
|
||||
const [isPasswordSynched, setIsPasswordSynched] = useState(false)
|
||||
|
||||
const [isChecked, setIsChecked] = useState(false)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
|
||||
useEffect(() => setIsChecked(isSynced), [isModalOpened, isSynced])
|
||||
|
||||
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setIsPasswordInvalid(false)
|
||||
setEnteredPassword(e.target.value)
|
||||
@ -47,7 +62,6 @@ export const ModalSettings = () => {
|
||||
handleCloseModal()
|
||||
setEnteredPassword('')
|
||||
setIsPasswordInvalid(false)
|
||||
setIsPasswordSynched(false)
|
||||
}
|
||||
|
||||
const handleChangeCheckbox = (e: unknown, checked: boolean) => {
|
||||
@ -57,19 +71,21 @@ export const ModalSettings = () => {
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setIsPasswordInvalid(false)
|
||||
setIsPasswordSynched(false)
|
||||
|
||||
if (enteredPassword.trim().length < 6) {
|
||||
return setIsPasswordInvalid(true)
|
||||
}
|
||||
try {
|
||||
setIsLoading(true)
|
||||
await swicCall('saveKey', npub, enteredPassword)
|
||||
notify('Key saved', 'success')
|
||||
dbi.addSynced(npub) // Sync npub
|
||||
setEnteredPassword('')
|
||||
setIsPasswordInvalid(false)
|
||||
setIsPasswordSynched(true)
|
||||
setIsLoading(false)
|
||||
} catch (error) {
|
||||
setIsPasswordInvalid(false)
|
||||
setIsPasswordSynched(false)
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,7 +95,7 @@ export const ModalSettings = () => {
|
||||
<StyledSettingContainer onSubmit={handleSubmit}>
|
||||
<Stack direction={'row'} justifyContent={'space-between'}>
|
||||
<SectionTitle>Cloud sync</SectionTitle>
|
||||
{isPasswordSynched && (
|
||||
{isSynced && (
|
||||
<StyledSynchedText>
|
||||
<CheckmarkIcon /> Synched
|
||||
</StyledSynchedText>
|
||||
@ -91,7 +107,7 @@ export const ModalSettings = () => {
|
||||
checked={isChecked}
|
||||
/>
|
||||
<Typography variant='caption'>
|
||||
Use this login on multiple devices
|
||||
Use this key on multiple devices
|
||||
</Typography>
|
||||
</Box>
|
||||
<Input
|
||||
@ -111,7 +127,9 @@ export const ModalSettings = () => {
|
||||
type={isPasswordShown ? 'text' : 'password'}
|
||||
onChange={handlePasswordChange}
|
||||
value={enteredPassword}
|
||||
helperText={isPasswordInvalid ? 'Invalid password' : ''}
|
||||
helperText={
|
||||
isPasswordInvalid ? 'Invalid password' : ''
|
||||
}
|
||||
placeholder='Enter a password'
|
||||
helperTextProps={{
|
||||
sx: {
|
||||
@ -122,8 +140,27 @@ export const ModalSettings = () => {
|
||||
}}
|
||||
disabled={!isChecked}
|
||||
/>
|
||||
<StyledButton type='submit' fullWidth disabled={!isChecked}>
|
||||
Sync
|
||||
{isSynced ? (
|
||||
<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>
|
||||
</StyledSettingContainer>
|
||||
<Button onClick={onClose}>Done</Button>
|
||||
|
@ -8,9 +8,9 @@ import {
|
||||
} from '@mui/material'
|
||||
|
||||
export const StyledSettingContainer = styled((props: StackProps) => (
|
||||
<Stack gap={'1rem'} component={'form'} {...props} />
|
||||
<Stack gap={'0.75rem'} component={'form'} {...props} />
|
||||
))(({ theme }) => ({
|
||||
padding: '0.75rem',
|
||||
padding: '1rem',
|
||||
borderRadius: '1rem',
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
@ -22,6 +22,9 @@ export const StyledButton = styled(Button)(({ theme }) => {
|
||||
background: theme.palette.secondary.main,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
':disabled': {
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -12,9 +12,9 @@ import { swicCall } from '@/modules/swic'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
export const ModalSignUp = () => {
|
||||
const { getModalOpened, handleClose } = useModalSearchParams()
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
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 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
|
||||
}
|
||||
|
||||
export type IExtraCloseOptions = {
|
||||
replace?: boolean
|
||||
onClose?: (s: URLSearchParams) => void
|
||||
}
|
||||
|
||||
export const useModalSearchParams = () => {
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
|
||||
@ -29,13 +34,20 @@ export const useModalSearchParams = () => {
|
||||
]
|
||||
}, [])
|
||||
|
||||
const handleClose =
|
||||
(modal: MODAL_PARAMS_KEYS, onClose?: (s: URLSearchParams) => void) =>
|
||||
const createHandleClose =
|
||||
(modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraCloseOptions) =>
|
||||
() => {
|
||||
const enumKey = getEnumParam(modal)
|
||||
searchParams.delete(enumKey)
|
||||
onClose && onClose(searchParams)
|
||||
setSearchParams(searchParams)
|
||||
extraOptions?.onClose && extraOptions?.onClose(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(
|
||||
@ -61,7 +73,7 @@ export const useModalSearchParams = () => {
|
||||
pathname: location.pathname,
|
||||
search: searchString,
|
||||
},
|
||||
{ replace: extraOptions?.replace || true },
|
||||
{ replace: !!extraOptions?.replace },
|
||||
)
|
||||
},
|
||||
[location, navigate, getEnumParam],
|
||||
@ -78,7 +90,8 @@ export const useModalSearchParams = () => {
|
||||
|
||||
return {
|
||||
getModalOpened,
|
||||
handleClose,
|
||||
createHandleClose,
|
||||
createHandleCloseReplace,
|
||||
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 { dbi, DbKey, DbPending, DbPerm } from './db'
|
||||
import { DbApp, dbi, DbKey, DbPending, DbPerm } from './db'
|
||||
import { Keys } from './keys'
|
||||
import NDK, {
|
||||
IEventHandlingStrategy,
|
||||
@ -10,7 +10,7 @@ import NDK, {
|
||||
} from '@nostr-dev-kit/ndk'
|
||||
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS } from '../utils/consts'
|
||||
import { Nip04 } from './nip04'
|
||||
import { getReqPerm, isPackagePerm } from '@/utils/helpers/helpers'
|
||||
import { getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
|
||||
//import { PrivateKeySigner } from './signer'
|
||||
|
||||
//const PERF_TEST = false
|
||||
@ -32,6 +32,7 @@ interface Key {
|
||||
interface Pending {
|
||||
req: DbPending
|
||||
cb: (allow: boolean, remember: boolean, options?: any) => void
|
||||
notified?: boolean
|
||||
}
|
||||
|
||||
interface IAllowCallbackParams {
|
||||
@ -145,6 +146,7 @@ export class NoauthBackend {
|
||||
private enckeys: DbKey[] = []
|
||||
private keys: Key[] = []
|
||||
private perms: DbPerm[] = []
|
||||
private apps: DbApp[] = []
|
||||
private doneReqIds: string[] = []
|
||||
private confirmBuffer: Pending[] = []
|
||||
private accessBuffer: DbPending[] = []
|
||||
@ -193,16 +195,25 @@ export class NoauthBackend {
|
||||
.matchAll({ type: 'window' })
|
||||
.then((clientList) => {
|
||||
console.log('clients', clientList.length)
|
||||
// FIXME find a client that has our
|
||||
// key page
|
||||
for (const client of clientList) {
|
||||
console.log('client', client.url)
|
||||
if (
|
||||
new URL(client.url).pathname === '/' &&
|
||||
'focus' in client
|
||||
)
|
||||
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())
|
||||
this.perms = await dbi.listPerms()
|
||||
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()
|
||||
|
||||
@ -381,21 +394,69 @@ export class NoauthBackend {
|
||||
// and update the notifications
|
||||
|
||||
for (const r of this.confirmBuffer) {
|
||||
const text = `Confirm "${r.req.method}" by "${r.req.appNpub}"`
|
||||
this.swg.registration.showNotification('Signer access', {
|
||||
body: text,
|
||||
tag: 'confirm-' + r.req.appNpub,
|
||||
actions: [
|
||||
{
|
||||
action: 'allow:' + r.req.id,
|
||||
title: 'Yes',
|
||||
},
|
||||
{
|
||||
action: 'disallow:' + r.req.id,
|
||||
title: 'No',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
if (r.notified) continue
|
||||
|
||||
const key = this.keys.find(k => k.npub === r.req.npub)
|
||||
if (!key) continue
|
||||
|
||||
const app = this.apps.find(a => a.appNpub === r.req.appNpub)
|
||||
if (r.req.method !== 'connect' && !app) continue
|
||||
|
||||
// FIXME use Nsec.app icon!
|
||||
const icon = 'https://nostr.band/android-chrome-192x192.png'
|
||||
|
||||
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()
|
||||
@ -509,6 +570,9 @@ export class NoauthBackend {
|
||||
icon: '',
|
||||
url: '',
|
||||
})
|
||||
|
||||
// reload
|
||||
self.apps = await dbi.listApps()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -771,6 +835,7 @@ export class NoauthBackend {
|
||||
}
|
||||
|
||||
private async deleteApp(appNpub: string) {
|
||||
this.apps = this.apps.filter((a) => a.appNpub !== appNpub)
|
||||
this.perms = this.perms.filter((p) => p.appNpub !== appNpub)
|
||||
await dbi.removeApp(appNpub)
|
||||
await dbi.removeAppPerms(appNpub)
|
||||
|
@ -48,23 +48,29 @@ export interface DbHistory {
|
||||
allowed: boolean
|
||||
}
|
||||
|
||||
export interface DbSyncHistory {
|
||||
npub: string
|
||||
}
|
||||
|
||||
export interface DbSchema extends Dexie {
|
||||
keys: Dexie.Table<DbKey, string>
|
||||
apps: Dexie.Table<DbApp, string>
|
||||
perms: Dexie.Table<DbPerm, string>
|
||||
pending: Dexie.Table<DbPending, string>
|
||||
history: Dexie.Table<DbHistory, string>
|
||||
syncHistory: Dexie.Table<DbSyncHistory, string>
|
||||
}
|
||||
|
||||
export const db = new Dexie('noauthdb') as DbSchema
|
||||
|
||||
db.version(7).stores({
|
||||
db.version(8).stores({
|
||||
keys: 'npub',
|
||||
apps: 'appNpub,npub,name,timestamp',
|
||||
perms: 'id,npub,appNpub,perm,value,timestamp',
|
||||
pending: 'id,npub,appNpub,timestamp,method',
|
||||
history: 'id,npub,appNpub,timestamp,method,allowed',
|
||||
requestHistory: 'id',
|
||||
syncHistory: 'npub',
|
||||
})
|
||||
|
||||
export const dbi = {
|
||||
@ -201,4 +207,12 @@ export const dbi = {
|
||||
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 { useAppSelector } from '@/store/hooks/redux'
|
||||
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 { Avatar, Box, Stack, Typography } from '@mui/material'
|
||||
import { Box, Stack, Typography } from '@mui/material'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { PermissionMenuButton } from './styled'
|
||||
import { PermissionsMenu } from './components/PermissionsMenu'
|
||||
import { useOpenMenu } from '@/hooks/useOpenMenu'
|
||||
import { ActivityList } from './components/ActivityList'
|
||||
|
||||
const getAppHistoryQuery = (appNpub: string) =>
|
||||
db.history.where('appNpub').equals(appNpub).toArray()
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { ACTION_TYPE } from '@/utils/consts'
|
||||
import { Permissions } from './components/Permissions/Permissions'
|
||||
import { StyledAppIcon } from './styled'
|
||||
import { useToggleConfirm } from '@/hooks/useToggleConfirm'
|
||||
import { ConfirmModal } from '@/shared/ConfirmModal/ConfirmModal'
|
||||
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 { appNpub = '', npub = '' } = useParams()
|
||||
const navigate = useNavigate()
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
const perms = useAppSelector((state) =>
|
||||
selectPermsByNpubAndAppNpub(state, npub, appNpub),
|
||||
)
|
||||
const currentApp = useAppSelector((state) =>
|
||||
selectAppByAppNpub(state, appNpub),
|
||||
)
|
||||
const history = useLiveQuery(
|
||||
() => {
|
||||
if (!appNpub.trim().length) return []
|
||||
return getAppHistoryQuery(appNpub)
|
||||
},
|
||||
[],
|
||||
[] as DbHistory[],
|
||||
)
|
||||
|
||||
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()
|
||||
const connectPerm = perms.find((perm) => perm.perm === 'connect')
|
||||
const { open, handleClose, handleShow } = useToggleConfirm()
|
||||
const { handleOpen: handleOpenModal } = useModalSearchParams()
|
||||
|
||||
const connectPerm = perms.find(
|
||||
(perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC,
|
||||
)
|
||||
|
||||
if (!currentApp) {
|
||||
return <Navigate to={`/key/${npub}`} />
|
||||
@ -43,52 +45,77 @@ const AppPage = () => {
|
||||
const { icon = '', name = '' } = currentApp || {}
|
||||
const appName = name || getShortenNpub(appNpub)
|
||||
const { timestamp } = connectPerm || {}
|
||||
|
||||
const connectedOn =
|
||||
connectPerm && timestamp
|
||||
? `Connected at ${formatTimestampDate(timestamp)}`
|
||||
: 'Not connected'
|
||||
|
||||
const handleDeleteApp = async () => {
|
||||
try {
|
||||
await swicCall('deleteApp', appNpub)
|
||||
notify(`App: «${appName}» successfully deleted!`, 'success')
|
||||
navigate(`key/${npub}`)
|
||||
} catch (error: any) {
|
||||
notify(error?.message || 'Failed to delete app', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack maxHeight={'100%'} overflow={'auto'}>
|
||||
<>
|
||||
<Stack
|
||||
marginBottom={'1rem'}
|
||||
direction={'row'}
|
||||
gap={'1rem'}
|
||||
width={'100%'}
|
||||
maxHeight={'100%'}
|
||||
overflow={'auto'}
|
||||
alignItems={'flex-start'}
|
||||
height={'100%'}
|
||||
>
|
||||
<Avatar
|
||||
src={icon}
|
||||
sx={{
|
||||
width: 70,
|
||||
height: 70,
|
||||
}}
|
||||
variant='rounded'
|
||||
/>
|
||||
<Box flex={'1'} overflow={'hidden'}>
|
||||
<Typography variant='h4' noWrap>
|
||||
{appName}
|
||||
</Typography>
|
||||
<Typography variant='body2' noWrap>
|
||||
{connectedOn}
|
||||
</Typography>
|
||||
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
|
||||
<Stack
|
||||
marginBottom={'1rem'}
|
||||
direction={'row'}
|
||||
gap={'1rem'}
|
||||
width={'100%'}
|
||||
>
|
||||
<StyledAppIcon src={icon} />
|
||||
<Box flex={'1'} overflow={'hidden'}>
|
||||
<Typography variant='h4' noWrap>
|
||||
{appName}
|
||||
</Typography>
|
||||
<Typography variant='body2' noWrap>
|
||||
{connectedOn}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Box marginBottom={'1rem'}>
|
||||
<SectionTitle marginBottom={'0.5rem'}>
|
||||
Disconnect
|
||||
</SectionTitle>
|
||||
<Button fullWidth onClick={handleShow}>
|
||||
Delete app
|
||||
</Button>
|
||||
</Box>
|
||||
<Permissions perms={perms} />
|
||||
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() =>
|
||||
handleOpenModal(MODAL_PARAMS_KEYS.ACTIVITY)
|
||||
}
|
||||
>
|
||||
Activity
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
<Box marginBottom={'1rem'}>
|
||||
<SectionTitle marginBottom={'0.5rem'}>Permissions</SectionTitle>
|
||||
<PermissionMenuButton onClick={handleOpen}>
|
||||
Basic/Advanced/Custom {perms.length}
|
||||
</PermissionMenuButton>
|
||||
<PermissionsMenu
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
perms={perms}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<ActivityList history={history} />
|
||||
</Stack>
|
||||
<ConfirmModal
|
||||
open={open}
|
||||
headingText='Delete app'
|
||||
description='Are you sure you want to delete this app?'
|
||||
onCancel={handleClose}
|
||||
onConfirm={handleDeleteApp}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
<ModalActivities appNpub={appNpub} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
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 { MenuItem, MenuItemProps, 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,
|
||||
},
|
||||
}))
|
||||
import { styled } from '@mui/material'
|
||||
|
||||
export const StyledButton = styled(Button)({
|
||||
textTransform: 'capitalize',
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
||||
import { styled } from '@mui/material'
|
||||
import { Avatar, AvatarProps, styled } from '@mui/material'
|
||||
|
||||
export const PermissionMenuButton = styled((props: AppButtonProps) => (
|
||||
<Button {...props} variant='outlined' fullWidth />
|
||||
))(() => ({}))
|
||||
export const StyledAppIcon = styled((props: AvatarProps) => (
|
||||
<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 { ItemKey } from './components/ItemKey'
|
||||
import { Box, Stack, Typography } from '@mui/material'
|
||||
import { AddAccountButton } from './styled'
|
||||
import { AddAccountButton, GetStartedButton, LearnMoreButton } from './styled'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectKeys } from '@/store'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
|
||||
const HomePage = () => {
|
||||
const keys = useAppSelector(selectKeys)
|
||||
@ -15,20 +16,43 @@ const HomePage = () => {
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
const handleClickAddAccount = () => handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||
|
||||
const handleLearnMore = () => {
|
||||
// @ts-ignore
|
||||
window.open(`https://info.${DOMAIN}`, '_blank').focus();
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack maxHeight={'100%'} overflow={'auto'}>
|
||||
<SectionTitle marginBottom={'0.5rem'}>
|
||||
{isNoKeys ? 'Welcome!' : 'Keys:'}
|
||||
{isNoKeys ? 'Welcome' : 'Keys:'}
|
||||
</SectionTitle>
|
||||
<Stack gap={'0.5rem'} overflow={'auto'}>
|
||||
{isNoKeys && (
|
||||
<Typography textAlign={'center'} variant='h5'>
|
||||
Hello, this is a key storage app for Nostr
|
||||
</Typography>
|
||||
<>
|
||||
<Typography textAlign={'left'} variant='h6' paddingTop='1em'>
|
||||
Nsec.app is a novel key storage app for Nostr.
|
||||
</Typography>
|
||||
<GetStartedButton onClick={handleClickAddAccount}>
|
||||
Get started
|
||||
</GetStartedButton>
|
||||
<Typography textAlign={'left'} variant='h6' paddingTop='2em'>
|
||||
Your keys are stored in your browser and
|
||||
can be used in many Nostr apps without the
|
||||
need for a browser extension.
|
||||
</Typography>
|
||||
<LearnMoreButton onClick={handleLearnMore}>
|
||||
Learn more
|
||||
</LearnMoreButton>
|
||||
</>
|
||||
)}
|
||||
{!isNoKeys && (
|
||||
<Fragment>
|
||||
<Box flex={1} overflow={'auto'} borderRadius={'8px'}>
|
||||
<Box
|
||||
flex={1}
|
||||
overflow={'auto'}
|
||||
borderRadius={'8px'}
|
||||
padding={'0.25rem'}
|
||||
>
|
||||
{keys.map((key) => (
|
||||
<ItemKey {...key} key={key.npub} />
|
||||
))}
|
||||
|
@ -40,8 +40,8 @@ const StyledKeyContainer = styled((props: StackProps) => (
|
||||
return {
|
||||
boxShadow:
|
||||
theme.palette.mode === 'dark'
|
||||
? '2px 2px 8px 0px rgba(92, 92, 92, 0.2)'
|
||||
: '2px 2px 8px 0px rgba(0, 0, 0, 0.2)',
|
||||
? '0px 1px 6px 0px rgba(92, 92, 92, 0.2)'
|
||||
: '0px 1px 6px 0px rgba(0, 0, 0, 0.2)',
|
||||
borderRadius: '12px',
|
||||
padding: '0.5rem 1rem',
|
||||
background: theme.palette.background.paper,
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
||||
import { styled } from '@mui/material'
|
||||
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
||||
import PlayArrowOutlinedIcon from '@mui/icons-material/PlayArrowOutlined'
|
||||
import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'
|
||||
|
||||
export const AddAccountButton = styled((props: AppButtonProps) => (
|
||||
<Button {...props} startIcon={<PersonAddAltRoundedIcon />} />
|
||||
@ -8,3 +10,17 @@ export const AddAccountButton = styled((props: AppButtonProps) => (
|
||||
alignSelf: 'center',
|
||||
padding: '0.35rem 1rem',
|
||||
}))
|
||||
|
||||
export const GetStartedButton = styled((props: AppButtonProps) => (
|
||||
<Button {...props} startIcon={<PlayArrowOutlinedIcon />} />
|
||||
))(() => ({
|
||||
alignSelf: 'left',
|
||||
padding: '0.35rem 1rem',
|
||||
}))
|
||||
|
||||
export const LearnMoreButton = styled((props: AppButtonProps) => (
|
||||
<Button {...props} startIcon={<HelpOutlineOutlinedIcon />} />
|
||||
))(() => ({
|
||||
alignSelf: 'left',
|
||||
padding: '0.35rem 1rem',
|
||||
}))
|
||||
|
@ -1,327 +1,89 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { SectionTitle } from '../../shared/SectionTitle/SectionTitle'
|
||||
import { useAppSelector } from '../../store/hooks/redux'
|
||||
import {
|
||||
askNotificationPermission,
|
||||
getShortenNpub,
|
||||
} from '../../utils/helpers/helpers'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { fetchProfile } from '../../modules/nostr'
|
||||
import { Badge, Box, CircularProgress, Stack } from '@mui/material'
|
||||
import { Stack } from '@mui/material'
|
||||
import { StyledIconButton } from './styled'
|
||||
import { SettingsIcon, ShareIcon } from '@/assets'
|
||||
import { AppLink } from '@/shared/AppLink/AppLink'
|
||||
import { MetaEvent } from '@/types/meta-event'
|
||||
import { Apps } from './components/Apps'
|
||||
import { ModalConnectApp } from '@/components/Modal/ModalConnectApp/ModalConnectApp'
|
||||
import { StyledInput } from './components/styled'
|
||||
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { ModalSettings } from '@/components/Modal/ModalSettings/ModalSettings'
|
||||
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 { ModalConfirmEvent } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent'
|
||||
import { DbPending } from '@/modules/db'
|
||||
import { ACTION_TYPE } from '@/utils/consts'
|
||||
|
||||
export type IPendingsByAppNpub = {
|
||||
[appNpub: string]: {
|
||||
pending: DbPending[]
|
||||
isConnected: boolean
|
||||
}
|
||||
}
|
||||
|
||||
type IShownConfirmModals = {
|
||||
[reqId: string]: boolean
|
||||
}
|
||||
import { useProfile } from './hooks/useProfile'
|
||||
import { useBackgroundSigning } from './hooks/useBackgroundSigning'
|
||||
import { BackgroundSigningWarning } from './components/BackgroundSigningWarning'
|
||||
import UserValueSection from './components/UserValueSection'
|
||||
import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { checkNpubSyncQuerier } from './utils'
|
||||
|
||||
const KeyPage = () => {
|
||||
const { apps, pending, perms } = useAppSelector((state) => state.content)
|
||||
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 isConfirmConnectModalOpened = getModalOpened(
|
||||
MODAL_PARAMS_KEYS.CONFIRM_CONNECT,
|
||||
)
|
||||
const isConfirmEventModalOpened = getModalOpened(
|
||||
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
|
||||
)
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||
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 { userNameWithPrefix } = useProfile(npub)
|
||||
const { handleEnableBackground, showWarning, isEnabling } =
|
||||
useBackgroundSigning()
|
||||
|
||||
const filteredApps = apps.filter((a) => a.npub === npub)
|
||||
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 { prepareEventPendings } = useTriggerConfirmModal(
|
||||
npub,
|
||||
pending,
|
||||
perms,
|
||||
)
|
||||
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 = () =>
|
||||
handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Stack gap={'1rem'} height={'100%'}>
|
||||
{showWarning && (
|
||||
<Warning
|
||||
message={
|
||||
<Stack
|
||||
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}
|
||||
<BackgroundSigningWarning
|
||||
isEnabling={isEnabling}
|
||||
onEnableBackSigning={handleEnableBackground}
|
||||
/>
|
||||
)}
|
||||
{renderUserValueSection(
|
||||
'Your login',
|
||||
userNameWithPrefix,
|
||||
EXPLANATION_MODAL_KEYS.NPUB,
|
||||
npub + '@nsec.app',
|
||||
)}
|
||||
{renderUserValueSection(
|
||||
'Your NPUB',
|
||||
npub,
|
||||
EXPLANATION_MODAL_KEYS.NPUB,
|
||||
npub,
|
||||
)}
|
||||
<UserValueSection
|
||||
title='Your login'
|
||||
value={userNameWithPrefix}
|
||||
copyValue={npub + '@nsec.app'}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||
/>
|
||||
<UserValueSection
|
||||
title='Your NPUB'
|
||||
value={npub}
|
||||
copyValue={npub}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||
/>
|
||||
|
||||
<Stack direction={'row'} gap={'0.75rem'}>
|
||||
<StyledIconButton onClick={handleOpenConnectAppModal}>
|
||||
<ShareIcon />
|
||||
Connect app
|
||||
</StyledIconButton>
|
||||
<Badge sx={{ flex: 1 }} badgeContent={''} color='error'>
|
||||
<StyledIconButton
|
||||
bgcolor_variant='secondary'
|
||||
onClick={handleOpenSettingsModal}
|
||||
>
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</StyledIconButton>
|
||||
</Badge>
|
||||
|
||||
<StyledIconButton
|
||||
bgcolor_variant='secondary'
|
||||
onClick={handleOpenSettingsModal}
|
||||
withBadge={!isSynced}
|
||||
>
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</StyledIconButton>
|
||||
</Stack>
|
||||
|
||||
<Apps apps={filteredApps} npub={npub} />
|
||||
</Stack>
|
||||
<ModalConnectApp />
|
||||
<ModalSettings />
|
||||
<ModalSettings isSynced={isSynced} />
|
||||
<ModalExplanation />
|
||||
<ModalConfirmConnect />
|
||||
<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',
|
||||
boxShadow: 'none',
|
||||
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 { Box, Button, ButtonProps, styled } from '@mui/material'
|
||||
import { Box, Button, ButtonProps, styled, Badge } from '@mui/material'
|
||||
|
||||
type StyledIconButtonProps = ButtonProps & {
|
||||
bgcolor_variant?: 'primary' | 'secondary'
|
||||
withBadge?: boolean
|
||||
}
|
||||
|
||||
export const StyledIconButton = styled((props: StyledIconButtonProps) => (
|
||||
<Button {...props} />
|
||||
))(({ bgcolor_variant = 'primary', theme }) => {
|
||||
export const StyledIconButton = styled(
|
||||
({ withBadge, ...props }: StyledIconButtonProps) => {
|
||||
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'
|
||||
return {
|
||||
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 passwordInputRef = useRef<HTMLInputElement | null>(null)
|
||||
|
||||
// if (isKeysExists) return <Navigate to={'/home'} />
|
||||
if (isKeysExists) return <Navigate to={'/home'} />
|
||||
|
||||
async function generateKey() {
|
||||
try {
|
||||
|
@ -21,7 +21,7 @@ const AppRoutes = () => {
|
||||
<Routes>
|
||||
<Route path='/' element={<Layout />}>
|
||||
<Route path='/' element={<Navigate to={'/home'} />} />
|
||||
<Route path='/welcome' element={<WelcomePage />} />
|
||||
{/* <Route path='/welcome' element={<WelcomePage />} /> */}
|
||||
<Route path='/home' element={<HomePage />} />
|
||||
<Route path='/key/:npub' element={<KeyPage />} />
|
||||
<Route
|
||||
|
@ -43,5 +43,11 @@ const StyledButton = styled(
|
||||
background: theme.palette.primary.main,
|
||||
},
|
||||
color: theme.palette.text.secondary,
|
||||
':disabled': {
|
||||
'&.button:is(:hover, :active, &)': {
|
||||
background: theme.palette.backgroundSecondary.default,
|
||||
},
|
||||
color: theme.palette.backgroundSecondary.paper,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@ -34,4 +34,5 @@ const StyledCheckbox = styled(
|
||||
),
|
||||
)(() => ({
|
||||
'& .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 & {
|
||||
withCloseButton?: boolean
|
||||
fixedHeight?: string
|
||||
}
|
||||
|
||||
const Transition = forwardRef(function Transition(
|
||||
@ -27,10 +28,12 @@ export const Modal: FC<ModalProps> = ({
|
||||
title,
|
||||
onClose,
|
||||
withCloseButton = true,
|
||||
fixedHeight,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<StyledDialog
|
||||
fixedHeight={fixedHeight}
|
||||
{...props}
|
||||
onClose={onClose}
|
||||
TransitionComponent={Transition}
|
||||
@ -46,7 +49,13 @@ export const Modal: FC<ModalProps> = ({
|
||||
</StyledCloseButtonWrapper>
|
||||
)}
|
||||
{title && <StyledDialogTitle>{title}</StyledDialogTitle>}
|
||||
<StyledDialogContent>{children}</StyledDialogContent>
|
||||
<StyledDialogContent
|
||||
sx={{
|
||||
paddingTop: withCloseButton ? '1.5rem' : '0',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StyledDialogContent>
|
||||
</StyledDialog>
|
||||
)
|
||||
}
|
||||
|
@ -10,37 +10,43 @@ import {
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
|
||||
export const StyledDialog = styled((props: DialogProps) => (
|
||||
<Dialog
|
||||
{...props}
|
||||
classes={{
|
||||
container: 'container',
|
||||
paper: 'paper',
|
||||
}}
|
||||
slotProps={{
|
||||
backdrop: {
|
||||
sx: {
|
||||
backdropFilter: 'blur(2px)',
|
||||
export const StyledDialog = styled(
|
||||
(props: DialogProps & { fixedHeight?: string }) => (
|
||||
<Dialog
|
||||
{...props}
|
||||
classes={{
|
||||
container: 'container',
|
||||
paper: 'paper',
|
||||
}}
|
||||
slotProps={{
|
||||
backdrop: {
|
||||
sx: {
|
||||
backdropFilter: 'blur(2px)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
))(({ theme }) => ({
|
||||
'& .container': {
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
'& .paper': {
|
||||
margin: '0',
|
||||
width: '100%',
|
||||
borderTopLeftRadius: '2rem',
|
||||
borderTopRightRadius: '2rem',
|
||||
background:
|
||||
theme.palette.mode === 'light'
|
||||
? '#fff'
|
||||
: theme.palette.secondary.main,
|
||||
},
|
||||
}))
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
),
|
||||
)(({ theme, fixedHeight = '' }) => {
|
||||
const fixedHeightStyles = fixedHeight ? { height: fixedHeight } : {}
|
||||
return {
|
||||
'& .container': {
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
'& .paper': {
|
||||
margin: '0',
|
||||
width: '100%',
|
||||
borderTopLeftRadius: '2rem',
|
||||
borderTopRightRadius: '2rem',
|
||||
background:
|
||||
theme.palette.mode === 'light'
|
||||
? '#fff'
|
||||
: theme.palette.secondary.main,
|
||||
...fixedHeightStyles,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export const StyledDialogTitle = styled((props: DialogTitleProps) => (
|
||||
<DialogTitle {...props} variant='h5' />
|
||||
@ -56,6 +62,8 @@ export const StyledDialogContent = styled((props: DialogContentProps) => (
|
||||
))(() => {
|
||||
return {
|
||||
padding: '0 1rem 1rem',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -8,6 +8,7 @@ export enum MODAL_PARAMS_KEYS {
|
||||
SIGN_UP = 'sign-up',
|
||||
CONFIRM_CONNECT = 'confirm-connect',
|
||||
CONFIRM_EVENT = 'confirm-event',
|
||||
ACTIVITY = 'activity',
|
||||
}
|
||||
|
||||
export enum EXPLANATION_MODAL_KEYS {
|
||||
|
@ -1,9 +1,18 @@
|
||||
export const NIP46_RELAYS = ['wss://relay.login.nostrapps.org']
|
||||
export const NOAUTHD_URL = process.env.REACT_APP_NOAUTHD_URL
|
||||
export const WEB_PUSH_PUBKEY = process.env.REACT_APP_WEB_PUSH_PUBKEY
|
||||
export const DOMAIN = process.env.REACT_APP_DOMAIN
|
||||
|
||||
export enum ACTION_TYPE {
|
||||
BASIC = 'basic',
|
||||
ADVANCED = 'advanced',
|
||||
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 { ACTION_TYPE, NIP46_RELAYS } from '../consts'
|
||||
import { DbPending } from '@/modules/db'
|
||||
|
||||
export async function log(s: string) {
|
||||
const log = document.getElementById('log')
|
||||
if (log) log.innerHTML = s
|
||||
}
|
||||
import { MetaEvent } from '@/types/meta-event'
|
||||
|
||||
export async function call(cb: () => any) {
|
||||
try {
|
||||
@ -19,6 +15,14 @@ export const getShortenNpub = (npub = '') => {
|
||||
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 = '') => {
|
||||
if (!npub) return ''
|
||||
const { data: pubkey } = nip19.decode(npub)
|
||||
@ -29,11 +33,9 @@ export async function askNotificationPermission() {
|
||||
return new Promise<void>((ok, rej) => {
|
||||
// Let's check if the browser supports notifications
|
||||
if (!('Notification' in window)) {
|
||||
log('This browser does not support notifications.')
|
||||
rej()
|
||||
rej('This browser does not support notifications.')
|
||||
} else {
|
||||
Notification.requestPermission().then(() => {
|
||||
log('notifications perm' + Notification.permission)
|
||||
if (Notification.permission === 'granted') ok()
|
||||
else rej()
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user