Merge pull request #5 from nostrband/fix/modal-replace-notifs

Fix/modal replace notifs
This commit is contained in:
Nostr.Band 2024-02-02 12:59:31 +03:00 committed by GitHub
commit 878bae6c2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 1266 additions and 645 deletions

3
.env
View File

@ -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

View File

@ -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(() => {

View File

@ -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(

View File

@ -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}

View File

@ -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()

View File

@ -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>
)

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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',
},
}
})

View File

@ -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
View 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

View File

@ -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,
}
}

View 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,
}
}

View File

@ -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)

View File

@ -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
}
},
}

View File

@ -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} />
</>
)
}

View 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>
)
}

View 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>
)
}

View 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',
}))

View File

@ -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>
</>
)
}

View File

@ -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>
)
}

View 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}
/>
</>
)
}

View File

@ -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}
/>
</>
)
}

View 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>
)
}

View 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',
}))

View File

@ -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>
)
}

View File

@ -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',

View File

@ -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,
}))

View 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[] = []

View File

@ -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} />
))}

View File

@ -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,

View File

@ -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',
}))

View File

@ -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} />

View 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}
/>
)
}

View 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

View File

@ -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`,
},
}))

View 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 }
}

View 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,
}
}

View 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,
}
}

View File

@ -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',

View 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
}

View File

@ -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 {

View File

@ -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

View File

@ -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,
},
}
})

View File

@ -34,4 +34,5 @@ const StyledCheckbox = styled(
),
)(() => ({
'& .MuiSvgIcon-root': { fontSize: '1.5rem' },
marginLeft: '-10px',
}))

View 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>
)
}

View 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,
}))

View 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>
}

View 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',
},
}))

View File

@ -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>
)
}

View File

@ -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',
}
})

View File

@ -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 {

View File

@ -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',
}

View File

@ -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()
})