add prettier

This commit is contained in:
Bekbolsun
2024-02-06 15:49:05 +06:00
parent 14940a4345
commit be8cfcb3a5
118 changed files with 35826 additions and 36649 deletions

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
node_modules/

9
.prettierrc Normal file
View File

@@ -0,0 +1,9 @@
{
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"printWidth": 120,
"bracketSpacing": true,
"endOfLine": "lf"
}

62994
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,99 +1,101 @@
{ {
"name": "noauth", "name": "noauth",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@hookform/resolvers": "^3.3.4", "@hookform/resolvers": "^3.3.4",
"@mui/icons-material": "^5.14.19", "@mui/icons-material": "^5.14.19",
"@mui/material": "^5.14.20", "@mui/material": "^5.14.20",
"@nostr-dev-kit/ndk": "^2.4.0", "@nostr-dev-kit/ndk": "^2.4.0",
"@reduxjs/toolkit": "^2.0.1", "@reduxjs/toolkit": "^2.0.1",
"@testing-library/jest-dom": "^5.17.0", "@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2", "@types/jest": "^27.5.2",
"@types/node": "^17.0.45", "@types/node": "^17.0.45",
"@types/react": "^18.2.38", "@types/react": "^18.2.38",
"@types/react-copy-to-clipboard": "^5.0.7", "@types/react-copy-to-clipboard": "^5.0.7",
"@types/react-dom": "^18.2.17", "@types/react-dom": "^18.2.17",
"crypto": "^1.0.1", "crypto": "^1.0.1",
"date-fns": "^3.3.1", "date-fns": "^3.3.1",
"dexie": "^3.2.4", "dexie": "^3.2.4",
"dexie-react-hooks": "^1.1.7", "dexie-react-hooks": "^1.1.7",
"lodash.isequal": "^4.5.0", "lodash.isequal": "^4.5.0",
"memoize-one": "^6.0.0", "memoize-one": "^6.0.0",
"nostr-tools": "^1.17.0", "nostr-tools": "^1.17.0",
"notistack": "^3.0.1", "notistack": "^3.0.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-hook-form": "^7.50.0", "react-hook-form": "^7.50.0",
"react-redux": "^9.0.3", "react-redux": "^9.0.3",
"react-router-dom": "^6.20.1", "react-router-dom": "^6.20.1",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"redux-persist": "^6.0.0", "redux-persist": "^6.0.0",
"typescript": "^5.3.2", "typescript": "^5.3.2",
"use-debounce": "^10.0.0", "use-debounce": "^10.0.0",
"web-vitals": "^2.1.4", "web-vitals": "^2.1.4",
"workbox-background-sync": "^6.6.0", "workbox-background-sync": "^6.6.0",
"workbox-broadcast-update": "^6.6.0", "workbox-broadcast-update": "^6.6.0",
"workbox-cacheable-response": "^6.5.4", "workbox-cacheable-response": "^6.5.4",
"workbox-core": "^6.6.0", "workbox-core": "^6.6.0",
"workbox-expiration": "^6.6.0", "workbox-expiration": "^6.6.0",
"workbox-google-analytics": "^6.6.0", "workbox-google-analytics": "^6.6.0",
"workbox-navigation-preload": "^6.6.0", "workbox-navigation-preload": "^6.6.0",
"workbox-precaching": "^6.6.0", "workbox-precaching": "^6.6.0",
"workbox-range-requests": "^6.6.0", "workbox-range-requests": "^6.6.0",
"workbox-routing": "^6.6.0", "workbox-routing": "^6.6.0",
"workbox-strategies": "^6.6.0", "workbox-strategies": "^6.6.0",
"workbox-streams": "^6.6.0", "workbox-streams": "^6.6.0",
"yup": "^1.3.3" "yup": "^1.3.3"
}, },
"overrides": { "overrides": {
"react-scripts": { "react-scripts": {
"typescript": "^5.3.2" "typescript": "^5.3.2"
} }
}, },
"scripts": { "scripts": {
"start": "react-app-rewired start", "start": "react-app-rewired start",
"build": "react-app-rewired build", "build": "react-app-rewired build",
"test": "react-app-rewired test", "test": "react-app-rewired test",
"eject": "react-app-rewired eject", "eject": "react-app-rewired eject",
"serve": "npm run build && serve -s build" "serve": "npm run build && serve -s build",
}, "format": "npx prettier --write src"
"eslintConfig": { },
"extends": [ "eslintConfig": {
"react-app", "extends": [
"react-app/jest" "react-app",
] "react-app/jest"
}, ]
"browserslist": { },
"production": [ "browserslist": {
">0.2%", "production": [
"not dead", ">0.2%",
"not op_mini all" "not dead",
], "not op_mini all"
"development": [ ],
"last 1 chrome version", "development": [
"last 1 firefox version", "last 1 chrome version",
"last 1 safari version" "last 1 firefox version",
] "last 1 safari version"
}, ]
"devDependencies": { },
"@types/lodash.isequal": "^4.5.8", "devDependencies": {
"assert": "^2.1.0", "@types/lodash.isequal": "^4.5.8",
"buffer": "^6.0.3", "assert": "^2.1.0",
"crypto-browserify": "^3.12.0", "buffer": "^6.0.3",
"customize-cra": "^1.0.0", "crypto-browserify": "^3.12.0",
"https-browserify": "^1.0.0", "customize-cra": "^1.0.0",
"os-browserify": "^0.3.0", "https-browserify": "^1.0.0",
"process": "^0.11.10", "os-browserify": "^0.3.0",
"react-app-rewired": "^2.2.1", "prettier": "^3.2.5",
"serve": "^14.2.1", "process": "^0.11.10",
"stream-browserify": "^3.0.0", "react-app-rewired": "^2.2.1",
"stream-http": "^3.2.0", "serve": "^14.2.1",
"url": "^0.11.3" "stream-browserify": "^3.0.0",
} "stream-http": "^3.2.0",
"url": "^0.11.3"
}
} }

View File

@@ -1,9 +1,9 @@
import React from 'react'; import React from 'react'
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react'
import App from './App'; import App from './App'
test('renders learn react link', () => { test('renders learn react link', () => {
render(<App />); render(<App />)
const linkElement = screen.getByText(/learn react/i); const linkElement = screen.getByText(/learn react/i)
expect(linkElement).toBeInTheDocument(); expect(linkElement).toBeInTheDocument()
}); })

View File

@@ -2,12 +2,7 @@ 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'
import { import { setApps, setKeys, setPending, setPerms } from './store/reducers/content.slice'
setApps,
setKeys,
setPending,
setPerms,
} from './store/reducers/content.slice'
import AppRoutes from './routes/AppRoutes' import AppRoutes from './routes/AppRoutes'
import { fetchProfile, ndk } from './modules/nostr' import { fetchProfile, ndk } from './modules/nostr'
import { useModalSearchParams } from './hooks/useModalSearchParams' import { useModalSearchParams } from './hooks/useModalSearchParams'
@@ -18,86 +13,86 @@ import { ModalSignUp } from './components/Modal/ModalSignUp/ModalSignUp'
import { ModalLogin } from './components/Modal/ModalLogin/ModalLogin' import { ModalLogin } from './components/Modal/ModalLogin/ModalLogin'
function App() { function App() {
const [render, setRender] = useState(0) const [render, setRender] = useState(0)
const { handleOpen } = useModalSearchParams() const { handleOpen } = useModalSearchParams()
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const [isConnected, setIsConnected] = useState(false) const [isConnected, setIsConnected] = useState(false)
const load = useCallback(async () => { const load = useCallback(async () => {
const keys: DbKey[] = await dbi.listKeys() const keys: DbKey[] = await dbi.listKeys()
console.log(keys, 'keys') console.log(keys, 'keys')
dispatch(setKeys({ keys })) dispatch(setKeys({ keys }))
const loadProfiles = async () => { const loadProfiles = async () => {
const newKeys = [] const newKeys = []
for (const key of keys) { for (const key of keys) {
// make it async // make it async
const response = await fetchProfile(key.npub) const response = await fetchProfile(key.npub)
if (!response) { if (!response) {
newKeys.push(key) newKeys.push(key)
} else { } else {
newKeys.push({ ...key, profile: response }) newKeys.push({ ...key, profile: response })
} }
} }
dispatch(setKeys({ keys: newKeys })) dispatch(setKeys({ keys: newKeys }))
} }
// async load to avoid blocking main code below // async load to avoid blocking main code below
loadProfiles() loadProfiles()
const apps = await dbi.listApps() const apps = await dbi.listApps()
dispatch( dispatch(
setApps({ setApps({
apps: apps.map((app) => ({ apps: apps.map((app) => ({
...app, ...app,
// MOCK IMAGE // MOCK IMAGE
icon: 'https://nostr.band/android-chrome-192x192.png', icon: 'https://nostr.band/android-chrome-192x192.png',
})), })),
}), })
) )
const perms = await dbi.listPerms() const perms = await dbi.listPerms()
dispatch(setPerms({ perms })) dispatch(setPerms({ perms }))
const pending = await dbi.listPending() const pending = await dbi.listPending()
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 // eslint-disable-next-line
}, [dispatch]) }, [dispatch])
useEffect(() => { useEffect(() => {
if (isConnected) load() if (isConnected) load()
}, [render, isConnected, load]) }, [render, isConnected, load])
useEffect(() => { useEffect(() => {
ndk.connect().then(() => { ndk.connect().then(() => {
console.log('NDK connected', { ndk }) console.log('NDK connected', { ndk })
setIsConnected(true) setIsConnected(true)
}) })
// eslint-disable-next-line // eslint-disable-next-line
}, []) }, [])
// subscribe to updates from the service worker // subscribe to updates from the service worker
swicOnRender(() => { swicOnRender(() => {
console.log('render') console.log('render')
setRender((r) => r + 1) setRender((r) => r + 1)
}) })
return ( return (
<> <>
<AppRoutes /> <AppRoutes />
<ModalInitial /> <ModalInitial />
<ModalImportKeys /> <ModalImportKeys />
<ModalSignUp /> <ModalSignUp />
<ModalLogin /> <ModalLogin />
</> </>
) )
} }
export default App export default App

View File

@@ -10,14 +10,14 @@ import { ReactComponent as UnchekedLightIcon } from './icons/unchecked-light.svg
import { default as AddImageIcon } from './icons/add-image.svg' import { default as AddImageIcon } from './icons/add-image.svg'
export { export {
AppLogo, AppLogo,
ShareIcon, ShareIcon,
SettingsIcon, SettingsIcon,
CopyIcon, CopyIcon,
CheckmarkIcon, CheckmarkIcon,
CheckedIcon, CheckedIcon,
CheckedLightIcon, CheckedLightIcon,
UnchekedIcon, UnchekedIcon,
UnchekedLightIcon, UnchekedLightIcon,
AddImageIcon, AddImageIcon,
} }

View File

@@ -12,156 +12,122 @@ import { useState } from 'react'
import { swicCall } from '@/modules/swic' import { swicCall } from '@/modules/swic'
import { ACTION_TYPE } from '@/utils/consts' import { ACTION_TYPE } from '@/utils/consts'
export const ModalConfirmConnect = () => { export const ModalConfirmConnect = () => {
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT) const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
const { npub = '' } = useParams<{ npub: string }>() const { npub = '' } = useParams<{ npub: string }>()
const apps = useAppSelector((state) => selectAppsByNpub(state, npub)) const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>( const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.BASIC)
ACTION_TYPE.BASIC,
)
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
const appNpub = searchParams.get('appNpub') || '' const appNpub = searchParams.get('appNpub') || ''
const pendingReqId = searchParams.get('reqId') || '' const pendingReqId = searchParams.get('reqId') || ''
const isPopup = searchParams.get('popup') === 'true' const isPopup = searchParams.get('popup') === 'true'
const triggerApp = apps.find((app) => app.appNpub === appNpub) const triggerApp = apps.find((app) => app.appNpub === appNpub)
const { name, icon = '' } = triggerApp || {} const { name, icon = '' } = triggerApp || {}
const appName = name || getShortenNpub(appNpub) const appName = name || getShortenNpub(appNpub)
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => { const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
if (!value) return undefined if (!value) return undefined
return setSelectedActionType(value) return setSelectedActionType(value)
} }
const handleCloseModal = createHandleCloseReplace( const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
MODAL_PARAMS_KEYS.CONFIRM_CONNECT, onClose: async (sp) => {
{ sp.delete('appNpub')
onClose: async (sp) => { sp.delete('reqId')
sp.delete('appNpub') await swicCall('confirm', pendingReqId, false, false)
sp.delete('reqId') },
await swicCall('confirm', pendingReqId, false, false) })
} const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
}, onClose: (sp) => {
) sp.delete('appNpub')
const closeModalAfterRequest = createHandleCloseReplace( sp.delete('reqId')
MODAL_PARAMS_KEYS.CONFIRM_CONNECT, },
{ })
onClose: (sp) => {
sp.delete('appNpub')
sp.delete('reqId')
},
}
)
async function confirmPending( async function confirmPending(id: string, allow: boolean, remember: boolean, options?: any) {
id: string, call(async () => {
allow: boolean, await swicCall('confirm', id, allow, remember, options)
remember: boolean, console.log('confirmed', id, allow, remember, options)
options?: any closeModalAfterRequest()
) { })
call(async () => { if (isPopup) window.close()
await swicCall('confirm', id, allow, remember, options) }
console.log('confirmed', id, allow, remember, options)
closeModalAfterRequest()
})
if (isPopup) window.close();
}
const allow = () => { const allow = () => {
const options: any = {}; const options: any = {}
if (selectedActionType === ACTION_TYPE.BASIC) if (selectedActionType === ACTION_TYPE.BASIC) options.perms = [ACTION_TYPE.BASIC]
options.perms = [ACTION_TYPE.BASIC]; // else
// else // options.perms = ['connect','get_public_key'];
// options.perms = ['connect','get_public_key']; confirmPending(pendingReqId, true, true, options)
confirmPending(pendingReqId, true, true, options) }
}
const disallow = () => { const disallow = () => {
confirmPending(pendingReqId, false, true) confirmPending(pendingReqId, false, true)
} }
if (isPopup) { if (isPopup) {
document.addEventListener('visibilitychange', function() { document.addEventListener('visibilitychange', function () {
if (document.visibilityState == 'hidden') { if (document.visibilityState == 'hidden') {
disallow(); disallow()
} }
}) })
} }
return ( return (
<Modal <Modal open={isModalOpened} withCloseButton={!isPopup} onClose={!isPopup ? handleCloseModal : undefined}>
open={isModalOpened} <Stack gap={'1rem'} paddingTop={'1rem'}>
withCloseButton={!isPopup} <Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
onClose={!isPopup ? handleCloseModal : undefined} <Avatar
> variant="square"
<Stack gap={'1rem'} paddingTop={'1rem'}> sx={{
<Stack width: 56,
direction={'row'} height: 56,
gap={'1rem'} }}
alignItems={'center'} src={icon}
marginBottom={'1rem'} />
> <Box>
<Avatar <Typography variant="h5" fontWeight={600}>
variant='square' {appName}
sx={{ </Typography>
width: 56, <Typography variant="body2" color={'GrayText'}>
height: 56, Would like to connect to your account
}} </Typography>
src={icon} </Box>
/> </Stack>
<Box> <StyledToggleButtonsGroup value={selectedActionType} onChange={handleActionTypeChange} exclusive>
<Typography variant='h5' fontWeight={600}> <ActionToggleButton
{appName} value={ACTION_TYPE.BASIC}
</Typography> title="Basic permissions"
<Typography variant='body2' color={'GrayText'}> description="Read your public key, sign notes and reactions"
Would like to connect to your account // hasinfo
</Typography> />
</Box> {/* <ActionToggleButton
</Stack>
<StyledToggleButtonsGroup
value={selectedActionType}
onChange={handleActionTypeChange}
exclusive
>
<ActionToggleButton
value={ACTION_TYPE.BASIC}
title='Basic permissions'
description='Read your public key, sign notes and reactions'
// hasinfo
/>
{/* <ActionToggleButton
value={ACTION_TYPE.ADVANCED} value={ACTION_TYPE.ADVANCED}
title='Advanced' title='Advanced'
description='Use for trusted apps only' description='Use for trusted apps only'
hasinfo hasinfo
/> */} /> */}
<ActionToggleButton <ActionToggleButton
value={ACTION_TYPE.CUSTOM} value={ACTION_TYPE.CUSTOM}
title='On demand' title="On demand"
description='Assign permissions when the app asks for them' description="Assign permissions when the app asks for them"
/> />
</StyledToggleButtonsGroup> </StyledToggleButtonsGroup>
<Stack direction={'row'} gap={'1rem'}> <Stack direction={'row'} gap={'1rem'}>
<StyledButton <StyledButton onClick={disallow} varianttype="secondary">
onClick={disallow} Disallow
varianttype='secondary' </StyledButton>
> <StyledButton fullWidth onClick={allow}>
Disallow {/* Allow {selectedActionType} actions */}
</StyledButton> Connect
<StyledButton </StyledButton>
fullWidth </Stack>
onClick={allow} </Stack>
> </Modal>
{/* Allow {selectedActionType} actions */} )
Connect
</StyledButton>
</Stack>
</Stack>
</Modal>
)
} }

View File

@@ -1,32 +1,25 @@
import { AppButtonProps, Button } from '@/shared/Button/Button' import { AppButtonProps, Button } from '@/shared/Button/Button'
import { import { ToggleButtonGroup, ToggleButtonGroupProps, styled } from '@mui/material'
ToggleButtonGroup,
ToggleButtonGroupProps,
styled,
} from '@mui/material'
export const StyledButton = styled((props: AppButtonProps) => ( export const StyledButton = styled((props: AppButtonProps) => <Button {...props} />)(() => ({
<Button {...props} /> borderRadius: '19px',
))(() => ({ fontWeight: 600,
borderRadius: '19px', padding: '0.75rem 1rem',
fontWeight: 600, maxHeight: '41px',
padding: '0.75rem 1rem',
maxHeight: '41px',
})) }))
export const StyledToggleButtonsGroup = styled( export const StyledToggleButtonsGroup = styled((props: ToggleButtonGroupProps) => <ToggleButtonGroup {...props} />)(
(props: ToggleButtonGroupProps) => <ToggleButtonGroup {...props} />, () => ({
)(() => ({ gap: '0.75rem',
gap: '0.75rem', marginBottom: '1rem',
marginBottom: '1rem', justifyContent: 'space-between',
justifyContent: 'space-between', '&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped:not(:first-of-type)': {
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped:not(:first-of-type)': margin: '0',
{ border: 'initial',
margin: '0', },
border: 'initial', '&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped': {
}, border: 'initial',
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped': { borderRadius: '1rem',
border: 'initial', },
borderRadius: '1rem', })
}, )
}))

View File

@@ -3,30 +3,23 @@ import { ToggleButtonProps, Typography } from '@mui/material'
import { StyledToggleButton } from './styled' import { StyledToggleButton } from './styled'
type ActionToggleButtonProps = ToggleButtonProps & { type ActionToggleButtonProps = ToggleButtonProps & {
description?: string description?: string
hasinfo?: boolean hasinfo?: boolean
} }
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({ export const ActionToggleButton: FC<ActionToggleButtonProps> = ({ hasinfo = false, ...props }) => {
hasinfo = false, const { title, description = '' } = props
...props return (
}) => { <StyledToggleButton {...props}>
const { title, description = '' } = props <Typography variant="body2">{title}</Typography>
return ( <Typography className="description" variant="caption" color={'GrayText'}>
<StyledToggleButton {...props}> {description}
<Typography variant='body2'>{title}</Typography> </Typography>
<Typography {hasinfo && (
className='description' <Typography className="info" color={'GrayText'}>
variant='caption' Info
color={'GrayText'} </Typography>
> )}
{description} </StyledToggleButton>
</Typography> )
{hasinfo && (
<Typography className='info' color={'GrayText'}>
Info
</Typography>
)}
</StyledToggleButton>
)
} }

View File

@@ -1,32 +1,32 @@
import { ToggleButton, ToggleButtonProps, styled } from '@mui/material' import { ToggleButton, ToggleButtonProps, styled } from '@mui/material'
export const StyledToggleButton = styled((props: ToggleButtonProps) => ( export const StyledToggleButton = styled((props: ToggleButtonProps) => (
<ToggleButton classes={{ selected: 'selected' }} {...props} /> <ToggleButton classes={{ selected: 'selected' }} {...props} />
))(({ theme }) => ({ ))(({ theme }) => ({
'&:is(&, :hover, :active)': { '&:is(&, :hover, :active)': {
background: theme.palette.backgroundSecondary.default, background: theme.palette.backgroundSecondary.default,
}, },
color: theme.palette.text.primary, color: theme.palette.text.primary,
flex: '1 0 6.25rem', flex: '1 0 6.25rem',
height: '100px', height: '100px',
borderRadius: '1rem', borderRadius: '1rem',
border: `2px solid transparent !important`, border: `2px solid transparent !important`,
'&.selected': { '&.selected': {
border: `2px solid ${theme.palette.text.primary} !important`, border: `2px solid ${theme.palette.text.primary} !important`,
}, },
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
justifyContent: 'flex-start', justifyContent: 'flex-start',
textTransform: 'initial', textTransform: 'initial',
'& .description': { '& .description': {
display: 'inline-block', display: 'inline-block',
textAlign: 'left', textAlign: 'left',
lineHeight: '15px', lineHeight: '15px',
margin: '0.5rem 0 0.25rem', margin: '0.5rem 0 0.25rem',
}, },
'& .info': { '& .info': {
fontSize: '10px', fontSize: '10px',
fontWeight: 500, fontWeight: 500,
}, },
})) }))

View File

@@ -2,26 +2,13 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
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 { call, getShortenNpub, getSignReqKind } from '@/utils/helpers/helpers' import { call, getShortenNpub, getSignReqKind } from '@/utils/helpers/helpers'
import { import { Avatar, Box, List, ListItem, ListItemIcon, ListItemText, Stack, Typography } from '@mui/material'
Avatar,
Box,
List,
ListItem,
ListItemIcon,
ListItemText,
Stack,
Typography,
} from '@mui/material'
import { useParams, useSearchParams } from 'react-router-dom' import { useParams, useSearchParams } from 'react-router-dom'
import { useAppSelector } from '@/store/hooks/redux' import { useAppSelector } from '@/store/hooks/redux'
import { selectAppsByNpub } from '@/store' import { selectAppsByNpub } from '@/store'
import { ActionToggleButton } from './сomponents/ActionToggleButton' import { ActionToggleButton } from './сomponents/ActionToggleButton'
import { FC, useEffect, useMemo, useState } from 'react' import { FC, useEffect, useMemo, useState } from 'react'
import { import { StyledActionsListContainer, StyledButton, StyledToggleButtonsGroup } from './styled'
StyledActionsListContainer,
StyledButton,
StyledToggleButtonsGroup,
} 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 { Checkbox } from '@/shared/Checkbox/Checkbox' import { Checkbox } from '@/shared/Checkbox/Checkbox'
@@ -30,209 +17,161 @@ import { ACTIONS } from '@/utils/consts'
import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal' import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal'
enum ACTION_TYPE { enum ACTION_TYPE {
ALWAYS = 'ALWAYS', ALWAYS = 'ALWAYS',
ONCE = 'ONCE', ONCE = 'ONCE',
ALLOW_ALL = 'ALLOW_ALL', ALLOW_ALL = 'ALLOW_ALL',
} }
const ACTION_LABELS = { const ACTION_LABELS = {
[ACTION_TYPE.ALWAYS]: 'Always', [ACTION_TYPE.ALWAYS]: 'Always',
[ACTION_TYPE.ONCE]: 'Just Once', [ACTION_TYPE.ONCE]: 'Just Once',
[ACTION_TYPE.ALLOW_ALL]: 'All Advanced Actions', [ACTION_TYPE.ALLOW_ALL]: 'All Advanced Actions',
} }
type ModalConfirmEventProps = { type ModalConfirmEventProps = {
confirmEventReqs: IPendingsByAppNpub confirmEventReqs: IPendingsByAppNpub
} }
type PendingRequest = DbPending & { checked: boolean } type PendingRequest = DbPending & { checked: boolean }
export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs }) => {
confirmEventReqs, const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
}) => { const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const [searchParams] = useSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
const [searchParams] = useSearchParams()
const appNpub = searchParams.get('appNpub') || '' const appNpub = searchParams.get('appNpub') || ''
const isPopup = searchParams.get('popup') === 'true' const isPopup = searchParams.get('popup') === 'true'
const { npub = '' } = useParams<{ npub: string }>() const { npub = '' } = useParams<{ npub: string }>()
const apps = useAppSelector((state) => selectAppsByNpub(state, npub)) const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>( const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.ALWAYS)
ACTION_TYPE.ALWAYS, const [pendingRequests, setPendingRequests] = useState<PendingRequest[]>([])
)
const [pendingRequests, setPendingRequests] = useState<PendingRequest[]>([])
const currentAppPendingReqs = useMemo( const currentAppPendingReqs = useMemo(() => confirmEventReqs[appNpub]?.pending || [], [confirmEventReqs, appNpub])
() => confirmEventReqs[appNpub]?.pending || [],
[confirmEventReqs, appNpub],
)
useEffect(() => { useEffect(() => {
setPendingRequests( setPendingRequests(currentAppPendingReqs.map((pr) => ({ ...pr, checked: true })))
currentAppPendingReqs.map((pr) => ({ ...pr, checked: true })), }, [currentAppPendingReqs])
)
}, [currentAppPendingReqs])
const triggerApp = apps.find((app) => app.appNpub === appNpub) const triggerApp = apps.find((app) => app.appNpub === appNpub)
const { name, icon = '' } = triggerApp || {} const { name, icon = '' } = triggerApp || {}
const appName = name || getShortenNpub(appNpub) const appName = name || getShortenNpub(appNpub)
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => { const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
if (!value) return undefined if (!value) return undefined
return setSelectedActionType(value) return setSelectedActionType(value)
} }
const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked) const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked)
const handleCloseModal = createHandleCloseReplace( const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
MODAL_PARAMS_KEYS.CONFIRM_EVENT, onClose: (sp) => {
{ sp.delete('appNpub')
onClose: (sp) => { sp.delete('reqId')
sp.delete('appNpub') selectedPendingRequests.forEach(async (req) => await swicCall('confirm', req.id, false, false))
sp.delete('reqId') },
selectedPendingRequests.forEach( })
async (req) => await swicCall('confirm', req.id, false, false),
)
}
}
)
const closeModalAfterRequest = createHandleCloseReplace( const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
MODAL_PARAMS_KEYS.CONFIRM_EVENT, onClose: (sp) => {
{ sp.delete('appNpub')
onClose: (sp) => { sp.delete('reqId')
sp.delete('appNpub') },
sp.delete('reqId') })
}
}
)
async function confirmPending(allow: boolean) { async function confirmPending(allow: boolean) {
selectedPendingRequests.forEach((req) => { selectedPendingRequests.forEach((req) => {
call(async () => { call(async () => {
const remember = selectedActionType !== ACTION_TYPE.ONCE const remember = selectedActionType !== ACTION_TYPE.ONCE
await swicCall('confirm', req.id, allow, remember) await swicCall('confirm', req.id, allow, remember)
console.log('confirmed', req.id, selectedActionType, allow) console.log('confirmed', req.id, selectedActionType, allow)
}) })
}) })
closeModalAfterRequest() closeModalAfterRequest()
if (isPopup) window.close(); if (isPopup) window.close()
} }
const handleChangeCheckbox = (reqId: string) => () => { const handleChangeCheckbox = (reqId: string) => () => {
const newPendingRequests = pendingRequests.map((req) => { const newPendingRequests = pendingRequests.map((req) => {
if (req.id === reqId) return { ...req, checked: !req.checked } if (req.id === reqId) return { ...req, checked: !req.checked }
return req return req
}) })
setPendingRequests(newPendingRequests) setPendingRequests(newPendingRequests)
} }
const getAction = (req: PendingRequest) => { const getAction = (req: PendingRequest) => {
const action = ACTIONS[req.method] const action = ACTIONS[req.method]
if (req.method === 'sign_event') { if (req.method === 'sign_event') {
const kind = getSignReqKind(req) const kind = getSignReqKind(req)
if (kind !== undefined) return `${action} of kind ${kind}` if (kind !== undefined) return `${action} of kind ${kind}`
} }
return action return action
} }
if (isPopup) { if (isPopup) {
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (document.visibilityState == 'hidden') { if (document.visibilityState == 'hidden') {
confirmPending(false); confirmPending(false)
} }
}) })
} }
return ( return (
<Modal <Modal open={isModalOpened} withCloseButton={!isPopup} onClose={!isPopup ? handleCloseModal : undefined}>
open={isModalOpened} <Stack gap={'1rem'} paddingTop={'1rem'}>
withCloseButton={!isPopup} <Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
onClose={!isPopup ? handleCloseModal : undefined} <Avatar
> variant="square"
<Stack gap={'1rem'} paddingTop={'1rem'}> sx={{
<Stack width: 56,
direction={'row'} height: 56,
gap={'1rem'} borderRadius: '12px',
alignItems={'center'} }}
marginBottom={'1rem'} src={icon}
> />
<Avatar <Box>
variant='square' <Typography variant="h5" fontWeight={600}>
sx={{ {appName}
width: 56, </Typography>
height: 56, <Typography variant="body2" color={'GrayText'}>
borderRadius: '12px', Would like your permission to
}} </Typography>
src={icon} </Box>
/> </Stack>
<Box>
<Typography variant='h5' fontWeight={600}>
{appName}
</Typography>
<Typography variant='body2' color={'GrayText'}>
Would like your permission to
</Typography>
</Box>
</Stack>
<StyledActionsListContainer marginBottom={'1rem'}> <StyledActionsListContainer marginBottom={'1rem'}>
<SectionTitle>Actions</SectionTitle> <SectionTitle>Actions</SectionTitle>
<List> <List>
{pendingRequests.map((req) => { {pendingRequests.map((req) => {
return ( return (
<ListItem key={req.id}> <ListItem key={req.id}>
<ListItemIcon> <ListItemIcon>
<Checkbox <Checkbox checked={req.checked} onChange={handleChangeCheckbox(req.id)} />
checked={req.checked} </ListItemIcon>
onChange={handleChangeCheckbox( <ListItemText>{getAction(req)}</ListItemText>
req.id, </ListItem>
)} )
/> })}
</ListItemIcon> </List>
<ListItemText> </StyledActionsListContainer>
{getAction(req)} <StyledToggleButtonsGroup value={selectedActionType} onChange={handleActionTypeChange} exclusive>
</ListItemText> <ActionToggleButton value={ACTION_TYPE.ALWAYS} title="Always" />
</ListItem> <ActionToggleButton value={ACTION_TYPE.ONCE} title="Just once" />
) {/* <ActionToggleButton
})}
</List>
</StyledActionsListContainer>
<StyledToggleButtonsGroup
value={selectedActionType}
onChange={handleActionTypeChange}
exclusive
>
<ActionToggleButton
value={ACTION_TYPE.ALWAYS}
title='Always'
/>
<ActionToggleButton
value={ACTION_TYPE.ONCE}
title='Just once'
/>
{/* <ActionToggleButton
value={ACTION_TYPE.ALLOW_ALL} value={ACTION_TYPE.ALLOW_ALL}
title='Allow All Advanced Actions' title='Allow All Advanced Actions'
hasinfo hasinfo
/> */} /> */}
</StyledToggleButtonsGroup> </StyledToggleButtonsGroup>
<Stack direction={'row'} gap={'1rem'}> <Stack direction={'row'} gap={'1rem'}>
<StyledButton <StyledButton onClick={() => confirmPending(false)} varianttype="secondary">
onClick={() => confirmPending(false)} Disallow {ACTION_LABELS[selectedActionType]}
varianttype='secondary' </StyledButton>
> <StyledButton onClick={() => confirmPending(true)}>Allow {ACTION_LABELS[selectedActionType]}</StyledButton>
Disallow {ACTION_LABELS[selectedActionType]} </Stack>
</StyledButton> </Stack>
<StyledButton onClick={() => confirmPending(true)}> </Modal>
Allow {ACTION_LABELS[selectedActionType]} )
</StyledButton>
</Stack>
</Stack>
</Modal>
)
} }

