Compare commits

...

20 Commits

Author SHA1 Message Date
326d824451 Merge branch 'develop' of https://github.com/nostrband/noauth into feature/prettier-config 2024-02-06 22:51:50 +06:00
cc9840760b Add disallow on window close in popup mode 2024-02-06 19:03:56 +03:00
be8cfcb3a5 add prettier 2024-02-06 15:49:05 +06:00
14940a4345 Add popup confirm mode, make on-demand mean connect+get_public_key 2024-02-06 11:41:51 +03:00
fa4c5d3532 Start OAuth-flow support by sending authUrl replies 2024-02-06 09:53:01 +03:00
e80a41bfa0 Change relay to .env variable 2024-02-05 19:08:33 +03:00
6c2a12c924 Fix updateUI to ensure all action windows are notified 2024-02-05 16:18:20 +03:00
8aabb45917 Add name saving to login flow 2024-02-05 16:01:26 +03:00
5b57b42111 Add name processing for signup, add pow to nip98 and to sendName, minor UI changes 2024-02-05 14:29:25 +03:00
9c18310fd9 Merge pull request #4 from nostrband/refactor/sync-npub
Refactor/login
2024-02-05 09:12:22 +03:00
f2e70a998d Merge branch 'main' into develop 2024-02-02 13:40:27 +03:00
b2e1a43f1b Fix createHandleCloseReplace implementation 2024-02-02 13:39:56 +03:00
878bae6c2f Merge pull request #5 from nostrband/fix/modal-replace-notifs
Fix/modal replace notifs
2024-02-02 12:59:31 +03:00
ae7b39c851 Fix modal replace logic, fix notif click, fix notif messages, fix activity history order 2024-02-02 12:51:30 +03:00
020ab18e56 Merge pull request #3 from nostrband/refactor/sync-npub
add sync npub logic & change perms and activity history design & add …
2024-02-02 09:51:20 +03:00
41de75ff6e Fix typo in encryptKeyPass 2024-01-30 15:42:53 +03:00
8ae416047d Merge pull request #2 from nostrband/develop
App page
2024-01-30 15:39:33 +03:00
cddf0b7805 Merge branch 'develop' of https://github.com/nostrband/noauth into develop 2024-01-30 11:19:35 +03:00
c28ef815ac Fix notifCallback reset after notif 2024-01-30 11:19:32 +03:00
50e31ceb1c Merge pull request #1 from nostrband/develop
init develop branch
2024-01-29 11:34:02 +03:00
119 changed files with 35916 additions and 36376 deletions

5
.env
View File

