add sync npub logic & change perms and activity history design & add delete app/perm requests
This commit is contained in:
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(() => {
|
||||||
|
|||||||
@@ -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,14 +45,6 @@ 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> = ({
|
||||||
@@ -173,7 +166,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}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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, handleClose } = useModalSearchParams()
|
||||||
@@ -15,6 +16,7 @@ export const ModalImportKeys = () => {
|
|||||||
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
const handleCloseModal = handleClose(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'}
|
||||||
@@ -60,7 +57,7 @@ export const ModalImportKeys = () => {
|
|||||||
onChange={handleNsecChange}
|
onChange={handleNsecChange}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
<Button>Import nsec</Button>
|
<Button type='submit'>Import nsec</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,13 +19,18 @@ 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, 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 = {
|
||||||
|
isSynced: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||||
const { getModalOpened, handleClose } = useModalSearchParams()
|
const { getModalOpened, handleClose } = useModalSearchParams()
|
||||||
const { npub = '' } = useParams<{ npub: string }>()
|
const { npub = '' } = useParams<{ npub: string }>()
|
||||||
|
|
||||||
@@ -31,10 +42,11 @@ export const ModalSettings = () => {
|
|||||||
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)
|
||||||
|
|
||||||
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
setIsPasswordInvalid(false)
|
setIsPasswordInvalid(false)
|
||||||
setEnteredPassword(e.target.value)
|
setEnteredPassword(e.target.value)
|
||||||
@@ -47,7 +59,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 +68,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 +92,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>
|
||||||
@@ -94,37 +107,58 @@ export const ModalSettings = () => {
|
|||||||
Use this login on multiple devices
|
Use this login on multiple devices
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Input
|
{isSynced ? (
|
||||||
fullWidth
|
<Typography variant='body2' color={'GrayText'}>
|
||||||
endAdornment={
|
This uploads your <u>private key</u>, encrypted by
|
||||||
<IconButton
|
your password, to Nsec App's server.
|
||||||
size='small'
|
</Typography>
|
||||||
onClick={handlePasswordTypeChange}
|
) : (
|
||||||
|
<>
|
||||||
|
<Input
|
||||||
|
fullWidth
|
||||||
|
endAdornment={
|
||||||
|
<IconButton
|
||||||
|
size='small'
|
||||||
|
onClick={handlePasswordTypeChange}
|
||||||
|
>
|
||||||
|
{isPasswordShown ? (
|
||||||
|
<VisibilityOffOutlinedIcon htmlColor='#6b6b6b' />
|
||||||
|
) : (
|
||||||
|
<VisibilityOutlinedIcon htmlColor='#6b6b6b' />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
}
|
||||||
|
type={isPasswordShown ? 'text' : 'password'}
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
value={enteredPassword}
|
||||||
|
helperText={
|
||||||
|
isPasswordInvalid ? 'Invalid password' : ''
|
||||||
|
}
|
||||||
|
placeholder='Enter a password'
|
||||||
|
helperTextProps={{
|
||||||
|
sx: {
|
||||||
|
'&.helper_text': {
|
||||||
|
color: 'red',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
disabled={!isChecked}
|
||||||
|
/>
|
||||||
|
<StyledButton
|
||||||
|
type='submit'
|
||||||
|
fullWidth
|
||||||
|
disabled={!isChecked}
|
||||||
>
|
>
|
||||||
{isPasswordShown ? (
|
Sync{' '}
|
||||||
<VisibilityOffOutlinedIcon htmlColor='#6b6b6b' />
|
{isLoading && (
|
||||||
) : (
|
<CircularProgress
|
||||||
<VisibilityOutlinedIcon htmlColor='#6b6b6b' />
|
sx={{ marginLeft: '0.5rem' }}
|
||||||
|
size={'1rem'}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</IconButton>
|
</StyledButton>
|
||||||
}
|
</>
|
||||||
type={isPasswordShown ? 'text' : 'password'}
|
)}
|
||||||
onChange={handlePasswordChange}
|
|
||||||
value={enteredPassword}
|
|
||||||
helperText={isPasswordInvalid ? 'Invalid password' : ''}
|
|
||||||
placeholder='Enter a password'
|
|
||||||
helperTextProps={{
|
|
||||||
sx: {
|
|
||||||
'&.helper_text': {
|
|
||||||
color: 'red',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
disabled={!isChecked}
|
|
||||||
/>
|
|
||||||
<StyledButton type='submit' fullWidth disabled={!isChecked}>
|
|
||||||
Sync
|
|
||||||
</StyledButton>
|
|
||||||
</StyledSettingContainer>
|
</StyledSettingContainer>
|
||||||
<Button onClick={onClose}>Done</Button>
|
<Button onClick={onClose}>Done</Button>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
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
|
||||||
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,12 +48,17 @@ 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
|
||||||
@@ -65,6 +70,7 @@ db.version(7).stores({
|
|||||||
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,79 @@ 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, {
|
||||||
|
replace: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
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, handleClose } = useModalSearchParams()
|
||||||
|
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.ACTIVITY)
|
||||||
|
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.ACTIVITY)
|
||||||
|
|
||||||
|
const history = useLiveQuery(
|
||||||
|
getActivityHistoryQuerier(appNpub),
|
||||||
|
[],
|
||||||
|
HistoryDefaultValue,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
open={isModalOpened}
|
||||||
|
onClose={handleCloseModal}
|
||||||
|
fixedHeight='calc(100% - 5rem)'
|
||||||
|
title='Activities 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,
|
||||||
|
}))
|
||||||
|
|||||||
15
src/pages/AppPage/utils.ts
Normal file
15
src/pages/AppPage/utils.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { DbHistory, db } from '@/modules/db'
|
||||||
|
|
||||||
|
export const getActivityHistoryQuerier = (appNpub: string) => () => {
|
||||||
|
if (!appNpub.trim().length) return []
|
||||||
|
|
||||||
|
const result = db.history
|
||||||
|
.where('appNpub')
|
||||||
|
.equals(appNpub)
|
||||||
|
.limit(30)
|
||||||
|
.toArray()
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
export const HistoryDefaultValue: DbHistory[] = []
|
||||||
@@ -28,7 +28,12 @@ const HomePage = () => {
|
|||||||
)}
|
)}
|
||||||
{!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,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 enable push subscription')
|
||||||
|
notify('Push notifications enabled!', 'success')
|
||||||
|
setShowWarning(false)
|
||||||
|
} catch (error: any) {
|
||||||
|
notify(
|
||||||
|
`Failed to enable push subscription: ${error.message}`,
|
||||||
|
'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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -6,4 +6,12 @@ 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