View File

@@ -1,42 +1,31 @@
import { AppButtonProps, Button } from '@/shared/Button/Button' import { AppButtonProps, Button } from '@/shared/Button/Button'
import { import { Stack, StackProps, ToggleButtonGroup, ToggleButtonGroupProps, styled } from '@mui/material'
Stack,
StackProps,
ToggleButtonGroup,
ToggleButtonGroupProps,
styled,
} from '@mui/material'
export const StyledButton = styled((props: AppButtonProps) => ( export const StyledButton = styled((props: AppButtonProps) => <Button {...props} />)(() => ({
<Button {...props} /> borderRadius: '19px',
))(() => ({ fontWeight: 600,
borderRadius: '19px', padding: '0.75rem 1rem',
fontWeight: 600, maxHeight: '41px',
padding: '0.75rem 1rem',
maxHeight: '41px',
})) }))
export const StyledToggleButtonsGroup = styled( export const StyledToggleButtonsGroup = styled((props: ToggleButtonGroupProps) => <ToggleButtonGroup {...props} />)(
(props: ToggleButtonGroupProps) => <ToggleButtonGroup {...props} />, () => ({
)(() => ({ gap: '0.75rem',
gap: '0.75rem', marginBottom: '1rem',
marginBottom: '1rem', justifyContent: 'space-between',
justifyContent: 'space-between', '&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped:not(:first-of-type)': {
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped:not(:first-of-type)': margin: '0',
{ border: 'initial',
margin: '0', },
border: 'initial', '&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped': {
}, border: 'initial',
'&.MuiToggleButtonGroup-root .MuiToggleButtonGroup-grouped': { borderRadius: '1rem',
border: 'initial', },
borderRadius: '1rem', })
}, )
}))
export const StyledActionsListContainer = styled((props: StackProps) => ( export const StyledActionsListContainer = styled((props: StackProps) => <Stack {...props} />)(({ theme }) => ({
<Stack {...props} /> padding: '0.75rem',
))(({ theme }) => ({ background: theme.palette.backgroundSecondary.default,
padding: '0.75rem', borderRadius: '1rem',
background: theme.palette.backgroundSecondary.default,
borderRadius: '1rem',
})) }))

View File

@@ -3,22 +3,19 @@ import { ToggleButtonProps, Typography } from '@mui/material'
import { StyledToggleButton } from './styled' import { StyledToggleButton } from './styled'
type ActionToggleButtonProps = ToggleButtonProps & { type ActionToggleButtonProps = ToggleButtonProps & {
hasinfo?: boolean hasinfo?: boolean
} }
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({ export const ActionToggleButton: FC<ActionToggleButtonProps> = ({ hasinfo = false, ...props }) => {
hasinfo = false, const { title } = props
...props return (
}) => { <StyledToggleButton {...props}>
const { title } = props <Typography variant="body2">{title}</Typography>
return ( {hasinfo && (
<StyledToggleButton {...props}> <Typography className="info" color={'GrayText'}>
<Typography variant='body2'>{title}</Typography> Info
{hasinfo && ( </Typography>
<Typography className='info' color={'GrayText'}> )}
Info </StyledToggleButton>
</Typography> )
)}
</StyledToggleButton>
)
} }

View File

@@ -1,33 +1,33 @@
import { ToggleButton, ToggleButtonProps, styled } from '@mui/material' import { ToggleButton, ToggleButtonProps, styled } from '@mui/material'
export const StyledToggleButton = styled((props: ToggleButtonProps) => ( export const StyledToggleButton = styled((props: ToggleButtonProps) => (
<ToggleButton classes={{ selected: 'selected' }} {...props} /> <ToggleButton classes={{ selected: 'selected' }} {...props} />
))(({ theme }) => ({ ))(({ theme }) => ({
'&:is(&, :hover, :active)': { '&:is(&, :hover, :active)': {
background: theme.palette.backgroundSecondary.default, background: theme.palette.backgroundSecondary.default,
}, },
color: theme.palette.text.primary, color: theme.palette.text.primary,
flex: '1 0 6.25rem', flex: '1 0 6.25rem',
height: '100px', height: '100px',
borderRadius: '1rem', borderRadius: '1rem',
border: `2px solid transparent !important`, border: `2px solid transparent !important`,
'&.selected': { '&.selected': {
border: `2px solid ${theme.palette.text.primary} !important`, border: `2px solid ${theme.palette.text.primary} !important`,
}, },
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
alignItems: 'flex-start', alignItems: 'flex-start',
justifyContent: 'flex-start', justifyContent: 'flex-start',
textTransform: 'initial', textTransform: 'initial',
textAlign: 'left', textAlign: 'left',
'& .description': { '& .description': {
display: 'inline-block', display: 'inline-block',
textAlign: 'left', textAlign: 'left',
lineHeight: '15px', lineHeight: '15px',
margin: '0.5rem 0 0.25rem', margin: '0.5rem 0 0.25rem',
}, },
'& .info': { '& .info': {
fontSize: '10px', fontSize: '10px',
fontWeight: 500, fontWeight: 500,
}, },
})) }))

View File

@@ -12,81 +12,64 @@ import { useRef } from 'react'
import { useParams } from 'react-router-dom' import { useParams } from 'react-router-dom'
export const ModalConnectApp = () => { export const ModalConnectApp = () => {
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams() const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
const timerRef = useRef<NodeJS.Timeout>() const timerRef = useRef<NodeJS.Timeout>()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP) const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP)
const handleCloseModal = createHandleCloseReplace( const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONNECT_APP, {
MODAL_PARAMS_KEYS.CONNECT_APP, onClose: () => {
{ clearTimeout(timerRef.current)
onClose: () => { },
clearTimeout(timerRef.current) })
}
}
)
const notify = useEnqueueSnackbar() const notify = useEnqueueSnackbar()
const { npub = '' } = useParams<{ npub: string }>() const { npub = '' } = useParams<{ npub: string }>()
const bunkerStr = getBunkerLink(npub) const bunkerStr = getBunkerLink(npub)
const handleShareBunker = async () => { const handleShareBunker = async () => {
const shareData = { const shareData = {
text: bunkerStr, text: bunkerStr,
} }
try { try {
if (navigator.share && navigator.canShare(shareData)) { if (navigator.share && navigator.canShare(shareData)) {
await navigator.share(shareData) await navigator.share(shareData)
} else { } else {
navigator.clipboard.writeText(bunkerStr) navigator.clipboard.writeText(bunkerStr)
} }
} catch (err) { } catch (err) {
console.log(err) console.log(err)
notify('Your browser does not support sharing data', 'warning') notify('Your browser does not support sharing data', 'warning')
} }
} }
const handleCopy = () => { const handleCopy = () => {
timerRef.current = setTimeout(() => { timerRef.current = setTimeout(() => {
handleCloseModal() handleCloseModal()
}, 3000) }, 3000)
} }
return ( return (
<Modal <Modal open={isModalOpened} title="Share your profile" onClose={handleCloseModal}>
open={isModalOpened} <Stack gap={'1rem'} alignItems={'center'}>
title='Share your profile' <Typography variant="caption">Please, copy this code and paste it into the app to log in</Typography>
onClose={handleCloseModal} <Input
> sx={{
<Stack gap={'1rem'} alignItems={'center'}> gap: '0.5rem',
<Typography variant='caption'> }}
Please, copy this code and paste it into the app to log in fullWidth
</Typography> value={bunkerStr}
<Input endAdornment={<InputCopyButton value={bunkerStr} onCopy={handleCopy} />}
sx={{ />
gap: '0.5rem', <AppLink title="What is this?" onClick={() => handleOpen(MODAL_PARAMS_KEYS.EXPLANATION)} />
}} <Button fullWidth onClick={handleShareBunker}>
fullWidth Share it
value={bunkerStr} </Button>
endAdornment={ <Button fullWidth onClick={handleCloseModal}>
<InputCopyButton Done
value={bunkerStr} </Button>
onCopy={handleCopy} </Stack>
/> </Modal>
} )
/>
<AppLink
title='What is this?'
onClick={() => handleOpen(MODAL_PARAMS_KEYS.EXPLANATION)}
/>
<Button fullWidth onClick={handleShareBunker}>
Share it
</Button>
<Button fullWidth onClick={handleCloseModal}>
Done
</Button>
</Stack>
</Modal>
)
} }

View File

@@ -7,39 +7,37 @@ import { Button } from '@/shared/Button/Button'
import { useSearchParams } from 'react-router-dom' import { useSearchParams } from 'react-router-dom'
type ModalExplanationProps = { type ModalExplanationProps = {
explanationText?: string explanationText?: string
} }
export const ModalExplanation: FC<ModalExplanationProps> = ({ export const ModalExplanation: FC<ModalExplanationProps> = ({ explanationText = '' }) => {
explanationText = '', const { getModalOpened } = useModalSearchParams()
}) => { const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EXPLANATION)
const { getModalOpened } = useModalSearchParams() const [searchParams, setSearchParams] = useSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EXPLANATION)
const [searchParams, setSearchParams] = useSearchParams()
const handleCloseModal = () => { const handleCloseModal = () => {
searchParams.delete('type') searchParams.delete('type')
searchParams.delete(MODAL_PARAMS_KEYS.EXPLANATION) searchParams.delete(MODAL_PARAMS_KEYS.EXPLANATION)
setSearchParams(searchParams) setSearchParams(searchParams)
} }
return ( return (
<Modal <Modal
title='What is this?' title="What is this?"
open={isModalOpened} open={isModalOpened}
onClose={handleCloseModal} onClose={handleCloseModal}
PaperProps={{ PaperProps={{
sx: { sx: {
minHeight: '60%', minHeight: '60%',
}, },
}} }}
> >
<Stack height={'100%'}> <Stack height={'100%'}>
<Typography flex={1}>{explanationText}</Typography> <Typography flex={1}>{explanationText}</Typography>
<Button fullWidth onClick={handleCloseModal}> <Button fullWidth onClick={handleCloseModal}>
Got it! Got it!
</Button> </Button>
</Stack> </Stack>
</Modal> </Modal>
) )
} }

View File

@@ -11,56 +11,51 @@ import { StyledAppLogo } from './styled'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
export const ModalImportKeys = () => { export const ModalImportKeys = () => {
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.IMPORT_KEYS) const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.IMPORT_KEYS)
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.IMPORT_KEYS) const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.IMPORT_KEYS)
const notify = useEnqueueSnackbar() const notify = useEnqueueSnackbar()
const navigate = useNavigate() const navigate = useNavigate()
const [enteredNsec, setEnteredNsec] = useState('') const [enteredNsec, setEnteredNsec] = useState('')
const handleNsecChange = (e: ChangeEvent<HTMLInputElement>) => { const handleNsecChange = (e: ChangeEvent<HTMLInputElement>) => {
setEnteredNsec(e.target.value) setEnteredNsec(e.target.value)
} }
const handleSubmit = async (e: FormEvent) => { const handleSubmit = async (e: FormEvent) => {
e.preventDefault() e.preventDefault()
try { try {
if (!enteredNsec.trim().length) return if (!enteredNsec.trim().length) return
const enteredName = '' // FIXME get from input const enteredName = '' // FIXME get from input
const k: any = await swicCall('importKey', enteredName, enteredNsec) const k: any = await swicCall('importKey', enteredName, enteredNsec)
notify('Key imported!', 'success') notify('Key imported!', 'success')
navigate(`/key/${k.npub}`) navigate(`/key/${k.npub}`)
} catch (error: any) { } catch (error: any) {
notify(error.message, 'error') notify(error.message, 'error')
} }
} }
return ( return (
<Modal open={isModalOpened} onClose={handleCloseModal}> <Modal open={isModalOpened} onClose={handleCloseModal}>
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit}> <Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
<Stack <Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
direction={'row'} <StyledAppLogo />
gap={'1rem'} <Typography fontWeight={600} variant="h5">
alignItems={'center'} Import keys
alignSelf={'flex-start'} </Typography>
> </Stack>
<StyledAppLogo /> <Input
<Typography fontWeight={600} variant='h5'> label="Enter a NSEC"
Import keys placeholder="Your NSEC"
</Typography> value={enteredNsec}
</Stack> onChange={handleNsecChange}
<Input fullWidth
label='Enter a NSEC' type="password"
placeholder='Your NSEC' />
value={enteredNsec} <Button type="submit">Import nsec</Button>
onChange={handleNsecChange} </Stack>
fullWidth </Modal>
type='password' )
/>
<Button type='submit'>Import nsec</Button>
</Stack>
</Modal>
)
} }

View File

@@ -2,13 +2,13 @@ import { AppLogo } from '@/assets'
import { Box, styled } from '@mui/material' import { Box, styled } from '@mui/material'
export const StyledAppLogo = styled((props) => ( export const StyledAppLogo = styled((props) => (
<Box {...props}> <Box {...props}>
<AppLogo /> <AppLogo />
</Box> </Box>
))({ ))({
background: '#00000054', background: '#00000054',
padding: '0.75rem', padding: '0.75rem',
borderRadius: '16px', borderRadius: '16px',
display: 'grid', display: 'grid',
placeItems: 'center', placeItems: 'center',
}) })

View File

@@ -7,52 +7,38 @@ import { Fade, Stack } from '@mui/material'
import { AppLink } from '@/shared/AppLink/AppLink' import { AppLink } from '@/shared/AppLink/AppLink'
export const ModalInitial = () => { export const ModalInitial = () => {
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams() const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL) const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL)
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.INITIAL) const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.INITIAL)
const [showAdvancedContent, setShowAdvancedContent] = useState(false) const [showAdvancedContent, setShowAdvancedContent] = useState(false)
const handleShowAdvanced = () => { const handleShowAdvanced = () => {
setShowAdvancedContent(true) setShowAdvancedContent(true)
} }
useEffect(() => { useEffect(() => {
return () => { return () => {
if (isModalOpened) { if (isModalOpened) {
setShowAdvancedContent(false) setShowAdvancedContent(false)
} }
} }
}, [isModalOpened]) }, [isModalOpened])
return ( return (
<Modal open={isModalOpened} onClose={handleCloseModal}> <Modal open={isModalOpened} onClose={handleCloseModal}>
<Stack paddingTop={'0.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</Button>
Sign up <Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.LOGIN)}>Login</Button>
</Button> <AppLink title="Advanced" alignSelf={'center'} onClick={handleShowAdvanced} />
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.LOGIN)}>
Login
</Button>
<AppLink
title='Advanced'
alignSelf={'center'}
onClick={handleShowAdvanced}
/>
{showAdvancedContent && ( {showAdvancedContent && (
<Fade in> <Fade in>
<Button <Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS)}>Import keys</Button>
onClick={() => </Fade>
handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS) )}
} </Stack>
> </Modal>
Import keys )
</Button>
</Fade>
)}
</Stack>
</Modal>
)
} }

View File

@@ -18,128 +18,110 @@ import { DOMAIN } from '@/utils/consts'
import { fetchNip05 } from '@/utils/helpers/helpers' import { fetchNip05 } from '@/utils/helpers/helpers'
export const ModalLogin = () => { export const ModalLogin = () => {
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.LOGIN) const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.LOGIN)
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN) const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN)
const notify = useEnqueueSnackbar() const notify = useEnqueueSnackbar()
const navigate = useNavigate() const navigate = useNavigate()
const { const {
handleSubmit, handleSubmit,
reset, reset,
register, register,
formState: { errors }, formState: { errors },
} = useForm<FormInputType>({ } = useForm<FormInputType>({
defaultValues: { defaultValues: {
username: '', username: '',
password: '', password: '',
}, },
resolver: yupResolver(schema), resolver: yupResolver(schema),
mode: 'onSubmit', mode: 'onSubmit',
}) })
const [isPasswordShown, setIsPasswordShown] = useState(false) const [isPasswordShown, setIsPasswordShown] = useState(false)
const handlePasswordTypeChange = () => const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState)
setIsPasswordShown((prevState) => !prevState)
const cleanUpStates = useCallback(() => { const cleanUpStates = useCallback(() => {
setIsPasswordShown(false) setIsPasswordShown(false)
reset() reset()
}, [reset]) }, [reset])
const submitHandler = async (values: FormInputType) => { const submitHandler = async (values: FormInputType) => {
try { try {
let npub = values.username let npub = values.username
let name = '' let name = ''
if (!npub.startsWith('npub1')) { if (!npub.startsWith('npub1')) {
name = npub name = npub
if (!npub.includes('@')) { if (!npub.includes('@')) {
npub += '@' + DOMAIN npub += '@' + DOMAIN
} else { } else {
const nameDomain = npub.split('@') const nameDomain = npub.split('@')
if (nameDomain[1] === DOMAIN) if (nameDomain[1] === DOMAIN) name = nameDomain[0]
name = nameDomain[0]; }
} }
} if (npub.includes('@')) {
if (npub.includes('@')) { const npubNip05 = await fetchNip05(npub)
const npubNip05 = await fetchNip05(npub) if (!npubNip05) throw new Error(`Username ${npub} not found`)
if (!npubNip05) throw new Error(`Username ${npub} not found`) npub = npubNip05
npub = npubNip05 }
} const passphrase = values.password
const passphrase = values.password
console.log('fetch', npub, name) console.log('fetch', npub, name)
const k: any = await swicCall('fetchKey', npub, passphrase, name) const k: any = await swicCall('fetchKey', npub, passphrase, name)
notify(`Fetched ${k.npub}`, 'success') notify(`Fetched ${k.npub}`, 'success')
cleanUpStates() cleanUpStates()
navigate(`/key/${k.npub}`) navigate(`/key/${k.npub}`)
} catch (error: any) { } catch (error: any) {
console.log("error", error); console.log('error', error)
notify(error?.message || 'Something went wrong!', 'error') notify(error?.message || 'Something went wrong!', 'error')
} }
} }
useEffect(() => { useEffect(() => {
return () => { return () => {
if (isModalOpened) { if (isModalOpened) {
// modal closed // modal closed
cleanUpStates() cleanUpStates()
} }
} }
}, [isModalOpened, cleanUpStates]) }, [isModalOpened, cleanUpStates])
return ( return (
<Modal open={isModalOpened} onClose={handleCloseModal}> <Modal open={isModalOpened} onClose={handleCloseModal}>
<Stack <Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
gap={'1rem'} <Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
component={'form'} <StyledAppLogo />
onSubmit={handleSubmit(submitHandler)} <Typography fontWeight={600} variant="h5">
> Login
<Stack </Typography>
direction={'row'} </Stack>
gap={'1rem'} <Input
alignItems={'center'} label="Username or nip05 or npub"
alignSelf={'flex-start'} fullWidth
> placeholder="name or name@domain.com or npub1..."
<StyledAppLogo /> {...register('username')}
<Typography fontWeight={600} variant='h5'> error={!!errors.username}
Login />
</Typography> <Input
</Stack> label="Password"
<Input fullWidth
label='Username or nip05 or npub' placeholder="Your password"
fullWidth {...register('password')}
placeholder='name or name@domain.com or npub1...' endAdornment={
{...register('username')} <IconButton size="small" onClick={handlePasswordTypeChange}>
error={!!errors.username} {isPasswordShown ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlinedIcon />}
/> </IconButton>
<Input }
label='Password' type={isPasswordShown ? 'text' : 'password'}
fullWidth error={!!errors.password}
placeholder='Your password' />
{...register('password')} <Button type="submit" fullWidth>
endAdornment={ Add account
<IconButton </Button>
size='small' </Stack>
onClick={handlePasswordTypeChange} </Modal>
> )
{isPasswordShown ? (
<VisibilityOffOutlinedIcon />
) : (
<VisibilityOutlinedIcon />
)}
</IconButton>
}
type={isPasswordShown ? 'text' : 'password'}
error={!!errors.password}
/>
<Button type='submit' fullWidth>
Add account
</Button>
</Stack>
</Modal>
)
} }

View File

@@ -1,18 +1,16 @@
import * as yup from 'yup' import * as yup from 'yup'
export const schema = yup.object().shape({ export const schema = yup.object().shape({
username: yup username: yup
.string() .string()
.test('Domain validation', 'The domain is required!', function (value) { .test('Domain validation', 'The domain is required!', function (value) {
if (!value || !value.trim().length) return false if (!value || !value.trim().length) return false
const USERNAME_WITH_DOMAIN_REGEXP = new RegExp( const USERNAME_WITH_DOMAIN_REGEXP = new RegExp(/^[\w-.]+@([\w-]+\.)+[\w-]{2,8}$/g)
/^[\w-.]+@([\w-]+\.)+[\w-]{2,8}$/g, return USERNAME_WITH_DOMAIN_REGEXP.test(value)
) })
return USERNAME_WITH_DOMAIN_REGEXP.test(value) .required(),
}) password: yup.string().required().min(4),
.required(),
password: yup.string().required().min(4),
}) })
export type FormInputType = yup.InferType<typeof schema> export type FormInputType = yup.InferType<typeof schema>

View File

@@ -2,13 +2,13 @@ import { AppLogo } from '@/assets'
import { Box, styled } from '@mui/material' import { Box, styled } from '@mui/material'
export const StyledAppLogo = styled((props) => ( export const StyledAppLogo = styled((props) => (
<Box {...props}> <Box {...props}>
<AppLogo /> <AppLogo />
</Box> </Box>
))({ ))({
background: '#00000054', background: '#00000054',
padding: '0.75rem', padding: '0.75rem',
borderRadius: '16px', borderRadius: '16px',
display: 'grid', display: 'grid',
placeItems: 'center', placeItems: 'center',
}) })

View File