@ -2,5 +2,6 @@
# change if you're using a different noauthd server
REACT_APP_WEB_PUSH_PUBKEY=BNW_39YcKbV4KunFxFhvMW5JUs8AljfFnGUeZpaerO-gwCoWyQat5ol0xOGB8MLaqqCbz0iptd2Qv3SToSGynMk
#REACT_APP_NOAUTHD_URL=http://localhost:8000
REACT_APP_NOAUTHD_URL=https://noauthd.login.nostrapps.org
REACT_APP_DOMAIN=nsec.app
REACT_APP_NOAUTHD_URL=https://noauthd.nsec.app
REACT_APP_DOMAIN=nsec.app
REACT_APP_RELAY=wss://relay.nsec.app

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,76 +12,64 @@ import { useRef } from 'react'
import { useParams } from 'react-router-dom'
export const ModalConnectApp = () => {
const { getModalOpened, handleClose, handleOpen } = useModalSearchParams()
const timerRef = useRef<NodeJS.Timeout>()
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
const timerRef = useRef<NodeJS.Timeout>()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP)
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.CONNECT_APP, () => {
clearTimeout(timerRef.current)
})
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP)
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONNECT_APP, {
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 shareData = {
text: bunkerStr,
}
try {
if (navigator.share && navigator.canShare(shareData)) {
await navigator.share(shareData)
} else {
navigator.clipboard.writeText(bunkerStr)
}
} catch (err) {
console.log(err)
notify('Your browser does not support sharing data', 'warning')
}
}
const handleShareBunker = async () => {
const shareData = {
text: bunkerStr,
}
try {
if (navigator.share && navigator.canShare(shareData)) {
await navigator.share(shareData)
} else {
navigator.clipboard.writeText(bunkerStr)
}
} catch (err) {
console.log(err)
notify('Your browser does not support sharing data', 'warning')
}
}
const handleCopy = () => {
timerRef.current = setTimeout(() => {
handleCloseModal()
}, 3000)
}
const handleCopy = () => {
timerRef.current = setTimeout(() => {
handleCloseModal()
}, 3000)
}
return (
<Modal
open={isModalOpened}
title='Share your profile'
onClose={handleCloseModal}
>
<Stack gap={'1rem'} alignItems={'center'}>
<Typography variant='caption'>
Please, copy this code and paste it into the app to log in
</Typography>
<Input
sx={{
gap: '0.5rem',
}}
fullWidth
value={bunkerStr}
endAdornment={
<InputCopyButton
value={bunkerStr}
onCopy={handleCopy}
/>
}
/>
<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>
)
return (
<Modal open={isModalOpened} title="Share your profile" onClose={handleCloseModal}>
<Stack gap={'1rem'} alignItems={'center'}>
<Typography variant="caption">Please, copy this code and paste it into the app to log in</Typography>
<Input
sx={{
gap: '0.5rem',
}}
fullWidth
value={bunkerStr}
endAdornment={<InputCopyButton value={bunkerStr} onCopy={handleCopy} />}
/>
<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'
type ModalExplanationProps = {
explanationText?: string
explanationText?: string
}
export const ModalExplanation: FC<ModalExplanationProps> = ({
explanationText = '',
}) => {
const { getModalOpened } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EXPLANATION)
const [searchParams, setSearchParams] = useSearchParams()
export const ModalExplanation: FC<ModalExplanationProps> = ({ explanationText = '' }) => {
const { getModalOpened } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EXPLANATION)
const [searchParams, setSearchParams] = useSearchParams()
const handleCloseModal = () => {
searchParams.delete('type')
searchParams.delete(MODAL_PARAMS_KEYS.EXPLANATION)
setSearchParams(searchParams)
}
const handleCloseModal = () => {
searchParams.delete('type')
searchParams.delete(MODAL_PARAMS_KEYS.EXPLANATION)
setSearchParams(searchParams)
}
return (
<Modal
title='What is this?'
open={isModalOpened}
onClose={handleCloseModal}
PaperProps={{
sx: {
minHeight: '60%',
},
}}
>
<Stack height={'100%'}>
<Typography flex={1}>{explanationText}</Typography>
<Button fullWidth onClick={handleCloseModal}>
Got it!
</Button>
</Stack>
</Modal>
)
return (
<Modal
title="What is this?"
open={isModalOpened}
onClose={handleCloseModal}
PaperProps={{
sx: {
minHeight: '60%',
},
}}
>
<Stack height={'100%'}>
<Typography flex={1}>{explanationText}</Typography>
<Button fullWidth onClick={handleCloseModal}>
Got it!
</Button>
</Stack>
</Modal>
)
}

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { swicCall } from '@/modules/swic'
@ -6,7 +6,6 @@ import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { IconButton, Stack, Typography } from '@mui/material'
import { StyledAppLogo } from './styled'
import { nip19 } from 'nostr-tools'
import { Input } from '@/shared/Input/Input'
import { Button } from '@/shared/Button/Button'
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
@ -15,124 +14,114 @@ import { useNavigate } from 'react-router-dom'
import { useForm } from 'react-hook-form'
import { FormInputType, schema } from './const'
import { yupResolver } from '@hookform/resolvers/yup'
import { DOMAIN } from '@/utils/consts'
import { fetchNip05 } from '@/utils/helpers/helpers'
export const ModalLogin = () => {
const { getModalOpened, handleClose } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.LOGIN)
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.LOGIN)
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.LOGIN)
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN)
const notify = useEnqueueSnackbar()
const notify = useEnqueueSnackbar()
const navigate = useNavigate()
const navigate = useNavigate()
const {
handleSubmit,
reset,
register,
formState: { errors },
} = useForm<FormInputType>({
defaultValues: {
username: '',
password: '',
},
resolver: yupResolver(schema),
mode: 'onSubmit',
})
const {
handleSubmit,
reset,
register,
formState: { errors },
} = useForm<FormInputType>({
defaultValues: {
username: '',
password: '',
},
resolver: yupResolver(schema),
mode: 'onSubmit',
})
const [isPasswordShown, setIsPasswordShown] = useState(false)
const [isPasswordShown, setIsPasswordShown] = useState(false)
const handlePasswordTypeChange = () =>
setIsPasswordShown((prevState) => !prevState)
const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState)
const cleanUpStates = useCallback(() => {
setIsPasswordShown(false)
reset()
}, [reset])
const cleanUpStates = useCallback(() => {
setIsPasswordShown(false)
reset()
}, [reset])
const submitHandler = async (values: FormInputType) => {
try {
const [username, domain] = values.username.split('@')
const response = await fetch(
`https://${domain}/.well-known/nostr.json?name=${username}`,
)
const getNpub: {
names: {
[name: string]: string
}
} = await response.json()
const submitHandler = async (values: FormInputType) => {
try {
let npub = values.username
let name = ''
if (!npub.startsWith('npub1')) {
name = npub
if (!npub.includes('@')) {
npub += '@' + DOMAIN
} else {
const nameDomain = npub.split('@')
if (nameDomain[1] === DOMAIN) name = nameDomain[0]
}
}
if (npub.includes('@')) {
const npubNip05 = await fetchNip05(npub)
if (!npubNip05) throw new Error(`Username ${npub} not found`)
npub = npubNip05
}
const passphrase = values.password
const pubkey = getNpub.names[username]
const npub = nip19.npubEncode(pubkey)
const passphrase = values.password
console.log('fetch', npub, name)
const k: any = await swicCall('fetchKey', npub, passphrase, name)
notify(`Fetched ${k.npub}`, 'success')
cleanUpStates()
navigate(`/key/${k.npub}`)
} catch (error: any) {
console.log('error', error)
notify(error?.message || 'Something went wrong!', 'error')
}
}
console.log('fetch', npub, passphrase)
const k: any = await swicCall('fetchKey', npub, passphrase)
notify(`Fetched ${k.npub}`, 'success')
cleanUpStates()
navigate(`/key/${k.npub}`)
} catch (error: any) {
notify(error?.message || 'Something went wrong!', 'error')
}
}
useEffect(() => {
return () => {
if (isModalOpened) {
// modal closed
cleanUpStates()
}
}
}, [isModalOpened, cleanUpStates])
useEffect(() => {
return () => {
if (isModalOpened) {
// modal closed
cleanUpStates()
}
}
}, [isModalOpened, cleanUpStates])
return (
<Modal open={isModalOpened} onClose={handleCloseModal}>
<Stack
gap={'1rem'}
component={'form'}
onSubmit={handleSubmit(submitHandler)}
>
<Stack
direction={'row'}
gap={'1rem'}
alignItems={'center'}
alignSelf={'flex-start'}
>
<StyledAppLogo />
<Typography fontWeight={600} variant='h5'>
Login
</Typography>
</Stack>
<Input
label='Enter a Username'
fullWidth
placeholder='user@nsec.app'
{...register('username')}
error={!!errors.username}
/>
<Input
label='Password'
fullWidth
placeholder='Your password'
{...register('password')}
endAdornment={
<IconButton
size='small'
onClick={handlePasswordTypeChange}
>
{isPasswordShown ? (
<VisibilityOffOutlinedIcon />
) : (
<VisibilityOutlinedIcon />
)}
</IconButton>
}
type={isPasswordShown ? 'text' : 'password'}
error={!!errors.password}
/>
<Button type='submit' fullWidth>
Login
</Button>
</Stack>
</Modal>
)
return (
<Modal open={isModalOpened} onClose={handleCloseModal}>
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
<StyledAppLogo />
<Typography fontWeight={600} variant="h5">
Login
</Typography>
</Stack>
<Input
label="Username or nip05 or npub"
fullWidth
placeholder="name or name@domain.com or npub1..."
{...register('username')}
error={!!errors.username}
/>
<Input
label="Password"
fullWidth
placeholder="Your password"
{...register('password')}
endAdornment={
<IconButton size="small" onClick={handlePasswordTypeChange}>
{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'
export const schema = yup.object().shape({
username: yup
.string()
.test('Domain validation', 'The domain is required!', function (value) {
if (!value || !value.trim().length) return false
username: yup
.string()
.test('Domain validation', 'The domain is required!', function (value) {
if (!value || !value.trim().length) return false
const USERNAME_WITH_DOMAIN_REGEXP = new RegExp(
/^[\w-.]+@([\w-]+\.)+[\w-]{2,8}$/g,
)
return USERNAME_WITH_DOMAIN_REGEXP.test(value)
})
.required(),
password: yup.string().required().min(4),
const USERNAME_WITH_DOMAIN_REGEXP = new RegExp(/^[\w-.]+@([\w-]+\.)+[\w-]{2,8}$/g)
return USERNAME_WITH_DOMAIN_REGEXP.test(value)
})
.required(),
password: yup.string().required().min(4),
})
export type FormInputType = yup.InferType<typeof schema>

View File

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

View File

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

View File

@ -10,87 +10,91 @@ import { Button } from '@/shared/Button/Button'
import { CheckmarkIcon } from '@/assets'
import { swicCall } from '@/modules/swic'
import { useNavigate } from 'react-router-dom'
import { DOMAIN, NOAUTHD_URL } from '@/utils/consts'
import { fetchNip05 } from '@/utils/helpers/helpers'
export const ModalSignUp = () => {
const { getModalOpened, handleClose } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SIGN_UP)
const handleCloseModal = handleClose(MODAL_PARAMS_KEYS.SIGN_UP)
const notify = useEnqueueSnackbar()
const theme = useTheme()
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SIGN_UP)
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SIGN_UP)
const notify = useEnqueueSnackbar()
const theme = useTheme()
const navigate = useNavigate()
const navigate = useNavigate()
const [enteredValue, setEnteredValue] = useState('')
const [enteredValue, setEnteredValue] = useState('')
const [isAvailable, setIsAvailable] = useState(false)
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
setEnteredValue(e.target.value)
}
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
setEnteredValue(e.target.value)
const name = e.target.value.trim()
if (name) {
const npubNip05 = await fetchNip05(`${name}@${DOMAIN}`)
setIsAvailable(!npubNip05)
} else {
setIsAvailable(false)
}
}
const isAvailable = enteredValue.trim().length > 2
const inputHelperText = enteredValue ? (
isAvailable ? (
<>
<CheckmarkIcon /> Available
</>
) : (
<>Already taken</>
)
) : (
"Don't worry, username can be changed later."
)
const inputHelperText = isAvailable ? (
<>
<CheckmarkIcon /> Available
</>
) : (
"Don't worry, username can be changed later."
)
const handleSubmit = async (e: React.FormEvent) => {
const name = enteredValue.trim()
if (!name.length) return
e.preventDefault()
try {
const k: any = await swicCall('generateKey', name)
notify(`Account created for "${name}"`, 'success')
navigate(`/key/${k.npub}`)
} catch (error: any) {
notify(error.message, 'error')
}
}
const handleSubmit = async (e: React.FormEvent) => {
if (!enteredValue.trim().length) return
e.preventDefault()
try {
const k: any = await swicCall('generateKey')
notify(`New key ${k.npub}`, 'success')
navigate(`/key/${k.npub}`)
} catch (error: any) {
notify(error.message, 'error')
}
}
return (
<Modal open={isModalOpened} onClose={handleCloseModal}>
<Stack
paddingTop={'1rem'}
gap={'1rem'}
component={'form'}
onSubmit={handleSubmit}
>
<Stack
direction={'row'}
gap={'1rem'}
alignItems={'center'}
alignSelf={'flex-start'}
>
<StyledAppLogo />
<Typography fontWeight={600} variant='h5'>
Sign up
</Typography>
</Stack>
<Input
label='Enter a Username'
fullWidth
placeholder='Username'
helperText={inputHelperText}
endAdornment={
<Typography color={'#FFFFFFA8'}>@nsec.app</Typography>
}
onChange={handleInputChange}
value={enteredValue}
helperTextProps={{
sx: {
'&.helper_text': {
color: isAvailable
? theme.palette.success.main
: theme.palette.textSecondaryDecorate.main,
},
},
}}
/>
<Button fullWidth type='submit'>
Sign up
</Button>
</Stack>
</Modal>
)
return (
<Modal open={isModalOpened} onClose={handleCloseModal}>
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
<StyledAppLogo />
<Typography fontWeight={600} variant="h5">
Sign up
</Typography>
</Stack>
<Input
label="Enter a Username"
fullWidth
placeholder="Username"
helperText={inputHelperText}
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
onChange={handleInputChange}
value={enteredValue}
helperTextProps={{
sx: {
'&.helper_text': {
color:
enteredValue && isAvailable
? theme.palette.success.main
: enteredValue && !isAvailable
? 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'
export const StyledAppLogo = styled((props) => (
<Box {...props}>
<AppLogo />
</Box>
<Box {...props}>
<AppLogo />
</Box>
))({
background: '#00000054',
padding: '0.75rem',
borderRadius: '16px',
display: 'grid',
placeItems: 'center',
background: '#00000054',
padding: '0.75rem',
borderRadius: '16px',
display: 'grid',
placeItems: 'center',
})

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,33 +1,20 @@
import {
useSnackbar as useDefaultSnackbar,
OptionsObject,
VariantType,
} from 'notistack'
import { useSnackbar as useDefaultSnackbar, OptionsObject, VariantType } from 'notistack'
import { Notification } from '../components/Notification/Notification'
export const useEnqueueSnackbar = () => {
const { enqueueSnackbar } = useDefaultSnackbar()
const { enqueueSnackbar } = useDefaultSnackbar()
const showSnackbar = (
message: string,
variant: Exclude<VariantType, 'default' | 'info'> = 'success',
) => {
enqueueSnackbar(message, {
anchorOrigin: {
vertical: 'top',
horizontal: 'right',
},
content: (id) => {
return (
<Notification
id={id}
message={message}
alertvariant={variant}
/>
)
},
} as OptionsObject)
}
const showSnackbar = (message: string, variant: Exclude<VariantType, 'default' | 'info'> = 'success') => {
enqueueSnackbar(message, {
anchorOrigin: {
vertical: 'top',
horizontal: 'right',
},
content: (id) => {
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/
function useIsIOS() {
const [isIOS, setIsIOS] = useState(false)
const [isIOS, setIsIOS] = useState(false)
useEffect(() => {
const isIOSUserAgent =
iOSRegex.test(navigator.userAgent) ||
(navigator.userAgent.includes('Mac') && 'ontouchend' in document)
setIsIOS(isIOSUserAgent)
}, [])
useEffect(() => {
const isIOSUserAgent =
iOSRegex.test(navigator.userAgent) || (navigator.userAgent.includes('Mac') && 'ontouchend' in document)
setIsIOS(isIOSUserAgent)
}, [])
return isIOS
return isIOS
}
export default useIsIOS

View File

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

View File

@ -1,21 +1,21 @@
import React, { useState } from 'react'
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>) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
const handleOpen = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget)
}
const handleClose = () => {
setAnchorEl(null)
}
return {
open,
handleOpen,
handleClose,
anchorEl,
}
return {
open,
handleOpen,
handleClose,
anchorEl,
}
}

View File

@ -1,15 +1,15 @@
import { useCallback, useState } from 'react'
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 {
open: showConfirm,
handleShow,
handleClose,
}
return {
open: showConfirm,
handleShow,
handleClose,
}
}

View File

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

View File

@ -13,19 +13,19 @@ import { SnackbarProvider } from 'notistack'
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ThemeProvider>
<SnackbarProvider maxSnack={3} autoHideDuration={3000}>
<App />
</SnackbarProvider>
</ThemeProvider>
</PersistGate>
</Provider>
</BrowserRouter>
</React.StrictMode>,
<React.StrictMode>
<BrowserRouter>
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<ThemeProvider>
<SnackbarProvider maxSnack={3} autoHideDuration={3000}>
<App />
</SnackbarProvider>
</ThemeProvider>
</PersistGate>
</Provider>
</BrowserRouter>
</React.StrictMode>
)
// 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'
export const Header = () => {
const { npub = '' } = useParams<{ npub: string }>()
const [profile, setProfile] = useState<MetaEvent | null>(null)
const { npub = '' } = useParams<{ npub: string }>()
const [profile, setProfile] = useState<MetaEvent | null>(null)
const load = useCallback(async () => {
if (!npub) return setProfile(null)
const load = useCallback(async () => {
if (!npub) return setProfile(null)
try {
const response = await fetchProfile(npub)
setProfile(response as any)
} catch (e) {
return setProfile(null)
}
}, [npub])
try {
const response = await fetchProfile(npub)
setProfile(response as any)
} catch (e) {
return setProfile(null)
}
}, [npub])
useEffect(() => {
load()
}, [load])
useEffect(() => {
load()
}, [load])
const showProfile = Boolean(npub || profile)
const userName = profile?.info?.name || getShortenNpub(npub)
const userAvatar = profile?.info?.picture || ''
const showProfile = Boolean(npub || profile)
const userName = profile?.info?.name || getShortenNpub(npub)
const userAvatar = profile?.info?.picture || ''
return (
<StyledAppBar position='fixed'>
<Toolbar sx={{ padding: '12px' }}>
<Stack
direction={'row'}
justifyContent={'space-between'}
alignItems={'center'}
width={'100%'}
>
{showProfile ? (
<Stack
gap={'1rem'}
direction={'row'}
alignItems={'center'}
flex={1}
>
<Avatar src={userAvatar} alt={userName} />
<Typography fontWeight={600}>{userName}</Typography>
</Stack>
) : (
<StyledAppName>
<AppLogo />
<span>Nsec.app</span>
</StyledAppName>
)}
return (
<StyledAppBar position="fixed">
<Toolbar sx={{ padding: '12px' }}>
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'} width={'100%'}>
{showProfile ? (
<Stack gap={'1rem'} direction={'row'} alignItems={'center'} flex={1}>
<Avatar src={userAvatar} alt={userName} />
<Typography fontWeight={600}>{userName}</Typography>
</Stack>
) : (
<StyledAppName>
<AppLogo />
<span>Nsec.app</span>
</StyledAppName>
)}
{showProfile ? <ProfileMenu /> : <Menu />}
</Stack>
</Toolbar>
</StyledAppBar>
)
{showProfile ? <ProfileMenu /> : <Menu />}
</Stack>
</Toolbar>
</StyledAppBar>
)
}

View File

@ -1,48 +1,30 @@
import { DbKey } from '@/modules/db'
import { getShortenNpub } from '@/utils/helpers/helpers'
import {
Avatar,
ListItemIcon,
MenuItem,
Stack,
Typography,
} from '@mui/material'
import { Avatar, ListItemIcon, MenuItem, Stack, Typography } from '@mui/material'
import React, { FC } from 'react'
type ListProfilesProps = {
keys: DbKey[]
onClickItem: (key: DbKey) => void
keys: DbKey[]
onClickItem: (key: DbKey) => void
}
export const ListProfiles: FC<ListProfilesProps> = ({
keys = [],
onClickItem,
}) => {
return (
<Stack maxHeight={'10rem'} overflow={'auto'}>
{keys.map((key) => {
const userName =
key?.profile?.info?.name || getShortenNpub(key.npub)
const userAvatar = key?.profile?.info?.picture || ''
return (
<MenuItem
sx={{ gap: '0.5rem' }}
onClick={() => onClickItem(key)}
key={key.npub}
>
<ListItemIcon>
<Avatar
src={userAvatar}
alt={userName}
sx={{ width: 36, height: 36 }}
/>
</ListItemIcon>
<Typography variant='body2' noWrap>
{userName}
</Typography>
</MenuItem>
)
})}
</Stack>
)
export const ListProfiles: FC<ListProfilesProps> = ({ keys = [], onClickItem }) => {
return (
<Stack maxHeight={'10rem'} overflow={'auto'}>
{keys.map((key) => {
const userName = key?.profile?.info?.name || getShortenNpub(key.npub)
const userAvatar = key?.profile?.info?.picture || ''
return (
<MenuItem sx={{ gap: '0.5rem' }} onClick={() => onClickItem(key)} key={key.npub}>
<ListItemIcon>
<Avatar 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'
export const Menu = () => {
const themeMode = useAppSelector((state) => state.ui.themeMode)
const keys = useAppSelector(selectKeys)
const dispatch = useAppDispatch()
const { handleOpen } = useModalSearchParams()
const themeMode = useAppSelector((state) => state.ui.themeMode)
const keys = useAppSelector(selectKeys)
const dispatch = useAppDispatch()
const { handleOpen } = useModalSearchParams()
const isDarkMode = themeMode === 'dark'
const isNoKeys = !keys || keys.length === 0
const isDarkMode = themeMode === 'dark'
const isNoKeys = !keys || keys.length === 0
const {
anchorEl,
handleClose,
handleOpen: handleOpenMenu,
open,
} = useOpenMenu()
const { anchorEl, handleClose, handleOpen: handleOpenMenu, open } = useOpenMenu()
const handleChangeMode = () => {
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
}
const handleNavigateToAuth = () => {
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
handleClose()
}
const handleChangeMode = () => {
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
}
const handleNavigateToAuth = () => {
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
handleClose()
}
const themeIcon = isDarkMode ? (
<DarkModeIcon htmlColor='#fff' />
) : (
<LightModeIcon htmlColor='#feb94a' />
)
const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#feb94a" />
return (
<>
<MenuButton onClick={handleOpenMenu}>
<MenuRoundedIcon color='inherit' />
</MenuButton>
<MuiMenu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
sx={{
zIndex: 1302,
}}
>
<MenuItem
Icon={
isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />
}
onClick={handleNavigateToAuth}
title={isNoKeys ? 'Sign up' : 'Add account'}
/>
<MenuItem
Icon={themeIcon}
onClick={handleChangeMode}
title='Change theme'
/>
</MuiMenu>
</>
)
return (
<>
<MenuButton onClick={handleOpenMenu}>
<MenuRoundedIcon color="inherit" />
</MenuButton>
<MuiMenu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
sx={{
zIndex: 1302,
}}
>
<MenuItem
Icon={isNoKeys ? <LoginIcon /> : <PersonAddAltRoundedIcon />}
onClick={handleNavigateToAuth}
title={isNoKeys ? 'Sign up' : 'Add account'}
/>
<MenuItem Icon={themeIcon} onClick={handleChangeMode} title="Change theme" />
</MuiMenu>
</>
)
}

View File

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

View File

@ -18,86 +18,61 @@ import { ListProfiles } from './ListProfiles'
import { DbKey } from '@/modules/db'
export const ProfileMenu = () => {
const {
anchorEl,
handleOpen: handleOpenMenu,
open,
handleClose,
} = useOpenMenu()
const { handleOpen } = useModalSearchParams()
const { anchorEl, handleOpen: handleOpenMenu, open, handleClose } = useOpenMenu()
const { handleOpen } = useModalSearchParams()
const keys = useAppSelector(selectKeys)
const isNoKeys = !keys || keys.length === 0
const themeMode = useAppSelector((state) => state.ui.themeMode)
const isDarkMode = themeMode === 'dark'
const keys = useAppSelector(selectKeys)
const isNoKeys = !keys || keys.length === 0
const themeMode = useAppSelector((state) => state.ui.themeMode)
const isDarkMode = themeMode === 'dark'
const dispatch = useAppDispatch()
const navigate = useNavigate()
const dispatch = useAppDispatch()
const navigate = useNavigate()
const handleNavigateToAuth = () => {
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
handleClose()
}
const handleNavigateToAuth = () => {
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
handleClose()
}
const handleNavigateHome = () => {
navigate('/home')
handleClose()
}
const handleNavigateHome = () => {
navigate('/home')
handleClose()
}
const handleChangeMode = () => {
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
}
const handleChangeMode = () => {
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
}
const handleNavigateToKeyInnerPage = (key: DbKey) => {
navigate('/key/' + key.npub)
handleClose()
}
const handleNavigateToKeyInnerPage = (key: DbKey) => {
navigate('/key/' + key.npub)
handleClose()
}
const themeIcon = isDarkMode ? (
<DarkModeIcon htmlColor='#fff' />
) : (
<LightModeIcon htmlColor='#feb94a' />
)
const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#feb94a" />
return (
<>
<MenuButton onClick={handleOpenMenu}>
<KeyboardArrowDownRoundedIcon
color='inherit'
fontSize='large'
/>
</MenuButton>
<Menu
open={open}
anchorEl={anchorEl}
onClose={handleClose}
sx={{
zIndex: 1302,
}}
>
<ListProfiles
keys={keys}
onClickItem={handleNavigateToKeyInnerPage}
/>
<Divider />
<MenuItem
Icon={<HomeRoundedIcon />}
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>
</>
)
return (
<>
<MenuButton onClick={handleOpenMenu}>
<KeyboardArrowDownRoundedIcon color="inherit" fontSize="large" />
</MenuButton>
<Menu
open={open}
anchorEl={anchorEl}
onClose={handleClose}
sx={{
zIndex: 1302,
}}
>
<ListProfiles keys={keys} onClickItem={handleNavigateToKeyInnerPage} />
<Divider />
<MenuItem Icon={<HomeRoundedIcon />} 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 {
IconButton,
IconButtonProps,
MenuItem,
MenuItemProps,
styled,
} from '@mui/material'
import { IconButton, IconButtonProps, MenuItem, MenuItemProps, styled } from '@mui/material'
export const MenuButton = styled((props: IconButtonProps) => (
<IconButton {...props} />
))(({ theme }) => {
const isDark = theme.palette.mode === 'dark'
return {
borderRadius: '1rem',
background: isDark ? '#333333A8' : 'transparent',
color: isDark ? '#FFFFFFA8' : 'initial',
width: 42,
height: 42,
}
export const MenuButton = styled((props: IconButtonProps) => <IconButton {...props} />)(({ theme }) => {
const isDark = theme.palette.mode === 'dark'
return {
borderRadius: '1rem',
background: isDark ? '#333333A8' : 'transparent',
color: isDark ? '#FFFFFFA8' : 'initial',
width: 42,
height: 42,
}
})
export const StyledMenuItem = styled((props: MenuItemProps) => (
<MenuItem {...props} />
))(() => ({
padding: '0.5rem 1rem',
export const StyledMenuItem = styled((props: MenuItemProps) => <MenuItem {...props} />)(() => ({
padding: '0.5rem 1rem',
}))

View File

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

View File

@ -1,45 +1,37 @@
import { FC } from 'react'
import { Outlet } from 'react-router-dom'
import { Header } from './Header/Header'
import {
Container,
ContainerProps,
Divider,
DividerProps,
styled,
} from '@mui/material'
import { Container, ContainerProps, Divider, DividerProps, styled } from '@mui/material'
export const Layout: FC = () => {
return (
<StyledContainer maxWidth='md'>
<Header />
<StyledDivider />
<main>
<Outlet />
</main>
</StyledContainer>
)
return (
<StyledContainer maxWidth="md">
<Header />
<StyledDivider />
<main>
<Outlet />
</main>
</StyledContainer>
)
}
const StyledContainer = styled((props: ContainerProps) => (
<Container maxWidth='sm' {...props} />
))({
height: '100%',
display: 'flex',
flexDirection: 'column',
paddingBottom: '1rem',
position: 'relative',
'& > main': {
flex: 1,
maxHeight: '100%',
paddingTop: 'calc(66px + 1rem)',
},
const StyledContainer = styled((props: ContainerProps) => <Container maxWidth="sm" {...props} />)({
height: '100%',
display: 'flex',
flexDirection: 'column',
paddingBottom: '1rem',
position: 'relative',
'& > main': {
flex: 1,
maxHeight: '100%',
paddingTop: 'calc(66px + 1rem)',
},
})
const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({
position: 'absolute',
top: '66px',
width: '100%',
left: 0,
height: '2px',
position: 'absolute',
top: '66px',
width: '100%',
left: 0,
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'
export interface DbKey {
npub: string
nip05?: string
name?: string
avatar?: string
relays?: string[]
enckey: string
profile?: MetaEvent | null
npub: string
nip05?: string
name?: string
avatar?: string
relays?: string[]
enckey: string
profile?: MetaEvent | null
}
export interface DbApp {
appNpub: string
npub: string
name: string
icon: string
url: string
timestamp: number
appNpub: string
npub: string
name: string
icon: string
url: string
timestamp: number
}
export interface DbPerm {
id: string
npub: string
appNpub: string
perm: string
value: string
timestamp: number
id: string
npub: string
appNpub: string
perm: string
value: string
timestamp: number
}
export interface DbPending {
id: string
npub: string
appNpub: string
timestamp: number
method: string
params: string
id: string
npub: string
appNpub: string
timestamp: number
method: string
params: string
}
export interface DbHistory {
id: string
npub: string
appNpub: string
timestamp: number
method: string
params: string
allowed: boolean
id: string
npub: string
appNpub: string
timestamp: number
method: string
params: string
allowed: boolean
}
export interface DbSyncHistory {
npub: string
npub: string
}
export interface DbSchema extends Dexie {
keys: Dexie.Table<DbKey, string>
apps: Dexie.Table<DbApp, string>
perms: Dexie.Table<DbPerm, string>
pending: Dexie.Table<DbPending, string>
history: Dexie.Table<DbHistory, string>
syncHistory: Dexie.Table<DbSyncHistory, string>
keys: Dexie.Table<DbKey, string>
apps: Dexie.Table<DbApp, string>
perms: Dexie.Table<DbPerm, string>
pending: Dexie.Table<DbPending, string>
history: Dexie.Table<DbHistory, string>
syncHistory: Dexie.Table<DbSyncHistory, string>
}
export const db = new Dexie('noauthdb') as DbSchema
db.version(8).stores({
keys: 'npub',
apps: 'appNpub,npub,name,timestamp',
perms: 'id,npub,appNpub,perm,value,timestamp',
pending: 'id,npub,appNpub,timestamp,method',
history: 'id,npub,appNpub,timestamp,method,allowed',
requestHistory: 'id',
syncHistory: 'npub',
keys: 'npub',
apps: 'appNpub,npub,name,timestamp',
perms: 'id,npub,appNpub,perm,value,timestamp',
pending: 'id,npub,appNpub,timestamp,method',
history: 'id,npub,appNpub,timestamp,method,allowed',
requestHistory: 'id',
syncHistory: 'npub',
})
export const dbi = {
addKey: async (key: DbKey) => {
try {
await db.keys.add(key)
} catch (error) {
console.log(`db addKey error: ${error}`)
}
},
listKeys: async (): Promise<DbKey[]> => {
try {
return await db.keys.toArray()
} catch (error) {
console.log(`db listKeys error: ${error}`)
return []
}
},
getApp: async (appNpub: string) => {
try {
return await db.apps.get(appNpub)
} catch (error) {
console.log(`db getApp error: ${error}`)
}
},
addApp: async (app: DbApp) => {
try {
await db.apps.add(app)
} catch (error) {
console.log(`db addApp error: ${error}`)
}
},
listApps: async (): Promise<DbApp[]> => {
try {
return await db.apps.toArray()
} catch (error) {
console.log(`db listApps error: ${error}`)
return []
}
},
removeApp: async (appNpub: string) => {
try {
return await db.apps.delete(appNpub)
} catch (error) {
console.log(`db removeApp error: ${error}`)
}
},
addPerm: async (perm: DbPerm) => {
try {
await db.perms.add(perm)
} catch (error) {
console.log(`db addPerm error: ${error}`)
}
},
listPerms: async (): Promise<DbPerm[]> => {
try {
return await db.perms.toArray()
} catch (error) {
console.log(`db listPerms error: ${error}`)
return []
}
},
removePerm: async (id: string) => {
try {
return await db.perms.delete(id)
} catch (error) {
console.log(`db removePerm error: ${error}`)
}
},
removeAppPerms: async (appNpub: string) => {
try {
return await db.perms.where({ appNpub }).delete()
} catch (error) {
console.log(`db removeAppPerms error: ${error}`)
}
},
addPending: async (r: DbPending) => {
try {
return db.transaction('rw', db.pending, db.history, async () => {
const exists =
(await db.pending.where('id').equals(r.id).toArray())
.length > 0 ||
(await db.history.where('id').equals(r.id).toArray())
.length > 0
if (exists) return false
addKey: async (key: DbKey) => {
try {
await db.keys.add(key)
} catch (error) {
console.log(`db addKey error: ${error}`)
}
},
listKeys: async (): Promise<DbKey[]> => {
try {
return await db.keys.toArray()
} catch (error) {
console.log(`db listKeys error: ${error}`)
return []
}
},
getApp: async (appNpub: string) => {
try {
return await db.apps.get(appNpub)
} catch (error) {
console.log(`db getApp error: ${error}`)
}
},
addApp: async (app: DbApp) => {
try {
await db.apps.add(app)
} catch (error) {
console.log(`db addApp error: ${error}`)
}
},
listApps: async (): Promise<DbApp[]> => {
try {
return await db.apps.toArray()
} catch (error) {
console.log(`db listApps error: ${error}`)
return []
}
},
removeApp: async (appNpub: string) => {
try {
return await db.apps.delete(appNpub)
} catch (error) {
console.log(`db removeApp error: ${error}`)
}
},
addPerm: async (perm: DbPerm) => {
try {
await db.perms.add(perm)
} catch (error) {
console.log(`db addPerm error: ${error}`)
}
},
listPerms: async (): Promise<DbPerm[]> => {
try {
return await db.perms.toArray()
} catch (error) {
console.log(`db listPerms error: ${error}`)
return []
}
},
removePerm: async (id: string) => {
try {
return await db.perms.delete(id)
} catch (error) {
console.log(`db removePerm error: ${error}`)
}
},
removeAppPerms: async (appNpub: string) => {
try {
return await db.perms.where({ appNpub }).delete()
} catch (error) {
console.log(`db removeAppPerms error: ${error}`)
}
},
addPending: async (r: DbPending) => {
try {
return db.transaction('rw', db.pending, db.history, async () => {
const exists =
(await db.pending.where('id').equals(r.id).toArray()).length > 0 ||
(await db.history.where('id').equals(r.id).toArray()).length > 0
if (exists) return false
await db.pending.add(r)
return true
})
} catch (error) {
console.log(`db addPending error: ${error}`)
return false
}
},
removePending: async (id: string) => {
try {
return await db.pending.delete(id)
} catch (error) {
console.log(`db removePending error: ${error}`)
}
},
listPending: async (): Promise<DbPending[]> => {
try {
return await db.pending.toArray()
} catch (error) {
console.log(`db listPending error: ${error}`)
return []
}
},
confirmPending: async (id: string, allowed: boolean) => {
try {
db.transaction('rw', db.pending, db.history, async () => {
const r: DbPending | undefined = await db.pending
.where('id')
.equals(id)
.first()
if (!r) throw new Error('Pending not found ' + id)
const h: DbHistory = {
...r,
allowed,
}
await db.pending.delete(id)
await db.history.add(h)
})
} catch (error) {
console.log(`db addPending error: ${error}`)
}
},
addConfirmed: async (r: DbHistory) => {
try {
await db.history.add(r)
} catch (error) {
console.log(`db addConfirmed error: ${error}`)
return false
}
},
addSynced: async (npub: string) => {
try {
await db.syncHistory.add({ npub })
} catch (error) {
console.log(`db addSynced error: ${error}`)
return false
}
},
await db.pending.add(r)
return true
})
} catch (error) {
console.log(`db addPending error: ${error}`)
return false
}
},
removePending: async (id: string) => {
try {
return await db.pending.delete(id)
} catch (error) {
console.log(`db removePending error: ${error}`)
}
},
listPending: async (): Promise<DbPending[]> => {
try {
return await db.pending.toArray()
} catch (error) {
console.log(`db listPending error: ${error}`)
return []
}
},
confirmPending: async (id: string, allowed: boolean) => {
try {
db.transaction('rw', db.pending, db.history, async () => {
const r: DbPending | undefined = await db.pending.where('id').equals(id).first()
if (!r) throw new Error('Pending not found ' + id)
const h: DbHistory = {
...r,
allowed,
}
await db.pending.delete(id)
await db.history.add(h)
})
} catch (error) {
console.log(`db addPending error: ${error}`)
}
},
addConfirmed: async (r: DbHistory) => {
try {
await db.history.add(r)
} catch (error) {
console.log(`db addConfirmed error: ${error}`)
return false
}
},
addSynced: async (npub: string) => {
try {
await db.syncHistory.add({ npub })
} 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 * as secp from "./vendor/secp256k1.js";
export async function encrypt(
publicKey: string,
message: string,
privateKey: string,
): Promise<string> {
const key = secp.getSharedSecret(privateKey, "02" + publicKey);
const normalizedKey = getNormalizedX(key);
const encoder = new TextEncoder();
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,
);
export async function encrypt(publicKey: string, message: string, privateKey: string): Promise<string> {
const key = secp.getSharedSecret(privateKey, '02' + publicKey)
const normalizedKey = getNormalizedX(key)
const encoder = new TextEncoder()
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 ivb64 = toBase64(new Uint8Array(iv.buffer));
return `${ctb64}?iv=${ivb64}`;
const ctb64 = toBase64(new Uint8Array(ciphertext))
const ivb64 = toBase64(new Uint8Array(iv.buffer))
return `${ctb64}?iv=${ivb64}`
}
export async function decrypt(
privateKey: string,
publicKey: string,
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(privateKey: string, publicKey: string, 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(
data: string,
sharedSecret: Uint8Array,
): Promise<string | Error> {
const [ctb64, ivb64] = data.split("?iv=");
const normalizedKey = getNormalizedX(sharedSecret);
export async function decrypt_with_shared_secret(data: string, sharedSecret: Uint8Array): Promise<string | Error> {
const [ctb64, ivb64] = data.split('?iv=')
const normalizedKey = getNormalizedX(sharedSecret)
const cryptoKey = await crypto.subtle.importKey(
"raw",
normalizedKey,
{ name: "AES-CBC" },
false,
["decrypt"],
);
let ciphertext: BufferSource;
let iv: BufferSource;
try {
ciphertext = decodeBase64(ctb64);
iv = decodeBase64(ivb64);
} catch (e) {
return new Error(`failed to decode, ${e}`);
}
const cryptoKey = await crypto.subtle.importKey('raw', normalizedKey, { name: 'AES-CBC' }, false, ['decrypt'])
let ciphertext: BufferSource
let iv: BufferSource
try {
ciphertext = decodeBase64(ctb64)
iv = decodeBase64(ivb64)
} catch (e) {
return new Error(`failed to decode, ${e}`)
}
try {
const plaintext = await crypto.subtle.decrypt(
{ name: "AES-CBC", iv },
cryptoKey,
ciphertext,
);
const text = utf8Decode(plaintext);
return text;
} catch (e) {
return new Error(`failed to decrypt, ${e}`);
}
try {
const plaintext = await crypto.subtle.decrypt({ name: 'AES-CBC', iv }, cryptoKey, ciphertext)
const text = utf8Decode(plaintext)
return text
} catch (e) {
return new Error(`failed to decrypt, ${e}`)
}
}
export function utf8Encode(str: string) {
let encoder = new TextEncoder();
return encoder.encode(str);
let encoder = new TextEncoder()
return encoder.encode(str)
}
export function utf8Decode(bin: Uint8Array | ArrayBuffer): string {
let decoder = new TextDecoder();
return decoder.decode(bin);
let decoder = new TextDecoder()
return decoder.decode(bin)
}
function toBase64(uInt8Array: Uint8Array) {
let strChunks = new Array(uInt8Array.length);
let i = 0;
// @ts-ignore
for (let byte of uInt8Array) {
strChunks[i] = String.fromCharCode(byte); // bytes to utf16 string
i++;
}
return btoa(strChunks.join(""));
let strChunks = new Array(uInt8Array.length)
let i = 0
// @ts-ignore
for (let byte of uInt8Array) {
strChunks[i] = String.fromCharCode(byte) // bytes to utf16 string
i++
}
return btoa(strChunks.join(''))
}
function decodeBase64(base64String: string) {
const binaryString = atob(base64String);
const length = binaryString.length;
const bytes = new Uint8Array(length);
const binaryString = atob(base64String)
const length = binaryString.length
const bytes = new Uint8Array(length)
for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes;
for (let i = 0; i < length; i++) {
bytes[i] = binaryString.charCodeAt(i)
}
return bytes
}
function getNormalizedX(key: Uint8Array): Uint8Array {
return key.slice(1, 33);
return key.slice(1, 33)
}
function randomBytes(bytesLength: number = 32) {
return crypto.getRandomValues(new Uint8Array(bytesLength));
return crypto.getRandomValues(new Uint8Array(bytesLength))
}
export function utf16Encode(str: string): number[] {
let array = new Array(str.length);
for (let i = 0; i < str.length; i++) {
array[i] = str.charCodeAt(i);
}
return array;
}
let array = new Array(str.length)
for (let i = 0; i < str.length; i++) {
array[i] = str.charCodeAt(i)
}
return array
}

View File

@ -1,5 +1,5 @@
import crypto, { pbkdf2 } from 'crypto';
import { getPublicKey, nip19 } from 'nostr-tools';
import crypto, { pbkdf2 } from 'crypto'
import { getPublicKey, nip19 } from 'nostr-tools'
// encrypted keys have a prefix and version
// 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_ALGO = 'sha256'
// encryption
const ALGO = 'aes-256-cbc';
const ALGO = 'aes-256-cbc'
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 ALGO_LOCAL = 'AES-CBC';
const KEY_SIZE_LOCAL = 256;
const ALGO_LOCAL = 'AES-CBC'
const KEY_SIZE_LOCAL = 256
export class Keys {
subtle: any
@ -37,9 +37,7 @@ export class Keys {
return ASCII_REGEX.test(passphrase)
}
public async generatePassKey(pubkey: string, passphrase: string)
: Promise<{ passkey: Buffer, pwh: string }> {
public async generatePassKey(pubkey: string, passphrase: string): Promise<{ passkey: Buffer; pwh: string }> {
const salt = Buffer.from(pubkey, 'hex')
// 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
// 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
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) => {
// NOTE: we should use Argon2 or scrypt later, for now
@ -57,7 +55,11 @@ export class Keys {
else {
pbkdf2(key, passphrase, ITERATIONS_PWH, HASH_SIZE, HASH_ALGO, (err, hash) => {
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() {
const chrome = navigator.userAgent.indexOf("Chrome") > -1;
const safari = navigator.userAgent.indexOf("Safari") > -1;
const chrome = navigator.userAgent.indexOf('Chrome') > -1
const safari = navigator.userAgent.indexOf('Safari') > -1
return safari && !chrome
}
@ -81,8 +83,8 @@ export class Keys {
{ name: ALGO_LOCAL, length: KEY_SIZE_LOCAL },
// NOTE: important to make sure it's not visible in
// dev console in IndexedDB
/*extractable*/false,
["encrypt", "decrypt"]
/*extractable*/ false,
['encrypt', 'decrypt']
)
}
@ -94,25 +96,30 @@ export class Keys {
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
const parts = enckey.split(':')
if (parts.length !== 4) throw new Error("Bad encrypted key")
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[2].length !== IV_SIZE * 2) throw new Error("Bad encrypted key iv")
if (parts[3].length < 30) throw new Error("Bad encrypted key data")
const iv = Buffer.from(parts[2], 'hex');
const data = Buffer.from(parts[3], 'hex');
if (parts.length !== 4) throw new Error('Bad encrypted key')
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[2].length !== IV_SIZE * 2) throw new Error('Bad encrypted key iv')
if (parts[3].length < 30) throw new Error('Bad encrypted key data')
const iv = Buffer.from(parts[2], 'hex')
const data = Buffer.from(parts[3], 'hex')
const decrypted = await this.subtle.decrypt({ name: ALGO_LOCAL, iv }, localKey, data)
const { type, data: value } = nip19.decode(Buffer.from(decrypted).toString())
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")
return (value as string)
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')
return value as string
}
public async encryptKeyPass({ key, passphrase }: { key: string, passphrase: string })
: Promise<{ enckey: string, pwh: string }> {
public async encryptKeyPass({
key,
passphrase,
}: {
key: string
passphrase: string
}): Promise<{ enckey: string; pwh: string }> {
const start = Date.now()
const nsec = nip19.nsecEncode(key)
const pubkey = getPublicKey(key)
@ -120,21 +127,29 @@ export class Keys {
const iv = crypto.randomBytes(IV_SIZE)
const cipher = crypto.createCipheriv(ALGO, passkey, iv)
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 {
enckey: `${PREFIX}:${VERSION}:${iv.toString('hex')}:${encrypted.toString('hex')}}`,
pwh
enckey: `${PREFIX}:${VERSION}:${iv.toString('hex')}:${encrypted.toString('hex')}`,
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 parts = enckey.split(':')
if (parts.length !== 4) throw new Error("Bad encrypted key")
if (parts[0] !== PREFIX) throw new Error("Bad encrypted key prefix")
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[3].length < 30) throw new Error("Bad encrypted key data")
if (parts.length !== 4) throw new Error('Bad encrypted key')
if (parts[0] !== PREFIX) throw new Error('Bad encrypted key prefix')
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[3].length < 30) throw new Error('Bad encrypted key data')
const { passkey } = await this.generatePassKey(pubkey, passphrase)
const iv = Buffer.from(parts[2], '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 nsec = decrypted.toString()
const { type, data: value } = nip19.decode(nsec)
if (type !== "nsec") throw new Error("Bad encrypted key payload type")
if (value.length !== 64) throw new Error("Bad encrypted key payload length")
console.log("decrypted key in ", Date.now() - start)
return nsec;
if (type !== 'nsec') throw new Error('Bad encrypted key payload type')
if (value.length !== 64) throw new Error('Bad encrypted key payload length')
console.log('decrypted key in ', Date.now() - start)
return nsec
}
}
}

View File

@ -7,25 +7,25 @@ export const utf8Decoder = new TextDecoder('utf-8')
export const utf8Encoder = new TextEncoder()
function toBase64(uInt8Array: Uint8Array) {
let strChunks = new Array(uInt8Array.length);
let i = 0;
let strChunks = new Array(uInt8Array.length)
let i = 0
// @ts-ignore
for (let byte of uInt8Array) {
strChunks[i] = String.fromCharCode(byte); // bytes to utf16 string
i++;
strChunks[i] = String.fromCharCode(byte) // bytes to utf16 string
i++
}
return btoa(strChunks.join(""));
return btoa(strChunks.join(''))
}
function fromBase64(base64String: string) {
const binaryString = atob(base64String);
const length = binaryString.length;
const bytes = new Uint8Array(length);
const binaryString = atob(base64String)
const length = binaryString.length
const bytes = new Uint8Array(length)
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 {
@ -65,7 +65,7 @@ export class Nip04 {
// let ctb64 = toBase64(new Uint8Array(ciphertext))
// 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}`
}
@ -85,7 +85,4 @@ export class Nip04 {
let text = utf8Decoder.decode(plaintext)
return text
}
}

View File

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

51
src/modules/pow.ts Normal file
View File

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

View File

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

View File

@ -7,64 +7,72 @@ let nextReqId = 1
let onRender: (() => void) | null = null
export async function swicRegister() {
serviceWorkerRegistration.register({
onSuccess(registration) {
console.log('sw registered')
swr = registration
},
onError(e) {
console.log(`error ${e}`)
},
})
serviceWorkerRegistration.register({
onSuccess(registration) {
console.log('sw registered')
swr = registration
},
onError(e) {
console.log(`error ${e}`)
},
})
navigator.serviceWorker.ready.then((r) => (swr = r))
navigator.serviceWorker.ready.then((r) => {
console.log('sw ready')
swr = r
if (navigator.serviceWorker.controller) {
console.log(`This page is currently controlled by: ${navigator.serviceWorker.controller}`)
} else {
console.log('This page is not currently controlled by a service worker.')
}
})
navigator.serviceWorker.addEventListener('message', (event) => {
onMessage((event as MessageEvent).data)
})
navigator.serviceWorker.addEventListener('message', (event) => {
onMessage((event as MessageEvent).data)
})
}
function onMessage(data: any) {
const { id, result, error } = data
console.log('SW message', id, result, error)
const { id, result, error } = data
console.log('SW message', id, result, error)
if (!id) {
if (onRender) onRender()
return
}
if (!id) {
if (onRender) onRender()
return
}
const r = reqs.get(id)
if (!r) {
console.log('Unexpected message from service worker', data)
return
}
const r = reqs.get(id)
if (!r) {
console.log('Unexpected message from service worker', data)
return
}
reqs.delete(id)
if (error) r.rej(error)
else r.ok(result)
reqs.delete(id)
if (error) r.rej(error)
else r.ok(result)
}
export async function swicCall(method: string, ...args: any[]) {
const id = nextReqId
nextReqId++
const id = nextReqId
nextReqId++
return new Promise((ok, rej) => {
if (!swr || !swr.active) {
rej(new Error('No active service worker'))
return
}
return new Promise((ok, rej) => {
if (!swr || !swr.active) {
rej(new Error('No active service worker'))
return
}
reqs.set(id, { ok, rej })
const msg = {
id,
method,
args: [...args],
}
console.log('sending to SW', msg)
swr.active.postMessage(msg)
})
reqs.set(id, { ok, rej })
const msg = {
id,
method,
args: [...args],
}
console.log('sending to SW', msg)
swr.active.postMessage(msg)
})
}
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'
const ThemeProvider: FC<PropsWithChildren> = ({ children }) => {
const themeMode = useAppSelector((state) => state.ui.themeMode)
const isDarkMode = themeMode === 'dark'
const themeMode = useAppSelector((state) => state.ui.themeMode)
const isDarkMode = themeMode === 'dark'
return (
<ThemeMuiProvider theme={isDarkMode ? darkTheme : lightTheme}>
<CssBaseline />
{children}
</ThemeMuiProvider>
)
return (
<ThemeMuiProvider theme={isDarkMode ? darkTheme : lightTheme}>
<CssBaseline />
{children}
</ThemeMuiProvider>
)
}
export default ThemeProvider

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,77 +18,72 @@ import UserValueSection from './components/UserValueSection'
import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
import { useLiveQuery } from 'dexie-react-hooks'
import { checkNpubSyncQuerier } from './utils'
import { DOMAIN } from '@/utils/consts'
const KeyPage = () => {
const { npub = '' } = useParams<{ npub: string }>()
const { apps, pending, perms } = useAppSelector((state) => state.content)
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
const { npub = '' } = useParams<{ npub: string }>()
const { keys, apps, pending, perms } = useAppSelector((state) => state.content)
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
const { handleOpen } = useModalSearchParams()
const { handleOpen } = useModalSearchParams()
const { userNameWithPrefix } = useProfile(npub)
const { handleEnableBackground, showWarning, isEnabling } =
useBackgroundSigning()
// const { userNameWithPrefix } = useProfile(npub)
const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning()
const filteredApps = apps.filter((a) => a.npub === npub)
const { prepareEventPendings } = useTriggerConfirmModal(
npub,
pending,
perms,
)
const key = keys.find((k) => k.npub === npub)
let username = ''
if (key?.name) {
if (key.name.includes('@')) username = key.name
else username = `${key?.name}@${DOMAIN}`
}
const handleOpenConnectAppModal = () =>
handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
const filteredApps = apps.filter((a) => a.npub === npub)
const { prepareEventPendings } = useTriggerConfirmModal(npub, pending, perms)
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
return (
<>
<Stack gap={'1rem'} height={'100%'}>
{showWarning && (
<BackgroundSigningWarning
isEnabling={isEnabling}
onEnableBackSigning={handleEnableBackground}
/>
)}
<UserValueSection
title='Your login'
value={userNameWithPrefix}
copyValue={npub + '@nsec.app'}
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
/>
<UserValueSection
title='Your NPUB'
value={npub}
copyValue={npub}
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
/>
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
<Stack direction={'row'} gap={'0.75rem'}>
<StyledIconButton onClick={handleOpenConnectAppModal}>
<ShareIcon />
Connect app
</StyledIconButton>
return (
<>
<Stack gap={'1rem'} height={'100%'}>
{showWarning && (
<BackgroundSigningWarning isEnabling={isEnabling} onEnableBackSigning={handleEnableBackground} />
)}
<UserValueSection
title="Your login"
value={username}
copyValue={username}
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
/>
<UserValueSection
title="Your NPUB"
value={npub}
copyValue={npub}
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
/>
<StyledIconButton
bgcolor_variant='secondary'
onClick={handleOpenSettingsModal}
withBadge={!isSynced}
>
<SettingsIcon />
Settings
</StyledIconButton>
</Stack>
<Stack direction={'row'} gap={'0.75rem'}>
<StyledIconButton onClick={handleOpenConnectAppModal}>
<ShareIcon />
Connect app
</StyledIconButton>
<Apps apps={filteredApps} npub={npub} />
</Stack>
<ModalConnectApp />
<ModalSettings isSynced={isSynced} />
<ModalExplanation />
<ModalConfirmConnect />
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
</>
)
<StyledIconButton bgcolor_variant="secondary" onClick={handleOpenSettingsModal} withBadge={!isSynced}>
<SettingsIcon />
Settings
</StyledIconButton>
</Stack>
<Apps apps={filteredApps} npub={npub} />
</Stack>
<ModalConnectApp />
<ModalSettings isSynced={isSynced} />
<ModalExplanation />
<ModalConfirmConnect />
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
</>
)
}
export default KeyPage

View File

@ -11,57 +11,41 @@ import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { ItemApp } from './ItemApp'
type AppsProps = {
apps: DbApp[]
npub: string
apps: DbApp[]
npub: string
}
export const Apps: FC<AppsProps> = ({ apps = [], npub = '' }) => {
const notify = useEnqueueSnackbar()
const notify = useEnqueueSnackbar()
// eslint-disable-next-line
async function deletePerm(id: string) {
call(async () => {
await swicCall('deletePerm', id)
notify('Perm deleted!', 'success')
})
}
// eslint-disable-next-line
async function deletePerm(id: string) {
call(async () => {
await swicCall('deletePerm', id)
notify('Perm deleted!', 'success')
})
}
return (
<Box
flex={1}
marginBottom={'1rem'}
display={'flex'}
flexDirection={'column'}
overflow={'auto'}
>
<Stack
direction={'row'}
alignItems={'center'}
justifyContent={'space-between'}
marginBottom={'0.5rem'}
>
<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>
)}
return (
<Box flex={1} marginBottom={'1rem'} display={'flex'} flexDirection={'column'} overflow={'auto'}>
<Stack direction={'row'} alignItems={'center'} justifyContent={'space-between'} marginBottom={'0.5rem'}>
<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}>
{apps.map((a) => (
<ItemApp {...a} key={a.appNpub} />
))}
</Stack>
</Box>
)
<Stack gap={'0.5rem'} overflow={'auto'} flex={1}>
{apps.map((a) => (
<ItemApp {...a} key={a.appNpub} />
))}
</Stack>
</Box>
)
}

View File

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

View File

@ -9,35 +9,25 @@ import { StyledItemAppContainer } from './styled'
type ItemAppProps = DbApp
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => {
const appName = name || getShortenNpub(appNpub)
return (
<StyledItemAppContainer
direction={'row'}
alignItems={'center'}
gap={'0.5rem'}
padding={'0.5rem 0'}
component={Link}
to={`/key/${npub}/app/${appNpub}`}
>
<Avatar
variant='square'
sx={{ width: 56, height: 56 }}
src={icon}
alt={name}
/>
<Stack>
<Typography noWrap display={'block'} variant='body2'>
{appName}
</Typography>
<Typography
noWrap
display={'block'}
variant='caption'
color={'GrayText'}
>
Basic actions
</Typography>
</Stack>
</StyledItemAppContainer>
)
const appName = name || getShortenNpub(appNpub)
return (
<StyledItemAppContainer
direction={'row'}
alignItems={'center'}
gap={'0.5rem'}
padding={'0.5rem 0'}
component={Link}
to={`/key/${npub}/app/${appNpub}`}
>
<Avatar variant="square" sx={{ width: 56, height: 56 }} src={icon} alt={name} />
<Stack>
<Typography noWrap display={'block'} variant="body2">
{appName}
</Typography>
<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'
type UserValueSectionProps = {
title: string
value: string
explanationType: EXPLANATION_MODAL_KEYS
copyValue: string
title: string
value: string
explanationType: EXPLANATION_MODAL_KEYS
copyValue: string
}
const UserValueSection: FC<UserValueSectionProps> = ({
title,
value,
explanationType,
copyValue,
}) => {
const { handleOpen } = useModalSearchParams()
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, copyValue }) => {
const { handleOpen } = useModalSearchParams()
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
handleOpen(MODAL_PARAMS_KEYS.EXPLANATION, {
search: {
type,
},
})
}
return (
<Box>
<Stack
direction={'row'}
alignItems={'center'}
justifyContent={'space-between'}
marginBottom={'0.5rem'}
>
<SectionTitle>{title}</SectionTitle>
<AppLink
title='What is this?'
onClick={() => handleOpenExplanationModal(explanationType)}
/>
</Stack>
<StyledInput
value={value}
readOnly
endAdornment={<InputCopyButton value={copyValue} />}
/>
</Box>
)
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
handleOpen(MODAL_PARAMS_KEYS.EXPLANATION, {
search: {
type,
},
})
}
return (
<Box>
<Stack direction={'row'} alignItems={'center'} justifyContent={'space-between'} marginBottom={'0.5rem'}>
<SectionTitle>{title}</SectionTitle>
<AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} />
</Stack>
<StyledInput value={value} readOnly endAdornment={<InputCopyButton value={copyValue} />} />
</Box>
)
}
export default UserValueSection

View File

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

View File

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

View File

@ -2,28 +2,30 @@ import { useCallback, useEffect, useState } from 'react'
import { fetchProfile } from '@/modules/nostr'
import { MetaEvent } from '@/types/meta-event'
import { getProfileUsername } from '@/utils/helpers/helpers'
import { DOMAIN } from '@/utils/consts'
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 userNameWithPrefix = userName + '@nsec.app'
const userName = getProfileUsername(profile, npub)
// FIXME use nip05?
const userNameWithPrefix = userName + '@' + DOMAIN
const loadProfile = useCallback(async () => {
try {
const response = await fetchProfile(npub)
setProfile(response)
} catch (error) {
console.error('Failed to fetch profile:', error)
}
}, [npub])
const loadProfile = useCallback(async () => {
try {
const response = await fetchProfile(npub)
setProfile(response)
} catch (error) {
console.error('Failed to fetch profile:', error)
}
}, [npub])
useEffect(() => {
loadProfile()
}, [loadProfile])
useEffect(() => {
loadProfile()
}, [loadProfile])
return {
profile,
userNameWithPrefix,
}
return {
profile,
userNameWithPrefix,
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,15 @@
import { ReportHandler } from 'web-vitals';
import { ReportHandler } from 'web-vitals'
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
getCLS(onPerfEntry)
getFID(onPerfEntry)
getFCP(onPerfEntry)
getLCP(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 LoadingSpinner = () => (
<Stack height={'100%'} justifyContent={'center'} alignItems={'center'}>
<CircularProgress />
</Stack>
<Stack height={'100%'} justifyContent={'center'} alignItems={'center'}>
<CircularProgress />
</Stack>
)
const AppRoutes = () => {
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path='/' element={<Layout />}>
<Route path='/' element={<Navigate to={'/home'} />} />
{/* <Route path='/welcome' element={<WelcomePage />} /> */}
<Route path='/home' element={<HomePage />} />
<Route path='/key/:npub' element={<KeyPage />} />
<Route
path='/key/:npub/app/:appNpub'
element={<AppPage />}
/>
<Route
path='/key/:npub/:req_id'
element={<ConfirmPage />}
/>
</Route>
<Route path='*' element={<Navigate to={'/home'} />} />
</Routes>
</Suspense>
)
return (
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Layout />}>
<Route path="/" element={<Navigate to={'/home'} />} />
{/* <Route path='/welcome' element={<WelcomePage />} /> */}
<Route path="/home" element={<HomePage />} />
<Route path="/key/:npub" element={<KeyPage />} />
<Route path="/key/:npub/app/:appNpub" element={<AppPage />} />
<Route path="/key/:npub/:req_id" element={<ConfirmPage />} />
</Route>
<Route path="*" element={<Navigate to={'/home'} />} />
</Routes>
</Suspense>
)
}
export default AppRoutes

View File

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

View File

@ -16,34 +16,34 @@ const isLocalhost = Boolean(
window.location.hostname === '[::1]' ||
// 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}$/)
);
)
type Config = {
onSuccess?: (registration: ServiceWorkerRegistration) => void;
onUpdate?: (registration: ServiceWorkerRegistration) => void;
onError?: (e: any) => void;
};
onSuccess?: (registration: ServiceWorkerRegistration) => void
onUpdate?: (registration: ServiceWorkerRegistration) => void
onError?: (e: any) => void
}
export function register(config?: Config) {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// 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) {
// 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
// serve assets; see https://github.com/facebook/create-react-app/issues/2374
if (config && config.onError) {
config.onError(new Error("Wrong origin"));
config.onError(new Error('Wrong origin'))
}
return;
return
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`
if (isLocalhost) {
// 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
// service worker/PWA documentation.
@ -51,16 +51,16 @@ export function register(config?: Config) {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://cra.link/PWA'
);
});
)
})
} else {
// Is not localhost. Just register service worker
registerValidSW(swUrl, config);
registerValidSW(swUrl, config)
}
});
})
} else {
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)
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
const installingWorker = registration.installing
if (installingWorker == null) {
return;
return
}
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
@ -83,33 +83,33 @@ function registerValidSW(swUrl: string, config?: Config) {
console.log(
'New content is available and will be used when all ' +
'tabs for this page are closed. See https://cra.link/PWA.'
);
)
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
config.onUpdate(registration)
}
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "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
if (config && config.onSuccess) {
config.onSuccess(registration);
config.onSuccess(registration)
}
}
}
};
};
}
}
})
.catch((error) => {
console.error('Error during service worker registration:', error);
console.error('Error during service worker registration:', error)
if (config && config.onError) {
config.onError(new Error(`Install error: ${error}`));
config.onError(new Error(`Install error: ${error}`))
}
});
})
}
function checkValidServiceWorker(swUrl: string, config?: Config) {
@ -119,35 +119,32 @@ function checkValidServiceWorker(swUrl: string, config?: Config) {
})
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get('content-type');
if (
response.status === 404 ||
(contentType != null && contentType.indexOf('javascript') === -1)
) {
const contentType = response.headers.get('content-type')
if (response.status === 404 || (contentType != null && contentType.indexOf('javascript') === -1)) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
window.location.reload()
})
})
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl, config);
registerValidSW(swUrl, config)
}
})
.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() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready
.then((registration) => {
registration.unregister();
registration.unregister()
})
.catch((error) => {
console.error(error.message);
});
console.error(error.message)
})
}
}

View File

@ -2,4 +2,4 @@
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// 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'
type AppLinkProps = {
title: string
title: string
} & TypographyProps
export const AppLink: FC<AppLinkProps> = ({ title = '', ...rest }) => {
return <StyledTypography {...rest}>{title}</StyledTypography>
return <StyledTypography {...rest}>{title}</StyledTypography>
}
const StyledTypography = styled((props: TypographyProps) => (
<Typography {...props} variant='caption' />
))(({ theme }) => {
return {
color: theme.palette.textSecondaryDecorate.main,
cursor: 'pointer',
'&:active': {
textDecoration: 'underline',
},
}
const StyledTypography = styled((props: TypographyProps) => <Typography {...props} variant="caption" />)(({
theme,
}) => {
return {
color: theme.palette.textSecondaryDecorate.main,
cursor: 'pointer',
'&:active': {
textDecoration: 'underline',
},
}
})

View File

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

View File

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

View File

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

View File

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

View File

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

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