@@ -2,18 +2,8 @@ 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 { import { Box, CircularProgress, IconButton, Stack, Typography } from '@mui/material'
Box, import { StyledButton, StyledSettingContainer, StyledSynchedText } from './styled'
CircularProgress,
IconButton,
Stack,
Typography,
} from '@mui/material'
import {
StyledButton,
StyledSettingContainer,
StyledSynchedText,
} from './styled'
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle' import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
import { CheckmarkIcon } from '@/assets' import { CheckmarkIcon } from '@/assets'
import { Input } from '@/shared/Input/Input' import { Input } from '@/shared/Input/Input'
@@ -27,144 +17,123 @@ import { useParams } from 'react-router-dom'
import { dbi } from '@/modules/db' import { dbi } from '@/modules/db'
type ModalSettingsProps = { type ModalSettingsProps = {
isSynced: boolean isSynced: boolean
} }
export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => { export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
const { npub = '' } = useParams<{ npub: string }>() const { npub = '' } = useParams<{ npub: string }>()
const notify = useEnqueueSnackbar() const notify = useEnqueueSnackbar()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS) const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS)
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SETTINGS) const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SETTINGS)
const [enteredPassword, setEnteredPassword] = useState('') const [enteredPassword, setEnteredPassword] = useState('')
const [isPasswordShown, setIsPasswordShown] = useState(false) const [isPasswordShown, setIsPasswordShown] = useState(false)
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false) const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
const [isChecked, setIsChecked] = useState(false) const [isChecked, setIsChecked] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
useEffect(() => setIsChecked(isSynced), [isModalOpened, isSynced])
useEffect(() => setIsChecked(isSynced), [isModalOpened, isSynced]) const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
setIsPasswordInvalid(false)
setEnteredPassword(e.target.value)
}
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => { const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState)
setIsPasswordInvalid(false)
setEnteredPassword(e.target.value)
}
const handlePasswordTypeChange = () => const onClose = () => {
setIsPasswordShown((prevState) => !prevState) handleCloseModal()
setEnteredPassword('')
setIsPasswordInvalid(false)
}
const onClose = () => { const handleChangeCheckbox = (e: unknown, checked: boolean) => {
handleCloseModal() setIsChecked(checked)
setEnteredPassword('') }
setIsPasswordInvalid(false)
}
const handleChangeCheckbox = (e: unknown, checked: boolean) => { const handleSubmit = async (e: React.FormEvent) => {
setIsChecked(checked) e.preventDefault()
} setIsPasswordInvalid(false)
const handleSubmit = async (e: React.FormEvent) => { if (enteredPassword.trim().length < 6) {
e.preventDefault() return setIsPasswordInvalid(true)
setIsPasswordInvalid(false) }
try {
setIsLoading(true)
await swicCall('saveKey', npub, enteredPassword)
notify('Key saved', 'success')
dbi.addSynced(npub) // Sync npub
setEnteredPassword('')
setIsPasswordInvalid(false)
setIsLoading(false)
} catch (error) {
setIsPasswordInvalid(false)
setIsLoading(false)
}
}
if (enteredPassword.trim().length < 6) { return (
return setIsPasswordInvalid(true) <Modal open={isModalOpened} onClose={onClose} title="Settings">
} <Stack gap={'1rem'}>
try { <StyledSettingContainer onSubmit={handleSubmit}>
setIsLoading(true) <Stack direction={'row'} justifyContent={'space-between'}>
await swicCall('saveKey', npub, enteredPassword) <SectionTitle>Cloud sync</SectionTitle>
notify('Key saved', 'success') {isSynced && (
dbi.addSynced(npub) // Sync npub <StyledSynchedText>
setEnteredPassword('') <CheckmarkIcon /> Synched
setIsPasswordInvalid(false) </StyledSynchedText>
setIsLoading(false) )}
} catch (error) { </Stack>
setIsPasswordInvalid(false) <Box>
setIsLoading(false) <Checkbox onChange={handleChangeCheckbox} checked={isChecked} />
} <Typography variant="caption">Use this key on multiple devices</Typography>
} </Box>
<Input
return ( fullWidth
<Modal open={isModalOpened} onClose={onClose} title='Settings'> endAdornment={
<Stack gap={'1rem'}> <IconButton size="small" onClick={handlePasswordTypeChange}>
<StyledSettingContainer onSubmit={handleSubmit}> {isPasswordShown ? (
<Stack direction={'row'} justifyContent={'space-between'}> <VisibilityOffOutlinedIcon htmlColor="#6b6b6b" />
<SectionTitle>Cloud sync</SectionTitle> ) : (
{isSynced && ( <VisibilityOutlinedIcon htmlColor="#6b6b6b" />
<StyledSynchedText> )}
<CheckmarkIcon /> Synched </IconButton>
</StyledSynchedText> }
)} type={isPasswordShown ? 'text' : 'password'}
</Stack> onChange={handlePasswordChange}
<Box> value={enteredPassword}
<Checkbox helperText={isPasswordInvalid ? 'Invalid password' : ''}
onChange={handleChangeCheckbox} placeholder="Enter a password"
checked={isChecked} helperTextProps={{
/> sx: {
<Typography variant='caption'> '&.helper_text': {
Use this key on multiple devices color: 'red',
</Typography> },
</Box> },
<Input }}
fullWidth disabled={!isChecked}
endAdornment={ />
<IconButton {isSynced ? (
size='small' <Typography variant="body2" color={'GrayText'}>
onClick={handlePasswordTypeChange} To change your password, type a new one and sync.
> </Typography>
{isPasswordShown ? ( ) : (
<VisibilityOffOutlinedIcon htmlColor='#6b6b6b' /> <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
<VisibilityOutlinedIcon htmlColor='#6b6b6b' /> another device.
)} </Typography>
</IconButton> )}
} <StyledButton type="submit" fullWidth disabled={!isChecked}>
type={isPasswordShown ? 'text' : 'password'} Sync {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
onChange={handlePasswordChange} </StyledButton>
value={enteredPassword} </StyledSettingContainer>
helperText={ <Button onClick={onClose}>Done</Button>
isPasswordInvalid ? 'Invalid password' : '' </Stack>
} </Modal>
placeholder='Enter a password' )
helperTextProps={{
sx: {
'&.helper_text': {
color: 'red',
},
},
}}
disabled={!isChecked}
/>
{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>
</Stack>
</Modal>
)
} }

View File

@@ -1,37 +1,31 @@
import { Button } from '@/shared/Button/Button' import { Button } from '@/shared/Button/Button'
import { import { Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material'
Stack,
StackProps,
Typography,
TypographyProps,
styled,
} from '@mui/material'
export const StyledSettingContainer = styled((props: StackProps) => ( export const StyledSettingContainer = styled((props: StackProps) => (
<Stack gap={'0.75rem'} component={'form'} {...props} /> <Stack gap={'0.75rem'} component={'form'} {...props} />
))(({ theme }) => ({ ))(({ theme }) => ({
padding: '1rem', 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,
})) }))
export const StyledButton = styled(Button)(({ theme }) => { export const StyledButton = styled(Button)(({ theme }) => {
return { return {
'&.button:is(:hover, :active, &)': { '&.button:is(:hover, :active, &)': {
background: theme.palette.secondary.main, background: theme.palette.secondary.main,
color: theme.palette.text.primary, color: theme.palette.text.primary,
}, },
':disabled': { ':disabled': {
cursor: 'not-allowed', cursor: 'not-allowed',
}, },
} }
}) })
export const StyledSynchedText = styled((props: TypographyProps) => ( export const StyledSynchedText = styled((props: TypographyProps) => <Typography variant="caption" {...props} />)(({
<Typography variant='caption' {...props} /> theme,
))(({ theme }) => { }) => {
return { return {
color: theme.palette.success.main, color: theme.palette.success.main,
} }
}) })

View File

@@ -14,103 +14,87 @@ import { DOMAIN, NOAUTHD_URL } from '@/utils/consts'
import { fetchNip05 } from '@/utils/helpers/helpers' import { fetchNip05 } from '@/utils/helpers/helpers'
export const ModalSignUp = () => { export const ModalSignUp = () => {
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SIGN_UP) const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SIGN_UP)
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SIGN_UP) const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SIGN_UP)
const notify = useEnqueueSnackbar() const notify = useEnqueueSnackbar()
const theme = useTheme() const theme = useTheme()
const navigate = useNavigate() const navigate = useNavigate()
const [enteredValue, setEnteredValue] = useState('') const [enteredValue, setEnteredValue] = useState('')
const [isAvailable, setIsAvailable] = useState(false) const [isAvailable, setIsAvailable] = useState(false)
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => { const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
setEnteredValue(e.target.value) setEnteredValue(e.target.value)
const name = e.target.value.trim() const name = e.target.value.trim()
if (name) { if (name) {
const npubNip05 = await fetchNip05(`${name}@${DOMAIN}`) const npubNip05 = await fetchNip05(`${name}@${DOMAIN}`)
setIsAvailable(!npubNip05) setIsAvailable(!npubNip05)
} else { } else {
setIsAvailable(false) setIsAvailable(false)
} }
} }
const inputHelperText = enteredValue const inputHelperText = enteredValue ? (
? ( isAvailable ? (
isAvailable ? ( <>
<> <CheckmarkIcon /> Available
<CheckmarkIcon /> Available </>
</> ) : (
) : ( <>Already taken</>
<> )
Already taken ) : (
</> "Don't worry, username can be changed later."
) )
) : (
"Don't worry, username can be changed later."
);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
const name = enteredValue.trim() const name = enteredValue.trim()
if (!name.length) return if (!name.length) return
e.preventDefault() e.preventDefault()
try { try {
const k: any = await swicCall('generateKey', name) const k: any = await swicCall('generateKey', name)
notify(`Account created for "${name}"`, 'success') notify(`Account created for "${name}"`, 'success')
navigate(`/key/${k.npub}`) navigate(`/key/${k.npub}`)
} catch (error: any) { } catch (error: any) {
notify(error.message, 'error') notify(error.message, 'error')
} }
} }
return ( return (
<Modal open={isModalOpened} onClose={handleCloseModal}> <Modal open={isModalOpened} onClose={handleCloseModal}>
<Stack <Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
paddingTop={'1rem'} <Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
gap={'1rem'} <StyledAppLogo />
component={'form'} <Typography fontWeight={600} variant="h5">
onSubmit={handleSubmit} Sign up
> </Typography>
<Stack </Stack>
direction={'row'} <Input
gap={'1rem'} label="Enter a Username"
alignItems={'center'} fullWidth
alignSelf={'flex-start'} placeholder="Username"
> helperText={inputHelperText}
<StyledAppLogo /> endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
<Typography fontWeight={600} variant='h5'> onChange={handleInputChange}
Sign up value={enteredValue}
</Typography> helperTextProps={{
</Stack> sx: {
<Input '&.helper_text': {
label='Enter a Username' color:
fullWidth enteredValue && isAvailable
placeholder='Username' ? theme.palette.success.main
helperText={inputHelperText} : enteredValue && !isAvailable
endAdornment={ ? theme.palette.error.main
<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography> : theme.palette.textSecondaryDecorate.main,
} },
onChange={handleInputChange} },
value={enteredValue} }}
helperTextProps={{ />
sx: { <Button fullWidth type="submit">
'&.helper_text': { Create account
color: enteredValue && isAvailable </Button>
? theme.palette.success.main </Stack>
: (enteredValue && !isAvailable </Modal>
? theme.palette.error.main )
: theme.palette.textSecondaryDecorate.main
)
,
},
},
}}
/>
<Button fullWidth type='submit'>
Create account
</Button>
</Stack>
</Modal>
)
} }

View File

@@ -2,13 +2,13 @@ import { AppLogo } from '@/assets'
import { Box, styled } from '@mui/material' import { Box, styled } from '@mui/material'
export const StyledAppLogo = styled((props) => ( export const StyledAppLogo = styled((props) => (
<Box {...props}> <Box {...props}>
<AppLogo /> <AppLogo />
</Box> </Box>
))({ ))({
background: '#00000054', background: '#00000054',
padding: '0.75rem', padding: '0.75rem',
borderRadius: '16px', borderRadius: '16px',
display: 'grid', display: 'grid',
placeItems: 'center', placeItems: 'center',
}) })

View File

@@ -5,21 +5,19 @@ import CloseIcon from '@mui/icons-material/Close'
import { NotificationProps } from './types' import { NotificationProps } from './types'
import { StyledAlert, StyledContainer } from './styled' import { StyledAlert, StyledContainer } from './styled'
export const Notification = forwardRef<HTMLDivElement, NotificationProps>( export const Notification = forwardRef<HTMLDivElement, NotificationProps>(({ message, alertvariant, id }, ref) => {
({ message, alertvariant, id }, ref) => { const { closeSnackbar } = useSnackbar()
const { closeSnackbar } = useSnackbar()
const closeSnackBarHandler = () => closeSnackbar(id) const closeSnackBarHandler = () => closeSnackbar(id)
return ( return (
<StyledAlert alertvariant={alertvariant} ref={ref}> <StyledAlert alertvariant={alertvariant} ref={ref}>
<StyledContainer> <StyledContainer>
<Typography variant='body1'>{message}</Typography> <Typography variant="body1">{message}</Typography>
<IconButton onClick={closeSnackBarHandler} color='inherit'> <IconButton onClick={closeSnackBarHandler} color="inherit">
<CloseIcon color='inherit' /> <CloseIcon color="inherit" />
</IconButton> </IconButton>
</StyledContainer> </StyledContainer>
</StyledAlert> </StyledAlert>
) )
}, })
)

View File

@@ -3,7 +3,7 @@ import { VariantType } from 'notistack'
type Variant = Exclude<VariantType, 'default' | 'info'> type Variant = Exclude<VariantType, 'default' | 'info'>
export const BORDER_STYLES: Record<Variant, string> = { export const BORDER_STYLES: Record<Variant, string> = {
error: '#b90e0a', error: '#b90e0a',
success: '#32cd32', success: '#32cd32',
warning: '#FF9500', warning: '#FF9500',
} }

View File

@@ -4,43 +4,41 @@ import { BORDER_STYLES } from './const'
import { forwardRef } from 'react' import { forwardRef } from 'react'
export const StyledAlert = styled( export const StyledAlert = styled(
forwardRef<HTMLDivElement, StyledAlertProps>((props, ref) => ( forwardRef<HTMLDivElement, StyledAlertProps>((props, ref) => <Alert {...props} ref={ref} icon={false} />)
<Alert {...props} ref={ref} icon={false} />
)),
)(({ alertvariant }) => ({ )(({ alertvariant }) => ({
width: '100%', width: '100%',
maxHeight: 56, maxHeight: 56,
padding: '0.5rem 1rem', padding: '0.5rem 1rem',
backgroundColor: '#FFF', backgroundColor: '#FFF',
borderRadius: 4, borderRadius: 4,
border: `solid ${BORDER_STYLES[alertvariant]} 1px`, border: `solid ${BORDER_STYLES[alertvariant]} 1px`,
color: BORDER_STYLES[alertvariant], color: BORDER_STYLES[alertvariant],
fontSize: 12, fontSize: 12,
fontWeight: '500', fontWeight: '500',
'& .MuiAlert-message': { '& .MuiAlert-message': {
display: 'flex', display: 'flex',
minWidth: '100%', minWidth: '100%',
justifyContent: 'space-between', justifyContent: 'space-between',
overflow: 'hidden', overflow: 'hidden',
padding: 0, padding: 0,
}, },
})) }))
export const StyledContainer = styled(Box)(() => ({ export const StyledContainer = styled(Box)(() => ({
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
gap: '1rem', gap: '1rem',
width: '100%', width: '100%',
'& > .MuiTypography-root': { '& > .MuiTypography-root': {
flex: 1, flex: 1,
width: '100%', width: '100%',
wordBreak: 'break-word', wordBreak: 'break-word',
display: '-webkit-box', display: '-webkit-box',
WebkitLineClamp: 2, WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical', WebkitBoxOrient: 'vertical',
overflow: 'hidden', overflow: 'hidden',
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
fontWeight: 500, fontWeight: 500,
}, },
})) }))

View File

@@ -2,10 +2,10 @@ import { AlertProps } from '@mui/material'
import { SnackbarKey, VariantType } from 'notistack' import { SnackbarKey, VariantType } from 'notistack'
export type StyledAlertProps = Omit<AlertProps, 'id'> & { export type StyledAlertProps = Omit<AlertProps, 'id'> & {
alertvariant: Exclude<VariantType, 'default' | 'info'> alertvariant: Exclude<VariantType, 'default' | 'info'>
} }
export type NotificationProps = { export type NotificationProps = {
message: string message: string
id: SnackbarKey id: SnackbarKey
} & StyledAlertProps } & StyledAlertProps

View File

@@ -3,17 +3,17 @@ import { IconContainer, StyledContainer } from './styled'
import { BoxProps, Typography } from '@mui/material' import { BoxProps, Typography } from '@mui/material'
type WarningProps = { type WarningProps = {
message: string | ReactNode message: string | ReactNode
Icon?: ReactNode Icon?: ReactNode
} & BoxProps } & BoxProps
export const Warning: FC<WarningProps> = ({ message, Icon, ...restProps }) => { export const Warning: FC<WarningProps> = ({ message, Icon, ...restProps }) => {
return ( return (
<StyledContainer {...restProps}> <StyledContainer {...restProps}>
{Icon && <IconContainer>{Icon}</IconContainer>} {Icon && <IconContainer>{Icon}</IconContainer>}
<Typography flex={1} noWrap> <Typography flex={1} noWrap>
{message} {message}
</Typography> </Typography>
</StyledContainer> </StyledContainer>
) )
} }

View File

@@ -1,26 +1,22 @@
import { Box, BoxProps, styled } from '@mui/material' import { Box, BoxProps, styled } from '@mui/material'
export const StyledContainer = styled((props: BoxProps) => <Box {...props} />)( export const StyledContainer = styled((props: BoxProps) => <Box {...props} />)(() => {
() => { return {
return { borderRadius: '4px',
borderRadius: '4px', border: '1px solid grey',
border: '1px solid grey', padding: '0.5rem',
padding: '0.5rem', display: 'flex',
display: 'flex', alignItems: 'center',
alignItems: 'center', gap: '1rem',
gap: '1rem', cursor: 'pointer',
cursor: 'pointer', }
} })
},
)
export const IconContainer = styled((props: BoxProps) => <Box {...props} />)( export const IconContainer = styled((props: BoxProps) => <Box {...props} />)(() => ({
() => ({ width: '40px',
width: '40px', height: '40px',
height: '40px', borderRadius: '50%',
borderRadius: '50%', background: 'blue',
background: 'blue', display: 'grid',
display: 'grid', placeItems: 'center',
placeItems: 'center', }))
}),
)

View File

@@ -1,33 +1,20 @@
import { import { useSnackbar as useDefaultSnackbar, OptionsObject, VariantType } from 'notistack'
useSnackbar as useDefaultSnackbar,
OptionsObject,
VariantType,
} from 'notistack'
import { Notification } from '../components/Notification/Notification' import { Notification } from '../components/Notification/Notification'
export const useEnqueueSnackbar = () => { export const useEnqueueSnackbar = () => {
const { enqueueSnackbar } = useDefaultSnackbar() const { enqueueSnackbar } = useDefaultSnackbar()
const showSnackbar = ( const showSnackbar = (message: string, variant: Exclude<VariantType, 'default' | 'info'> = 'success') => {
message: string, enqueueSnackbar(message, {
variant: Exclude<VariantType, 'default' | 'info'> = 'success', anchorOrigin: {
) => { vertical: 'top',
enqueueSnackbar(message, { horizontal: 'right',
anchorOrigin: { },
vertical: 'top', content: (id) => {
horizontal: 'right', return <Notification id={id} message={message} alertvariant={variant} />
}, },
content: (id) => { } as OptionsObject)
return ( }
<Notification
id={id}
message={message}
alertvariant={variant}
/>
)
},
} as OptionsObject)
}
return showSnackbar return showSnackbar
} }

View File

@@ -8,16 +8,15 @@ import { useState, useEffect } from 'react'
const iOSRegex = /iPad|iPhone|iPod/ const iOSRegex = /iPad|iPhone|iPod/
function useIsIOS() { function useIsIOS() {
const [isIOS, setIsIOS] = useState(false) const [isIOS, setIsIOS] = useState(false)
useEffect(() => { useEffect(() => {
const isIOSUserAgent = const isIOSUserAgent =
iOSRegex.test(navigator.userAgent) || iOSRegex.test(navigator.userAgent) || (navigator.userAgent.includes('Mac') && 'ontouchend' in document)
(navigator.userAgent.includes('Mac') && 'ontouchend' in document) setIsIOS(isIOSUserAgent)
setIsIOS(isIOSUserAgent) }, [])
}, [])
return isIOS return isIOS
} }
export default useIsIOS export default useIsIOS

View File

@@ -1,96 +1,84 @@
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { useCallback } from 'react' import { useCallback } from 'react'
import { import { createSearchParams, useLocation, useNavigate, useSearchParams } from 'react-router-dom'
createSearchParams,
useLocation,
useNavigate,
useSearchParams,
} from 'react-router-dom'
type SearchParamsType = { type SearchParamsType = {
[key: string]: string [key: string]: string
} }
export type IExtraOptions = { export type IExtraOptions = {
search?: SearchParamsType search?: SearchParamsType
replace?: boolean replace?: boolean
append?: boolean append?: boolean
} }
export type IExtraCloseOptions = { export type IExtraCloseOptions = {
replace?: boolean replace?: boolean
onClose?: (s: URLSearchParams) => void onClose?: (s: URLSearchParams) => void
} }
export const useModalSearchParams = () => { export const useModalSearchParams = () => {
const [searchParams, setSearchParams] = useSearchParams() const [searchParams, setSearchParams] = useSearchParams()
const location = useLocation() const location = useLocation()
const navigate = useNavigate() const navigate = useNavigate()
const getEnumParam = useCallback((modal: MODAL_PARAMS_KEYS) => { const getEnumParam = useCallback((modal: MODAL_PARAMS_KEYS) => {
return Object.values(MODAL_PARAMS_KEYS)[ return Object.values(MODAL_PARAMS_KEYS)[Object.values(MODAL_PARAMS_KEYS).indexOf(modal)]
Object.values(MODAL_PARAMS_KEYS).indexOf(modal) }, [])
]
}, [])
const createHandleClose = const createHandleClose = (modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraCloseOptions) => () => {
(modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraCloseOptions) => const enumKey = getEnumParam(modal)
() => { searchParams.delete(enumKey)
const enumKey = getEnumParam(modal) extraOptions?.onClose && extraOptions?.onClose(searchParams)
searchParams.delete(enumKey) console.log({ searchParams })
extraOptions?.onClose && extraOptions?.onClose(searchParams) setSearchParams(searchParams, { replace: !!extraOptions?.replace })
console.log({ searchParams }) }
setSearchParams(searchParams, { replace: !!extraOptions?.replace })
}
const createHandleCloseReplace = const createHandleCloseReplace = (modal: MODAL_PARAMS_KEYS, extraOptions: IExtraCloseOptions = {}) => {
(modal: MODAL_PARAMS_KEYS, extraOptions: IExtraCloseOptions = {}) => { return createHandleClose(modal, { ...extraOptions, replace: true })
return createHandleClose(modal, { ...extraOptions, replace: true }) }
}
const handleOpen = useCallback( const handleOpen = useCallback(
(modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraOptions) => { (modal: MODAL_PARAMS_KEYS, extraOptions?: IExtraOptions) => {
const enumKey = getEnumParam(modal) const enumKey = getEnumParam(modal)
let searchParamsData: SearchParamsType = { [enumKey]: 'true' } let searchParamsData: SearchParamsType = { [enumKey]: 'true' }
if (extraOptions?.search) { if (extraOptions?.search) {
searchParamsData = { searchParamsData = {
...searchParamsData, ...searchParamsData,
...extraOptions.search, ...extraOptions.search,
} }
} }
const searchString = !extraOptions?.append const searchString = !extraOptions?.append
? createSearchParams(searchParamsData).toString() ? createSearchParams(searchParamsData).toString()
: `${location.search}&${createSearchParams( : `${location.search}&${createSearchParams(searchParamsData).toString()}`
searchParamsData,
).toString()}`
navigate( navigate(
{ {
pathname: location.pathname, pathname: location.pathname,
search: searchString, search: searchString,
}, },
{ replace: !!extraOptions?.replace }, { replace: !!extraOptions?.replace }
) )
}, },
[location, navigate, getEnumParam], [location, navigate, getEnumParam]
) )
const getModalOpened = useCallback( const getModalOpened = useCallback(
(modal: MODAL_PARAMS_KEYS) => { (modal: MODAL_PARAMS_KEYS) => {
const enumKey = getEnumParam(modal) const enumKey = getEnumParam(modal)
const modalOpened = searchParams.get(enumKey) === 'true' const modalOpened = searchParams.get(enumKey) === 'true'
return modalOpened return modalOpened
}, },
[getEnumParam, searchParams], [getEnumParam, searchParams]
) )
return { return {
getModalOpened, getModalOpened,
createHandleClose, createHandleClose,
createHandleCloseReplace, createHandleCloseReplace,
handleOpen, handleOpen,
} }
} }

View File

@@ -1,21 +1,21 @@
import React, { useState } from 'react' import React, { useState } from 'react'
export const useOpenMenu = () => { export const useOpenMenu = () => {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null) const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
const open = Boolean(anchorEl) const open = Boolean(anchorEl)
const handleOpen = (event: React.MouseEvent<HTMLElement>) => { const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget) setAnchorEl(event.currentTarget)
} }
const handleClose = () => { const handleClose = () => {
setAnchorEl(null) setAnchorEl(null)
} }
return { return {
open, open,
handleOpen, handleOpen,
handleClose, handleClose,
anchorEl, anchorEl,
} }
} }

View File

@@ -1,15 +1,15 @@
import { useCallback, useState } from 'react' import { useCallback, useState } from 'react'
export const useToggleConfirm = () => { export const useToggleConfirm = () => {
const [showConfirm, setShowConfirm] = useState(false) const [showConfirm, setShowConfirm] = useState(false)
const handleShow = useCallback(() => setShowConfirm(true), []) const handleShow = useCallback(() => setShowConfirm(true), [])
const handleClose = useCallback(() => setShowConfirm(false), []) const handleClose = useCallback(() => setShowConfirm(false), [])
return { return {
open: showConfirm, open: showConfirm,
handleShow, handleShow,
handleClose, handleClose,
} }
} }

View File

@@ -1,23 +1,21 @@
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans',
'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased;
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
-moz-osx-font-smoothing: grayscale;
} }
code { code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
monospace;
} }
html, html,
body, body,
#root { #root {
height: 100%; height: 100%;
} }

View File

@@ -13,19 +13,19 @@ import { SnackbarProvider } from 'notistack'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement) const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter> <BrowserRouter>
<Provider store={store}> <Provider store={store}>
<PersistGate loading={null} persistor={persistor}> <PersistGate loading={null} persistor={persistor}>
<ThemeProvider> <ThemeProvider>
<SnackbarProvider maxSnack={3} autoHideDuration={3000}> <SnackbarProvider maxSnack={3} autoHideDuration={3000}>
<App /> <App />
</SnackbarProvider> </SnackbarProvider>
</ThemeProvider> </ThemeProvider>
</PersistGate> </PersistGate>
</Provider> </Provider>
</BrowserRouter> </BrowserRouter>
</React.StrictMode>, </React.StrictMode>
) )
// If you want your app to work offline and load faster, you can change // If you want your app to work offline and load faster, you can change

View File

@@ -10,57 +10,47 @@ import { ProfileMenu } from './components/ProfileMenu'
import { getShortenNpub } from '@/utils/helpers/helpers' import { getShortenNpub } from '@/utils/helpers/helpers'
export const Header = () => { export const Header = () => {
const { npub = '' } = useParams<{ npub: string }>() const { npub = '' } = useParams<{ npub: string }>()
const [profile, setProfile] = useState<MetaEvent | null>(null) const [profile, setProfile] = useState<MetaEvent | null>(null)
const load = useCallback(async () => { const load = useCallback(async () => {
if (!npub) return setProfile(null) if (!npub) return setProfile(null)
try { try {
const response = await fetchProfile(npub) const response = await fetchProfile(npub)
setProfile(response as any) setProfile(response as any)
} catch (e) { } catch (e) {
return setProfile(null) return setProfile(null)
} }
}, [npub]) }, [npub])
useEffect(() => { useEffect(() => {
load() load()
}, [load]) }, [load])
const showProfile = Boolean(npub || profile) const showProfile = Boolean(npub || profile)
const userName = profile?.info?.name || getShortenNpub(npub) const userName = profile?.info?.name || getShortenNpub(npub)
const userAvatar = profile?.info?.picture || '' const userAvatar = profile?.info?.picture || ''
return ( return (
<StyledAppBar position='fixed'> <StyledAppBar position="fixed">
<Toolbar sx={{ padding: '12px' }}> <Toolbar sx={{ padding: '12px' }}>
<Stack <Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'} width={'100%'}>
direction={'row'} {showProfile ? (
justifyContent={'space-between'} <Stack gap={'1rem'} direction={'row'} alignItems={'center'} flex={1}>
alignItems={'center'} <Avatar src={userAvatar} alt={userName} />
width={'100%'} <Typography fontWeight={600}>{userName}</Typography>
> </Stack>
{showProfile ? ( ) : (
<Stack <StyledAppName>
gap={'1rem'} <AppLogo />
direction={'row'} <span>Nsec.app</span>
alignItems={'center'} </StyledAppName>
flex={1} )}
>
<Avatar src={userAvatar} alt={userName} />
<Typography fontWeight={600}>{userName}</Typography>
</Stack>
) : (
<StyledAppName>
<AppLogo />
<span>Nsec.app</span>
</StyledAppName>
)}
{showProfile ? <ProfileMenu /> : <Menu />} {showProfile ? <ProfileMenu /> : <Menu />}
</Stack> </Stack>
</Toolbar> </Toolbar>
</StyledAppBar> </StyledAppBar>
) )
} }

View File

@@ -1,48 +1,30 @@
import { DbKey } from '@/modules/db' import { DbKey } from '@/modules/db'
import { getShortenNpub } from '@/utils/helpers/helpers' import { getShortenNpub } from '@/utils/helpers/helpers'
import { import { Avatar, ListItemIcon, MenuItem, Stack, Typography } from '@mui/material'
Avatar,
ListItemIcon,
MenuItem,
Stack,
Typography,
} from '@mui/material'
import React, { FC } from 'react' import React, { FC } from 'react'
type ListProfilesProps = { type ListProfilesProps = {
keys: DbKey[] keys: DbKey[]
onClickItem: (key: DbKey) => void onClickItem: (key: DbKey) => void
} }
export const ListProfiles: FC<ListProfilesProps> = ({ export const ListProfiles: FC<ListProfilesProps> = ({ keys = [], onClickItem }) => {
keys = [], return (
onClickItem, <Stack maxHeight={'10rem'} overflow={'auto'}>
}) => { {keys.map((key) => {
return ( const userName = key?.profile?.info?.name || getShortenNpub(key.npub)
<Stack maxHeight={'10rem'} overflow={'auto'}> const userAvatar = key?.profile?.info?.picture || ''
{keys.map((key) => { return (
const userName = <MenuItem sx={{ gap: '0.5rem' }} onClick={() => onClickItem(key)} key={key.npub}>
key?.profile?.info?.name || getShortenNpub(key.npub) <ListItemIcon>
const userAvatar = key?.profile?.info?.picture || '' <Avatar src={userAvatar} alt={userName} sx={{ width: 36, height: 36 }} />
return ( </ListItemIcon>
<MenuItem <Typography variant="body2" noWrap>
sx={{ gap: '0.5rem' }} {userName}
onClick={() => onClickItem(key)} </Typography>
key={key.npub} </MenuItem>
> )
<ListItemIcon> })}
<Avatar </Stack>
src={userAvatar} )
alt={userName}
sx={{ width: 36, height: 36 }}
/>
</ListItemIcon>
<Typography variant='body2' noWrap>
{userName}
</Typography>
</MenuItem>
)
})}
</Stack>
)
} }

View File

@@ -14,61 +14,46 @@ import MenuRoundedIcon from '@mui/icons-material/MenuRounded'
import { selectKeys } from '@/store' import { selectKeys } from '@/store'
export const Menu = () => { export const Menu = () => {
const themeMode = useAppSelector((state) => state.ui.themeMode) const themeMode = useAppSelector((state) => state.ui.themeMode)
const keys = useAppSelector(selectKeys) const keys = useAppSelector(selectKeys)
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const { handleOpen } = useModalSearchParams() const { handleOpen } = useModalSearchParams()
const isDarkMode = themeMode === 'dark' const isDarkMode = themeMode === 'dark'
const isNoKeys = !keys || keys.length === 0 const isNoKeys = !keys || keys.length === 0
const { const { anchorEl, handleClose, handleOpen: handleOpenMenu, open } = useOpenMenu()
anchorEl,
handleClose,
handleOpen: handleOpenMenu,
open,
} = useOpenMenu()
const handleChangeMode = () => { const handleChangeMode = () => {
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' })) dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
} }
const handleNavigateToAuth = () => { const handleNavigateToAuth = () => {
handleOpen(MODAL_PARAMS_KEYS.INITIAL) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
handleClose() handleClose()
} }
const themeIcon = isDarkMode ? ( const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#feb94a" />
<DarkModeIcon htmlColor='#fff' />
) : (
<LightModeIcon htmlColor='#feb94a' />
)
return ( return (
<> <>
<MenuButton onClick={handleOpenMenu}> <MenuButton onClick={handleOpenMenu}>
<MenuRoundedIcon color='inherit' /> <MenuRoundedIcon color="inherit" />
</MenuButton> </MenuButton>
<MuiMenu <MuiMenu
anchorEl={anchorEl} anchorEl={anchorEl}
open={open} open={open}
onClose={handleClose} onClose={handleClose}
sx={{ sx={{
zIndex: 1302, zIndex: 1302,
}} }}
> >
<MenuItem <MenuItem
Icon={ Icon={isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />}
isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon /> onClick={handleNavigateToAuth}
} title={isNoKeys ? 'Sign up' : 'Add account'}
onClick={handleNavigateToAuth} />
title={isNoKeys ? 'Sign up' : 'Add account'} <MenuItem Icon={themeIcon} onClick={handleChangeMode} title="Change theme" />
/> </MuiMenu>
<MenuItem </>
Icon={themeIcon} )
onClick={handleChangeMode}
title='Change theme'
/>
</MuiMenu>
</>
)
} }

View File

@@ -1,24 +1,20 @@
import React, { FC, ReactNode } from 'react' import React, { FC, ReactNode } from 'react'
import { StyledMenuItem } from './styled' import { StyledMenuItem } from './styled'
import { import { ListItemIcon, MenuItemProps as MuiMenuItemProps, Typography } from '@mui/material'
ListItemIcon,
MenuItemProps as MuiMenuItemProps,
Typography,
} from '@mui/material'
type MenuItemProps = { type MenuItemProps = {
onClick: () => void onClick: () => void
title: string title: string
Icon: ReactNode Icon: ReactNode
} & MuiMenuItemProps } & MuiMenuItemProps
export const MenuItem: FC<MenuItemProps> = ({ onClick, Icon, title }) => { export const MenuItem: FC<MenuItemProps> = ({ onClick, Icon, title }) => {
return ( return (
<StyledMenuItem onClick={onClick}> <StyledMenuItem onClick={onClick}>
<ListItemIcon>{Icon}</ListItemIcon> <ListItemIcon>{Icon}</ListItemIcon>
<Typography fontWeight={500} variant='body2' noWrap> <Typography fontWeight={500} variant="body2" noWrap>
{title} {title}
</Typography> </Typography>
</StyledMenuItem> </StyledMenuItem>
) )
} }

View File

@@ -18,86 +18,61 @@ import { ListProfiles } from './ListProfiles'
import { DbKey } from '@/modules/db' import { DbKey } from '@/modules/db'
export const ProfileMenu = () => { export const ProfileMenu = () => {
const { const { anchorEl, handleOpen: handleOpenMenu, open, handleClose } = useOpenMenu()
anchorEl, const { handleOpen } = useModalSearchParams()
handleOpen: handleOpenMenu,
open,
handleClose,
} = useOpenMenu()
const { handleOpen } = useModalSearchParams()
const keys = useAppSelector(selectKeys) const keys = useAppSelector(selectKeys)
const isNoKeys = !keys || keys.length === 0 const isNoKeys = !keys || keys.length === 0
const themeMode = useAppSelector((state) => state.ui.themeMode) const themeMode = useAppSelector((state) => state.ui.themeMode)
const isDarkMode = themeMode === 'dark' const isDarkMode = themeMode === 'dark'
const dispatch = useAppDispatch() const dispatch = useAppDispatch()
const navigate = useNavigate() const navigate = useNavigate()
const handleNavigateToAuth = () => { const handleNavigateToAuth = () => {
handleOpen(MODAL_PARAMS_KEYS.INITIAL) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
handleClose() handleClose()
} }
const handleNavigateHome = () => { const handleNavigateHome = () => {
navigate('/home') navigate('/home')
handleClose() handleClose()
} }
const handleChangeMode = () => { const handleChangeMode = () => {
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' })) dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
} }
const handleNavigateToKeyInnerPage = (key: DbKey) => { const handleNavigateToKeyInnerPage = (key: DbKey) => {
navigate('/key/' + key.npub) navigate('/key/' + key.npub)
handleClose() handleClose()
} }
const themeIcon = isDarkMode ? ( const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#feb94a" />
<DarkModeIcon htmlColor='#fff' />
) : (
<LightModeIcon htmlColor='#feb94a' />
)
return ( return (
<> <>
<MenuButton onClick={handleOpenMenu}> <MenuButton onClick={handleOpenMenu}>
<KeyboardArrowDownRoundedIcon <KeyboardArrowDownRoundedIcon color="inherit" fontSize="large" />
color='inherit' </MenuButton>
fontSize='large' <Menu
/> open={open}
</MenuButton> anchorEl={anchorEl}
<Menu onClose={handleClose}
open={open} sx={{
anchorEl={anchorEl} zIndex: 1302,
onClose={handleClose} }}
sx={{ >
zIndex: 1302, <ListProfiles keys={keys} onClickItem={handleNavigateToKeyInnerPage} />
}} <Divider />
> <MenuItem Icon={<HomeRoundedIcon />} onClick={handleNavigateHome} title="Home" />
<ListProfiles <MenuItem
keys={keys} Icon={isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />}
onClickItem={handleNavigateToKeyInnerPage} onClick={handleNavigateToAuth}
/> title={isNoKeys ? 'Sign up' : 'Add account'}
<Divider /> />
<MenuItem <MenuItem Icon={themeIcon} onClick={handleChangeMode} title="Change theme" />
Icon={<HomeRoundedIcon />} </Menu>
onClick={handleNavigateHome} </>
title='Home' )
/>
<MenuItem
Icon={
isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />
}
onClick={handleNavigateToAuth}
title={isNoKeys ? 'Sign up' : 'Add account'}
/>
<MenuItem
Icon={themeIcon}
onClick={handleChangeMode}
title='Change theme'
/>
</Menu>
</>
)
} }

View File

@@ -1,26 +1,16 @@
import { import { IconButton, IconButtonProps, MenuItem, MenuItemProps, styled } from '@mui/material'
IconButton,
IconButtonProps,
MenuItem,
MenuItemProps,
styled,
} from '@mui/material'
export const MenuButton = styled((props: IconButtonProps) => ( export const MenuButton = styled((props: IconButtonProps) => <IconButton {...props} />)(({ theme }) => {
<IconButton {...props} /> const isDark = theme.palette.mode === 'dark'
))(({ theme }) => { return {
const isDark = theme.palette.mode === 'dark' borderRadius: '1rem',
return { background: isDark ? '#333333A8' : 'transparent',
borderRadius: '1rem', color: isDark ? '#FFFFFFA8' : 'initial',
background: isDark ? '#333333A8' : 'transparent', width: 42,
color: isDark ? '#FFFFFFA8' : 'initial', height: 42,
width: 42, }
height: 42,
}
}) })
export const StyledMenuItem = styled((props: MenuItemProps) => ( export const StyledMenuItem = styled((props: MenuItemProps) => <MenuItem {...props} />)(() => ({
<MenuItem {...props} /> padding: '0.5rem 1rem',
))(() => ({
padding: '0.5rem 1rem',
})) }))

View File

@@ -2,30 +2,30 @@ import { AppBar, Typography, TypographyProps, styled } from '@mui/material'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
export const StyledAppBar = styled(AppBar)(({ theme }) => { export const StyledAppBar = styled(AppBar)(({ theme }) => {
return { return {
color: theme.palette.primary.main, color: theme.palette.primary.main,
boxShadow: 'none', boxShadow: 'none',
marginBottom: '1rem', marginBottom: '1rem',
background: theme.palette.background.default, background: theme.palette.background.default,
zIndex: 1301, zIndex: 1301,
maxWidth: 'inherit', maxWidth: 'inherit',
left: '50%', left: '50%',
transform: 'translateX(-50%)', transform: 'translateX(-50%)',
} }
}) })
export const StyledAppName = styled((props: TypographyProps) => ( export const StyledAppName = styled((props: TypographyProps) => (
<Typography component={Link} to={'/'} flexGrow={1} {...props} /> <Typography component={Link} to={'/'} flexGrow={1} {...props} />
))(() => ({ ))(() => ({
'&:not(:hover)': { '&:not(:hover)': {
textDecoration: 'initial', textDecoration: 'initial',
}, },
color: 'inherit', color: 'inherit',
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: '0.75rem', gap: '0.75rem',
fontWeight: 600, fontWeight: 600,
fontSize: '1rem', fontSize: '1rem',
lineHeight: '22.4px', lineHeight: '22.4px',
marginLeft: '0.5rem', marginLeft: '0.5rem',
})) }))

View File

@@ -1,45 +1,37 @@
import { FC } from 'react' import { FC } from 'react'
import { Outlet } from 'react-router-dom' import { Outlet } from 'react-router-dom'
import { Header } from './Header/Header' import { Header } from './Header/Header'
import { import { Container, ContainerProps, Divider, DividerProps, styled } from '@mui/material'
Container,
ContainerProps,
Divider,
DividerProps,
styled,
} from '@mui/material'
export const Layout: FC = () => { export const Layout: FC = () => {
return ( return (
<StyledContainer maxWidth='md'> <StyledContainer maxWidth="md">
<Header /> <Header />
<StyledDivider /> <StyledDivider />
<main> <main>
<Outlet /> <Outlet />
</main> </main>
</StyledContainer> </StyledContainer>
) )
} }
const StyledContainer = styled((props: ContainerProps) => ( const StyledContainer = styled((props: ContainerProps) => <Container maxWidth="sm" {...props} />)({
<Container maxWidth='sm' {...props} /> height: '100%',
))({ display: 'flex',
height: '100%', flexDirection: 'column',
display: 'flex', paddingBottom: '1rem',
flexDirection: 'column', position: 'relative',
paddingBottom: '1rem', '& > main': {
position: 'relative', flex: 1,
'& > main': { maxHeight: '100%',
flex: 1, paddingTop: 'calc(66px + 1rem)',
maxHeight: '100%', },
paddingTop: 'calc(66px + 1rem)',
},
}) })
const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({ const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({
position: 'absolute', position: 'absolute',
top: '66px', top: '66px',
width: '100%', width: '100%',
left: 0, left: 0,
height: '2px', height: '2px',
}) })

File diff suppressed because it is too large Load Diff

View File

@@ -2,217 +2,212 @@ import { MetaEvent } from '@/types/meta-event'
import Dexie from 'dexie' import Dexie from 'dexie'
export interface DbKey { export interface DbKey {
npub: string npub: string
nip05?: string nip05?: string
name?: string name?: string
avatar?: string avatar?: string
relays?: string[] relays?: string[]
enckey: string enckey: string
profile?: MetaEvent | null profile?: MetaEvent | null
} }
export interface DbApp { export interface DbApp {
appNpub: string appNpub: string
npub: string npub: string
name: string name: string
icon: string icon: string
url: string url: string
timestamp: number timestamp: number
} }
export interface DbPerm { export interface DbPerm {
id: string id: string
npub: string npub: string
appNpub: string appNpub: string
perm: string perm: string
value: string value: string
timestamp: number timestamp: number
} }
export interface DbPending { export interface DbPending {
id: string id: string
npub: string npub: string
appNpub: string appNpub: string
timestamp: number timestamp: number
method: string method: string
params: string params: string
} }
export interface DbHistory { export interface DbHistory {
id: string id: string
npub: string npub: string
appNpub: string appNpub: string
timestamp: number timestamp: number
method: string method: string
params: string params: string
allowed: boolean allowed: boolean
} }
export interface DbSyncHistory { export interface DbSyncHistory {
npub: string 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> syncHistory: Dexie.Table<DbSyncHistory, string>
} }
export const db = new Dexie('noauthdb') as DbSchema export const db = new Dexie('noauthdb') as DbSchema
db.version(8).stores({ db.version(8).stores({
keys: 'npub', keys: 'npub',
apps: 'appNpub,npub,name,timestamp', apps: 'appNpub,npub,name,timestamp',
perms: 'id,npub,appNpub,perm,value,timestamp', perms: 'id,npub,appNpub,perm,value,timestamp',
pending: 'id,npub,appNpub,timestamp,method', pending: 'id,npub,appNpub,timestamp,method',
history: 'id,npub,appNpub,timestamp,method,allowed', history: 'id,npub,appNpub,timestamp,method,allowed',
requestHistory: 'id', requestHistory: 'id',
syncHistory: 'npub', syncHistory: 'npub',
}) })
export const dbi = { export const dbi = {
addKey: async (key: DbKey) => { addKey: async (key: DbKey) => {
try { try {
await db.keys.add(key) await db.keys.add(key)
} catch (error) { } catch (error) {
console.log(`db addKey error: ${error}`) console.log(`db addKey error: ${error}`)
} }
}, },
listKeys: async (): Promise<DbKey[]> => { listKeys: async (): Promise<DbKey[]> => {
try { try {
return await db.keys.toArray() return await db.keys.toArray()
} catch (error) { } catch (error) {
console.log(`db listKeys error: ${error}`) console.log(`db listKeys error: ${error}`)
return [] return []
} }
}, },
getApp: async (appNpub: string) => { getApp: async (appNpub: string) => {
try { try {
return await db.apps.get(appNpub) return await db.apps.get(appNpub)
} catch (error) { } catch (error) {
console.log(`db getApp error: ${error}`) console.log(`db getApp error: ${error}`)
} }
}, },
addApp: async (app: DbApp) => { addApp: async (app: DbApp) => {
try { try {
await db.apps.add(app) await db.apps.add(app)
} catch (error) { } catch (error) {
console.log(`db addApp error: ${error}`) console.log(`db addApp error: ${error}`)
} }
}, },
listApps: async (): Promise<DbApp[]> => { listApps: async (): Promise<DbApp[]> => {
try { try {
return await db.apps.toArray() return await db.apps.toArray()
} catch (error) { } catch (error) {
console.log(`db listApps error: ${error}`) console.log(`db listApps error: ${error}`)
return [] return []
} }
}, },
removeApp: async (appNpub: string) => { removeApp: async (appNpub: string) => {
try { try {
return await db.apps.delete(appNpub) return await db.apps.delete(appNpub)
} catch (error) { } catch (error) {
console.log(`db removeApp error: ${error}`) console.log(`db removeApp error: ${error}`)
} }
}, },
addPerm: async (perm: DbPerm) => { addPerm: async (perm: DbPerm) => {
try { try {
await db.perms.add(perm) await db.perms.add(perm)
} catch (error) { } catch (error) {
console.log(`db addPerm error: ${error}`) console.log(`db addPerm error: ${error}`)
} }
}, },
listPerms: async (): Promise<DbPerm[]> => { listPerms: async (): Promise<DbPerm[]> => {
try { try {
return await db.perms.toArray() return await db.perms.toArray()
} catch (error) { } catch (error) {
console.log(`db listPerms error: ${error}`) console.log(`db listPerms error: ${error}`)
return [] return []
} }
}, },
removePerm: async (id: string) => { removePerm: async (id: string) => {
try { try {
return await db.perms.delete(id) return await db.perms.delete(id)
} catch (error) { } catch (error) {
console.log(`db removePerm error: ${error}`) console.log(`db removePerm error: ${error}`)
} }
}, },
removeAppPerms: async (appNpub: string) => { removeAppPerms: async (appNpub: string) => {
try { try {
return await db.perms.where({ appNpub }).delete() return await db.perms.where({ appNpub }).delete()
} catch (error) { } catch (error) {
console.log(`db removeAppPerms error: ${error}`) console.log(`db removeAppPerms error: ${error}`)
} }
}, },
addPending: async (r: DbPending) => { addPending: async (r: DbPending) => {
try { try {
return db.transaction('rw', db.pending, db.history, async () => { return db.transaction('rw', db.pending, db.history, async () => {
const exists = const exists =
(await db.pending.where('id').equals(r.id).toArray()) (await db.pending.where('id').equals(r.id).toArray()).length > 0 ||
.length > 0 || (await db.history.where('id').equals(r.id).toArray()).length > 0
(await db.history.where('id').equals(r.id).toArray()) if (exists) return false
.length > 0
if (exists) return false
await db.pending.add(r) await db.pending.add(r)
return true return true
}) })
} catch (error) { } catch (error) {
console.log(`db addPending error: ${error}`) console.log(`db addPending error: ${error}`)
return false return false
} }
}, },
removePending: async (id: string) => { removePending: async (id: string) => {
try { try {
return await db.pending.delete(id) return await db.pending.delete(id)
} catch (error) { } catch (error) {
console.log(`db removePending error: ${error}`) console.log(`db removePending error: ${error}`)
} }
}, },
listPending: async (): Promise<DbPending[]> => { listPending: async (): Promise<DbPending[]> => {
try { try {
return await db.pending.toArray() return await db.pending.toArray()
} catch (error) { } catch (error) {
console.log(`db listPending error: ${error}`) console.log(`db listPending error: ${error}`)
return [] return []
} }
}, },
confirmPending: async (id: string, allowed: boolean) => { confirmPending: async (id: string, allowed: boolean) => {
try { try {
db.transaction('rw', db.pending, db.history, async () => { db.transaction('rw', db.pending, db.history, async () => {
const r: DbPending | undefined = await db.pending const r: DbPending | undefined = await db.pending.where('id').equals(id).first()
.where('id') if (!r) throw new Error('Pending not found ' + id)
.equals(id) const h: DbHistory = {
.first() ...r,
if (!r) throw new Error('Pending not found ' + id) allowed,
const h: DbHistory = { }
...r, await db.pending.delete(id)
allowed, await db.history.add(h)
} })
await db.pending.delete(id) } catch (error) {
await db.history.add(h) console.log(`db addPending error: ${error}`)
}) }
} catch (error) { },
console.log(`db addPending error: ${error}`) addConfirmed: async (r: DbHistory) => {
} try {
}, await db.history.add(r)
addConfirmed: async (r: DbHistory) => { } catch (error) {
try { console.log(`db addConfirmed error: ${error}`)
await db.history.add(r) return false
} catch (error) { }
console.log(`db addConfirmed error: ${error}`) },
return false addSynced: async (npub: string) => {
} try {
}, await db.syncHistory.add({ npub })
addSynced: async (npub: string) => { } catch (error) {
try { console.log(`db addSynced error: ${error}`)
await db.syncHistory.add({ npub }) return false
} catch (error) { }
console.log(`db addSynced error: ${error}`) },
return false
}
},
} }

View File

@@ -4,123 +4,92 @@ ende stands for encryption decryption
import { secp256k1 as secp } from '@noble/curves/secp256k1' import { secp256k1 as secp } from '@noble/curves/secp256k1'
//import * as secp from "./vendor/secp256k1.js"; //import * as secp from "./vendor/secp256k1.js";
export async function encrypt( export async function encrypt(publicKey: string, message: string, privateKey: string): Promise<string> {
publicKey: string, const key = secp.getSharedSecret(privateKey, '02' + publicKey)
message: string, const normalizedKey = getNormalizedX(key)
privateKey: string, const encoder = new TextEncoder()
): Promise<string> { const iv = Uint8Array.from(randomBytes(16))
const key = secp.getSharedSecret(privateKey, "02" + publicKey); const plaintext = encoder.encode(message)
const normalizedKey = getNormalizedX(key); const cryptoKey = await crypto.subtle.importKey('raw', normalizedKey, { name: 'AES-CBC' }, false, ['encrypt'])
const encoder = new TextEncoder(); const ciphertext = await crypto.subtle.encrypt({ name: 'AES-CBC', iv }, cryptoKey, plaintext)
const iv = Uint8Array.from(randomBytes(16));
const plaintext = encoder.encode(message);
const cryptoKey = await crypto.subtle.importKey(
"raw",
normalizedKey,
{ name: "AES-CBC" },
false,
["encrypt"],
);
const ciphertext = await crypto.subtle.encrypt(
{ name: "AES-CBC", iv },
cryptoKey,
plaintext,
);
const ctb64 = toBase64(new Uint8Array(ciphertext)); const ctb64 = toBase64(new Uint8Array(ciphertext))
const ivb64 = toBase64(new Uint8Array(iv.buffer)); const ivb64 = toBase64(new Uint8Array(iv.buffer))
return `${ctb64}?iv=${ivb64}`; return `${ctb64}?iv=${ivb64}`
} }
export async function decrypt( export async function decrypt(privateKey: string, publicKey: string, data: string): Promise<string | Error> {
privateKey: string, const key = secp.getSharedSecret(privateKey, '02' + publicKey) // this line is very slow
publicKey: string, return decrypt_with_shared_secret(data, key)
data: string,
): Promise<string | Error> {
const key = secp.getSharedSecret(privateKey, "02" + publicKey); // this line is very slow
return decrypt_with_shared_secret(data, key);
} }
export async function decrypt_with_shared_secret( export async function decrypt_with_shared_secret(data: string, sharedSecret: Uint8Array): Promise<string | Error> {
data: string, const [ctb64, ivb64] = data.split('?iv=')
sharedSecret: Uint8Array, const normalizedKey = getNormalizedX(sharedSecret)
): Promise<string | Error> {
const [ctb64, ivb64] = data.split("?iv=");
const normalizedKey = getNormalizedX(sharedSecret);
const cryptoKey = await crypto.subtle.importKey( const cryptoKey = await crypto.subtle.importKey('raw', normalizedKey, { name: 'AES-CBC' }, false, ['decrypt'])
"raw", let ciphertext: BufferSource
normalizedKey, let iv: BufferSource
{ name: "AES-CBC" }, try {
false, ciphertext = decodeBase64(ctb64)
["decrypt"], iv = decodeBase64(ivb64)
); } catch (e) {
let ciphertext: BufferSource; return new Error(`failed to decode, ${e}`)
let iv: BufferSource; }
try {
ciphertext = decodeBase64(ctb64);
iv = decodeBase64(ivb64);
} catch (e) {
return new Error(`failed to decode, ${e}`);
}
try { try {
const plaintext = await crypto.subtle.decrypt( const plaintext = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, ciphertext)
{ name: "AES-CBC", iv }, const text = utf8Decode(plaintext)
cryptoKey, return text
ciphertext, } catch (e) {
); return new Error(`failed to decrypt, ${e}`)
const text = utf8Decode(plaintext); }
return text;
} catch (e) {
return new Error(`failed to decrypt, ${e}`);
}
} }
export function utf8Encode(str: string) { export function utf8Encode(str: string) {
let encoder = new TextEncoder(); let encoder = new TextEncoder()
return encoder.encode(str); return encoder.encode(str)
} }
export function utf8Decode(bin: Uint8Array | ArrayBuffer): string { export function utf8Decode(bin: Uint8Array | ArrayBuffer): string {
let decoder = new TextDecoder(); let decoder = new TextDecoder()
return decoder.decode(bin); return decoder.decode(bin)
} }
function toBase64(uInt8Array: Uint8Array) { function toBase64(uInt8Array: Uint8Array) {
let strChunks = new Array(uInt8Array.length); let strChunks = new Array(uInt8Array.length)
let i = 0; let i = 0
// @ts-ignore // @ts-ignore
for (let byte of uInt8Array) { for (let byte of uInt8Array) {
strChunks[i] = String.fromCharCode(byte); // bytes to utf16 string strChunks[i] = String.fromCharCode(byte) // bytes to utf16 string
i++; i++
} }
return btoa(strChunks.join("")); return btoa(strChunks.join(''))
} }
function decodeBase64(base64String: string) { function decodeBase64(base64String: string) {
const binaryString = atob(base64String); const binaryString = atob(base64String)
const length = binaryString.length; const length = binaryString.length
const bytes = new Uint8Array(length); const bytes = new Uint8Array(length)
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i); bytes[i] = binaryString.charCodeAt(i)
} }
return bytes; return bytes
} }
function getNormalizedX(key: Uint8Array): Uint8Array { function getNormalizedX(key: Uint8Array): Uint8Array {
return key.slice(1, 33); return key.slice(1, 33)
} }
function randomBytes(bytesLength: number = 32) { function randomBytes(bytesLength: number = 32) {
return crypto.getRandomValues(new Uint8Array(bytesLength)); return crypto.getRandomValues(new Uint8Array(bytesLength))
} }
export function utf16Encode(str: string): number[] { export function utf16Encode(str: string): number[] {
let array = new Array(str.length); let array = new Array(str.length)
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
array[i] = str.charCodeAt(i); array[i] = str.charCodeAt(i)
} }
return array; return array
} }

View File

@@ -1,5 +1,5 @@
import crypto, { pbkdf2 } from 'crypto'; import crypto, { pbkdf2 } from 'crypto'
import { getPublicKey, nip19 } from 'nostr-tools'; import { getPublicKey, nip19 } from 'nostr-tools'
// encrypted keys have a prefix and version // encrypted keys have a prefix and version
// so that we'd be able to switch to a better // so that we'd be able to switch to a better
@@ -17,14 +17,14 @@ const ITERATIONS_PWH = 100000
const HASH_SIZE = 32 const HASH_SIZE = 32
const HASH_ALGO = 'sha256' const HASH_ALGO = 'sha256'
// encryption // encryption
const ALGO = 'aes-256-cbc'; const ALGO = 'aes-256-cbc'
const IV_SIZE = 16 const IV_SIZE = 16
// valid passwords are a limited ASCII only, see notes below // valid passwords are a limited ASCII only, see notes below
const ASCII_REGEX = /^[A-Za-z0-9!@#$%^&*()]{4,}$/ const ASCII_REGEX = /^[A-Za-z0-9!@#$%^&*()]{4,}$/
const ALGO_LOCAL = 'AES-CBC'; const ALGO_LOCAL = 'AES-CBC'
const KEY_SIZE_LOCAL = 256; const KEY_SIZE_LOCAL = 256
export class Keys { export class Keys {
subtle: any subtle: any
@@ -37,9 +37,7 @@ export class Keys {
return ASCII_REGEX.test(passphrase) return ASCII_REGEX.test(passphrase)
} }
public async generatePassKey(pubkey: string, passphrase: string) public async generatePassKey(pubkey: string, passphrase: string): Promise<{ passkey: Buffer; pwh: string }> {
: Promise<{ passkey: Buffer, pwh: string }> {
const salt = Buffer.from(pubkey, 'hex') const salt = Buffer.from(pubkey, 'hex')
// https://nodejs.org/api/crypto.html#using-strings-as-inputs-to-cryptographic-apis // https://nodejs.org/api/crypto.html#using-strings-as-inputs-to-cryptographic-apis
@@ -47,7 +45,7 @@ export class Keys {
// We could use string.normalize() to make sure all JS implementations // We could use string.normalize() to make sure all JS implementations
// are compatible, but since we're looking to make this thing a standard // are compatible, but since we're looking to make this thing a standard
// then the simplest way is to exclude unicode and only work with ASCII // then the simplest way is to exclude unicode and only work with ASCII
if (!this.isValidPassphase(passphrase)) throw new Error("Password must be 4+ ASCII chars") if (!this.isValidPassphase(passphrase)) throw new Error('Password must be 4+ ASCII chars')
return new Promise((ok, fail) => { return new Promise((ok, fail) => {
// NOTE: we should use Argon2 or scrypt later, for now // NOTE: we should use Argon2 or scrypt later, for now
@@ -57,7 +55,11 @@ export class Keys {
else { else {
pbkdf2(key, passphrase, ITERATIONS_PWH, HASH_SIZE, HASH_ALGO, (err, hash) => { pbkdf2(key, passphrase, ITERATIONS_PWH, HASH_SIZE, HASH_ALGO, (err, hash) => {
if (err) fail(err) if (err) fail(err)
else ok({ passkey: key, pwh: hash.toString('hex') }) else
ok({
passkey: key,
pwh: hash.toString('hex'),
})
}) })
} }
}) })
@@ -65,8 +67,8 @@ export class Keys {
} }
private isSafari() { private isSafari() {
const chrome = navigator.userAgent.indexOf("Chrome") > -1; const chrome = navigator.userAgent.indexOf('Chrome') > -1
const safari = navigator.userAgent.indexOf("Safari") > -1; const safari = navigator.userAgent.indexOf('Safari') > -1
return safari && !chrome return safari && !chrome
} }
@@ -81,8 +83,8 @@ export class Keys {
{ name: ALGO_LOCAL, length: KEY_SIZE_LOCAL }, { name: ALGO_LOCAL, length: KEY_SIZE_LOCAL },
// NOTE: important to make sure it's not visible in // NOTE: important to make sure it's not visible in
// dev console in IndexedDB // dev console in IndexedDB
/*extractable*/false, /*extractable*/ false,
["encrypt", "decrypt"] ['encrypt', 'decrypt']
) )
} }
@@ -94,25 +96,30 @@ export class Keys {
return `${PREFIX_LOCAL}:${VERSION_LOCAL}:${iv.toString('hex')}:${Buffer.from(encrypted).toString('hex')}}` return `${PREFIX_LOCAL}:${VERSION_LOCAL}:${iv.toString('hex')}:${Buffer.from(encrypted).toString('hex')}}`
} }
public async decryptKeyLocal({ enckey, localKey }: { enckey: string, localKey: CryptoKey | {} }): Promise<string> { public async decryptKeyLocal({ enckey, localKey }: { enckey: string; localKey: CryptoKey | {} }): Promise<string> {
if (this.isSafari()) return enckey if (this.isSafari()) return enckey
const parts = enckey.split(':') const parts = enckey.split(':')
if (parts.length !== 4) throw new Error("Bad encrypted key") if (parts.length !== 4) throw new Error('Bad encrypted key')
if (parts[0] !== PREFIX_LOCAL) throw new Error("Bad encrypted key prefix") if (parts[0] !== PREFIX_LOCAL) throw new Error('Bad encrypted key prefix')
if (parts[1] !== VERSION_LOCAL) throw new Error("Bad encrypted key version") if (parts[1] !== VERSION_LOCAL) throw new Error('Bad encrypted key version')
if (parts[2].length !== IV_SIZE * 2) throw new Error("Bad encrypted key iv") if (parts[2].length !== IV_SIZE * 2) throw new Error('Bad encrypted key iv')
if (parts[3].length < 30) throw new Error("Bad encrypted key data") if (parts[3].length < 30) throw new Error('Bad encrypted key data')
const iv = Buffer.from(parts[2], 'hex'); const iv = Buffer.from(parts[2], 'hex')
const data = Buffer.from(parts[3], 'hex'); const data = Buffer.from(parts[3], 'hex')
const decrypted = await this.subtle.decrypt({ name: ALGO_LOCAL, iv }, localKey, data) const decrypted = await this.subtle.decrypt({ name: ALGO_LOCAL, iv }, localKey, data)
const { type, data: value } = nip19.decode(Buffer.from(decrypted).toString()) const { type, data: value } = nip19.decode(Buffer.from(decrypted).toString())
if (type !== "nsec") throw new Error("Bad encrypted key payload type") if (type !== 'nsec') throw new Error('Bad encrypted key payload type')
if ((value as string).length !== 64) throw new Error("Bad encrypted key payload length") if ((value as string).length !== 64) throw new Error('Bad encrypted key payload length')
return (value as string) return value as string
} }
public async encryptKeyPass({ key, passphrase }: { key: string, passphrase: string }) public async encryptKeyPass({
: Promise<{ enckey: string, pwh: string }> { key,
passphrase,
}: {
key: string
passphrase: string
}): Promise<{ enckey: string; pwh: string }> {
const start = Date.now() const start = Date.now()
const nsec = nip19.nsecEncode(key) const nsec = nip19.nsecEncode(key)
const pubkey = getPublicKey(key) const pubkey = getPublicKey(key)
@@ -120,21 +127,29 @@ export class Keys {
const iv = crypto.randomBytes(IV_SIZE) const iv = crypto.randomBytes(IV_SIZE)
const cipher = crypto.createCipheriv(ALGO, passkey, iv) const cipher = crypto.createCipheriv(ALGO, passkey, iv)
const encrypted = Buffer.concat([cipher.update(nsec), cipher.final()]) const encrypted = Buffer.concat([cipher.update(nsec), cipher.final()])
console.log("encrypted key in ", Date.now() - start) console.log('encrypted key in ', Date.now() - start)
return { return {
enckey: `${PREFIX}:${VERSION}:${iv.toString('hex')}:${encrypted.toString('hex')}`, enckey: `${PREFIX}:${VERSION}:${iv.toString('hex')}:${encrypted.toString('hex')}`,
pwh pwh,
} }
} }
public async decryptKeyPass({ pubkey, enckey, passphrase }: { pubkey: string, enckey: string, passphrase: string }): Promise<string> { public async decryptKeyPass({
pubkey,
enckey,
passphrase,
}: {
pubkey: string
enckey: string
passphrase: string
}): Promise<string> {
const start = Date.now() const start = Date.now()
const parts = enckey.split(':') const parts = enckey.split(':')
if (parts.length !== 4) throw new Error("Bad encrypted key") if (parts.length !== 4) throw new Error('Bad encrypted key')
if (parts[0] !== PREFIX) throw new Error("Bad encrypted key prefix") if (parts[0] !== PREFIX) throw new Error('Bad encrypted key prefix')
if (parts[1] !== VERSION) throw new Error("Bad encrypted key version") if (parts[1] !== VERSION) throw new Error('Bad encrypted key version')
if (parts[2].length !== IV_SIZE * 2) throw new Error("Bad encrypted key iv") if (parts[2].length !== IV_SIZE * 2) throw new Error('Bad encrypted key iv')
if (parts[3].length < 30) throw new Error("Bad encrypted key data") if (parts[3].length < 30) throw new Error('Bad encrypted key data')
const { passkey } = await this.generatePassKey(pubkey, passphrase) const { passkey } = await this.generatePassKey(pubkey, passphrase)
const iv = Buffer.from(parts[2], 'hex') const iv = Buffer.from(parts[2], 'hex')
const data = Buffer.from(parts[3], 'hex') const data = Buffer.from(parts[3], 'hex')
@@ -142,9 +157,9 @@ export class Keys {
const decrypted = Buffer.concat([decipher.update(data), decipher.final()]) const decrypted = Buffer.concat([decipher.update(data), decipher.final()])
const nsec = decrypted.toString() const nsec = decrypted.toString()
const { type, data: value } = nip19.decode(nsec) const { type, data: value } = nip19.decode(nsec)
if (type !== "nsec") throw new Error("Bad encrypted key payload type") if (type !== 'nsec') throw new Error('Bad encrypted key payload type')
if (value.length !== 64) throw new Error("Bad encrypted key payload length") if (value.length !== 64) throw new Error('Bad encrypted key payload length')
console.log("decrypted key in ", Date.now() - start) console.log('decrypted key in ', Date.now() - start)
return nsec; return nsec
} }
} }

View File

@@ -7,25 +7,25 @@ export const utf8Decoder = new TextDecoder('utf-8')
export const utf8Encoder = new TextEncoder() export const utf8Encoder = new TextEncoder()
function toBase64(uInt8Array: Uint8Array) { function toBase64(uInt8Array: Uint8Array) {
let strChunks = new Array(uInt8Array.length); let strChunks = new Array(uInt8Array.length)
let i = 0; let i = 0
// @ts-ignore // @ts-ignore
for (let byte of uInt8Array) { for (let byte of uInt8Array) {
strChunks[i] = String.fromCharCode(byte); // bytes to utf16 string strChunks[i] = String.fromCharCode(byte) // bytes to utf16 string
i++; i++
} }
return btoa(strChunks.join("")); return btoa(strChunks.join(''))
} }
function fromBase64(base64String: string) { function fromBase64(base64String: string) {
const binaryString = atob(base64String); const binaryString = atob(base64String)
const length = binaryString.length; const length = binaryString.length
const bytes = new Uint8Array(length); const bytes = new Uint8Array(length)
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i); bytes[i] = binaryString.charCodeAt(i)
} }
return bytes; return bytes
} }
function getNormalizedX(key: Uint8Array): Uint8Array { function getNormalizedX(key: Uint8Array): Uint8Array {
@@ -65,7 +65,7 @@ export class Nip04 {
// let ctb64 = toBase64(new Uint8Array(ciphertext)) // let ctb64 = toBase64(new Uint8Array(ciphertext))
// let ivb64 = toBase64(new Uint8Array(iv.buffer)) // let ivb64 = toBase64(new Uint8Array(iv.buffer))
console.log("nip04_encrypt", text, "t1", t2 - t1, "t2", t3 - t2, "t3", Date.now() - t3) console.log('nip04_encrypt', text, 't1', t2 - t1, 't2', t3 - t2, 't3', Date.now() - t3)
return `${ctb64}?iv=${ivb64}` return `${ctb64}?iv=${ivb64}`
} }
@@ -85,7 +85,4 @@ export class Nip04 {
let text = utf8Decoder.decode(plaintext) let text = utf8Decoder.decode(plaintext)
return text return text
} }
} }

View File

@@ -5,91 +5,79 @@ import NDK, { NDKEvent, NostrEvent } from '@nostr-dev-kit/ndk'
import { nip19 } from 'nostr-tools' import { nip19 } from 'nostr-tools'
export const ndk = new NDK({ export const ndk = new NDK({
explicitRelayUrls: [ explicitRelayUrls: ['wss://relay.nostr.band/all', 'wss://relay.nostr.band', 'wss://relay.damus.io', 'wss://nos.lol'],
'wss://relay.nostr.band/all',
'wss://relay.nostr.band',
'wss://relay.damus.io',
'wss://nos.lol',
],
}) })
export function nostrEvent(e: Required<NDKEvent>) { export function nostrEvent(e: Required<NDKEvent>) {
return { return {
id: e.id, id: e.id,
created_at: e.created_at, created_at: e.created_at,
pubkey: e.pubkey, pubkey: e.pubkey,
kind: e.kind, kind: e.kind,
tags: e.tags, tags: e.tags,
content: e.content, content: e.content,
sig: e.sig, sig: e.sig,
} }
} }
function rawEvent(e: Required<NDKEvent>): AugmentedEvent { function rawEvent(e: Required<NDKEvent>): AugmentedEvent {
return { return {
...nostrEvent(e), ...nostrEvent(e),
identifier: getTagValue(e as NDKEvent, 'd'), identifier: getTagValue(e as NDKEvent, 'd'),
order: e.created_at as number, order: e.created_at as number,
} }
} }
function parseContentJson(c: string): object { function parseContentJson(c: string): object {
try { try {
return JSON.parse(c) return JSON.parse(c)
} catch (e) { } catch (e) {
console.log('Bad json: ', c, e) console.log('Bad json: ', c, e)
return {} return {}
} }
} }
export function getTags( export function getTags(e: AugmentedEvent | NDKEvent | MetaEvent, name: string): string[][] {
e: AugmentedEvent | NDKEvent | MetaEvent, return e.tags.filter((t: string[]) => t.length > 0 && t[0] === name)
name: string,
): string[][] {
return e.tags.filter((t: string[]) => t.length > 0 && t[0] === name)
} }
export function getTag( export function getTag(e: AugmentedEvent | NDKEvent | MetaEvent, name: string): string[] | null {
e: AugmentedEvent | NDKEvent | MetaEvent, const tags = getTags(e, name)
name: string, if (tags.length === 0) return null
): string[] | null { return tags[0]
const tags = getTags(e, name)
if (tags.length === 0) return null
return tags[0]
} }
export function getTagValue( export function getTagValue(
e: AugmentedEvent | NDKEvent | MetaEvent, e: AugmentedEvent | NDKEvent | MetaEvent,
name: string, name: string,
index: number = 0, index: number = 0,
def: string = '', def: string = ''
): string { ): string {
const tag = getTag(e, name) const tag = getTag(e, name)
if (tag === null || !tag.length || (index && index >= tag.length)) if (tag === null || !tag.length || (index && index >= tag.length)) return def
return def return tag[1 + index]
return tag[1 + index]
} }
export function parseProfileJson(e: NostrEvent): Meta { export function parseProfileJson(e: NostrEvent): Meta {
// all meta fields are optional so 'as' works fine // all meta fields are optional so 'as' works fine
const profile = createMeta(parseContentJson(e.content)) const profile = createMeta(parseContentJson(e.content))
profile.pubkey = e.pubkey profile.pubkey = e.pubkey
profile.npub = nip19.npubEncode(e.pubkey) profile.npub = nip19.npubEncode(e.pubkey)
return profile return profile
} }
export async function fetchProfile(npub: string): Promise<MetaEvent | null> { export async function fetchProfile(npub: string): Promise<MetaEvent | null> {
const npubToken = npub.includes('#') ? npub.split('#')[0] : npub const npubToken = npub.includes('#') ? npub.split('#')[0] : npub
const { type, data: pubkey } = nip19.decode(npubToken) const { type, data: pubkey } = nip19.decode(npubToken)
if (type !== 'npub') return null if (type !== 'npub') return null
const event = await ndk.fetchEvent({ kinds: [0], authors: [pubkey] }) const event = await ndk.fetchEvent({ kinds: [0], authors: [pubkey] })
if (event) { if (event) {
const augmentedEvent = rawEvent(event as Required<NDKEvent>) const augmentedEvent = rawEvent(event as Required<NDKEvent>)
const m = createMetaEvent(augmentedEvent) const m = createMetaEvent(augmentedEvent)
m.info = parseProfileJson(m) m.info = parseProfileJson(m)
return m return m
} }
return event return event
} }

View File

@@ -1,51 +1,51 @@
// based on https://git.v0l.io/Kieran/snort/src/branch/main/packages/system/src/pow-util.ts // based on https://git.v0l.io/Kieran/snort/src/branch/main/packages/system/src/pow-util.ts
import { sha256 } from "@noble/hashes/sha256"; import { sha256 } from '@noble/hashes/sha256'
import { bytesToHex } from "@noble/hashes/utils"; import { bytesToHex } from '@noble/hashes/utils'
export interface NostrPowEvent { export interface NostrPowEvent {
id?: string; id?: string
pubkey: string; pubkey: string
created_at: number; created_at: number
kind?: number; kind?: number
tags: Array<Array<string>>; tags: Array<Array<string>>
content: string; content: string
sig?: string; sig?: string
} }
export function minePow(e: NostrPowEvent, target: number) { export function minePow(e: NostrPowEvent, target: number) {
let ctr = 0; let ctr = 0
let nonceTagIdx = e.tags.findIndex(a => a[0] === "nonce"); let nonceTagIdx = e.tags.findIndex((a) => a[0] === 'nonce')
if (nonceTagIdx === -1) { if (nonceTagIdx === -1) {
nonceTagIdx = e.tags.length; nonceTagIdx = e.tags.length
e.tags.push(["nonce", ctr.toString(), target.toString()]); e.tags.push(['nonce', ctr.toString(), target.toString()])
} }
do { do {
e.tags[nonceTagIdx][1] = (++ctr).toString(); e.tags[nonceTagIdx][1] = (++ctr).toString()
e.id = createId(e); e.id = createId(e)
} while (countLeadingZeros(e.id) < target); } while (countLeadingZeros(e.id) < target)
return e; return e
} }
function createId(e: NostrPowEvent) { function createId(e: NostrPowEvent) {
const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content]; const payload = [0, e.pubkey, e.created_at, e.kind, e.tags, e.content]
return bytesToHex(sha256(JSON.stringify(payload))); return bytesToHex(sha256(JSON.stringify(payload)))
} }
export function countLeadingZeros(hex: string) { export function countLeadingZeros(hex: string) {
let count = 0; let count = 0
for (let i = 0; i < hex.length; i++) { for (let i = 0; i < hex.length; i++) {
const nibble = parseInt(hex[i], 16); const nibble = parseInt(hex[i], 16)
if (nibble === 0) { if (nibble === 0) {
count += 4; count += 4
} else { } else {
count += Math.clz32(nibble) - 28; count += Math.clz32(nibble) - 28
break; break
} }
} }
return count; return count
} }

View File

@@ -8,67 +8,63 @@ import { Nip04 } from './nip04'
//import { decrypt, encrypt } from "./ende"; //import { decrypt, encrypt } from "./ende";
export class PrivateKeySigner implements NDKSigner { export class PrivateKeySigner implements NDKSigner {
private _user: NDKUser | undefined private _user: NDKUser | undefined
privateKey?: string privateKey?: string
private nip04: Nip04 private nip04: Nip04
public constructor(privateKey?: string) { public constructor(privateKey?: string) {
if (privateKey) { if (privateKey) {
this.privateKey = privateKey this.privateKey = privateKey
this._user = new NDKUser({ this._user = new NDKUser({
hexpubkey: getPublicKey(this.privateKey), hexpubkey: getPublicKey(this.privateKey),
}) })
} }
this.nip04 = new Nip04() this.nip04 = new Nip04()
} }
public static generate() { public static generate() {
const privateKey = generatePrivateKey() const privateKey = generatePrivateKey()
return new PrivateKeySigner(privateKey) return new PrivateKeySigner(privateKey)
} }
public async blockUntilReady(): Promise<NDKUser> { public async blockUntilReady(): Promise<NDKUser> {
if (!this._user) { if (!this._user) {
throw new Error('NDKUser not initialized') throw new Error('NDKUser not initialized')
} }
return this._user return this._user
} }
public async user(): Promise<NDKUser> { public async user(): Promise<NDKUser> {
await this.blockUntilReady() await this.blockUntilReady()
return this._user as NDKUser return this._user as NDKUser
} }
public async sign(event: NostrEvent): Promise<string> { public async sign(event: NostrEvent): Promise<string> {
if (!this.privateKey) { if (!this.privateKey) {
throw Error('Attempted to sign without a private key') throw Error('Attempted to sign without a private key')
} }
return getSignature(event as UnsignedEvent, this.privateKey) return getSignature(event as UnsignedEvent, this.privateKey)
} }
public async encrypt(recipient: NDKUser, value: string): Promise<string> { public async encrypt(recipient: NDKUser, value: string): Promise<string> {
if (!this.privateKey) { if (!this.privateKey) {
throw Error('Attempted to encrypt without a private key') throw Error('Attempted to encrypt without a private key')
} }
const recipientHexPubKey = recipient.hexpubkey const recipientHexPubKey = recipient.hexpubkey
return await this.nip04.encrypt( return await this.nip04.encrypt(this.privateKey, recipientHexPubKey, value)
this.privateKey, // return await encrypt(recipientHexPubKey, value, this.privateKey);
recipientHexPubKey, }
value,
)
// return await encrypt(recipientHexPubKey, value, this.privateKey);
}
public async decrypt(sender: NDKUser, value: string): Promise<string> { public async decrypt(sender: NDKUser, value: string): Promise<string> {
if (!this.privateKey) { if (!this.privateKey) {
throw Error('Attempted to decrypt without a private key') throw Error('Attempted to decrypt without a private key')
} }
const senderHexPubKey = sender.hexpubkey const senderHexPubKey = sender.hexpubkey
// console.log("nip04_decrypt", value) // console.log("nip04_decrypt", value)
return await this.nip04.decrypt(this.privateKey, senderHexPubKey, value) return await this.nip04.decrypt(this.privateKey, senderHexPubKey, value)
// return await decrypt(this.privateKey, senderHexPubKey, value) as string; // return await decrypt(this.privateKey, senderHexPubKey, value) as string;
} }
} }

View File

@@ -7,74 +7,72 @@ let nextReqId = 1
let onRender: (() => void) | null = null let onRender: (() => void) | null = null
export async function swicRegister() { export async function swicRegister() {
serviceWorkerRegistration.register({ serviceWorkerRegistration.register({
onSuccess(registration) { onSuccess(registration) {
console.log('sw registered') console.log('sw registered')
swr = registration swr = registration
}, },
onError(e) { onError(e) {
console.log(`error ${e}`) console.log(`error ${e}`)
}, },
}) })
navigator.serviceWorker.ready.then((r) => { navigator.serviceWorker.ready.then((r) => {
console.log("sw ready") console.log('sw ready')
swr = r swr = r
if (navigator.serviceWorker.controller) { if (navigator.serviceWorker.controller) {
console.log( console.log(`This page is currently controlled by: ${navigator.serviceWorker.controller}`)
`This page is currently controlled by: ${navigator.serviceWorker.controller}`, } else {
); console.log('This page is not currently controlled by a service worker.')
} else { }
console.log("This page is not currently controlled by a service worker."); })
}
})
navigator.serviceWorker.addEventListener('message', (event) => { navigator.serviceWorker.addEventListener('message', (event) => {
onMessage((event as MessageEvent).data) onMessage((event as MessageEvent).data)
}) })
} }
function onMessage(data: any) { function onMessage(data: any) {
const { id, result, error } = data const { id, result, error } = data
console.log('SW message', id, result, error) console.log('SW message', id, result, error)
if (!id) { if (!id) {
if (onRender) onRender() if (onRender) onRender()
return return
} }
const r = reqs.get(id) const r = reqs.get(id)
if (!r) { if (!r) {
console.log('Unexpected message from service worker', data) console.log('Unexpected message from service worker', data)
return return
} }
reqs.delete(id) reqs.delete(id)
if (error) r.rej(error) if (error) r.rej(error)
else r.ok(result) else r.ok(result)
} }
export async function swicCall(method: string, ...args: any[]) { export async function swicCall(method: string, ...args: any[]) {
const id = nextReqId const id = nextReqId
nextReqId++ nextReqId++
return new Promise((ok, rej) => { return new Promise((ok, rej) => {
if (!swr || !swr.active) { if (!swr || !swr.active) {
rej(new Error('No active service worker')) rej(new Error('No active service worker'))
return return
} }
reqs.set(id, { ok, rej }) reqs.set(id, { ok, rej })
const msg = { const msg = {
id, id,
method, method,
args: [...args], args: [...args],
} }
console.log('sending to SW', msg) console.log('sending to SW', msg)
swr.active.postMessage(msg) swr.active.postMessage(msg)
}) })
} }
export function swicOnRender(cb: () => void) { export function swicOnRender(cb: () => void) {
onRender = cb onRender = cb
} }

View File

@@ -4,15 +4,15 @@ import { darkTheme, lightTheme } from './theme'
import { useAppSelector } from '../../store/hooks/redux' import { useAppSelector } from '../../store/hooks/redux'
const ThemeProvider: FC<PropsWithChildren> = ({ children }) => { const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
const themeMode = useAppSelector((state) => state.ui.themeMode) const themeMode = useAppSelector((state) => state.ui.themeMode)
const isDarkMode = themeMode === 'dark' const isDarkMode = themeMode === 'dark'
return ( return (
<ThemeMuiProvider theme={isDarkMode ? darkTheme : lightTheme}> <ThemeMuiProvider theme={isDarkMode ? darkTheme : lightTheme}>
<CssBaseline /> <CssBaseline />
{children} {children}
</ThemeMuiProvider> </ThemeMuiProvider>
) )
} }
export default ThemeProvider export default ThemeProvider

View File

@@ -1,99 +1,99 @@
import { createTheme, Theme } from '@mui/material' import { createTheme, Theme } from '@mui/material'
declare module '@mui/material/styles' { declare module '@mui/material/styles' {
interface Palette { interface Palette {
textSecondaryDecorate: Palette['primary'] textSecondaryDecorate: Palette['primary']
backgroundSecondary: Palette['background'] backgroundSecondary: Palette['background']
} }
interface PaletteOptions { interface PaletteOptions {
textSecondaryDecorate?: Palette['primary'] textSecondaryDecorate?: Palette['primary']
backgroundSecondary?: Palette['background'] backgroundSecondary?: Palette['background']
} }
} }
const commonTheme: Theme = createTheme({ const commonTheme: Theme = createTheme({
typography: { typography: {
fontFamily: ['Inter', 'sans-serif'].join(','), fontFamily: ['Inter', 'sans-serif'].join(','),
}, },
components: { components: {
MuiButton: { MuiButton: {
styleOverrides: { styleOverrides: {
root: { root: {
textTransform: 'initial', textTransform: 'initial',
}, },
}, },
}, },
}, },
}) })
const lightTheme: Theme = createTheme({ const lightTheme: Theme = createTheme({
...commonTheme, ...commonTheme,
palette: { palette: {
mode: 'light', mode: 'light',
primary: { primary: {
main: '#000000', main: '#000000',
}, },
secondary: { secondary: {
main: '#E8E9EB', main: '#E8E9EB',
dark: '#ACACAC', dark: '#ACACAC',
}, },
error: { error: {
main: '#f44336', main: '#f44336',
}, },
background: { background: {
default: '#f7f7f7', default: '#f7f7f7',
paper: '#f7f7f7', paper: '#f7f7f7',
}, },
backgroundSecondary: { backgroundSecondary: {
default: '#E8E9EB', default: '#E8E9EB',
paper: '#f7f7f7', paper: '#f7f7f7',
}, },
text: { text: {
primary: '#000000', primary: '#000000',
secondary: '#ffffff', secondary: '#ffffff',
}, },
textSecondaryDecorate: { textSecondaryDecorate: {
main: '#6b6b6b', main: '#6b6b6b',
light: '#000', light: '#000',
dark: '#000', dark: '#000',
contrastText: '#000', contrastText: '#000',
}, },
}, },
}) })
const darkTheme: Theme = createTheme({ const darkTheme: Theme = createTheme({
...commonTheme, ...commonTheme,
palette: { palette: {
mode: 'dark', mode: 'dark',
primary: { primary: {
main: '#FFFFFF', main: '#FFFFFF',
}, },
secondary: { secondary: {
main: '#222222', main: '#222222',
}, },
error: { error: {
main: '#ef9a9a', main: '#ef9a9a',
}, },
background: { background: {
default: '#121212', default: '#121212',
paper: '#28282B', paper: '#28282B',
}, },
backgroundSecondary: { backgroundSecondary: {
default: '#0d0d0d', default: '#0d0d0d',
paper: '#28282B', paper: '#28282B',
}, },
text: { text: {
primary: '#ffffff', primary: '#ffffff',
secondary: '#000000', secondary: '#000000',
}, },
textSecondaryDecorate: { textSecondaryDecorate: {
main: '#6b6b6b', main: '#6b6b6b',
light: '#000', light: '#000',
dark: '#000', dark: '#000',
contrastText: '#000', contrastText: '#000',
}, },
}, },
}) })
export { lightTheme, darkTheme } export { lightTheme, darkTheme }

View File

@@ -20,103 +20,77 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
const AppPage = () => { const AppPage = () => {
const { appNpub = '', npub = '' } = useParams() const { appNpub = '', npub = '' } = useParams()
const navigate = useNavigate() const navigate = useNavigate()
const notify = useEnqueueSnackbar() const notify = useEnqueueSnackbar()
const perms = useAppSelector((state) => const perms = useAppSelector((state) => selectPermsByNpubAndAppNpub(state, npub, appNpub))
selectPermsByNpubAndAppNpub(state, npub, appNpub), const currentApp = useAppSelector((state) => selectAppByAppNpub(state, appNpub))
)
const currentApp = useAppSelector((state) =>
selectAppByAppNpub(state, appNpub),
)
const { open, handleClose, handleShow } = useToggleConfirm() const { open, handleClose, handleShow } = useToggleConfirm()
const { handleOpen: handleOpenModal } = useModalSearchParams() const { handleOpen: handleOpenModal } = useModalSearchParams()
const connectPerm = perms.find( const connectPerm = perms.find((perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC)
(perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC,
)
if (!currentApp) { if (!currentApp) {
return <Navigate to={`/key/${npub}`} /> return <Navigate to={`/key/${npub}`} />
} }
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 ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected'
connectPerm && timestamp
? `Connected at ${formatTimestampDate(timestamp)}`
: 'Not connected'
const handleDeleteApp = async () => { const handleDeleteApp = async () => {
try { try {
await swicCall('deleteApp', appNpub) await swicCall('deleteApp', appNpub)
notify(`App: «${appName}» successfully deleted!`, 'success') notify(`App: «${appName}» successfully deleted!`, 'success')
navigate(`/key/${npub}`) navigate(`/key/${npub}`)
} catch (error: any) { } catch (error: any) {
notify(error?.message || 'Failed to delete app', 'error') notify(error?.message || 'Failed to delete app', 'error')
} }
} }
return ( return (
<> <>
<Stack <Stack maxHeight={'100%'} overflow={'auto'} alignItems={'flex-start'} height={'100%'}>
maxHeight={'100%'} <IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
overflow={'auto'} <Stack marginBottom={'1rem'} direction={'row'} gap={'1rem'} width={'100%'}>
alignItems={'flex-start'} <StyledAppIcon src={icon} />
height={'100%'} <Box flex={'1'} overflow={'hidden'}>
> <Typography variant="h4" noWrap>
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} /> {appName}
<Stack </Typography>
marginBottom={'1rem'} <Typography variant="body2" noWrap>
direction={'row'} {connectedOn}
gap={'1rem'} </Typography>
width={'100%'} </Box>
> </Stack>
<StyledAppIcon src={icon} /> <Box marginBottom={'1rem'}>
<Box flex={'1'} overflow={'hidden'}> <SectionTitle marginBottom={'0.5rem'}>Disconnect</SectionTitle>
<Typography variant='h4' noWrap> <Button fullWidth onClick={handleShow}>
{appName} Delete app
</Typography> </Button>
<Typography variant='body2' noWrap> </Box>
{connectedOn} <Permissions perms={perms} />
</Typography>
</Box>
</Stack>
<Box marginBottom={'1rem'}>
<SectionTitle marginBottom={'0.5rem'}>
Disconnect
</SectionTitle>
<Button fullWidth onClick={handleShow}>
Delete app
</Button>
</Box>
<Permissions perms={perms} />
<Button <Button fullWidth onClick={() => handleOpenModal(MODAL_PARAMS_KEYS.ACTIVITY)}>
fullWidth Activity
onClick={() => </Button>
handleOpenModal(MODAL_PARAMS_KEYS.ACTIVITY) </Stack>
}
>
Activity
</Button>
</Stack>
<ConfirmModal <ConfirmModal
open={open} open={open}
headingText='Delete app' headingText="Delete app"
description='Are you sure you want to delete this app?' description="Are you sure you want to delete this app?"
onCancel={handleClose} onCancel={handleClose}
onConfirm={handleDeleteApp} onConfirm={handleDeleteApp}
onClose={handleClose} onClose={handleClose}
/> />
<ModalActivities appNpub={appNpub} /> <ModalActivities appNpub={appNpub} />
</> </>
) )
} }
export default AppPage export default AppPage

View File

@@ -10,36 +10,19 @@ import { ACTIONS } from '@/utils/consts'
type ItemActivityProps = DbHistory type ItemActivityProps = DbHistory
export const ItemActivity: FC<ItemActivityProps> = ({ export const ItemActivity: FC<ItemActivityProps> = ({ allowed, method, timestamp }) => {
allowed, return (
method, <StyledActivityItem>
timestamp, <Box display={'flex'} flexDirection={'column'} gap={'0.5rem'} flex={1}>
}) => { <Typography flex={1} fontWeight={700}>
return ( {ACTIONS[method] || method}
<StyledActivityItem> </Typography>
<Box <Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
display={'flex'} </Box>
flexDirection={'column'} <Box>{allowed ? <DoneRoundedIcon htmlColor="green" /> : <ClearRoundedIcon htmlColor="red" />}</Box>
gap={'0.5rem'} <IconButton>
flex={1} <MoreVertRoundedIcon />
> </IconButton>
<Typography flex={1} fontWeight={700}> </StyledActivityItem>
{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

@@ -8,32 +8,23 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
type ModalActivitiesProps = { type ModalActivitiesProps = {
appNpub: string appNpub: string
} }
export const ModalActivities: FC<ModalActivitiesProps> = ({ appNpub }) => { export const ModalActivities: FC<ModalActivitiesProps> = ({ appNpub }) => {
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.ACTIVITY) const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.ACTIVITY)
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.ACTIVITY) const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.ACTIVITY)
const history = useLiveQuery( const history = useLiveQuery(getActivityHistoryQuerier(appNpub), [], HistoryDefaultValue)
getActivityHistoryQuerier(appNpub),
[],
HistoryDefaultValue,
)
return ( return (
<Modal <Modal open={isModalOpened} onClose={handleCloseModal} fixedHeight="calc(100% - 5rem)" title="Activity history">
open={isModalOpened} <Box overflow={'auto'}>
onClose={handleCloseModal} {history.map((item) => {
fixedHeight='calc(100% - 5rem)' return <ItemActivity {...item} key={item.id} />
title='Activity history' })}
> </Box>
<Box overflow={'auto'}> </Modal>
{history.map((item) => { )
return <ItemActivity {...item} key={item.id} />
})}
</Box>
</Modal>
)
} }

View File

@@ -1,12 +1,10 @@
import styled from '@emotion/styled' import styled from '@emotion/styled'
import { Box, BoxProps } from '@mui/material' import { Box, BoxProps } from '@mui/material'
export const StyledActivityItem = styled((props: BoxProps) => ( export const StyledActivityItem = styled((props: BoxProps) => <Box {...props} />)(() => ({
<Box {...props} /> display: 'flex',
))(() => ({ gap: '0.5rem',
display: 'flex', justifyContent: 'space-between',
gap: '0.5rem', alignItems: 'center',
justifyContent: 'space-between', padding: '0.25rem',
alignItems: 'center',
padding: '0.25rem',
})) }))

View File

@@ -11,49 +11,31 @@ import { ItemPermissionMenu } from './ItemPermissionMenu'
import { useOpenMenu } from '@/hooks/useOpenMenu' import { useOpenMenu } from '@/hooks/useOpenMenu'
type ItemPermissionProps = { type ItemPermissionProps = {
permission: DbPerm permission: DbPerm
} }
export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => { export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => {
const { perm, value, timestamp, id } = permission || {} const { perm, value, timestamp, id } = permission || {}
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu() const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()
const isAllowed = value === '1' const isAllowed = value === '1'
return ( return (
<> <>
<StyledPermissionItem> <StyledPermissionItem>
<Box <Box display={'flex'} flexDirection={'column'} gap={'0.5rem'} flex={1}>
display={'flex'} <Typography flex={1} fontWeight={700}>
flexDirection={'column'} {ACTIONS[perm] || perm}
gap={'0.5rem'} </Typography>
flex={1} <Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
> </Box>
<Typography flex={1} fontWeight={700}> <Box>{isAllowed ? <DoneRoundedIcon htmlColor="green" /> : <ClearRoundedIcon htmlColor="red" />}</Box>
{ACTIONS[perm] || perm} <IconButton onClick={handleOpen}>
</Typography> <MoreVertRoundedIcon />
<Typography variant='body2'> </IconButton>
{formatTimestampDate(timestamp)} </StyledPermissionItem>
</Typography> <ItemPermissionMenu anchorEl={anchorEl} open={open} handleClose={handleClose} permId={id} />
</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

@@ -5,58 +5,51 @@ import { swicCall } from '@/modules/swic'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar' import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
type ItemPermissionMenuProps = { type ItemPermissionMenuProps = {
permId: string permId: string
handleClose: () => void handleClose: () => void
} & MenuProps } & MenuProps
export const ItemPermissionMenu: FC<ItemPermissionMenuProps> = ({ export const ItemPermissionMenu: FC<ItemPermissionMenuProps> = ({ open, anchorEl, handleClose, permId }) => {
open, const [showConfirm, setShowConfirm] = useState(false)
anchorEl, const notify = useEnqueueSnackbar()
handleClose,
permId,
}) => {
const [showConfirm, setShowConfirm] = useState(false)
const notify = useEnqueueSnackbar()
const handleShowConfirm = () => { const handleShowConfirm = () => {
setShowConfirm(true) setShowConfirm(true)
handleClose() handleClose()
} }
const handleCloseConfirm = () => setShowConfirm(false) const handleCloseConfirm = () => setShowConfirm(false)
const handleDeletePerm = async () => { const handleDeletePerm = async () => {
try { try {
await swicCall('deletePerm', permId) await swicCall('deletePerm', permId)
notify('Permission successfully deleted!', 'success') notify('Permission successfully deleted!', 'success')
handleCloseConfirm() handleCloseConfirm()
} catch (error: any) { } catch (error: any) {
notify(error?.message || 'Failed to delete permission', 'error') notify(error?.message || 'Failed to delete permission', 'error')
} }
} }
return ( return (
<> <>
<Menu <Menu
open={open} open={open}
anchorEl={anchorEl} anchorEl={anchorEl}
onClose={handleClose} onClose={handleClose}
anchorOrigin={{ anchorOrigin={{
horizontal: 'left', horizontal: 'left',
vertical: 'bottom', vertical: 'bottom',
}} }}
> >
<MenuItem onClick={handleShowConfirm}> <MenuItem onClick={handleShowConfirm}>Delete permission</MenuItem>
Delete permission </Menu>
</MenuItem> <ConfirmModal
</Menu> open={showConfirm}
<ConfirmModal onClose={handleCloseConfirm}
open={showConfirm} onCancel={handleCloseConfirm}
onClose={handleCloseConfirm} headingText="Delete permission"
onCancel={handleCloseConfirm} description="Are you sure you want to delete this permission?"
headingText='Delete permission' onConfirm={handleDeletePerm}
description='Are you sure you want to delete this permission?' />
onConfirm={handleDeletePerm} </>
/> )
</>
)
} }

View File

@@ -5,24 +5,18 @@ import { Box } from '@mui/material'
import { ItemPermission } from './ItemPermission' import { ItemPermission } from './ItemPermission'
type PermissionsProps = { type PermissionsProps = {
perms: DbPerm[] perms: DbPerm[]
} }
export const Permissions: FC<PermissionsProps> = ({ perms }) => { export const Permissions: FC<PermissionsProps> = ({ perms }) => {
return ( return (
<Box width={'100%'} marginBottom={'1rem'} flex={1} overflow={'auto'}> <Box width={'100%'} marginBottom={'1rem'} flex={1} overflow={'auto'}>
<SectionTitle marginBottom={'0.5rem'}>Permissions</SectionTitle> <SectionTitle marginBottom={'0.5rem'}>Permissions</SectionTitle>
<Box <Box flex={1} overflow={'auto'} display={'flex'} flexDirection={'column'} gap={'0.5rem'}>
flex={1} {perms.map((perm) => {
overflow={'auto'} return <ItemPermission key={perm.id} permission={perm} />
display={'flex'} })}
flexDirection={'column'} </Box>
gap={'0.5rem'} </Box>
> )
{perms.map((perm) => {
return <ItemPermission key={perm.id} permission={perm} />
})}
</Box>
</Box>
)
} }

View File

@@ -1,11 +1,9 @@
import { Box, BoxProps, styled } from '@mui/material' import { Box, BoxProps, styled } from '@mui/material'
export const StyledPermissionItem = styled((props: BoxProps) => ( export const StyledPermissionItem = styled((props: BoxProps) => <Box {...props} />)(() => ({
<Box {...props} /> display: 'flex',
))(() => ({ gap: '0.5rem',
display: 'flex', justifyContent: 'space-between',
gap: '0.5rem', alignItems: 'center',
justifyContent: 'space-between', padding: '0.5rem',
alignItems: 'center',
padding: '0.5rem',
})) }))

View File

@@ -2,5 +2,5 @@ import { Button } from '@/shared/Button/Button'
import { styled } from '@mui/material' import { styled } from '@mui/material'
export const StyledButton = styled(Button)({ export const StyledButton = styled(Button)({
textTransform: 'capitalize', textTransform: 'capitalize',
}) })

View File

@@ -1,8 +1,6 @@
import { Avatar, AvatarProps, styled } from '@mui/material' import { Avatar, AvatarProps, styled } from '@mui/material'
export const StyledAppIcon = styled((props: AvatarProps) => ( export const StyledAppIcon = styled((props: AvatarProps) => <Avatar {...props} variant="rounded" />)(() => ({
<Avatar {...props} variant='rounded' /> width: 70,
))(() => ({ height: 70,
width: 70,
height: 70,
})) }))

View File

@@ -1,18 +1,18 @@
import { DbHistory, db } from '@/modules/db' import { DbHistory, db } from '@/modules/db'
export const getActivityHistoryQuerier = (appNpub: string) => () => { export const getActivityHistoryQuerier = (appNpub: string) => () => {
if (!appNpub.trim().length) return [] if (!appNpub.trim().length) return []
const result = db.history const result = db.history
.where('appNpub') .where('appNpub')
.equals(appNpub) .equals(appNpub)
.reverse() .reverse()
.sortBy('timestamp') .sortBy('timestamp')
.then(a => a.slice(0, 30)) .then((a) => a.slice(0, 30))
// .limit(30) // .limit(30)
// .toArray() // .toArray()
return result return result
} }
export const HistoryDefaultValue: DbHistory[] = [] export const HistoryDefaultValue: DbHistory[] = []

View File

@@ -7,76 +7,67 @@ import { CheckmarkIcon } from '@/assets'
import { DOMAIN } from '@/utils/consts' import { DOMAIN } from '@/utils/consts'
const AuthPage = () => { const AuthPage = () => {
const isMobile = useMediaQuery('(max-width:600px)') const isMobile = useMediaQuery('(max-width:600px)')
const [enteredValue, setEnteredValue] = useState('') const [enteredValue, setEnteredValue] = useState('')
const theme = useTheme() const theme = useTheme()
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setEnteredValue(e.target.value) setEnteredValue(e.target.value)
} }
const isAvailable = enteredValue.trim().length > 2 const isAvailable = enteredValue.trim().length > 2
const inputHelperText = isAvailable ? ( const inputHelperText = isAvailable ? (
<> <>
<CheckmarkIcon /> Available <CheckmarkIcon /> Available
</> </>
) : ( ) : (
"Don't worry, username can be changed later." "Don't worry, username can be changed later."
) )
const mainContent = ( const mainContent = (
<> <>
<Input <Input
label='Enter a Username' label="Enter a Username"
fullWidth fullWidth
placeholder='Username' placeholder="Username"
helperText={inputHelperText} helperText={inputHelperText}
endAdornment={ endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography> onChange={handleInputChange}
} value={enteredValue}
onChange={handleInputChange} helperTextProps={{
value={enteredValue} sx: {
helperTextProps={{ '&.helper_text': {
sx: { color: isAvailable ? theme.palette.success.main : theme.palette.textSecondaryDecorate.main,
'&.helper_text': { },
color: isAvailable },
? theme.palette.success.main }}
: theme.palette.textSecondaryDecorate.main, />
}, <Button fullWidth>Sign up</Button>
}, </>
}} )
/>
<Button fullWidth>Sign up</Button>
</>
)
return ( return (
<Stack height={'100%'} position={'relative'}> <Stack height={'100%'} position={'relative'}>
{isMobile ? ( {isMobile ? (
<StyledContent> <StyledContent>
<Stack <Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
direction={'row'} <StyledAppLogo />
gap={'1rem'} <Typography fontWeight={600} variant="h5">
alignItems={'center'} Sign up
alignSelf={'flex-start'} </Typography>
> </Stack>
<StyledAppLogo /> {mainContent}
<Typography fontWeight={600} variant='h5'> </StyledContent>
Sign up ) : (
</Typography> <Stack gap={'1rem'} alignItems={'center'}>
</Stack> {mainContent}
{mainContent} </Stack>
</StyledContent> )}
) : ( </Stack>
<Stack gap={'1rem'} alignItems={'center'}> )
{mainContent}
</Stack>
)}
</Stack>
)
} }
export default AuthPage export default AuthPage

View File

@@ -1,32 +1,32 @@
import { AppLogo } from '@/assets' import { AppLogo } from '@/assets'
import { Stack, styled, StackProps, Box } from '@mui/material' import { Stack, styled, StackProps, Box } from '@mui/material'
export const StyledContent = styled((props: StackProps) => ( export const StyledContent = styled((props: StackProps) => <Stack {...props} gap={'1rem'} alignItems={'center'} />)(({
<Stack {...props} gap={'1rem'} alignItems={'center'} /> theme,
))(({ theme }) => { }) => {
return { return {
background: theme.palette.secondary.main, background: theme.palette.secondary.main,
position: 'absolute', position: 'absolute',
bottom: '-1rem', bottom: '-1rem',
left: '-1rem', left: '-1rem',
width: 'calc(100% + 2rem)', width: 'calc(100% + 2rem)',
height: '70%', height: '70%',
borderTopLeftRadius: '2rem', borderTopLeftRadius: '2rem',
borderTopRightRadius: '2rem', borderTopRightRadius: '2rem',
padding: '1rem', padding: '1rem',
maxWidth: '50rem', maxWidth: '50rem',
margin: '0 auto', margin: '0 auto',
} }
}) })
export const StyledAppLogo = styled((props) => ( export const StyledAppLogo = styled((props) => (
<Box {...props}> <Box {...props}>
<AppLogo /> <AppLogo />
</Box> </Box>
))({ ))({
background: '#00000054', background: '#00000054',
padding: '0.75rem', padding: '0.75rem',
borderRadius: '16px', borderRadius: '16px',
display: 'grid', display: 'grid',
placeItems: 'center', placeItems: 'center',
}) })

View File

@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
const ConfirmPage = () => { const ConfirmPage = () => {
return <div>ConfirmPage</div> return <div>ConfirmPage</div>
} }
export default ConfirmPage export default ConfirmPage

View File

@@ -10,61 +10,47 @@ import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { DOMAIN } from '@/utils/consts' import { DOMAIN } from '@/utils/consts'
const HomePage = () => { const HomePage = () => {
const keys = useAppSelector(selectKeys) const keys = useAppSelector(selectKeys)
const isNoKeys = !keys || keys.length === 0 const isNoKeys = !keys || keys.length === 0
const { handleOpen } = useModalSearchParams() const { handleOpen } = useModalSearchParams()
const handleClickAddAccount = () => handleOpen(MODAL_PARAMS_KEYS.INITIAL) const handleClickAddAccount = () => handleOpen(MODAL_PARAMS_KEYS.INITIAL)
const handleLearnMore = () => { const handleLearnMore = () => {
// @ts-ignore // @ts-ignore
window.open(`https://info.${DOMAIN}`, '_blank').focus(); window.open(`https://info.${DOMAIN}`, '_blank').focus()
} }
return ( return (
<Stack maxHeight={'100%'} overflow={'auto'}> <Stack maxHeight={'100%'} overflow={'auto'}>
<SectionTitle marginBottom={'0.5rem'}> <SectionTitle marginBottom={'0.5rem'}>{isNoKeys ? 'Welcome' : 'Accounts:'}</SectionTitle>
{isNoKeys ? 'Welcome' : 'Accounts:'} <Stack gap={'0.5rem'} overflow={'auto'}>
</SectionTitle> {isNoKeys && (
<Stack gap={'0.5rem'} overflow={'auto'}> <>
{isNoKeys && ( <Typography textAlign={'left'} variant="h6" paddingTop="1em">
<> Nsec.app is a novel key storage app for Nostr.
<Typography textAlign={'left'} variant='h6' paddingTop='1em'> </Typography>
Nsec.app is a novel key storage app for Nostr. <GetStartedButton onClick={handleClickAddAccount}>Get started</GetStartedButton>
</Typography> <Typography textAlign={'left'} variant="h6" paddingTop="2em">
<GetStartedButton onClick={handleClickAddAccount}> Your keys are stored in your browser and can be used in many Nostr apps without the need for a browser
Get started extension.
</GetStartedButton> </Typography>
<Typography textAlign={'left'} variant='h6' paddingTop='2em'> <LearnMoreButton onClick={handleLearnMore}>Learn more</LearnMoreButton>
Your keys are stored in your browser and </>
can be used in many Nostr apps without the )}
need for a browser extension. {!isNoKeys && (
</Typography> <Fragment>
<LearnMoreButton onClick={handleLearnMore}> <Box flex={1} overflow={'auto'} borderRadius={'8px'} padding={'0.25rem'}>
Learn more {keys.map((key) => (
</LearnMoreButton> <ItemKey {...key} key={key.npub} />
</> ))}
)} </Box>
{!isNoKeys && ( <AddAccountButton onClick={handleClickAddAccount}>Add account</AddAccountButton>
<Fragment> </Fragment>
<Box )}
flex={1} </Stack>
overflow={'auto'} </Stack>
borderRadius={'8px'} )
padding={'0.25rem'}
>
{keys.map((key) => (
<ItemKey {...key} key={key.npub} />
))}
</Box>
<AddAccountButton onClick={handleClickAddAccount}>
Add account
</AddAccountButton>
</Fragment>
)}
</Stack>
</Stack>
)
} }
export default HomePage export default HomePage

View File

@@ -1,61 +1,50 @@
import { FC } from 'react' import { FC } from 'react'
import { DbKey } from '../../../modules/db' import { DbKey } from '../../../modules/db'
import { import { Avatar, Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material'
Avatar,
Stack,
StackProps,
Typography,
TypographyProps,
styled,
} from '@mui/material'
import { getShortenNpub } from '../../../utils/helpers/helpers' import { getShortenNpub } from '../../../utils/helpers/helpers'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
type ItemKeyProps = DbKey type ItemKeyProps = DbKey
export const ItemKey: FC<ItemKeyProps> = (props) => { export const ItemKey: FC<ItemKeyProps> = (props) => {
const { npub, profile } = props const { npub, profile } = props
const navigate = useNavigate() const navigate = useNavigate()
const handleNavigate = () => { const handleNavigate = () => {
navigate('/key/' + npub) navigate('/key/' + npub)
} }
const { name = '', picture = '' } = profile?.info || {} const { name = '', picture = '' } = profile?.info || {}
const userName = name || getShortenNpub(npub) const userName = name || getShortenNpub(npub)
const userAvatar = picture || '' const userAvatar = picture || ''
return ( return (
<StyledKeyContainer onClick={handleNavigate}> <StyledKeyContainer onClick={handleNavigate}>
<Stack direction={'row'} alignItems={'center'} gap='1rem'> <Stack direction={'row'} alignItems={'center'} gap="1rem">
<Avatar src={userAvatar} alt={userName} /> <Avatar src={userAvatar} alt={userName} />
<StyledText variant='body1'>{userName}</StyledText> <StyledText variant="body1">{userName}</StyledText>
</Stack> </Stack>
</StyledKeyContainer> </StyledKeyContainer>
) )
} }
const StyledKeyContainer = styled((props: StackProps) => ( const StyledKeyContainer = styled((props: StackProps) => <Stack marginBottom={'0.5rem'} gap={'0.25rem'} {...props} />)(
<Stack marginBottom={'0.5rem'} gap={'0.25rem'} {...props} /> ({ theme }) => {
))(({ theme }) => { return {
return { boxShadow:
boxShadow: theme.palette.mode === 'dark' ? '0px 1px 6px 0px rgba(92, 92, 92, 0.2)' : '0px 1px 6px 0px rgba(0, 0, 0, 0.2)',
theme.palette.mode === 'dark' borderRadius: '12px',
? '0px 1px 6px 0px rgba(92, 92, 92, 0.2)' padding: '0.5rem 1rem',
: '0px 1px 6px 0px rgba(0, 0, 0, 0.2)', background: theme.palette.background.paper,
borderRadius: '12px', ':hover': {
padding: '0.5rem 1rem', background: `${theme.palette.background.paper}95`,
background: theme.palette.background.paper, },
':hover': { cursor: 'pointer',
background: `${theme.palette.background.paper}95`, }
}, }
cursor: 'pointer', )
}
})
export const StyledText = styled((props: TypographyProps) => ( export const StyledText = styled((props: TypographyProps) => <Typography {...props} />)({
<Typography {...props} /> fontWeight: 500,
))({ width: '100%',
fontWeight: 500, wordBreak: 'break-all',
width: '100%',
wordBreak: 'break-all',
}) })

View File

@@ -5,22 +5,22 @@ import PlayArrowOutlinedIcon from '@mui/icons-material/PlayArrowOutlined'
import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined' import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'
export const AddAccountButton = styled((props: AppButtonProps) => ( export const AddAccountButton = styled((props: AppButtonProps) => (
<Button {...props} startIcon={<PersonAddAltRoundedIcon />} /> <Button {...props} startIcon={<PersonAddAltRoundedIcon />} />
))(() => ({ ))(() => ({
alignSelf: 'center', alignSelf: 'center',
padding: '0.35rem 1rem', padding: '0.35rem 1rem',
})) }))
export const GetStartedButton = styled((props: AppButtonProps) => ( export const GetStartedButton = styled((props: AppButtonProps) => (
<Button {...props} startIcon={<PlayArrowOutlinedIcon />} /> <Button {...props} startIcon={<PlayArrowOutlinedIcon />} />
))(() => ({ ))(() => ({
alignSelf: 'left', alignSelf: 'left',
padding: '0.35rem 1rem', padding: '0.35rem 1rem',
})) }))
export const LearnMoreButton = styled((props: AppButtonProps) => ( export const LearnMoreButton = styled((props: AppButtonProps) => (
<Button {...props} startIcon={<HelpOutlineOutlinedIcon />} /> <Button {...props} startIcon={<HelpOutlineOutlinedIcon />} />
))(() => ({ ))(() => ({
alignSelf: 'left', alignSelf: 'left',
padding: '0.35rem 1rem', padding: '0.35rem 1rem',
})) }))

View File

@@ -21,84 +21,69 @@ import { checkNpubSyncQuerier } from './utils'
import { DOMAIN } from '@/utils/consts' import { DOMAIN } from '@/utils/consts'
const KeyPage = () => { const KeyPage = () => {
const { npub = '' } = useParams<{ npub: string }>() const { npub = '' } = useParams<{ npub: string }>()
const { keys, apps, pending, perms } = useAppSelector((state) => state.content) const { keys, apps, pending, perms } = useAppSelector((state) => state.content)
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false) const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
const { handleOpen } = useModalSearchParams() const { handleOpen } = useModalSearchParams()
// const { userNameWithPrefix } = useProfile(npub) // const { userNameWithPrefix } = useProfile(npub)
const { handleEnableBackground, showWarning, isEnabling } = const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning()
useBackgroundSigning()
const key = keys.find(k => k.npub === npub) const key = keys.find((k) => k.npub === npub)
let username = '' let username = ''
if (key?.name) { if (key?.name) {
if (key.name.includes('@')) if (key.name.includes('@')) username = key.name
username = key.name else username = `${key?.name}@${DOMAIN}`
else }
username = `${key?.name}@${DOMAIN}`
}
const filteredApps = apps.filter((a) => a.npub === npub) const filteredApps = apps.filter((a) => a.npub === npub)
const { prepareEventPendings } = useTriggerConfirmModal( const { prepareEventPendings } = useTriggerConfirmModal(npub, pending, perms)
npub,
pending,
perms,
)
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)
return ( return (
<> <>
<Stack gap={'1rem'} height={'100%'}> <Stack gap={'1rem'} height={'100%'}>
{showWarning && ( {showWarning && (
<BackgroundSigningWarning <BackgroundSigningWarning isEnabling={isEnabling} onEnableBackSigning={handleEnableBackground} />
isEnabling={isEnabling} )}
onEnableBackSigning={handleEnableBackground} <UserValueSection
/> title="Your login"
)} value={username}
<UserValueSection copyValue={username}
title='Your login' explanationType={EXPLANATION_MODAL_KEYS.NPUB}
value={username} />
copyValue={username} <UserValueSection
explanationType={EXPLANATION_MODAL_KEYS.NPUB} title="Your NPUB"
/> value={npub}
<UserValueSection copyValue={npub}
title='Your NPUB' explanationType={EXPLANATION_MODAL_KEYS.NPUB}
value={npub} />
copyValue={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>
<StyledIconButton <StyledIconButton bgcolor_variant="secondary" onClick={handleOpenSettingsModal} withBadge={!isSynced}>
bgcolor_variant='secondary' <SettingsIcon />
onClick={handleOpenSettingsModal} Settings
withBadge={!isSynced} </StyledIconButton>
> </Stack>
<SettingsIcon />
Settings
</StyledIconButton>
</Stack>
<Apps apps={filteredApps} npub={npub} /> <Apps apps={filteredApps} npub={npub} />
</Stack> </Stack>
<ModalConnectApp /> <ModalConnectApp />
<ModalSettings isSynced={isSynced} /> <ModalSettings isSynced={isSynced} />
<ModalExplanation /> <ModalExplanation />
<ModalConfirmConnect /> <ModalConfirmConnect />
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} /> <ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
</> </>
) )
} }
export default KeyPage export default KeyPage

View File

@@ -11,57 +11,41 @@ import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { ItemApp } from './ItemApp' import { ItemApp } from './ItemApp'
type AppsProps = { type AppsProps = {
apps: DbApp[] apps: DbApp[]
npub: string npub: string
} }
export const Apps: FC<AppsProps> = ({ apps = [], npub = '' }) => { export const Apps: FC<AppsProps> = ({ apps = [], npub = '' }) => {
const notify = useEnqueueSnackbar() const notify = useEnqueueSnackbar()
// eslint-disable-next-line // eslint-disable-next-line
async function deletePerm(id: string) { async function deletePerm(id: string) {
call(async () => { call(async () => {
await swicCall('deletePerm', id) await swicCall('deletePerm', id)
notify('Perm deleted!', 'success') notify('Perm deleted!', 'success')
}) })
} }
return ( return (
<Box <Box flex={1} marginBottom={'1rem'} display={'flex'} flexDirection={'column'} overflow={'auto'}>
flex={1} <Stack direction={'row'} alignItems={'center'} justifyContent={'space-between'} marginBottom={'0.5rem'}>
marginBottom={'1rem'} <SectionTitle>Connected apps</SectionTitle>
display={'flex'} <AppLink title="Discover Apps" />
flexDirection={'column'} </Stack>
overflow={'auto'} {!apps.length && (
> <StyledEmptyAppsBox>
<Stack <Typography className="message" variant="h5" fontWeight={600} textAlign={'center'}>
direction={'row'} No connected apps
alignItems={'center'} </Typography>
justifyContent={'space-between'} <Button>Discover Nostr Apps</Button>
marginBottom={'0.5rem'} </StyledEmptyAppsBox>
> )}
<SectionTitle>Connected apps</SectionTitle>
<AppLink title='Discover Apps' />
</Stack>
{!apps.length && (
<StyledEmptyAppsBox>
<Typography
className='message'
variant='h5'
fontWeight={600}
textAlign={'center'}
>
No connected apps
</Typography>
<Button>Discover Nostr Apps</Button>
</StyledEmptyAppsBox>
)}
<Stack gap={'0.5rem'} overflow={'auto'} flex={1}> <Stack gap={'0.5rem'} overflow={'auto'} flex={1}>
{apps.map((a) => ( {apps.map((a) => (
<ItemApp {...a} key={a.appNpub} /> <ItemApp {...a} key={a.appNpub} />
))} ))}
</Stack> </Stack>
</Box> </Box>
) )
} }

View File

@@ -4,24 +4,20 @@ import { CircularProgress, Stack } from '@mui/material'
import GppMaybeIcon from '@mui/icons-material/GppMaybe' import GppMaybeIcon from '@mui/icons-material/GppMaybe'
type BackgroundSigningWarningProps = { type BackgroundSigningWarningProps = {
isEnabling: boolean isEnabling: boolean
onEnableBackSigning: () => void onEnableBackSigning: () => void
} }
export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({ export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({ isEnabling, onEnableBackSigning }) => {
isEnabling, return (
onEnableBackSigning, <Warning
}) => { message={
return ( <Stack direction={'row'} alignItems={'center'} gap={'1rem'}>
<Warning Please enable push notifications {isEnabling ? <CircularProgress size={'1.5rem'} /> : null}
message={ </Stack>
<Stack direction={'row'} alignItems={'center'} gap={'1rem'}> }
Please enable push notifications{' '} Icon={<GppMaybeIcon htmlColor="white" />}
{isEnabling ? <CircularProgress size={'1.5rem'} /> : null} onClick={isEnabling ? undefined : onEnableBackSigning}
</Stack> />
} )
Icon={<GppMaybeIcon htmlColor='white' />}
onClick={isEnabling ? undefined : onEnableBackSigning}
/>
)
} }

View File

@@ -9,35 +9,25 @@ import { StyledItemAppContainer } from './styled'
type ItemAppProps = DbApp type ItemAppProps = DbApp
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => { export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => {
const appName = name || getShortenNpub(appNpub) const appName = name || getShortenNpub(appNpub)
return ( return (
<StyledItemAppContainer <StyledItemAppContainer
direction={'row'} direction={'row'}
alignItems={'center'} alignItems={'center'}
gap={'0.5rem'} gap={'0.5rem'}
padding={'0.5rem 0'} padding={'0.5rem 0'}
component={Link} component={Link}
to={`/key/${npub}/app/${appNpub}`} to={`/key/${npub}/app/${appNpub}`}
> >
<Avatar <Avatar variant="square" sx={{ width: 56, height: 56 }} src={icon} alt={name} />
variant='square' <Stack>
sx={{ width: 56, height: 56 }} <Typography noWrap display={'block'} variant="body2">
src={icon} {appName}
alt={name} </Typography>
/> <Typography noWrap display={'block'} variant="caption" color={'GrayText'}>
<Stack> Basic actions
<Typography noWrap display={'block'} variant='body2'> </Typography>
{appName} </Stack>
</Typography> </StyledItemAppContainer>
<Typography )
noWrap
display={'block'}
variant='caption'
color={'GrayText'}
>
Basic actions
</Typography>
</Stack>
</StyledItemAppContainer>
)
} }

View File

@@ -8,48 +8,31 @@ import { StyledInput } from '../styled'
import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { useModalSearchParams } from '@/hooks/useModalSearchParams'
type UserValueSectionProps = { type UserValueSectionProps = {
title: string title: string
value: string value: string
explanationType: EXPLANATION_MODAL_KEYS explanationType: EXPLANATION_MODAL_KEYS
copyValue: string copyValue: string
} }
const UserValueSection: FC<UserValueSectionProps> = ({ const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, copyValue }) => {
title, const { handleOpen } = useModalSearchParams()
value,
explanationType,
copyValue,
}) => {
const { handleOpen } = useModalSearchParams()
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => { const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
handleOpen(MODAL_PARAMS_KEYS.EXPLANATION, { handleOpen(MODAL_PARAMS_KEYS.EXPLANATION, {
search: { search: {
type, type,
}, },
}) })
} }
return ( return (
<Box> <Box>
<Stack <Stack direction={'row'} alignItems={'center'} justifyContent={'space-between'} marginBottom={'0.5rem'}>
direction={'row'} <SectionTitle>{title}</SectionTitle>
alignItems={'center'} <AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} />
justifyContent={'space-between'} </Stack>
marginBottom={'0.5rem'} <StyledInput value={value} readOnly endAdornment={<InputCopyButton value={copyValue} />} />
> </Box>
<SectionTitle>{title}</SectionTitle> )
<AppLink
title='What is this?'
onClick={() => handleOpenExplanationModal(explanationType)}
/>
</Stack>
<StyledInput
value={value}
readOnly
endAdornment={<InputCopyButton value={copyValue} />}
/>
</Box>
)
} }
export default UserValueSection export default UserValueSection

View File

@@ -3,42 +3,40 @@ import { Stack, StackProps, styled } from '@mui/material'
import { forwardRef } from 'react' import { forwardRef } from 'react'
export const StyledInput = styled( export const StyledInput = styled(
forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => { forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
return ( return (
<Input <Input
{...props} {...props}
ref={ref} ref={ref}
className='input' className="input"
containerProps={{ containerProps={{
className, className,
}} }}
fullWidth fullWidth
/> />
) )
}), })
)(({ theme }) => ({ )(({ theme }) => ({
'& > .input': { '& > .input': {
border: 'none', border: 'none',
background: theme.palette.secondary.main, background: theme.palette.secondary.main,
color: theme.palette.primary.main, color: theme.palette.primary.main,
'& .adornment': { '& .adornment': {
color: theme.palette.primary.main, color: theme.palette.primary.main,
}, },
}, },
})) }))
export const StyledItemAppContainer = styled( export const StyledItemAppContainer = styled(<C extends React.ElementType>(props: StackProps<C, { component?: C }>) => (
<C extends React.ElementType>(props: StackProps<C, { component?: C }>) => ( <Stack {...props} />
<Stack {...props} /> ))(({ theme }) => ({
), textDecoration: 'none',
)(({ theme }) => ({ boxShadow: 'none',
textDecoration: 'none', color: theme.palette.text.primary,
boxShadow: 'none', background: theme.palette.backgroundSecondary.default,
color: theme.palette.text.primary, borderRadius: '12px',
background: theme.palette.backgroundSecondary.default, padding: '0.5rem 1rem',
borderRadius: '12px', ':hover': {
padding: '0.5rem 1rem', background: `${theme.palette.backgroundSecondary.default}95`,
':hover': { },
background: `${theme.palette.backgroundSecondary.default}95`,
},
})) }))

View File

@@ -4,37 +4,34 @@ import { askNotificationPermission } from '@/utils/helpers/helpers'
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
export const useBackgroundSigning = () => { export const useBackgroundSigning = () => {
const [showWarning, setShowWarning] = useState(false) const [showWarning, setShowWarning] = useState(false)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const notify = useEnqueueSnackbar() const notify = useEnqueueSnackbar()
const checkBackgroundSigning = useCallback(async () => { const checkBackgroundSigning = useCallback(async () => {
if (!swr) return undefined if (!swr) return undefined
const isBackgroundEnable = await swr.pushManager.getSubscription() const isBackgroundEnable = await swr.pushManager.getSubscription()
setShowWarning(!isBackgroundEnable) setShowWarning(!isBackgroundEnable)
}, []) }, [])
const handleEnableBackground = useCallback(async () => { const handleEnableBackground = useCallback(async () => {
setIsLoading(true) setIsLoading(true)
try { try {
await askNotificationPermission() await askNotificationPermission()
const result = await swicCall('enablePush') const result = await swicCall('enablePush')
if (!result) throw new Error('Failed to activate the push subscription') if (!result) throw new Error('Failed to activate the push subscription')
notify('Push notifications enabled!', 'success') notify('Push notifications enabled!', 'success')
setShowWarning(false) setShowWarning(false)
} catch (error: any) { } catch (error: any) {
notify( notify(`Failed to enable push subscription: ${error}`, 'error')
`Failed to enable push subscription: ${error}`, }
'error', setIsLoading(false)
) checkBackgroundSigning()
} }, [notify, checkBackgroundSigning])
setIsLoading(false)
checkBackgroundSigning()
}, [notify, checkBackgroundSigning])
useEffect(() => { useEffect(() => {
checkBackgroundSigning() checkBackgroundSigning()
}, [checkBackgroundSigning]) }, [checkBackgroundSigning])
return { showWarning, isEnabling: isLoading, handleEnableBackground } return { showWarning, isEnabling: isLoading, handleEnableBackground }
} }

View File

@@ -5,27 +5,27 @@ import { getProfileUsername } from '@/utils/helpers/helpers'
import { DOMAIN } from '@/utils/consts' import { DOMAIN } from '@/utils/consts'
export const useProfile = (npub: string) => { export const useProfile = (npub: string) => {
const [profile, setProfile] = useState<MetaEvent | null>(null) const [profile, setProfile] = useState<MetaEvent | null>(null)
const userName = getProfileUsername(profile, npub) const userName = getProfileUsername(profile, npub)
// FIXME use nip05? // FIXME use nip05?
const userNameWithPrefix = userName + '@' + DOMAIN const userNameWithPrefix = userName + '@' + DOMAIN
const loadProfile = useCallback(async () => { const loadProfile = useCallback(async () => {
try { try {
const response = await fetchProfile(npub) const response = await fetchProfile(npub)
setProfile(response) setProfile(response)
} catch (error) { } catch (error) {
console.error('Failed to fetch profile:', error) console.error('Failed to fetch profile:', error)
} }
}, [npub]) }, [npub])
useEffect(() => { useEffect(() => {
loadProfile() loadProfile()
}, [loadProfile]) }, [loadProfile])
return { return {
profile, profile,
userNameWithPrefix, userNameWithPrefix,
} }
} }

View File

@@ -5,139 +5,102 @@ import { ACTION_TYPE } from '@/utils/consts'
import { useCallback, useEffect, useRef } from 'react' import { useCallback, useEffect, useRef } from 'react'
export type IPendingsByAppNpub = { export type IPendingsByAppNpub = {
[appNpub: string]: { [appNpub: string]: {
pending: DbPending[] pending: DbPending[]
isConnected: boolean isConnected: boolean
} }
} }
type IShownConfirmModals = { type IShownConfirmModals = {
[reqId: string]: boolean [reqId: string]: boolean
} }
export const useTriggerConfirmModal = ( export const useTriggerConfirmModal = (npub: string, pending: DbPending[], perms: DbPerm[]) => {
npub: string, const { handleOpen, getModalOpened } = useModalSearchParams()
pending: DbPending[],
perms: DbPerm[],
) => {
const { handleOpen, getModalOpened } = useModalSearchParams()
const isConfirmConnectModalOpened = getModalOpened( const isConfirmConnectModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
MODAL_PARAMS_KEYS.CONFIRM_CONNECT, const isConfirmEventModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
)
const isConfirmEventModalOpened = getModalOpened(
MODAL_PARAMS_KEYS.CONFIRM_EVENT,
)
const filteredPendingReqs = pending.filter((p) => p.npub === npub) const filteredPendingReqs = pending.filter((p) => p.npub === npub)
const filteredPerms = perms.filter((p) => p.npub === npub) const filteredPerms = perms.filter((p) => p.npub === npub)
const npubConnectPerms = filteredPerms.filter( const npubConnectPerms = filteredPerms.filter((perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC)
(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 excludeConnectPendings = filteredPendingReqs.filter(
(pr) => pr.method !== 'connect',
)
const connectPendings = filteredPendingReqs.filter(
(pr) => pr.method === 'connect',
)
const prepareEventPendings = const prepareEventPendings = excludeConnectPendings.reduce<IPendingsByAppNpub>((acc, current) => {
excludeConnectPendings.reduce<IPendingsByAppNpub>((acc, current) => { const isConnected = npubConnectPerms.some((cp) => cp.appNpub === current.appNpub)
const isConnected = npubConnectPerms.some( if (!acc[current.appNpub]) {
(cp) => cp.appNpub === current.appNpub, acc[current.appNpub] = {
) pending: [current],
if (!acc[current.appNpub]) { isConnected,
acc[current.appNpub] = { }
pending: [current], return acc
isConnected, }
} acc[current.appNpub].pending.push(current)
return acc acc[current.appNpub].isConnected = isConnected
} return acc
acc[current.appNpub].pending.push(current) }, {})
acc[current.appNpub].isConnected = isConnected
return acc
}, {})
const shownConnectModals = useRef<IShownConfirmModals>({}) const shownConnectModals = useRef<IShownConfirmModals>({})
const shownConfirmEventModals = useRef<IShownConfirmModals>({}) const shownConfirmEventModals = useRef<IShownConfirmModals>({})
useEffect(() => { useEffect(() => {
return () => { return () => {
shownConnectModals.current = {} shownConnectModals.current = {}
shownConfirmEventModals.current = {} shownConfirmEventModals.current = {}
} }
}, [npub, pending.length]) }, [npub, pending.length])
const handleOpenConfirmConnectModal = useCallback(() => { const handleOpenConfirmConnectModal = useCallback(() => {
if ( if (!filteredPendingReqs.length || isConfirmEventModalOpened || isConfirmConnectModalOpened) return undefined
!filteredPendingReqs.length ||
isConfirmEventModalOpened ||
isConfirmConnectModalOpened
)
return undefined
for (let i = 0; i < connectPendings.length; i++) { for (let i = 0; i < connectPendings.length; i++) {
const req = connectPendings[i] const req = connectPendings[i]
if (shownConnectModals.current[req.id]) { if (shownConnectModals.current[req.id]) {
continue continue
} }
shownConnectModals.current[req.id] = true shownConnectModals.current[req.id] = true
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, { handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
search: { search: {
appNpub: req.appNpub, appNpub: req.appNpub,
reqId: req.id, reqId: req.id,
}, },
}) })
break break
} }
}, [ }, [connectPendings, filteredPendingReqs.length, handleOpen, isConfirmEventModalOpened, isConfirmConnectModalOpened])
connectPendings,
filteredPendingReqs.length,
handleOpen,
isConfirmEventModalOpened,
isConfirmConnectModalOpened,
])
const handleOpenConfirmEventModal = useCallback(() => { const handleOpenConfirmEventModal = useCallback(() => {
if (!filteredPendingReqs.length || connectPendings.length) if (!filteredPendingReqs.length || connectPendings.length) return undefined
return undefined
for (let i = 0; i < Object.keys(prepareEventPendings).length; i++) { for (let i = 0; i < Object.keys(prepareEventPendings).length; i++) {
const appNpub = Object.keys(prepareEventPendings)[i] const appNpub = Object.keys(prepareEventPendings)[i]
if ( if (shownConfirmEventModals.current[appNpub] || !prepareEventPendings[appNpub].isConnected) {
shownConfirmEventModals.current[appNpub] || continue
!prepareEventPendings[appNpub].isConnected }
) {
continue
}
shownConfirmEventModals.current[appNpub] = true shownConfirmEventModals.current[appNpub] = true
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_EVENT, { handleOpen(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
search: { search: {
appNpub, appNpub,
}, },
}) })
break break
} }
}, [ }, [connectPendings.length, filteredPendingReqs.length, handleOpen, prepareEventPendings])
connectPendings.length,
filteredPendingReqs.length,
handleOpen,
prepareEventPendings,
])
useEffect(() => { useEffect(() => {
handleOpenConfirmEventModal() handleOpenConfirmEventModal()
}, [handleOpenConfirmEventModal]) }, [handleOpenConfirmEventModal])
useEffect(() => { useEffect(() => {
handleOpenConfirmConnectModal() handleOpenConfirmConnectModal()
}, [handleOpenConfirmConnectModal]) }, [handleOpenConfirmConnectModal])
return { return {
prepareEventPendings, prepareEventPendings,
} }
} }

View File

@@ -3,82 +3,76 @@ import { Box, Button, ButtonProps, styled, Badge } from '@mui/material'
import { forwardRef } from 'react' import { forwardRef } from 'react'
type StyledIconButtonProps = ButtonProps & { type StyledIconButtonProps = ButtonProps & {
bgcolor_variant?: 'primary' | 'secondary' bgcolor_variant?: 'primary' | 'secondary'
withBadge?: boolean withBadge?: boolean
} }
export const StyledIconButton = styled( export const StyledIconButton = styled(({ withBadge, ...props }: StyledIconButtonProps) => {
({ withBadge, ...props }: StyledIconButtonProps) => { if (withBadge) {
if (withBadge) { return (
return ( <Badge sx={{ flex: 1 }} badgeContent={''} color="error">
<Badge sx={{ flex: 1 }} badgeContent={''} color='error'> <Button {...props} />
<Button {...props} /> </Badge>
</Badge> )
) }
} return <Button {...props} />
return <Button {...props} /> })(({ bgcolor_variant = 'primary', theme }) => {
}, const isPrimary = bgcolor_variant === 'primary'
)(({ bgcolor_variant = 'primary', theme }) => { return {
const isPrimary = bgcolor_variant === 'primary' flex: '1',
return { padding: '0.75rem',
flex: '1', display: 'flex',
padding: '0.75rem', flexDirection: 'column',
display: 'flex', alignItems: 'flex-start',
flexDirection: 'column', gap: '0.5rem',
alignItems: 'flex-start', borderRadius: '1rem',
gap: '0.5rem', fontSize: '0.875rem',
borderRadius: '1rem', '&:is(:hover, :active, &)': {
fontSize: '0.875rem', background: isPrimary ? theme.palette.primary.main : theme.palette.secondary.main,
'&:is(:hover, :active, &)': { },
background: isPrimary color: isPrimary ? theme.palette.text.secondary : theme.palette.text.primary,
? theme.palette.primary.main }
: theme.palette.secondary.main,
},
color: isPrimary
? theme.palette.text.secondary
: theme.palette.text.primary,
}
}) })
export const StyledEmptyAppsBox = styled(Box)(({ theme }) => { export const StyledEmptyAppsBox = styled(Box)(({ theme }) => {
return { return {
minHeight: '186px', minHeight: '186px',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
background: theme.palette.secondary.main, background: theme.palette.secondary.main,
borderRadius: '24px', borderRadius: '24px',
padding: '1rem', padding: '1rem',
'& > .message': { '& > .message': {
flex: '1', flex: '1',
display: 'grid', display: 'grid',
placeItems: 'center', placeItems: 'center',
color: theme.palette.text.primary, color: theme.palette.text.primary,
opacity: '0.6', opacity: '0.6',
}, },
} }
}) })
export const StyledInput = styled( export const StyledInput = styled(
forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => { forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
return ( return (
<Input <Input
{...props} {...props}
ref={ref} ref={ref}
className='input' className="input"
containerProps={{ containerProps={{
className, className,
}} }}
fullWidth fullWidth
/> />
) )
}), })
)(({ theme }) => ({ )(({ theme }) => ({
'& > .input': { '& > .input': {
border: 'none', border: 'none',
background: theme.palette.secondary.main, background: theme.palette.secondary.main,
color: theme.palette.primary.main, color: theme.palette.primary.main,
'& .adornment': { '& .adornment': {
color: theme.palette.primary.main, color: theme.palette.primary.main,
}, },
}, },
})) }))

View File

@@ -1,6 +1,6 @@
import { db } from '@/modules/db' import { db } from '@/modules/db'
export const checkNpubSyncQuerier = (npub: string) => async () => { export const checkNpubSyncQuerier = (npub: string) => async () => {
const count = await db.syncHistory.where('npub').equals(npub).count() const count = await db.syncHistory.where('npub').equals(npub).count()
return count > 0 return count > 0
} }

View File

@@ -6,91 +6,73 @@ import { Box, Button, Stack, TextField } from '@mui/material'
import { useEnqueueSnackbar } from '../hooks/useEnqueueSnackbar' import { useEnqueueSnackbar } from '../hooks/useEnqueueSnackbar'
const WelcomePage = () => { const WelcomePage = () => {
const keys = useAppSelector((state) => state.content.keys) const keys = useAppSelector((state) => state.content.keys)
const notify = useEnqueueSnackbar() const notify = useEnqueueSnackbar()
const isKeysExists = keys.length > 0 const isKeysExists = keys.length > 0
const nsecInputRef = useRef<HTMLInputElement | null>(null) const nsecInputRef = useRef<HTMLInputElement | null>(null)
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 {
const k: any = await swicCall('generateKey') const k: any = await swicCall('generateKey')
notify(`New key ${k.npub}`, 'success') notify(`New key ${k.npub}`, 'success')
} catch (error: any) { } catch (error: any) {
notify(error.message, 'error') notify(error.message, 'error')
} }
} }
async function importKey() { async function importKey() {
try { try {
const nsec = nsecInputRef.current?.value const nsec = nsecInputRef.current?.value
if (!nsec) return if (!nsec) return
await swicCall('importKey', nsec) await swicCall('importKey', nsec)
} catch (error: any) { } catch (error: any) {
notify(error.message, 'error') notify(error.message, 'error')
} }
} }
async function fetchNewKey() { async function fetchNewKey() {
try { try {
const npub = npubInputRef.current?.value const npub = npubInputRef.current?.value
const passphrase = passwordInputRef.current?.value const passphrase = passwordInputRef.current?.value
const k: any = await swicCall('fetchKey', npub, passphrase) const k: any = await swicCall('fetchKey', npub, passphrase)
notify(`Fetched ${k.npub}`, 'success') notify(`Fetched ${k.npub}`, 'success')
} catch (error: any) { } catch (error: any) {
notify(error.message, 'error') notify(error.message, 'error')
} }
} }
return ( return (
<Stack gap={'1.5rem'}> <Stack gap={'1.5rem'}>
<Box alignSelf={'center'}> <Box alignSelf={'center'}>
<Button size='small' variant='contained' onClick={generateKey}> <Button size="small" variant="contained" onClick={generateKey}>
generate key generate key
</Button> </Button>
</Box> </Box>
<Stack alignItems={'center'} gap='0.5rem'> <Stack alignItems={'center'} gap="0.5rem">
<TextField <TextField variant="outlined" ref={nsecInputRef} placeholder="Enter nsec..." fullWidth size="small" />
variant='outlined' <Button size="small" variant="contained" onClick={importKey}>
ref={nsecInputRef} import key (DANGER!)
placeholder='Enter nsec...' </Button>
fullWidth </Stack>
size='small'
/>
<Button size='small' variant='contained' onClick={importKey}>
import key (DANGER!)
</Button>
</Stack>
<Stack alignItems={'center'} gap='0.5rem'> <Stack alignItems={'center'} gap="0.5rem">
<Stack width={'100%'} gap='0.5rem'> <Stack width={'100%'} gap="0.5rem">
<TextField <TextField variant="outlined" ref={npubInputRef} placeholder="Enter npub..." fullWidth size="small" />
variant='outlined' <TextField variant="outlined" ref={passwordInputRef} placeholder="Enter password" fullWidth size="small" />
ref={npubInputRef} </Stack>
placeholder='Enter npub...' <Button size="small" variant="contained" onClick={fetchNewKey}>
fullWidth fetch key
size='small' </Button>
/> </Stack>
<TextField </Stack>
variant='outlined' )
ref={passwordInputRef}
placeholder='Enter password'
fullWidth
size='small'
/>
</Stack>
<Button size='small' variant='contained' onClick={fetchNewKey}>
fetch key
</Button>
</Stack>
</Stack>
)
} }
export default WelcomePage export default WelcomePage

View File

@@ -1,15 +1,15 @@
import { ReportHandler } from 'web-vitals'; import { ReportHandler } from 'web-vitals'
const reportWebVitals = (onPerfEntry?: ReportHandler) => { const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry); getCLS(onPerfEntry)
getFID(onPerfEntry); getFID(onPerfEntry)
getFCP(onPerfEntry); getFCP(onPerfEntry)
getLCP(onPerfEntry); getLCP(onPerfEntry)
getTTFB(onPerfEntry); getTTFB(onPerfEntry)
}); })
} }
}; }
export default reportWebVitals; export default reportWebVitals

View File

@@ -10,33 +10,27 @@ const ConfirmPage = lazy(() => import('../pages/Confirm.Page'))
const AppPage = lazy(() => import('../pages/AppPage/App.Page')) const AppPage = lazy(() => import('../pages/AppPage/App.Page'))
const LoadingSpinner = () => ( const LoadingSpinner = () => (
<Stack height={'100%'} justifyContent={'center'} alignItems={'center'}> <Stack height={'100%'} justifyContent={'center'} alignItems={'center'}>
<CircularProgress /> <CircularProgress />
</Stack> </Stack>
) )
const AppRoutes = () => { const AppRoutes = () => {
return ( return (
<Suspense fallback={<LoadingSpinner />}> <Suspense fallback={<LoadingSpinner />}>
<Routes> <Routes>
<Route path='/' element={<Layout />}> <Route path="/" element={<Layout />}>
<Route path='/' element={<Navigate to={'/home'} />} /> <Route path="/" element={<Navigate to={'/home'} />} />
{/* <Route path='/welcome' element={<WelcomePage />} /> */} {/* <Route path='/welcome' element={<WelcomePage />} /> */}
<Route path='/home' element={<HomePage />} /> <Route path="/home" element={<HomePage />} />
<Route path='/key/:npub' element={<KeyPage />} /> <Route path="/key/:npub" element={<KeyPage />} />
<Route <Route path="/key/:npub/app/:appNpub" element={<AppPage />} />
path='/key/:npub/app/:appNpub' <Route path="/key/:npub/:req_id" element={<ConfirmPage />} />
element={<AppPage />} </Route>
/> <Route path="*" element={<Navigate to={'/home'} />} />
<Route </Routes>
path='/key/:npub/:req_id' </Suspense>
element={<ConfirmPage />} )
/>
</Route>
<Route path='*' element={<Navigate to={'/home'} />} />
</Routes>
</Suspense>
)
} }
export default AppRoutes export default AppRoutes

View File

@@ -30,61 +30,60 @@ precacheAndRoute(self.__WB_MANIFEST)
// https://developers.google.com/web/fundamentals/architecture/app-shell // https://developers.google.com/web/fundamentals/architecture/app-shell
const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$') const fileExtensionRegexp = new RegExp('/[^/?]+\\.[^/]+$')
registerRoute( registerRoute(
// Return false to exempt requests from being fulfilled by index.html. // Return false to exempt requests from being fulfilled by index.html.
({ request, url }: { request: Request; url: URL }) => { ({ request, url }: { request: Request; url: URL }) => {
// If this isn't a navigation, skip. // If this isn't a navigation, skip.
if (request.mode !== 'navigate') { if (request.mode !== 'navigate') {
return false return false
} }
// If this is a URL that starts with /_, skip. // If this is a URL that starts with /_, skip.
if (url.pathname.startsWith('/_')) { if (url.pathname.startsWith('/_')) {
return false return false
} }
// If this looks like a URL for a resource, because it contains // If this looks like a URL for a resource, because it contains
// a file extension, skip. // a file extension, skip.
if (url.pathname.match(fileExtensionRegexp)) { if (url.pathname.match(fileExtensionRegexp)) {
return false return false
} }
// Return true to signal that we want to use the handler. // Return true to signal that we want to use the handler.
return true return true
}, },
createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html'), createHandlerBoundToURL(process.env.PUBLIC_URL + '/index.html')
) )
// An example runtime caching route for requests that aren't handled by the // An example runtime caching route for requests that aren't handled by the
// precache, in this case same-origin .png requests like those from in public/ // precache, in this case same-origin .png requests like those from in public/
registerRoute( registerRoute(
// Add in any other file extensions or routing criteria as needed. // Add in any other file extensions or routing criteria as needed.
({ url }) => ({ url }) => url.origin === self.location.origin && url.pathname.endsWith('.png'),
url.origin === self.location.origin && url.pathname.endsWith('.png'), // Customize this strategy as needed, e.g., by changing to CacheFirst.
// Customize this strategy as needed, e.g., by changing to CacheFirst. new StaleWhileRevalidate({
new StaleWhileRevalidate({ cacheName: 'images',
cacheName: 'images', plugins: [
plugins: [ // Ensure that once this runtime cache reaches a maximum size the
// Ensure that once this runtime cache reaches a maximum size the // least-recently used images are removed.
// least-recently used images are removed. new ExpirationPlugin({ maxEntries: 50 }),
new ExpirationPlugin({ maxEntries: 50 }), ],
], })
}),
) )
// This allows the web app to trigger skipWaiting via // This allows the web app to trigger skipWaiting via
// registration.waiting.postMessage({type: 'SKIP_WAITING'}) // registration.waiting.postMessage({type: 'SKIP_WAITING'})
self.addEventListener('message', (event) => { self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') { if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting() self.skipWaiting()
} }
}) })
// Any other custom service worker logic can go here. // Any other custom service worker logic can go here.
async function start() { async function start() {
console.log('worker starting') console.log('worker starting')
const backend = new NoauthBackend(self) const backend = new NoauthBackend(self)
await backend.start() await backend.start()
} }
start() start()

View File

@@ -16,34 +16,34 @@ const isLocalhost = Boolean(
window.location.hostname === '[::1]' || window.location.hostname === '[::1]' ||
// 127.0.0.0/8 are considered localhost for IPv4. // 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/) window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
); )
type Config = { type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void; onSuccess?: (registration: ServiceWorkerRegistration) => void
onUpdate?: (registration: ServiceWorkerRegistration) => void; onUpdate?: (registration: ServiceWorkerRegistration) => void
onError?: (e: any) => void; onError?: (e: any) => void
}; }
export function register(config?: Config) { export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW. // The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href)
if (publicUrl.origin !== window.location.origin) { if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin // Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to // from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebook/create-react-app/issues/2374 // serve assets; see https://github.com/facebook/create-react-app/issues/2374
if (config && config.onError) { if (config && config.onError) {
config.onError(new Error("Wrong origin")); config.onError(new Error('Wrong origin'))
} }
return; return
} }
window.addEventListener('load', () => { window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) { if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not. // This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config); checkValidServiceWorker(swUrl, config)
// Add some additional logging to localhost, pointing developers to the // Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation. // service worker/PWA documentation.
@@ -51,16 +51,16 @@ export function register(config?: Config) {
console.log( console.log(
'This web app is being served cache-first by a service ' + 'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://cra.link/PWA' 'worker. To learn more, visit https://cra.link/PWA'
); )
}); })
} else { } else {
// Is not localhost. Just register service worker // Is not localhost. Just register service worker
registerValidSW(swUrl, config); registerValidSW(swUrl, config)
} }
}); })
} else { } else {
if (config && config.onError) { if (config && config.onError) {
config.onError(new Error("No service worker")); config.onError(new Error('No service worker'))
} }
} }
} }
@@ -70,9 +70,9 @@ function registerValidSW(swUrl: string, config?: Config) {
.register(swUrl) .register(swUrl)
.then((registration) => { .then((registration) => {
registration.onupdatefound = () => { registration.onupdatefound = () => {
const installingWorker = registration.installing; const installingWorker = registration.installing
if (installingWorker == null) { if (installingWorker == null) {
return; return
} }
installingWorker.onstatechange = () => { installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') { if (installingWorker.state === 'installed') {
@@ -83,33 +83,33 @@ function registerValidSW(swUrl: string, config?: Config) {
console.log( console.log(
'New content is available and will be used when all ' + 'New content is available and will be used when all ' +
'tabs for this page are closed. See https://cra.link/PWA.' 'tabs for this page are closed. See https://cra.link/PWA.'
); )
// Execute callback // Execute callback
if (config && config.onUpdate) { if (config && config.onUpdate) {
config.onUpdate(registration); config.onUpdate(registration)
} }
} else { } else {
// At this point, everything has been precached. // At this point, everything has been precached.
// It's the perfect time to display a // It's the perfect time to display a
// "Content is cached for offline use." message. // "Content is cached for offline use." message.
console.log('Content is cached for offline use.'); console.log('Content is cached for offline use.')
// Execute callback // Execute callback
if (config && config.onSuccess) { if (config && config.onSuccess) {
config.onSuccess(registration); config.onSuccess(registration)
} }
} }
} }
}; }
}; }
}) })
.catch((error) => { .catch((error) => {
console.error('Error during service worker registration:', error); console.error('Error during service worker registration:', error)
if (config && config.onError) { if (config && config.onError) {
config.onError(new Error(`Install error: ${error}`)); config.onError(new Error(`Install error: ${error}`))
} }
}); })
} }
function checkValidServiceWorker(swUrl: string, config?: Config) { function checkValidServiceWorker(swUrl: string, config?: Config) {
@@ -119,35 +119,32 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
}) })
.then((response) => { .then((response) => {
// Ensure service worker exists, and that we really are getting a JS file. // Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type'); const contentType = response.headers.get('content-type')
if ( if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
// No service worker found. Probably a different app. Reload the page. // No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => { navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => { registration.unregister().then(() => {
window.location.reload(); window.location.reload()
}); })
}); })
} else { } else {
// Service worker found. Proceed as normal. // Service worker found. Proceed as normal.
registerValidSW(swUrl, config); registerValidSW(swUrl, config)
} }
}) })
.catch(() => { .catch(() => {
console.log('No internet connection found. App is running in offline mode.'); console.log('No internet connection found. App is running in offline mode.')
}); })
} }
export function unregister() { export function unregister() {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready navigator.serviceWorker.ready
.then((registration) => { .then((registration) => {
registration.unregister(); registration.unregister()
}) })
.catch((error) => { .catch((error) => {
console.error(error.message); console.error(error.message)
}); })
} }
} }

View File

@@ -2,4 +2,4 @@
// allows you to do things like: // allows you to do things like:
// expect(element).toHaveTextContent(/react/i) // expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom // learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom'; import '@testing-library/jest-dom'

View File

@@ -2,21 +2,21 @@ import { Typography, TypographyProps, styled } from '@mui/material'
import React, { FC } from 'react' import React, { FC } from 'react'
type AppLinkProps = { type AppLinkProps = {
title: string title: string
} & TypographyProps } & TypographyProps
export const AppLink: FC<AppLinkProps> = ({ title = '', ...rest }) => { export const AppLink: FC<AppLinkProps> = ({ title = '', ...rest }) => {
return <StyledTypography {...rest}>{title}</StyledTypography> return <StyledTypography {...rest}>{title}</StyledTypography>
} }
const StyledTypography = styled((props: TypographyProps) => ( const StyledTypography = styled((props: TypographyProps) => <Typography {...props} variant="caption" />)(({
<Typography {...props} variant='caption' /> theme,
))(({ theme }) => { }) => {
return { return {
color: theme.palette.textSecondaryDecorate.main, color: theme.palette.textSecondaryDecorate.main,
cursor: 'pointer', cursor: 'pointer',
'&:active': { '&:active': {
textDecoration: 'underline', textDecoration: 'underline',
}, },
} }
}) })

View File

@@ -1,53 +1,45 @@
import { import { styled, Button as MuiButton, ButtonProps as MuiButtonProps } from '@mui/material'
styled,
Button as MuiButton,
ButtonProps as MuiButtonProps,
} from '@mui/material'
import { forwardRef } from 'react' import { forwardRef } from 'react'
export type AppButtonProps = MuiButtonProps & { export type AppButtonProps = MuiButtonProps & {
varianttype?: 'light' | 'default' | 'dark' | 'secondary' varianttype?: 'light' | 'default' | 'dark' | 'secondary'
} }
export const Button = forwardRef<HTMLButtonElement, AppButtonProps>( export const Button = forwardRef<HTMLButtonElement, AppButtonProps>(({ children, ...restProps }, ref) => {
({ children, ...restProps }, ref) => { return (
return ( <StyledButton classes={{ root: 'button' }} {...restProps} ref={ref}>
<StyledButton classes={{ root: 'button' }} {...restProps} ref={ref}> {children}
{children} </StyledButton>
</StyledButton> )
) })
},
)
const StyledButton = styled( const StyledButton = styled(
forwardRef<HTMLButtonElement, AppButtonProps>((props, ref) => ( forwardRef<HTMLButtonElement, AppButtonProps>((props, ref) => <MuiButton ref={ref} {...props} />)
<MuiButton ref={ref} {...props} />
)),
)(({ theme, varianttype = 'default' }) => { )(({ theme, varianttype = 'default' }) => {
const commonStyles = { const commonStyles = {
fontWeight: 500, fontWeight: 500,
borderRadius: '1rem', borderRadius: '1rem',
} }
if (varianttype === 'secondary') { if (varianttype === 'secondary') {
return { return {
...commonStyles, ...commonStyles,
'&.button:is(:hover, :active, &)': { '&.button:is(:hover, :active, &)': {
background: theme.palette.backgroundSecondary.default, background: theme.palette.backgroundSecondary.default,
}, },
color: theme.palette.text.primary, color: theme.palette.text.primary,
} }
} }
return { return {
...commonStyles, ...commonStyles,
'&.button:is(:hover, :active, &)': { '&.button:is(:hover, :active, &)': {
background: theme.palette.primary.main, background: theme.palette.primary.main,
}, },
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
':disabled': { ':disabled': {
'&.button:is(:hover, :active, &)': { '&.button:is(:hover, :active, &)': {
background: theme.palette.backgroundSecondary.default, background: theme.palette.backgroundSecondary.default,
}, },
color: theme.palette.backgroundSecondary.paper, color: theme.palette.backgroundSecondary.paper,
}, },
} }
}) })

View File

@@ -1,38 +1,27 @@
import { forwardRef } from 'react' import { forwardRef } from 'react'
import { Checkbox as MuiCheckbox, CheckboxProps, styled } from '@mui/material' import { Checkbox as MuiCheckbox, CheckboxProps, styled } from '@mui/material'
import { import { CheckedIcon, CheckedLightIcon, UnchekedIcon, UnchekedLightIcon } from '@/assets'
CheckedIcon,
CheckedLightIcon,
UnchekedIcon,
UnchekedLightIcon,
} from '@/assets'
import { useAppSelector } from '@/store/hooks/redux' import { useAppSelector } from '@/store/hooks/redux'
export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>( export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>((props, ref) => {
(props, ref) => { const { themeMode } = useAppSelector((state) => state.ui)
const { themeMode } = useAppSelector((state) => state.ui)
return <StyledCheckbox ref={ref} {...props} mode={themeMode} /> return <StyledCheckbox ref={ref} {...props} mode={themeMode} />
}, })
)
const StyledCheckbox = styled( const StyledCheckbox = styled(
forwardRef<HTMLButtonElement, CheckboxProps & { mode: 'dark' | 'light' }>( forwardRef<HTMLButtonElement, CheckboxProps & { mode: 'dark' | 'light' }>(({ mode, ...restProps }, ref) => {
({ mode, ...restProps }, ref) => { const isDarkMode = mode === 'dark'
const isDarkMode = mode === 'dark' return (
return ( <MuiCheckbox
<MuiCheckbox {...restProps}
{...restProps} ref={ref}
ref={ref} icon={isDarkMode ? <UnchekedLightIcon /> : <UnchekedIcon />}
icon={isDarkMode ? <UnchekedLightIcon /> : <UnchekedIcon />} checkedIcon={isDarkMode ? <CheckedLightIcon /> : <CheckedIcon />}
checkedIcon={ />
isDarkMode ? <CheckedLightIcon /> : <CheckedIcon /> )
} })
/>
)
},
),
)(() => ({ )(() => ({
'& .MuiSvgIcon-root': { fontSize: '1.5rem' }, '& .MuiSvgIcon-root': { fontSize: '1.5rem' },
marginLeft: '-10px', marginLeft: '-10px',
})) }))

View File

@@ -1,65 +1,58 @@
import React, { FC } from 'react' import React, { FC } from 'react'
import { import { Dialog, DialogActions, DialogContent, DialogProps, DialogTitle, Slide } from '@mui/material'
Dialog,
DialogActions,
DialogContent,
DialogProps,
DialogTitle,
Slide,
} from '@mui/material'
import { Button } from '../Button/Button' import { Button } from '../Button/Button'
import { TransitionProps } from '@mui/material/transitions' import { TransitionProps } from '@mui/material/transitions'
import { StyledDialogContentText } from './styled' import { StyledDialogContentText } from './styled'
const Transition = React.forwardRef(function Transition( const Transition = React.forwardRef(function Transition(
props: TransitionProps & { props: TransitionProps & {
children: React.ReactElement<any, any> children: React.ReactElement<any, any>
}, },
ref: React.Ref<unknown>, ref: React.Ref<unknown>
) { ) {
return <Slide direction='up' ref={ref} {...props} /> return <Slide direction="up" ref={ref} {...props} />
}) })
type ConfirmModalProps = { type ConfirmModalProps = {
onConfirm: () => void onConfirm: () => void
onCancel: () => void onCancel: () => void
headingText: string headingText: string
description?: string description?: string
} & DialogProps } & DialogProps
export const ConfirmModal: FC<ConfirmModalProps> = ({ export const ConfirmModal: FC<ConfirmModalProps> = ({
open, open,
onClose, onClose,
onConfirm, onConfirm,
onCancel, onCancel,
headingText = 'Confirm', headingText = 'Confirm',
description, description,
}) => { }) => {
return ( return (
<Dialog <Dialog
open={open} open={open}
TransitionComponent={Transition} TransitionComponent={Transition}
keepMounted keepMounted
onClose={onClose} onClose={onClose}
sx={{ zIndex: 1302 }} sx={{ zIndex: 1302 }}
PaperProps={{ PaperProps={{
sx: { sx: {
borderRadius: '10px', borderRadius: '10px',
}, },
}} }}
> >
<DialogTitle fontWeight={600} fontSize={'1.5rem'}> <DialogTitle fontWeight={600} fontSize={'1.5rem'}>
{headingText} {headingText}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<StyledDialogContentText>{description}</StyledDialogContentText> <StyledDialogContentText>{description}</StyledDialogContentText>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button varianttype='secondary' onClick={onCancel}> <Button varianttype="secondary" onClick={onCancel}>
Cancel Cancel
</Button> </Button>
<Button onClick={onConfirm}>Confirm</Button> <Button onClick={onConfirm}>Confirm</Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
) )
} }

View File

@@ -1,11 +1,7 @@
import { import { DialogContentText, DialogContentTextProps, styled } from '@mui/material'
DialogContentText,
DialogContentTextProps,
styled,
} from '@mui/material'
export const StyledDialogContentText = styled( export const StyledDialogContentText = styled((props: DialogContentTextProps) => <DialogContentText {...props} />)(
(props: DialogContentTextProps) => <DialogContentText {...props} />, ({ theme }) => ({
)(({ theme }) => ({ color: theme.palette.primary.main,
color: theme.palette.primary.main, })
})) )

View File

@@ -1,27 +1,26 @@
import { forwardRef, useRef } from "react"; import { forwardRef, useRef } from 'react'
import { Input, InputProps } from "../Input/Input"; import { Input, InputProps } from '../Input/Input'
export type DebounceProps = { export type DebounceProps = {
handleDebounce: (value: string) => void; handleDebounce: (value: string) => void
debounceTimeout: number; debounceTimeout: number
}; }
export const DebounceInput = (props: InputProps & DebounceProps) => { export const DebounceInput = (props: InputProps & DebounceProps) => {
const { handleDebounce, debounceTimeout, ...rest } = props; const { handleDebounce, debounceTimeout, ...rest } = props
const timerRef = useRef<number>(); const timerRef = useRef<number>()
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => { const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (timerRef.current) { if (timerRef.current) {
clearTimeout(timerRef.current); clearTimeout(timerRef.current)
} }
timerRef.current = window.setTimeout(() => { timerRef.current = window.setTimeout(() => {
handleDebounce(event.target.value); handleDebounce(event.target.value)
}, debounceTimeout); }, debounceTimeout)
}; }
// @ts-ignore // @ts-ignore
return <Input {...rest} onChange={handleChange} />; return <Input {...rest} onChange={handleChange} />
} }

View File

@@ -5,21 +5,21 @@ import useIsIOS from '@/hooks/useIsIOS'
import { StyledButton } from './styled' import { StyledButton } from './styled'
type IOSBackButtonProps = ButtonProps & { type IOSBackButtonProps = ButtonProps & {
onNavigate?: () => void onNavigate?: () => void
} }
export const IOSBackButton: FC<IOSBackButtonProps> = ({ onNavigate }) => { export const IOSBackButton: FC<IOSBackButtonProps> = ({ onNavigate }) => {
const isIOS = useIsIOS() const isIOS = useIsIOS()
const navigate = useNavigate() const navigate = useNavigate()
const handleNavigateBack = () => { const handleNavigateBack = () => {
if (onNavigate && typeof onNavigate === 'function') { if (onNavigate && typeof onNavigate === 'function') {
return onNavigate() return onNavigate()
} }
navigate(-1) navigate(-1)
} }
if (!isIOS) return null if (!isIOS) return null
return <StyledButton onClick={handleNavigateBack}>Back</StyledButton> return <StyledButton onClick={handleNavigateBack}>Back</StyledButton>
} }

Some files were not shown because too many files have changed in this diff Show More