Compare commits

..

No commits in common. "main" and "refactor/edit-app-info" have entirely different histories.

75 changed files with 522 additions and 1587 deletions

View File

@ -3,37 +3,37 @@ const path = require('path')
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
module.exports = function override(config) {
const fallback = config.resolve.fallback || {}
Object.assign(fallback, {
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
assert: require.resolve('assert'),
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
os: require.resolve('os-browserify'),
url: require.resolve('url'),
})
config.resolve.fallback = fallback
config.plugins = (config.plugins || []).concat([
new webpack.ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer'],
}),
])
config.module.rules.unshift({
test: /\.m?js$/,
resolve: {
fullySpecified: false, // disable the behavior
},
})
// turns off the plugin that forbids importing from node_modules for the above-mentioned stuff
config.resolve.plugins = config.resolve.plugins.filter((plugin) => {
return !(plugin instanceof ModuleScopePlugin)
})
const fallback = config.resolve.fallback || {}
Object.assign(fallback, {
crypto: require.resolve('crypto-browserify'),
stream: require.resolve('stream-browserify'),
assert: require.resolve('assert'),
http: require.resolve('stream-http'),
https: require.resolve('https-browserify'),
os: require.resolve('os-browserify'),
url: require.resolve('url'),
})
config.resolve.fallback = fallback
config.plugins = (config.plugins || []).concat([
new webpack.ProvidePlugin({
process: 'process/browser',
Buffer: ['buffer', 'Buffer'],
}),
])
config.module.rules.unshift({
test: /\.m?js$/,
resolve: {
fullySpecified: false, // disable the behavior
},
})
// turns off the plugin that forbids importing from node_modules for the above-mentioned stuff
config.resolve.plugins = config.resolve.plugins.filter((plugin) => {
return !(plugin instanceof ModuleScopePlugin)
})
config.resolve.alias = {
'@': path.resolve(__dirname, 'src'),
}
config.resolve.alias = {
'@': path.resolve(__dirname, 'src'),
}
return config
return config
}

23
package-lock.json generated
View File

@ -41,7 +41,6 @@
"redux-persist": "^6.0.0",
"typescript": "^5.3.2",
"use-debounce": "^10.0.0",
"usehooks-ts": "^2.14.0",
"web-vitals": "^2.1.4",
"workbox-background-sync": "^6.6.0",
"workbox-broadcast-update": "^6.6.0",
@ -17215,20 +17214,6 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/usehooks-ts": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.14.0.tgz",
"integrity": "sha512-jnhrjTRJoJS7cFxz63tRYc5mzTKf/h+Ii8P0PDHymT9qDe4ZA2/gzDRmDR4WGausg5X8wMIdghwi3BBCN9JKow==",
"dependencies": {
"lodash.debounce": "^4.0.8"
},
"engines": {
"node": ">=16.15.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/utf-8-validate": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
@ -30606,14 +30591,6 @@
"integrity": "sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==",
"requires": {}
},
"usehooks-ts": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-2.14.0.tgz",
"integrity": "sha512-jnhrjTRJoJS7cFxz63tRYc5mzTKf/h+Ii8P0PDHymT9qDe4ZA2/gzDRmDR4WGausg5X8wMIdghwi3BBCN9JKow==",
"requires": {
"lodash.debounce": "^4.0.8"
}
},
"utf-8-validate": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",

View File

@ -36,7 +36,6 @@
"redux-persist": "^6.0.0",
"typescript": "^5.3.2",
"use-debounce": "^10.0.0",
"usehooks-ts": "^2.14.0",
"web-vitals": "^2.1.4",
"workbox-background-sync": "^6.6.0",
"workbox-broadcast-update": "^6.6.0",

View File

@ -10,6 +10,12 @@
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700&display=swap"
rel="stylesheet"
/>
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.

View File

@ -1,6 +1,6 @@
{
"name": "Nsec.app - Nostr key management tool",
"short_name": "Nsec.app",
"name": "Nsec.app",
"short_name": "Nsec.app - Nostr key management tool",
"start_url": ".",
"icons": [
{

View File

@ -1,6 +1,6 @@
import { DbKey, dbi } from './modules/db'
import { useCallback, useEffect, useState } from 'react'
import { swicOnReload, swicOnRender } from './modules/swic'
import { swicOnRender } from './modules/swic'
import { useAppDispatch } from './store/hooks/redux'
import { setApps, setKeys, setPending, setPerms } from './store/reducers/content.slice'
import AppRoutes from './routes/AppRoutes'
@ -9,20 +9,16 @@ import { ModalInitial } from './components/Modal/ModalInitial/ModalInitial'
import { ModalImportKeys } from './components/Modal/ModalImportKeys/ModalImportKeys'
import { ModalSignUp } from './components/Modal/ModalSignUp/ModalSignUp'
import { ModalLogin } from './components/Modal/ModalLogin/ModalLogin'
import { useSessionStorage } from 'usehooks-ts'
import { RELOAD_STORAGE_KEY } from './utils/consts'
function App() {
const [render, setRender] = useState(0)
const dispatch = useAppDispatch()
// eslint-disable-next-line
const [_, setNeedReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
const [isConnected, setIsConnected] = useState(false)
const load = useCallback(async () => {
const keys: DbKey[] = await dbi.listKeys()
console.log(keys, 'keys')
dispatch(setKeys({ keys }))
const loadProfiles = async () => {
@ -59,6 +55,7 @@ function App() {
// rerender
// setRender((r) => r + 1)
// if (!keys.length) handleOpen(MODAL_PARAMS_KEYS.INITIAL)
// eslint-disable-next-line
}, [dispatch])
@ -68,7 +65,7 @@ function App() {
useEffect(() => {
ndk.connect().then(() => {
console.log('NDK connected')
console.log('NDK connected', { ndk })
setIsConnected(true)
})
// eslint-disable-next-line
@ -80,24 +77,6 @@ function App() {
setRender((r) => r + 1)
})
// subscribe to service worker updates
swicOnReload(() => {
console.log('reload')
setNeedReload(true)
})
useEffect(() => {
const handleBeforeUnload = () => {
setNeedReload(false)
}
window.addEventListener('beforeunload', handleBeforeUnload)
return () => {
window.removeEventListener('beforeunload', handleBeforeUnload)
}
// eslint-disable-next-line
}, [])
return (
<>
<AppRoutes />

View File

@ -119,7 +119,7 @@ export const ModalAppDetails = () => {
}
}
const isFormValid = !isEmptyString(name)
const isFormValid = !isEmptyString(url) && !isEmptyString(name)
return (
<Modal open={isModalOpened} onClose={handleCloseModal}>
@ -130,13 +130,6 @@ export const ModalAppDetails = () => {
</Typography>
</Stack>
<Input
label="Name"
fullWidth
placeholder="Enter app name"
onChange={handleInputChange('name')}
value={details.name}
/>
<Autocomplete
options={[]}
freeSolo
@ -157,6 +150,13 @@ export const ModalAppDetails = () => {
)
}}
/>
<Input
label="Name"
fullWidth
placeholder="Enter app name"
onChange={handleInputChange('name')}
value={details.name}
/>
<Input
label="Icon"
fullWidth

View File

@ -1,5 +1,4 @@
import { Input } from '@/shared/Input/Input'
import { AppInputProps } from '@/shared/Input/types'
import { AppInputProps, Input } from '@/shared/Input/Input'
import { styled } from '@mui/material'
import { forwardRef } from 'react'

View File

@ -1,22 +1,15 @@
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import {
askNotificationPermission,
call,
getAppIconTitle,
getDomain,
getReferrerAppUrl,
getShortenNpub,
} from '@/utils/helpers/helpers'
import { askNotificationPermission, call, getAppIconTitle, getDomain, getReferrerAppUrl, getShortenNpub } from '@/utils/helpers/helpers'
import { Avatar, Box, Stack, Typography } from '@mui/material'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { useAppSelector } from '@/store/hooks/redux'
import { selectAppsByNpub, selectKeys, selectPendingsByNpub } from '@/store'
import { StyledButton, StyledToggleButtonsGroup } from './styled'
import { ActionToggleButton } from './сomponents/ActionToggleButton'
import { useEffect, useState } from 'react'
import { swicCall, swicWaitStarted } from '@/modules/swic'
import { useState } from 'react'
import { swicCall } from '@/modules/swic'
import { ACTION_TYPE } from '@/utils/consts'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
@ -35,7 +28,6 @@ export const ModalConfirmConnect = () => {
const pending = useAppSelector((state) => selectPendingsByNpub(state, npub))
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.BASIC)
const [isLoaded, setIsLoaded] = useState(false)
const appNpub = searchParams.get('appNpub') || ''
const pendingReqId = searchParams.get('reqId') || ''
@ -45,7 +37,7 @@ export const ModalConfirmConnect = () => {
const triggerApp = apps.find((app) => app.appNpub === appNpub)
const { name, url = '', icon = '' } = triggerApp || {}
const appUrl = url || searchParams.get('appUrl') || getReferrerAppUrl()
const appUrl = url || searchParams.get('appUrl') || getReferrerAppUrl();
const appDomain = getDomain(appUrl)
const appName = name || appDomain || getShortenNpub(appNpub)
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
@ -61,45 +53,14 @@ export const ModalConfirmConnect = () => {
},
})
// NOTE: when opened directly to this modal using authUrl,
// we might not have pending requests visible yet bcs we haven't
// loaded them yet, which means this modal will be closed with
// the logic below. So now if it's popup then we wait for SW
// and then wait a little more to give it time to fetch
// pending reqs from db. Same logic implemented in confirm-event.
// FIXME move to a separate hook and reuse?
useEffect(() => {
if (isModalOpened) {
if (isPopup) {
console.log('waiting for sw')
// wait for SW to start
swicWaitStarted().then(() => {
// give it some time to load the pending reqs etc
console.log('waiting for sw done')
setTimeout(() => setIsLoaded(true), 500)
})
} else {
setIsLoaded(true)
}
} else {
setIsLoaded(false)
}
}, [isModalOpened, isPopup])
if (isLoaded) {
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
// NOTE: app doesn't exist yet!
// const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId)
// console.log("pending", {isModalOpened, isPendingReqIdExists, isNpubExists, /*isAppNpubExists,*/ pendingReqId, pending});
if (isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) {
// if (isPopup) window.close()
// else closeModalAfterRequest()
if (!isPopup) closeModalAfterRequest()
return null
}
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
// App doesn't exist yet!
// const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId)
// console.log("pending", {isModalOpened, isPendingReqIdExists, isNpubExists, /*isAppNpubExists,*/ pendingReqId, pending});
if (!isPopup && isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) {
closeModalAfterRequest()
return null
}
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
@ -132,7 +93,7 @@ export const ModalConfirmConnect = () => {
} catch (e: any) {
console.log('error', e)
notify('Please enable Notifications in website settings!', 'error')
// keep going
return
}
try {
@ -170,9 +131,7 @@ export const ModalConfirmConnect = () => {
if (isPopup) {
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
// FIXME it should be 'ignore once',
// not 'disallow & remember' - this is too strict
// disallow()
disallow()
}
})
}
@ -197,11 +156,11 @@ export const ModalConfirmConnect = () => {
>
{appAvatarTitle}
</Avatar>
<Box overflow={'auto'}>
<Typography variant="h5" fontWeight={600} noWrap>
<Box>
<Typography variant="h5" fontWeight={600}>
{appName}
</Typography>
<Typography variant="body2" color={'GrayText'} noWrap>
<Typography variant="body2" color={'GrayText'}>
New app would like to connect
</Typography>
</Box>
@ -220,7 +179,7 @@ export const ModalConfirmConnect = () => {
</StyledToggleButtonsGroup>
<Stack direction={'row'} gap={'1rem'}>
<StyledButton onClick={disallow} varianttype="secondary">
Ignore
Disallow
</StyledButton>
<StyledButton fullWidth onClick={allow}>
Connect

View File

@ -21,8 +21,5 @@ export const StyledToggleButtonsGroup = styled((props: ToggleButtonGroupProps) =
border: 'initial',
borderRadius: '1rem',
},
'@media screen and (max-width: 320px)': {
marginBottom: '0.25rem',
},
})
)

View File

@ -4,18 +4,22 @@ import { StyledToggleButton } from './styled'
type ActionToggleButtonProps = ToggleButtonProps & {
description?: string
hasinfo?: boolean
}
export const ActionToggleButton: FC<ActionToggleButtonProps> = (props) => {
export const ActionToggleButton: FC<ActionToggleButtonProps> = ({ hasinfo = false, ...props }) => {
const { title, description = '' } = props
return (
<StyledToggleButton {...props}>
<Typography variant="body2" noWrap className="title">
{title}
</Typography>
<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

@ -29,17 +29,4 @@ export const StyledToggleButton = styled((props: ToggleButtonProps) => (
fontSize: '10px',
fontWeight: 500,
},
'@media screen and (max-width: 320px)': {
'& .title': {
fontSize: '14px',
},
'& .description': {
margin: '0.25rem 0',
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
},
}))

View File

@ -10,7 +10,7 @@ import { ActionToggleButton } from './сomponents/ActionToggleButton'
import { FC, useEffect, useMemo, useState } from 'react'
import { StyledActionsListContainer, StyledButton, StyledToggleButtonsGroup } from './styled'
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
import { swicCall, swicWaitStarted } from '@/modules/swic'
import { swicCall } from '@/modules/swic'
import { Checkbox } from '@/shared/Checkbox/Checkbox'
import { DbPending } from '@/modules/db'
import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal'
@ -47,7 +47,6 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.ALWAYS)
const [pendingRequests, setPendingRequests] = useState<PendingRequest[]>([])
const [isLoaded, setIsLoaded] = useState(false)
const currentAppPendingReqs = useMemo(() => confirmEventReqs[appNpub]?.pending || [], [confirmEventReqs, appNpub])
@ -62,33 +61,25 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
},
})
useEffect(() => {
if (isModalOpened) {
if (isPopup) {
// wait for SW to start
swicWaitStarted().then(() => {
// give it some time to load the pending reqs etc
setTimeout(() => setIsLoaded(true), 500)
})
} else {
setIsLoaded(true)
}
} else {
setIsLoaded(false)
}
}, [isModalOpened, isPopup])
// FIXME: when opened directly to this modal using authUrl,
// we might not have pending requests visible yet bcs we haven't
// loaded them yet, which means this modal will be closed with
// the login below. It's fine if only one app has sent pending
// requests atm, bcs the modal would re-appear as soon as we load
// the requests. But if there are several pending reqs from other
// apps then popup might show a different one! Which is very
// contrary to what user expects. So:
// - if isPopup - dont close the modal with logic below
// - show some 'loading' indicator until we've got some requests
// for the specified appNpub
// FIXME is the same logic valid for Connect modal?
if (isLoaded) {
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
// console.log("confirm event", { confirmEventReqs, isModalOpened, isNpubExists, isAppNpubExists });
if (isModalOpened && (!currentAppPendingReqs.length || !isNpubExists || !isAppNpubExists)) {
// if (isPopup) window.close()
// else closeModalAfterRequest()
if (!isPopup)
closeModalAfterRequest()
return null
}
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
// console.log("confirm event", { confirmEventReqs, isModalOpened, isNpubExists, isAppNpubExists });
if (isModalOpened && (!currentAppPendingReqs.length || !isNpubExists || !isAppNpubExists)) {
closeModalAfterRequest()
return null
}
const triggerApp = apps.find((app) => app.appNpub === appNpub)

View File

@ -1,174 +0,0 @@
import { CheckmarkIcon } from '@/assets'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { swicCall } from '@/modules/swic'
import { Button } from '@/shared/Button/Button'
import { Input } from '@/shared/Input/Input'
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
import { Modal } from '@/shared/Modal/Modal'
import { selectKeys } from '@/store'
import { useAppSelector } from '@/store/hooks/redux'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { DOMAIN } from '@/utils/consts'
import { fetchNip05 } from '@/utils/helpers/helpers'
import { Stack, Typography, useTheme } from '@mui/material'
import { ChangeEvent, Fragment, useCallback, useEffect, useState } from 'react'
import { useParams } from 'react-router-dom'
import { useDebounce } from 'use-debounce'
import { StyledSettingContainer } from './styled'
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
export const ModalEditName = () => {
const keys = useAppSelector(selectKeys)
const notify = useEnqueueSnackbar()
const { npub = '' } = useParams<{ npub: string }>()
const key = keys.find((k) => k.npub === npub)
const name = key?.name || ''
const { palette } = useTheme()
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EDIT_NAME)
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.EDIT_NAME)
const [enteredName, setEnteredName] = useState('')
const [debouncedName] = useDebounce(enteredName, 300)
const isNameEqual = debouncedName === name
const [receiverNpub, setReceiverNpub] = useState('')
const [isAvailable, setIsAvailable] = useState(true)
const [isChecking, setIsChecking] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [isTransferLoading, setIsTransferLoading] = useState(false)
const checkIsUsernameAvailable = useCallback(async () => {
if (!debouncedName.trim().length) return undefined
try {
setIsChecking(true)
const npubNip05 = await fetchNip05(`${debouncedName}@${DOMAIN}`)
setIsAvailable(!npubNip05 || npubNip05 === npub)
setIsChecking(false)
} catch (error) {
setIsAvailable(true)
setIsChecking(false)
}
}, [debouncedName, npub])
useEffect(() => {
checkIsUsernameAvailable()
}, [checkIsUsernameAvailable])
useEffect(() => {
setEnteredName(name)
return () => {
if (isModalOpened) {
setEnteredName('')
setReceiverNpub('')
}
}
// eslint-disable-next-line
}, [isModalOpened])
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => setEnteredName(e.target.value)
const handleReceiverNpubChange = (e: ChangeEvent<HTMLInputElement>) => setReceiverNpub(e.target.value)
const getInputHelperText = () => {
if (!debouncedName.trim().length || isNameEqual) return ''
if (isChecking) return 'Loading...'
if (!isAvailable) return 'Already taken'
return (
<Fragment>
<CheckmarkIcon /> Available
</Fragment>
)
}
const inputHelperText = getInputHelperText()
const getHelperTextColor = useCallback(() => {
if (!debouncedName || isChecking || isNameEqual) return palette.textSecondaryDecorate.main
return isAvailable ? palette.success.main : palette.error.main
// deps
}, [debouncedName, isAvailable, isChecking, isNameEqual, palette])
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
if (isModalOpened && !isNpubExists) {
handleCloseModal()
return null
}
const isEditButtonDisabled = isNameEqual || !isAvailable || isChecking || isLoading || !enteredName.trim().length
const isTransferButtonDisabled = !name.length || !receiverNpub.trim().length || isTransferLoading
const handleEditName = async () => {
if (isEditButtonDisabled) return
try {
setIsLoading(true)
await swicCall('editName', npub, enteredName)
notify('Username updated!', 'success')
setIsLoading(false)
} catch (error: any) {
setIsLoading(false)
notify(error?.message || 'Failed to edit username!', 'error')
}
}
const handleTransferName = async () => {
if (isTransferButtonDisabled) return
try {
setIsTransferLoading(true)
await swicCall('transferName', npub, enteredName, receiverNpub)
notify('Username transferred!', 'success')
setIsTransferLoading(false)
setEnteredName('')
} catch (error: any) {
setIsTransferLoading(false)
notify(error?.message || 'Failed to transfer username!', 'error')
}
}
return (
<Modal open={isModalOpened} title="Username Settings" onClose={handleCloseModal}>
<Stack gap={'1rem'}>
<StyledSettingContainer>
<SectionTitle>Change name</SectionTitle>
<Input
label="User name"
fullWidth
placeholder="Enter a Username"
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
helperText={inputHelperText}
onChange={handleNameChange}
value={enteredName}
helperTextProps={{
sx: {
'&.helper_text': {
color: getHelperTextColor(),
},
},
}}
/>
<Button fullWidth disabled={isEditButtonDisabled} onClick={handleEditName}>
Save name {isLoading && <LoadingSpinner />}
</Button>
</StyledSettingContainer>
<StyledSettingContainer>
<SectionTitle>Transfer name</SectionTitle>
<Input
label="Receiver npub"
fullWidth
placeholder="npub1..."
onChange={handleReceiverNpubChange}
value={receiverNpub}
/>
<Button fullWidth onClick={handleTransferName} disabled={isTransferButtonDisabled}>
Transfer name
</Button>
</StyledSettingContainer>
</Stack>
</Modal>
)
}

View File

@ -1,10 +0,0 @@
import { Stack, StackProps, styled } from '@mui/material'
export const StyledSettingContainer = styled((props: StackProps) => (
<Stack gap={'0.75rem'} component={'form'} {...props} />
))(({ theme }) => ({
padding: '1rem',
borderRadius: '1rem',
background: theme.palette.background.default,
color: theme.palette.text.primary,
}))

View File

@ -6,6 +6,7 @@ import { Input } from '@/shared/Input/Input'
import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { Stack, Typography, useTheme } from '@mui/material'
import { StyledAppLogo } from './styled'
import { useNavigate } from 'react-router-dom'
import { useForm } from 'react-hook-form'
import { FormInputType, schema } from './const'
@ -18,7 +19,6 @@ import { DOMAIN } from '@/utils/consts'
import { CheckmarkIcon } from '@/assets'
import { getPublicKey, nip19 } from 'nostr-tools'
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
import { HeadingContainer } from './styled'
const FORM_DEFAULT_VALUES = {
username: '',
@ -149,16 +149,14 @@ export const ModalImportKeys = () => {
const nsecHelperText = getNsecHelperText()
return (
<Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
<HeadingContainer>
<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">
Import key
</Typography>
<Typography noWrap variant="body2" color={'GrayText'}>
Bring your existing Nostr keys to Nsec.app
</Typography>
</HeadingContainer>
</Stack>
<Input
label="Choose a username"
fullWidth

View File

@ -1,5 +1,5 @@
import { AppLogo } from '@/assets'
import { Box, Stack, StackProps, styled } from '@mui/material'
import { Box, styled } from '@mui/material'
export const StyledAppLogo = styled((props) => (
<Box {...props}>
@ -12,14 +12,3 @@ export const StyledAppLogo = styled((props) => (
display: 'grid',
placeItems: 'center',
}))
export const HeadingContainer = styled((props: StackProps) => <Stack {...props} />)(() => ({
gap: '0.2rem',
padding: '0 1rem',
alignSelf: 'flex-start',
overflow: 'auto',
width: '100%',
'@media screen and (max-width: 320px)': {
padding: '0 0.75rem',
},
}))

View File

@ -1,36 +1,43 @@
// import { useEffect } from 'react'
import React, { useEffect, useState } from 'react'
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 { Stack } from '@mui/material'
// import { AppLink } from '@/shared/AppLink/AppLink'
import { Fade, Stack } from '@mui/material'
import { AppLink } from '@/shared/AppLink/AppLink'
export const ModalInitial = () => {
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
const isModalOpened = getModalOpened(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>
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS)}>Import key</Button>
<AppLink title="Advanced" alignSelf={'center'} onClick={handleShowAdvanced} />
{showAdvancedContent && (
<Fade in>
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS)}>Import key</Button>
</Fade>
)}
</Stack>
</Modal>
)

View File

@ -5,14 +5,15 @@ import { swicCall } from '@/modules/swic'
import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { Stack, Typography } from '@mui/material'
import { StyledAppLogo } from './styled'
import { Input } from '@/shared/Input/Input'
import { Button } from '@/shared/Button/Button'
import { useNavigate, useSearchParams } from 'react-router-dom'
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, fetchNpubNames } from '@/utils/helpers/helpers'
import { fetchNip05 } from '@/utils/helpers/helpers'
import { usePassword } from '@/hooks/usePassword'
import { dbi } from '@/modules/db'
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
@ -31,14 +32,11 @@ export const ModalLogin = () => {
const navigate = useNavigate()
const { hidePassword, inputProps } = usePassword()
const [isLoading, setIsLoading] = useState(false)
const [searchParams] = useSearchParams()
const isPopup = searchParams.get('popup') === 'true'
const {
handleSubmit,
reset,
register,
setValue,
formState: { errors },
} = useForm<FormInputType>({
defaultValues: FORM_DEFAULT_VALUES,
@ -81,10 +79,7 @@ export const ModalLogin = () => {
notify(`Fetched ${k.npub}`, 'success')
dbi.addSynced(k.npub)
cleanUpStates()
setTimeout(() => {
// give frontend time to read the new key first
navigate(`/key/${k.npub}${isPopup ? '?popup=true' : ''}`)
}, 300)
navigate(`/key/${k.npub}`)
} catch (error: any) {
console.log('error', error)
notify(error?.message || 'Something went wrong!', 'error')
@ -92,22 +87,6 @@ export const ModalLogin = () => {
}
}
useEffect(() => {
if (isModalOpened) {
const npub = searchParams.get('npub') || ''
const appNpub = searchParams.get('appNpub') || ''
if (isPopup && isModalOpened) {
swicCall('fetchPendingRequests', npub, appNpub)
fetchNpubNames(npub).then((names) => {
if (names.length) {
setValue('username', `${names[0]}@${DOMAIN}`)
}
})
}
}
}, [searchParams, isModalOpened, isPopup, setValue])
useEffect(() => {
return () => {
if (isModalOpened) {
@ -118,15 +97,13 @@ export const ModalLogin = () => {
}, [isModalOpened, cleanUpStates])
return (
<Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
<Stack gap={'0.2rem'} padding={'0 1rem'} alignSelf={'flex-start'}>
<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>
<Typography noWrap variant="body2" color={'GrayText'}>
Sync keys from the cloud to this device
</Typography>
</Stack>
<Input
label="Username or nip05 or npub"
@ -142,14 +119,10 @@ export const ModalLogin = () => {
{...register('password')}
{...inputProps}
error={!!errors.password}
helperText={'Password you set in Cloud Sync settings'}
/>
<Stack gap={'0.5rem'}>
<Button type="submit" fullWidth disabled={isLoading}>
Login {isLoading && <LoadingSpinner />}
</Button>
</Stack>
<Button type="submit" fullWidth disabled={isLoading}>
Add account {isLoading && <LoadingSpinner />}
</Button>
</Stack>
</Modal>
)

View File

@ -4,6 +4,7 @@ import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { Stack, Typography, useTheme } from '@mui/material'
import React, { ChangeEvent, useEffect, useState } from 'react'
import { StyledAppLogo } from './styled'
import { Input } from '@/shared/Input/Input'
import { Button } from '@/shared/Button/Button'
import { CheckmarkIcon } from '@/assets'
@ -60,13 +61,9 @@ export const ModalSignUp = () => {
try {
setIsLoading(true)
const k: any = await swicCall('generateKey', name)
if (k.name) notify(`Account created for "${k.name}"`, 'success')
else notify(`Failed to assign name "${name}", try again`, 'error')
notify(`Account created for "${name}"`, 'success')
navigate(`/key/${k.npub}`)
setIsLoading(false)
setTimeout(() => {
// give frontend time to read the new key first
navigate(`/key/${k.npub}`)
}, 300)
} catch (error: any) {
notify(error?.message || 'Something went wrong!', 'error')
setIsLoading(false)
@ -84,15 +81,13 @@ export const ModalSignUp = () => {
}, [isModalOpened])
return (
<Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
<Modal open={isModalOpened} onClose={handleCloseModal}>
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
<Stack gap={'0.2rem'} padding={'0 1rem'} alignSelf={'flex-start'}>
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
<StyledAppLogo />
<Typography fontWeight={600} variant="h5">
Sign up
</Typography>
<Typography noWrap variant="body2" color={'GrayText'}>
Generate new Nostr keys
</Typography>
</Stack>
<Input
label="Username"
@ -115,11 +110,9 @@ export const ModalSignUp = () => {
},
}}
/>
<Stack gap={'0.5rem'}>
<Button fullWidth type="submit" disabled={isLoading}>
Create account {isLoading && <LoadingSpinner />}
</Button>
</Stack>
<Button fullWidth type="submit" disabled={isLoading}>
Create account {isLoading && <LoadingSpinner />}
</Button>
</Stack>
</Modal>
)

View File

@ -1,33 +0,0 @@
import { FC, memo, useCallback } from 'react'
import { Stack, Typography } from '@mui/material'
import { StyledAlert, StyledReloadButton } from './styled'
import { useSessionStorage } from 'usehooks-ts'
import { RELOAD_STORAGE_KEY } from '@/utils/consts'
type ReloadBadgeContentProps = {
onReload: () => void
}
const ReloadBadgeContent: FC<ReloadBadgeContentProps> = memo(({ onReload }) => {
return (
<StyledAlert>
<Stack direction={'row'} className="content">
<Typography flex={1} className="title">
New version available!
</Typography>
<StyledReloadButton onClick={onReload}>Reload</StyledReloadButton>
</Stack>
</StyledAlert>
)
})
export const ReloadBadge = () => {
const [needReload, setNeedReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
const handleReload = useCallback(() => {
setNeedReload(false)
window.location.reload()
}, [setNeedReload])
return <>{needReload && <ReloadBadgeContent onReload={handleReload} />}</>
}

View File

@ -1,50 +0,0 @@
import { AppButtonProps, Button } from '@/shared/Button/Button'
import { Alert, AlertProps, styled } from '@mui/material'
import RefreshIcon from '@mui/icons-material/Refresh'
export const StyledAlert = styled((props: AlertProps) => (
<Alert {...props} variant="outlined" severity="info" classes={{ message: 'message' }} />
))(() => {
return {
height: 'auto',
marginTop: '0.5rem',
alignItems: 'center',
'& .message': {
flex: 1,
overflow: 'initial',
},
'& .content': {
width: '100%',
alignItems: 'center',
gap: '1rem',
},
'& .title': {
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis',
},
'@media screen and (max-width: 320px)': {
'& .title': {
fontSize: '14px',
WebkitLineClamp: 2,
},
},
}
})
export const StyledReloadButton = styled((props: AppButtonProps) => <Button startIcon={<RefreshIcon />} {...props} />)(
({ theme }) => {
const isDarkTheme = theme.palette.mode === 'dark'
return {
padding: '0.25rem 1rem',
'&.button:is(:hover, :active, &)': {
background: isDarkTheme ? '#b8e7fb' : '#014361',
},
'@media screen and (max-width: 320px)': {
padding: '0.25rem 0.5rem',
},
}
}
)

View File

@ -1,27 +1,27 @@
import { FC, ReactNode } from 'react'
import { IconContainer, StyledContainer } from './styled'
import { BoxProps, Typography } from '@mui/material'
import { BoxProps, Stack, Typography } from '@mui/material'
type WarningProps = {
message?: string | ReactNode
hint?: string | ReactNode
icon?: ReactNode
} & BoxProps
export const Warning: FC<WarningProps> = ({ message, icon, ...restProps }) => {
const renderMessage = () => {
if (typeof message === 'string') {
return (
<Typography noWrap width={'100%'}>
{message}
</Typography>
)
}
return message
}
export const Warning: FC<WarningProps> = ({ hint, message, icon, ...restProps }) => {
return (
<StyledContainer {...restProps}>
{icon && <IconContainer>{icon}</IconContainer>}
{renderMessage()}
<Stack flex={1} direction={'column'} gap={'0.2rem'}>
<Typography noWrap>
{message}
</Typography>
{hint && (
<Typography>
{hint}
</Typography>
)}
</Stack>
</StyledContainer>
)
}

View File

@ -7,14 +7,13 @@ export const StyledContainer = styled((props: BoxProps) => <Box {...props} />)((
padding: '0.5rem',
display: 'flex',
alignItems: 'center',
gap: '0.5rem',
gap: '1rem',
cursor: 'pointer',
}
})
export const IconContainer = styled((props: BoxProps) => <Box {...props} />)(() => ({
width: '40px',
minWidth: '40px',
height: '40px',
borderRadius: '50%',
background: 'grey',

View File

@ -31,7 +31,7 @@ export const useModalSearchParams = () => {
const enumKey = getEnumParam(modal)
searchParams.delete(enumKey)
extraOptions?.onClose && extraOptions?.onClose(searchParams)
// console.log({ searchParams })
console.log({ searchParams })
setSearchParams(searchParams, { replace: !!extraOptions?.replace })
}

View File

@ -14,7 +14,7 @@ export const useProfile = (npub: string) => {
const [profile, setProfile] = useState<MetaEvent | null>(null)
const currentKey = useAppSelector((state) => selectKeyByNpub(state, npub))
const userName = currentKey?.name || getProfileUsername(profile)
const userName = getProfileUsername(profile) || currentKey?.name
const userAvatar = profile?.info?.picture || ''
const avatarTitle = getFirstLetter(userName)

View File

@ -1,63 +1,17 @@
@font-face {
font-family: 'Inter';
src:
local('Inter ExtraLight'),
local('Inter-ExtraLight'),
url('./assets/fonts/Inter/Inter-ExtraLight.ttf') format('truetype');
font-weight: 200;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src:
local('Inter Light'),
local('Inter-Light'),
url('./assets/fonts/Inter/Inter-Light.ttf') format('truetype');
font-weight: 300;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src:
local('Inter Regular'),
local('Inter-Regular'),
url('./assets/fonts/Inter/Inter-Regular.ttf') format('truetype');
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src:
local('Inter Medium'),
local('Inter-Medium'),
url('./assets/fonts/Inter/Inter-Medium.ttf') format('truetype');
font-weight: 500;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src:
local('Inter SemiBold'),
local('Inter-SemiBold'),
url('./assets/fonts/Inter/Inter-SemiBold.ttf') format('truetype');
font-weight: 600;
font-display: swap;
}
@font-face {
font-family: 'Inter';
src:
local('Inter Bold'),
local('Inter-Bold'),
url('./assets/fonts/Inter/Inter-Bold.ttf') format('truetype');
font-weight: 700;
font-display: swap;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Inter', sans-serif;
}
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;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
html,

View File

@ -1,13 +1,13 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import ThemeProvider from './modules/theme/ThemeProvider'
import App from './App'
import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'
import { swicRegister } from './modules/swic'
import { BrowserRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import { persistor, store } from './store'
import ThemeProvider from './modules/theme/ThemeProvider'
import { PersistGate } from 'redux-persist/integration/react'
import { SnackbarProvider } from 'notistack'

View File

@ -1,4 +1,4 @@
import { Avatar, Stack, Toolbar, Typography, Divider, DividerProps, styled } from '@mui/material'
import { Avatar, Stack, Toolbar, Typography } from '@mui/material'
import { StyledAppBar, StyledAppLogo, StyledAppName, StyledProfileContainer, StyledThemeButton } from './styled'
import { Menu } from './components/Menu'
import { useNavigate, useParams } from 'react-router-dom'
@ -8,14 +8,11 @@ import DarkModeIcon from '@mui/icons-material/DarkMode'
import LightModeIcon from '@mui/icons-material/LightMode'
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
import { setThemeMode } from '@/store/reducers/ui.slice'
import { useSessionStorage } from 'usehooks-ts'
import { RELOAD_STORAGE_KEY } from '@/utils/consts'
export const Header = () => {
const themeMode = useAppSelector((state) => state.ui.themeMode)
const navigate = useNavigate()
const dispatch = useAppDispatch()
const [needReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
const { npub = '' } = useParams<{ npub: string }>()
const { userName, userAvatar, avatarTitle } = useProfile(npub)
@ -33,7 +30,7 @@ export const Header = () => {
}
return (
<StyledAppBar position={needReload ? 'relative' : 'fixed'}>
<StyledAppBar position="fixed">
<Toolbar sx={{ padding: '12px' }}>
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'} width={'100%'}>
{showProfile && (
@ -59,15 +56,6 @@ export const Header = () => {
{showProfile ? <ProfileMenu /> : <Menu />}
</Stack>
</Toolbar>
<StyledDivider />
</StyledAppBar>
)
}
const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({
position: 'absolute',
bottom: 0,
width: '100%',
left: 0,
height: '2px',
})

View File

@ -4,21 +4,28 @@ import { Avatar, ListItemIcon, MenuItem, Typography } from '@mui/material'
import React, { FC } from 'react'
type ListItemProfileProps = {
onClickItem: () => void
onClickItem: () => void
} & DbKey
export const ListItemProfile: FC<ListItemProfileProps> = ({ onClickItem, npub }) => {
const { userName, userAvatar, avatarTitle } = useProfile(npub)
return (
<MenuItem sx={{ gap: '0.5rem' }} onClick={onClickItem}>
<ListItemIcon>
<Avatar src={userAvatar} alt={userName} sx={{ width: 36, height: 36 }}>
{avatarTitle}
</Avatar>
</ListItemIcon>
<Typography variant="body2" noWrap>
{userName}
</Typography>
</MenuItem>
)
export const ListItemProfile: FC<ListItemProfileProps> = ({
onClickItem,
npub,
}) => {
const { userName, userAvatar, avatarTitle } = useProfile(npub)
return (
<MenuItem sx={{ gap: '0.5rem' }} onClick={onClickItem}>
<ListItemIcon>
<Avatar
src={userAvatar}
alt={userName}
sx={{ width: 36, height: 36 }}
>
{avatarTitle}
</Avatar>
</ListItemIcon>
<Typography variant='body2' noWrap>
{userName}
</Typography>
</MenuItem>
)
}

View File

@ -1,19 +1,13 @@
import { FC } from 'react'
import { Outlet } from 'react-router-dom'
import { Header } from './Header/Header'
import { Container, ContainerProps, styled } from '@mui/material'
import { ReloadBadge } from '@/components/ReloadBadge/ReloadBadge'
import { useSessionStorage } from 'usehooks-ts'
import { RELOAD_STORAGE_KEY } from '@/utils/consts'
import { Container, ContainerProps, Divider, DividerProps, styled } from '@mui/material'
export const Layout: FC = () => {
const [needReload] = useSessionStorage(RELOAD_STORAGE_KEY, false)
const containerClassName = needReload ? 'reload' : ''
return (
<StyledContainer maxWidth="md" className={containerClassName}>
<ReloadBadge />
<StyledContainer maxWidth="md">
<Header />
<StyledDivider />
<main>
<Outlet />
</main>
@ -30,14 +24,14 @@ const StyledContainer = styled((props: ContainerProps) => <Container maxWidth="s
'& > main': {
flex: 1,
maxHeight: '100%',
},
'&:not(.reload) > main': {
paddingTop: 'calc(66px + 1rem)',
},
'@media screen and (max-width: 320px)': {
marginBottom: '0.25rem',
paddingLeft: '0.75rem',
paddingBottom: '0.75rem',
paddingRight: '0.75rem',
},
})
const StyledDivider = styled((props: DividerProps) => <Divider {...props} />)({
position: 'absolute',
top: '66px',
width: '100%',
left: 0,
height: '2px',
})

View File

@ -1,34 +1,24 @@
import { Event, generatePrivateKey, getPublicKey, nip19, verifySignature } from 'nostr-tools'
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools'
import { DbApp, dbi, DbKey, DbPending, DbPerm } from './db'
import { Keys } from './keys'
import NDK, {
IEventHandlingStrategy,
NDKEvent,
NDKNip46Backend,
NDKPrivateKeySigner,
NDKSigner,
NDKSubscription,
NDKSubscriptionCacheUsage,
NDKUser,
} from '@nostr-dev-kit/ndk'
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN, REQ_TTL } from '../utils/consts'
// import { Nip04 } from './nip04'
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN } from '../utils/consts'
import { Nip04 } from './nip04'
import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
import { NostrPowEvent, minePow } from './pow'
//import { PrivateKeySigner } from './signer'
//const PERF_TEST = false
enum DECISION {
ASK = '',
ALLOW = 'allow',
DISALLOW = 'disallow',
IGNORE = 'ignore',
}
export interface KeyInfo {
npub: string
nip05?: string
name?: string
locked: boolean
}
@ -38,17 +28,16 @@ interface Key {
backoff: number
signer: NDKSigner
backend: NDKNip46Backend
watcher: Watcher
}
interface Pending {
req: DbPending
cb: (allow: DECISION, remember: boolean, options?: any) => void
cb: (allow: boolean, remember: boolean, options?: any) => void
notified?: boolean
}
interface IAllowCallbackParams {
backend: Nip46Backend
backend: NDKNip46Backend
npub: string
id: string
method: string
@ -57,171 +46,86 @@ interface IAllowCallbackParams {
params?: any
}
class Watcher {
private ndk: NDK
private signer: NDKSigner
private onReply: (id: string) => void
private sub?: NDKSubscription
class Nip04KeyHandlingStrategy implements IEventHandlingStrategy {
private privkey: string
private nip04 = new Nip04()
constructor(ndk: NDK, signer: NDKSigner, onReply: (id: string) => void) {
this.ndk = ndk
this.signer = signer
this.onReply = onReply
constructor(privkey: string) {
this.privkey = privkey
}
async start() {
this.sub = this.ndk.subscribe(
{
kinds: [KIND_RPC],
authors: [(await this.signer.user()).pubkey],
since: Math.floor(Date.now() / 1000 - 10),
},
{
closeOnEose: false,
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
}
)
this.sub.on('event', async (e: NDKEvent) => {
const peer = e.tags.find((t) => t.length >= 2 && t[0] === 'p')
console.log('watcher got event', { e, peer })
if (!peer) return
const decryptedContent = await this.signer.decrypt(new NDKUser({ pubkey: peer[1] }), e.content)
const parsedContent = JSON.parse(decryptedContent)
const { id, method, params, result, error } = parsedContent
console.log('watcher got', { peer, id, method, params, result, error })
if (method || result === 'auth_url') return
this.onReply(id)
})
private async getKey(backend: NDKNip46Backend, id: string, remotePubkey: string, recipientPubkey: string) {
if (
!(await backend.pubkeyAllowed({
id,
pubkey: remotePubkey,
// @ts-ignore
method: 'get_nip04_key',
params: recipientPubkey,
}))
) {
backend.debug(`get_nip04_key request from ${remotePubkey} rejected`)
return undefined
}
return Buffer.from(this.nip04.createKey(this.privkey, recipientPubkey)).toString('hex')
}
stop() {
this.sub!.stop()
async handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]) {
const [recipientPubkey] = params
return await this.getKey(backend, id, remotePubkey, recipientPubkey)
}
}
class Nip46Backend extends NDKNip46Backend {
private allowCb: (params: IAllowCallbackParams) => Promise<DECISION>
private npub: string = ''
class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
readonly backend: NDKNip46Backend
readonly npub: string
readonly method: string
private body: IEventHandlingStrategy
private allowCb: (params: IAllowCallbackParams) => Promise<boolean>
public constructor(ndk: NDK, signer: NDKSigner, allowCb: (params: IAllowCallbackParams) => Promise<DECISION>) {
super(ndk, signer, () => Promise.resolve(true))
constructor(
backend: NDKNip46Backend,
npub: string,
method: string,
body: IEventHandlingStrategy,
allowCb: (params: IAllowCallbackParams) => Promise<boolean>
) {
this.backend = backend
this.npub = npub
this.method = method
this.body = body
this.allowCb = allowCb
signer.user().then((u) => (this.npub = nip19.npubEncode(u.pubkey)))
}
public async processEvent(event: NDKEvent) {
this.handleIncomingEvent(event)
}
protected async handleIncomingEvent(event: NDKEvent) {
const { id, method, params } = (await this.rpc.parseEvent(event)) as any
const remotePubkey = event.pubkey
let response: string | undefined
this.debug('incoming event', { id, method, params })
// validate signature explicitly
if (!verifySignature(event.rawEvent() as Event)) {
this.debug('invalid signature', event.rawEvent())
return
}
const decision = await this.allowCb({
backend: this,
npub: this.npub,
async handle(
backend: NDKNip46Backend,
id: string,
remotePubkey: string,
params: string[]
): Promise<string | undefined> {
console.log(Date.now(), 'handle', {
method: this.method,
id,
method,
remotePubkey,
params,
})
console.log(Date.now(), 'handle', { method, id, decision, remotePubkey, params })
if (decision === DECISION.IGNORE) return
const allow = decision === DECISION.ALLOW
const strategy = this.handlers[method]
if (allow) {
if (strategy) {
try {
response = await strategy.handle(this, id, remotePubkey, params)
console.log(Date.now(), 'req', id, 'method', method, 'result', response)
} catch (e: any) {
this.debug('error handling event', e, { id, method, params })
this.rpc.sendResponse(id, remotePubkey, 'error', undefined, e.message)
}
} else {
this.debug('unsupported method', { method, params })
}
}
if (response) {
this.debug(`sending response to ${remotePubkey}`, response)
this.rpc.sendResponse(id, remotePubkey, response)
} else {
this.rpc.sendResponse(id, remotePubkey, 'error', undefined, 'Not authorized')
}
const allow = await this.allowCb({
backend: this.backend,
npub: this.npub,
id,
method: this.method,
remotePubkey,
params,
})
if (!allow) return undefined
return this.body.handle(backend, id, remotePubkey, params).then((r) => {
console.log(Date.now(), 'req', id, 'method', this.method, 'result', r)
return r
})
}
}
// class Nip04KeyHandlingStrategy implements IEventHandlingStrategy {
// private privkey: string
// private nip04 = new Nip04()
// constructor(privkey: string) {
// this.privkey = privkey
// }
// private async getKey(backend: NDKNip46Backend, id: string, remotePubkey: string, recipientPubkey: string) {
// if (
// !(await backend.pubkeyAllowed({
// id,
// pubkey: remotePubkey,
// // @ts-ignore
// method: 'get_nip04_key',
// params: recipientPubkey,
// }))
// ) {
// backend.debug(`get_nip04_key request from ${remotePubkey} rejected`)
// return undefined
// }
// return Buffer.from(this.nip04.createKey(this.privkey, recipientPubkey)).toString('hex')
// }
// async handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]) {
// const [recipientPubkey] = params
// return await this.getKey(backend, id, remotePubkey, recipientPubkey)
// }
// }
// FIXME why do we need it? Just to print
// class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
// readonly backend: NDKNip46Backend
// readonly method: string
// private body: IEventHandlingStrategy
// constructor(
// backend: NDKNip46Backend,
// method: string,
// body: IEventHandlingStrategy
// ) {
// this.backend = backend
// this.method = method
// this.body = body
// }
// async handle(
// backend: NDKNip46Backend,
// id: string,
// remotePubkey: string,
// params: string[]
// ): Promise<string | undefined> {
// return this.body.handle(backend, id, remotePubkey, params).then((r) => {
// console.log(Date.now(), 'req', id, 'method', this.method, 'result', r)
// return r
// })
// }
// }
export class NoauthBackend {
readonly swg: ServiceWorkerGlobalScope
private keysModule: Keys
@ -233,21 +137,20 @@ export class NoauthBackend {
private confirmBuffer: Pending[] = []
private accessBuffer: DbPending[] = []
private notifCallback: (() => void) | null = null
private pendingNpubEvents = new Map<string, NDKEvent[]>()
private ndk = new NDK({
explicitRelayUrls: NIP46_RELAYS,
enableOutboxModel: false,
})
public constructor(swg: ServiceWorkerGlobalScope) {
this.swg = swg
this.keysModule = new Keys(swg.crypto.subtle)
this.ndk.connect()
const self = this
swg.addEventListener('activate', (event) => {
console.log('activate new sw worker')
this.reloadUI()
console.log('activate')
// swg.addEventListener('activate', event => event.waitUntil(swg.clients.claim()));
})
swg.addEventListener('install', (event) => {
console.log('install')
// swg.addEventListener('install', event => event.waitUntil(swg.skipWaiting()));
})
swg.addEventListener('push', (event) => {
@ -310,13 +213,6 @@ export class NoauthBackend {
this.apps = await dbi.listApps()
console.log('started apps', this.apps)
// drop old pending reqs
const pending = await dbi.listPending()
for (const p of pending) {
if (p.timestamp < Date.now() - REQ_TTL)
await dbi.removePending(p.id)
}
const sub = await this.swg.registration.pushManager.getSubscription()
for (const k of this.enckeys) {
@ -369,7 +265,7 @@ export class NoauthBackend {
if (r.status !== 200 && r.status !== 201) {
console.log('Fetch error', url, method, r.status)
const body = await r.json()
throw new Error('Failed to fetch ' + url, { cause: { body, status: r.status } })
throw new Error('Failed to fetch ' + url, { cause: body })
}
return await r.json()
@ -504,48 +400,13 @@ export class NoauthBackend {
})
} catch (e: any) {
console.log('error', e.cause)
if (e.cause && e.cause.body && e.cause.body.minPow > pow) pow = e.cause.body.minPow
if (e.cause && e.cause.minPow > pow) pow = e.cause.minPow
else throw e
}
}
throw new Error('Too many requests, retry later')
}
private async sendDeleteNameToServer(npub: string, name: string) {
const body = JSON.stringify({
npub,
name,
})
const method = 'DELETE'
const url = `${NOAUTHD_URL}/name`
return this.sendPostAuthd({
npub,
url,
method,
body,
})
}
private async sendTransferNameToServer(npub: string, name: string, newNpub: string) {
const body = JSON.stringify({
npub,
name,
newNpub,
})
const method = 'PUT'
const url = `${NOAUTHD_URL}/name`
return this.sendPostAuthd({
npub,
url,
method,
body,
})
}
private async sendTokenToServer(npub: string, token: string) {
const body = JSON.stringify({
npub,
@ -640,7 +501,6 @@ export class NoauthBackend {
return {
npub: k.npub,
nip05: k.nip05,
name: k.name,
locked: this.isLocked(k.npub),
}
}
@ -681,16 +541,11 @@ export class NoauthBackend {
await this.startKey({ npub, sk })
// assign nip05 before adding the key
// FIXME set name to db and if this call to 'send' fails
// then retry later
if (!existingName && name && !name.includes('@')) {
console.log('adding key', npub, name)
try {
await this.sendNameToServer(npub, name)
} catch (e) {
console.log('create name failed', e)
// clear it
await dbi.editName(npub, '')
dbKey.name = ''
}
await this.sendNameToServer(npub, name)
}
const sub = await this.swg.registration.pushManager.getSubscription()
@ -699,9 +554,7 @@ export class NoauthBackend {
return this.keyInfo(dbKey)
}
private getDecision(backend: Nip46Backend, req: DbPending): DECISION {
if (!(req.method in backend.handlers)) return DECISION.IGNORE
private getPerm(req: DbPending): string {
const reqPerm = getReqPerm(req)
const appPerms = this.perms.filter((p) => p.npub === req.npub && p.appNpub === req.appNpub)
@ -710,36 +563,27 @@ export class NoauthBackend {
// non-exact next
if (!perm) perm = appPerms.find((p) => isPackagePerm(p.perm, reqPerm))
if (perm) {
console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms)
return perm.value === '1' ? DECISION.ALLOW : DECISION.DISALLOW
}
const conn = appPerms.find((p) => p.perm === 'connect')
if (conn && conn.value === '0') {
console.log('req', req, 'perm', reqPerm, 'ignore by connect disallow')
return DECISION.IGNORE
}
return DECISION.ASK
console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms)
return perm?.value || ''
}
private async connectApp({
npub,
appNpub,
appUrl,
perms,
appName = '',
appIcon = '',
}: {
npub: string
appNpub: string
appUrl: string
appName?: string
appIcon?: string
perms: string[]
}) {
await dbi.addApp({
npub,
appNpub,
appUrl,
perms,
appName = '',
appIcon = ''
}: {
npub: string,
appNpub: string,
appUrl: string,
appName?: string,
appIcon?: string,
perms: string[]
}) {
await dbi.addApp({
appNpub: appNpub,
npub: npub,
timestamp: Date.now(),
@ -774,19 +618,19 @@ export class NoauthBackend {
method,
remotePubkey,
params,
}: IAllowCallbackParams): Promise<DECISION> {
}: IAllowCallbackParams): Promise<boolean> {
// same reqs usually come on reconnects
if (this.doneReqIds.includes(id)) {
console.log('request already done', id)
// FIXME maybe repeat the reply, but without the Notification?
return DECISION.IGNORE
return false
}
const appNpub = nip19.npubEncode(remotePubkey)
const connected = !!this.apps.find((a) => a.appNpub === appNpub)
if (!connected && method !== 'connect') {
console.log('ignoring request before connect', method, id, appNpub, npub)
return DECISION.IGNORE
return false
}
const req: DbPending = {
@ -801,21 +645,9 @@ export class NoauthBackend {
const self = this
return new Promise(async (ok) => {
// called when it's decided whether to allow this or not
const onAllow = async (manual: boolean, decision: DECISION, remember: boolean, options?: any) => {
const onAllow = async (manual: boolean, allow: boolean, remember: boolean, options?: any) => {
// confirm
console.log(Date.now(), decision, npub, method, options, params)
switch (decision) {
case DECISION.ASK:
throw new Error('Make a decision!')
case DECISION.IGNORE:
return // noop
case DECISION.ALLOW:
case DECISION.DISALLOW:
// fall through
}
const allow = decision === DECISION.ALLOW
console.log(Date.now(), allow ? 'allowed' : 'disallowed', npub, method, options, params)
if (manual) {
await dbi.confirmPending(id, allow)
@ -868,40 +700,35 @@ export class NoauthBackend {
// reload
this.perms = await dbi.listPerms()
}
// release this promise to send reply
// to this req
ok(decision)
// notify UI that it was confirmed
// if (!PERF_TEST)
this.updateUI()
// after replying to this req check pending
// reqs maybe they can be replied right away
if (remember) {
// confirm pending requests that might now have
// the proper perms
const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub)
console.log('updated perms', this.perms, 'otherReqs', otherReqs, 'connected', connected)
for (const r of otherReqs) {
const dec = this.getDecision(backend, r.req)
if (dec !== DECISION.ASK) {
r.cb(dec, false)
let perm = this.getPerm(r.req)
if (perm) {
r.cb(perm === '1', false)
}
}
}
// notify UI that it was confirmed
// if (!PERF_TEST)
this.updateUI()
// return to let nip46 flow proceed
ok(allow)
}
// check perms
const dec = this.getDecision(backend, req)
console.log(Date.now(), 'decision', req.id, dec)
const perm = this.getPerm(req)
console.log(Date.now(), 'perm', req.id, perm)
// have perm?
if (dec !== DECISION.ASK) {
if (perm) {
// reply immediately
onAllow(false, dec, false)
onAllow(false, perm === '1', false)
} else {
// put pending req to db
await dbi.addPending(req)
@ -912,7 +739,7 @@ export class NoauthBackend {
// put to a list of pending requests
this.confirmBuffer.push({
req,
cb: (decision, remember, options) => onAllow(true, decision, remember, options),
cb: (allow, remember, options) => onAllow(true, allow, remember, options),
})
// OAuth flow
@ -920,22 +747,12 @@ export class NoauthBackend {
const confirmMethod = isConnect ? 'confirm-connect' : 'confirm-event'
const authUrl = `${self.swg.location.origin}/key/${npub}?${confirmMethod}=true&appNpub=${appNpub}&reqId=${id}&popup=true`
console.log('sending authUrl', authUrl, 'for', req)
// NOTE: don't send auth_url immediately, wait some time
// to make sure other bunkers aren't replying
setTimeout(() => {
// request still there? (not dropped by the watcher)
if (self.confirmBuffer.find((r) => r.req.id === id)) {
// NOTE: if you set 'Update on reload' in the Chrome SW console
// then this message will cause a new tab opened by the peer,
// which will cause SW (this code) to reload, to fetch
// the pending requests and to re-send this event,
// looping for 10 seconds (our request age threshold)
backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl)
} else {
console.log("skip sending auth_url")
}
}, 500)
// NOTE: if you set 'Update on reload' in the Chrome SW console
// then this message will cause a new tab opened by the peer,
// which will cause SW (this code) to reload, to fetch
// the pending requests and to re-send this event,
// looping for 10 seconds (our request age threshold)
backend.rpc.sendResponse(id, remotePubkey, 'auth_url', KIND_RPC, authUrl)
// show notifs
// this.notify()
@ -955,30 +772,25 @@ export class NoauthBackend {
ndk.connect()
const signer = new NDKPrivateKeySigner(sk) // PrivateKeySigner
const backend = new Nip46Backend(ndk, signer, this.allowPermitCallback.bind(this)) // , () => Promise.resolve(true)
const watcher = new Watcher(ndk, signer, (id) => {
// drop pending request
const index = self.confirmBuffer.findIndex((r) => r.req.id === id)
if (index >= 0) self.confirmBuffer.splice(index, 1)
dbi.removePending(id).then(() => this.updateUI())
})
this.keys.push({ npub, backend, signer, ndk, backoff, watcher })
const backend = new NDKNip46Backend(ndk, signer, () => Promise.resolve(true))
this.keys.push({ npub, backend, signer, ndk, backoff })
// new method
// backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk)
backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk)
// // assign our own permission callback
// for (const method in backend.handlers) {
// backend.handlers[method] = new EventHandlingStrategyWrapper(
// backend,
// method,
// backend.handlers[method]
// )
// }
// assign our own permission callback
for (const method in backend.handlers) {
backend.handlers[method] = new EventHandlingStrategyWrapper(
backend,
npub,
method,
backend.handlers[method],
this.allowPermitCallback.bind(this)
)
}
// start
backend.start()
watcher.start()
console.log('started', npub)
// backoff reset on successfull connection
@ -1002,13 +814,11 @@ export class NoauthBackend {
const bo = self.keys.find((k) => k.npub === npub)?.backoff || 1000
setTimeout(() => {
console.log(new Date(), 'reconnect relays for key', npub, 'backoff', bo)
// @ts-ignore
for (const r of ndk.pool.relays.values()) r.disconnect()
// make sure it no longer activates
backend.handlers = {}
// stop watching
watcher.stop()
self.keys = self.keys.filter((k) => k.npub !== npub)
self.startKey({ npub, sk, backoff: Math.min(bo * 2, 60000) })
}, bo)
@ -1019,27 +829,6 @@ export class NoauthBackend {
r.on('connect', onConnect)
r.on('disconnect', onDisconnect)
}
const pendingEvents = this.pendingNpubEvents.get(npub)
if (pendingEvents) {
this.pendingNpubEvents.delete(npub)
for (const e of pendingEvents) {
backend.processEvent(e)
}
}
}
private async fetchPendingRequests(npub: string, appNpub: string) {
const { data: pubkey } = nip19.decode(npub)
const { data: appPubkey } = nip19.decode(appNpub)
const events = await this.ndk.fetchEvents({
kinds: [KIND_RPC],
'#p': [pubkey as string],
authors: [appPubkey as string],
})
console.log('fetched pending for', npub, events.size)
this.pendingNpubEvents.set(npub, [...events.values()])
}
public async unlock(npub: string) {
@ -1156,7 +945,7 @@ export class NoauthBackend {
this.updateUI()
} else {
console.log('confirming req', id, allow, remember, options)
req.cb(allow ? DECISION.ALLOW : DECISION.DISALLOW, remember, options)
req.cb(allow, remember, options)
}
}
@ -1174,36 +963,6 @@ export class NoauthBackend {
this.updateUI()
}
private async editName(npub: string, name: string) {
const key = this.enckeys.find((k) => k.npub === npub)
if (!key) throw new Error('Npub not found')
if (key.name) {
try {
await this.sendDeleteNameToServer(npub, key.name)
} catch (e: any) {
if (e.cause && e.cause.status !== 404) throw e
console.log("Deleted name didn't exist")
}
}
if (name) {
await this.sendNameToServer(npub, name)
}
await dbi.editName(npub, name)
key.name = name
this.updateUI()
}
private async transferName(npub: string, name: string, newNpub: string) {
const key = this.enckeys.find((k) => k.npub === npub)
if (!key) throw new Error('Npub not found')
if (!name) throw new Error('Empty name')
if (key.name !== name) throw new Error('Name changed, please reload')
await this.sendTransferNameToServer(npub, key.name, newNpub)
await dbi.editName(npub, '')
key.name = ''
this.updateUI()
}
private async enablePush(): Promise<boolean> {
const options = {
userVisibleOnly: true,
@ -1250,14 +1009,8 @@ export class NoauthBackend {
result = await this.deleteApp(args[0])
} else if (method === 'deletePerm') {
result = await this.deletePerm(args[0])
} else if (method === 'editName') {
result = await this.editName(args[0], args[1])
} else if (method === 'transferName') {
result = await this.transferName(args[0], args[1], args[2])
} else if (method === 'enablePush') {
result = await this.enablePush()
} else if (method === 'fetchPendingRequests') {
result = await this.fetchPendingRequests(args[0], args[1])
} else {
console.log('unknown method from UI ', method)
}
@ -1284,20 +1037,10 @@ export class NoauthBackend {
}
}
private async reloadUI() {
const clients = await this.swg.clients.matchAll({
includeUncontrolled: true,
})
console.log('reloadUI clients', clients.length)
for (const client of clients) {
client.postMessage({ result: 'reload' })
}
}
public async onPush(event: any) {
console.log('push', { data: event.data })
// noop - we just need browser to launch this worker
// FIXME use event.waitUntil and and unblock after we
// show a notification to avoid annoying the browser
// show a notification
}
}

View File

@ -89,16 +89,6 @@ export const dbi = {
return []
}
},
editName: async (npub: string, name: string): Promise<void> => {
try {
await db.keys.where({ npub }).modify({
name,
})
} catch (error) {
console.log(`db editName error: ${error}`)
return
}
},
getApp: async (appNpub: string) => {
try {
return await db.apps.get(appNpub)

View File

@ -43,7 +43,7 @@ export function isWeakPassphase(passphrase: string): boolean {
const compl = Math.pow(base, passphrase.length)
const thresh = Math.pow(11, 14)
// console.log({ big, small, number, punct, base, compl, thresh });
return compl < thresh
return compl < thresh;
}
export class Keys {

View File

@ -1,13 +1,10 @@
// service-worker client interface,
// works on the frontend, not sw
// service-worker client interface
import * as serviceWorkerRegistration from '../serviceWorkerRegistration'
export let swr: ServiceWorkerRegistration | null = null
const reqs = new Map<number, { ok: (r: any) => void; rej: (r: any) => void }>()
let nextReqId = 1
let onRender: (() => void) | null = null
let onReload: (() => void) | null = null
const queue: (() => Promise<void> | void)[] = []
export async function swicRegister() {
serviceWorkerRegistration.register({
@ -16,24 +13,18 @@ export async function swicRegister() {
swr = registration
},
onError(e) {
console.log('sw error', e)
},
onUpdate() {
// tell new SW that it should activate immediately
swr?.waiting?.postMessage({ type: 'SKIP_WAITING' })
console.log(`error ${e}`)
},
})
navigator.serviceWorker.ready.then(async (r) => {
console.log('sw ready, queue', queue.length)
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.')
}
while (queue.length) await queue.shift()!()
})
navigator.serviceWorker.addEventListener('message', (event) => {
@ -41,23 +32,12 @@ export async function swicRegister() {
})
}
export function swicWaitStarted() {
return new Promise<void>((ok) => {
if (swr && swr.active) ok()
else queue.push(ok)
})
}
function onMessage(data: any) {
const { id, result, error } = data
console.log('SW message', id, result, error)
if (!id) {
if (result === 'reload') {
if (onReload) onReload()
} else {
if (onRender) onRender()
}
if (onRender) onRender()
return
}
@ -77,31 +57,22 @@ export async function swicCall(method: string, ...args: any[]) {
nextReqId++
return new Promise((ok, rej) => {
const call = async () => {
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)
if (!swr || !swr.active) {
rej(new Error('No active service worker'))
return
}
if (swr && swr.active) call()
else queue.push(call)
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
}
export function swicOnReload(cb: () => void) {
onReload = cb
}

View File

@ -21,7 +21,6 @@ const commonTheme: Theme = createTheme({
styleOverrides: {
root: {
textTransform: 'initial',
color: 'red',
},
},
},
@ -97,6 +96,4 @@ const darkTheme: Theme = createTheme({
},
})
console.log(darkTheme)
export { lightTheme, darkTheme }

View File

@ -9,6 +9,7 @@ import { getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/help
import { Button } from '@/shared/Button/Button'
import { ACTION_TYPE } from '@/utils/consts'
import { Permissions } from './components/Permissions/Permissions'
import { StyledAppIcon } from './styled'
import { useToggleConfirm } from '@/hooks/useToggleConfirm'
import { ConfirmModal } from '@/shared/ConfirmModal/ConfirmModal'
import { swicCall } from '@/modules/swic'
@ -19,8 +20,6 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import MoreIcon from '@mui/icons-material/MoreVertRounded'
import { ModalAppDetails } from '@/components/Modal/ModalAppDetails/ModalAppDetails'
import { IconApp } from '@/shared/IconApp/IconApp'
import { HeadingContainer, AppInfoContainer, AppNameContainer } from './styled'
const AppPage = () => {
const keys = useAppSelector(selectKeys)
@ -44,10 +43,8 @@ const AppPage = () => {
const { icon = '', name = '', url = '' } = currentApp || {}
const appDomain = getDomain(url)
const shortAppNpub = getShortenNpub(appNpub)
const appName = name || appDomain || shortAppNpub
const appName = name || appDomain || getShortenNpub(appNpub)
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
const isAppNameExists = !!name || !!appDomain
const { timestamp } = connectPerm || {}
const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected'
@ -68,33 +65,22 @@ const AppPage = () => {
<>
<Stack maxHeight={'100%'} overflow={'auto'} alignItems={'flex-start'} height={'100%'}>
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
<HeadingContainer>
<IconApp size="big" picture={icon} alt={appAvatarTitle} />
<Box flex={'1'} overflow={'auto'} alignSelf={'flex-start'} width={'100%'}>
<AppInfoContainer>
<AppNameContainer>
<Typography className="app_name" variant="h4" noWrap>
{appName}
</Typography>
{isAppNameExists && (
<Typography noWrap display={'block'} variant="body1" color={'GrayText'}>
{shortAppNpub}
</Typography>
)}
</AppNameContainer>
<Stack marginBottom={'1rem'} direction={'row'} gap={'1rem'} width={'100%'}>
<StyledAppIcon src={icon}>{appAvatarTitle}</StyledAppIcon>
<Box flex={'1'} overflow={'hidden'}>
<Stack direction={'row'} alignItems={'center'} gap={'0.5rem'}>
<Typography variant="h4" noWrap flex={1}>
{appName}
</Typography>
<IconButton onClick={handleShowAppDetailsModal}>
<MoreIcon />
</IconButton>
</AppInfoContainer>
</Stack>
<Typography variant="body2" noWrap>
{connectedOn}
</Typography>
</Box>
</HeadingContainer>
</Stack>
<Box marginBottom={'1rem'}>
<SectionTitle marginBottom={'0.5rem'}>Disconnect</SectionTitle>
<Button fullWidth onClick={handleShow}>

View File

@ -1,4 +1,4 @@
import { FC } from 'react'
import React, { FC } from 'react'
import { DbHistory } from '@/modules/db'
import { Box, IconButton, Typography } from '@mui/material'
import { StyledActivityItem } from './styled'
@ -6,17 +6,16 @@ import { formatTimestampDate } from '@/utils/helpers/date'
import ClearRoundedIcon from '@mui/icons-material/ClearRounded'
import DoneRoundedIcon from '@mui/icons-material/DoneRounded'
import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded'
import { getReqActionName } from '@/utils/helpers/helpers'
import { ACTIONS } from '@/utils/consts'
type ItemActivityProps = DbHistory
export const ItemActivity: FC<ItemActivityProps> = (req) => {
const { allowed, timestamp } = req
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}>
{getReqActionName(req)}
{ACTIONS[method] || method}
</Typography>
<Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
</Box>

View File

@ -1,32 +1,6 @@
import { Box, BoxProps, Stack, StackProps, styled } from '@mui/material'
import { Avatar, AvatarProps, styled } from '@mui/material'
export const HeadingContainer = styled((props: StackProps) => <Stack {...props} />)(() => ({
width: '100%',
marginBottom: '1rem',
flexDirection: 'row',
gap: '1rem',
alignItems: 'center',
'@media screen and (max-width: 320px)': {
flexDirection: 'column',
gap: '0.5rem',
},
}))
export const AppInfoContainer = styled((props: StackProps) => <Stack {...props} direction={'row'} />)(() => ({
width: '100%',
flex: 1,
alignItems: 'flex-start',
gap: '0.5rem',
marginBottom: '0.5rem',
overflow: 'hidden',
'@media screen and (max-width: 320px)': {
alignSelf: 'flex-start',
},
}))
export const AppNameContainer = styled((props: BoxProps) => <Box {...props} />)(() => ({
display: 'flex',
flexDirection: 'column',
flex: 1,
overflow: 'auto',
export const StyledAppIcon = styled((props: AvatarProps) => <Avatar {...props} variant="rounded" />)(() => ({
width: 70,
height: 70,
}))

View File

@ -1,7 +1,6 @@
import { useCallback, useState } from 'react'
import { useAppSelector } from '../../store/hooks/redux'
import { Navigate, useParams, useSearchParams } from 'react-router-dom'
import { Box, IconButton, Stack } from '@mui/material'
import { Navigate, useParams } from 'react-router-dom'
import { Stack } from '@mui/material'
import { StyledIconButton } from './styled'
import { SettingsIcon, ShareIcon } from '@/assets'
import { Apps } from './components/Apps'
@ -19,20 +18,13 @@ import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
import { useLiveQuery } from 'dexie-react-hooks'
import { checkNpubSyncQuerier } from './utils'
import { DOMAIN } from '@/utils/consts'
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
import MoreHorizRoundedIcon from '@mui/icons-material/MoreHorizRounded'
import { ModalEditName } from '@/components/Modal/ModalEditName/ModalEditName'
import { useCallback } from 'react'
const KeyPage = () => {
const { npub = '' } = useParams<{ npub: string }>()
const { keys, apps, pending, perms } = useAppSelector((state) => state.content)
const [searchParams] = useSearchParams()
const [isCheckingSync, setIsChecking] = useState(true)
const handleStopChecking = () => setIsChecking(false)
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub, handleStopChecking), [npub], false)
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
const { handleOpen } = useModalSearchParams()
const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning()
@ -49,21 +41,10 @@ const KeyPage = () => {
const { prepareEventPendings } = useTriggerConfirmModal(npub, pending, perms)
const isKeyExists = npub.trim().length && key
const isPopup = searchParams.get('popup') === 'true'
// console.log({ isKeyExists, isPopup })
if (isPopup && !isKeyExists) {
searchParams.set('login', 'true')
searchParams.set('npub', npub)
const url = `/home?${searchParams.toString()}`
return <Navigate to={url} />
}
if (!isKeyExists) return <Navigate to={`/home`} />
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
const handleOpenEditNameModal = () => handleOpen(MODAL_PARAMS_KEYS.EDIT_NAME)
return (
<>
@ -74,20 +55,13 @@ const KeyPage = () => {
<UserValueSection
title="Your login"
value={username}
endAdornment={
<Box display={'flex'} alignItems={'center'} gap={'0.25rem'}>
<IconButton onClick={handleOpenEditNameModal} color={username ? 'default' : 'error'}>
<MoreHorizRoundedIcon />
</IconButton>
<InputCopyButton value={username} />
</Box>
}
copyValue={username}
explanationType={EXPLANATION_MODAL_KEYS.LOGIN}
/>
<UserValueSection
title="Your NPUB"
value={npub}
endAdornment={<InputCopyButton value={npub} />}
copyValue={npub}
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
/>
@ -97,11 +71,7 @@ const KeyPage = () => {
Connect app
</StyledIconButton>
<StyledIconButton
bgcolor_variant="secondary"
onClick={handleOpenSettingsModal}
withBadge={!isCheckingSync && !isSynced}
>
<StyledIconButton bgcolor_variant="secondary" onClick={handleOpenSettingsModal} withBadge={!isSynced}>
<SettingsIcon />
Settings
</StyledIconButton>
@ -109,13 +79,11 @@ const KeyPage = () => {
<Apps apps={filteredApps} npub={npub} />
</Stack>
<ModalConnectApp />
<ModalSettings isSynced={isSynced} />
<ModalExplanation />
<ModalConfirmConnect />
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
<ModalEditName />
</>
)
}

View File

@ -5,7 +5,9 @@ import { Box, Stack, Typography } from '@mui/material'
import { FC } from 'react'
import { StyledEmptyAppsBox } from '../styled'
import { Button } from '@/shared/Button/Button'
import { call } from '@/utils/helpers/helpers'
import { swicCall } from '@/modules/swic'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
import { ItemApp } from './ItemApp'
type AppsProps = {
@ -14,12 +16,22 @@ type AppsProps = {
}
export const Apps: FC<AppsProps> = ({ apps = [] }) => {
const notify = useEnqueueSnackbar()
// eslint-disable-next-line
async function deletePerm(id: string) {
call(async () => {
await swicCall('deletePerm', id)
notify('Perm deleted!', 'success')
})
}
const openAppStore = () => {
window.open('https://nostrapp.link', '_blank')
}
return (
<Box marginBottom={'1rem'} display={'flex'} flexDirection={'column'}>
<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" onClick={openAppStore} />
@ -33,7 +45,7 @@ export const Apps: FC<AppsProps> = ({ apps = [] }) => {
</StyledEmptyAppsBox>
)}
<Stack gap={'0.5rem'} overflow={'auto'} flex={1} paddingBottom={'0.75rem'}>
<Stack gap={'0.5rem'} overflow={'auto'} flex={1}>
{apps.map((a) => (
<ItemApp {...a} key={a.appNpub} />
))}

View File

@ -1,6 +1,6 @@
import { FC } from 'react'
import { Warning } from '@/components/Warning/Warning'
import { CircularProgress, Stack, Typography, TypographyProps, styled } from '@mui/material'
import { CircularProgress, Stack, Typography } from '@mui/material'
import AutoModeOutlinedIcon from '@mui/icons-material/AutoModeOutlined'
type BackgroundSigningWarningProps = {
@ -12,29 +12,18 @@ export const BackgroundSigningWarning: FC<BackgroundSigningWarningProps> = ({ is
return (
<Warning
message={
<Stack gap={'0.25rem'} overflow={'auto'} width={'100%'}>
<Typography variant="body1" noWrap fontWeight={'500'}>
Enable background service
</Typography>
<StyledHint>Please allow notifications for background operation.</StyledHint>
<Stack direction={'row'} alignItems={'center'} gap={'1rem'}>
Enable background service {isEnabling ? <CircularProgress size={'1.5rem'} /> : null}
</Stack>
}
icon={
isEnabling ? (
<CircularProgress size={'1.5rem'} sx={{ color: '#fff' }} />
) : (
<AutoModeOutlinedIcon htmlColor="white" />
)
hint={
<Typography variant='body2'>
Please allow notifications
for background operation.
</Typography>
}
icon={<AutoModeOutlinedIcon htmlColor="white" />}
onClick={isEnabling ? undefined : onEnableBackSigning}
/>
)
}
const StyledHint = styled((props: TypographyProps) => <Typography variant="body2" {...props} />)(() => ({
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
textOverflow: 'ellipsis',
}))

View File

@ -9,12 +9,9 @@ type ItemAppProps = DbApp
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name, url }) => {
const appDomain = getDomain(url)
const shortAppNpub = getShortenNpub(appNpub)
const appName = name || appDomain || shortAppNpub
const appName = name || appDomain || getShortenNpub(appNpub)
const appIcon = icon || `https://${appDomain}/favicon.ico`
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
const isAppNameExists = !!name || !!appDomain
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
return (
<StyledItemAppContainer
direction={'row'}
@ -24,18 +21,18 @@ export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name, url }) =>
component={Link}
to={`/key/${npub}/app/${appNpub}`}
>
<Avatar variant="rounded" sx={{ width: 56, height: 56 }} src={appIcon} alt={appName}>
<Avatar
variant="rounded"
sx={{ width: 56, height: 56 }}
src={appIcon}
alt={appName}
>
{appAvatarTitle}
</Avatar>
<Stack>
<Typography noWrap display={'block'} variant="body1">
<Typography noWrap display={'block'} variant="body2">
{appName}
</Typography>
{isAppNameExists && (
<Typography noWrap display={'block'} variant="body2" color={'GrayText'}>
{shortAppNpub}
</Typography>
)}
<Typography noWrap display={'block'} variant="caption" color={'GrayText'}>
Basic actions
</Typography>

View File

@ -3,6 +3,7 @@ import { Box, Stack } from '@mui/material'
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
import { AppLink } from '@/shared/AppLink/AppLink'
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
import { StyledInput } from '../styled'
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
@ -10,10 +11,10 @@ type UserValueSectionProps = {
title: string
value: string
explanationType: EXPLANATION_MODAL_KEYS
endAdornment?: React.ReactNode
copyValue: string
}
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, endAdornment }) => {
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, copyValue }) => {
const { handleOpen } = useModalSearchParams()
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
@ -29,7 +30,7 @@ const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanation
<SectionTitle>{title}</SectionTitle>
<AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} />
</Stack>
<StyledInput value={value} readOnly endAdornment={endAdornment} />
<StyledInput value={value} readOnly endAdornment={<InputCopyButton value={copyValue} />} />
</Box>
)
}

View File

@ -1,5 +1,4 @@
import { Input } from '@/shared/Input/Input'
import { AppInputProps } from '@/shared/Input/types'
import { Input, AppInputProps } from '@/shared/Input/Input'
import { Stack, StackProps, styled } from '@mui/material'
import { forwardRef } from 'react'

View File

@ -1,7 +1,7 @@
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { DbPending, DbPerm } from '@/modules/db'
import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { ACTION_TYPE, REQ_TTL } from '@/utils/consts'
import { ACTION_TYPE } from '@/utils/consts'
import { useCallback, useEffect, useRef } from 'react'
import { useSearchParams } from 'react-router-dom'
@ -25,7 +25,7 @@ export const useTriggerConfirmModal = (npub: string, pending: DbPending[], perms
const isConfirmConnectModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
const isConfirmEventModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
const filteredPendingReqs = pending.filter((p) => p.npub === npub && p.timestamp > Date.now() - REQ_TTL)
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)

View File

@ -1,5 +1,4 @@
import { Input } from '@/shared/Input/Input'
import { AppInputProps } from '@/shared/Input/types'
import { Input, AppInputProps } from '@/shared/Input/Input'
import { Box, Button, ButtonProps, styled, Badge } from '@mui/material'
import { forwardRef } from 'react'

View File

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

View File

@ -1,15 +1,13 @@
import { Suspense, lazy } from 'react'
import { Route, Routes, Navigate } from 'react-router-dom'
import HomePage from '../pages/HomePage/Home.Page'
import { Layout } from '../layout/Layout'
import { CircularProgress, Stack } from '@mui/material'
// Pages
import CreatePage from '@/pages/CreatePage/Create.Page'
import HomePage from '../pages/HomePage/Home.Page'
import KeyPage from '../pages/KeyPage/Key.Page'
const ConfirmPage = lazy(() => import('@/pages/Confirm.Page'))
const AppPage = lazy(() => import('@/pages/AppPage/App.Page'))
const KeyPage = lazy(() => import('../pages/KeyPage/Key.Page'))
const ConfirmPage = lazy(() => import('../pages/Confirm.Page'))
const AppPage = lazy(() => import('../pages/AppPage/App.Page'))
const LoadingSpinner = () => (
<Stack height={'100%'} justifyContent={'center'} alignItems={'center'}>
@ -23,6 +21,7 @@ const AppRoutes = () => {
<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 />} />

View File

@ -19,9 +19,6 @@ const StyledButton = styled(
const commonStyles = {
fontWeight: 500,
borderRadius: '1rem',
'@media screen and (max-width: 320px)': {
padding: '0.25rem 0.75rem',
},
}
if (varianttype === 'secondary') {
return {
@ -42,9 +39,9 @@ const StyledButton = styled(
background: theme.palette.primary.main,
},
color: theme.palette.text.secondary,
'&.button.disabled': {
'&.disabled': {
color: theme.palette.text.secondary,
background: `${theme.palette.primary.main}75`,
background: `${theme.palette.primary.main}50`,
cursor: 'not-allowed',
},
}

View File

@ -1,6 +1,5 @@
import { useRef } from 'react'
import { Input } from '../Input/Input'
import { AppInputProps } from '../Input/types'
import { Input, AppInputProps } from '../Input/Input'
export type DebounceProps = {
handleDebounce: (value: string) => void

View File

@ -9,8 +9,7 @@ export const StyledButton = styled((props: ButtonProps) => (
startIcon: 'icon',
}}
/>
))(({ theme }) => ({
color: theme.palette.primary.main,
))(() => ({
marginBottom: '0.5rem',
borderRadius: '8px',
'&:is(:hover,:active)': {
@ -19,7 +18,4 @@ export const StyledButton = styled((props: ButtonProps) => (
'& .icon': {
marginRight: '5px',
},
'@media screen and (max-width: 320px)': {
marginBottom: '0.25rem',
},
}))

View File

@ -1,11 +1,8 @@
import { FC, useEffect, useState } from 'react'
import { StyledAppIcon, StyledAppImg } from './styled'
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'
import { IIconApp } from './types'
import React, { FC, useEffect, useState } from 'react'
const failedCache = new Map<string, boolean>()
export const IconApp: FC<IIconApp> = ({ picture = '', alt, isRounded, isSmall, onClick, size, ...rest }) => {
export const IconApp: FC<{ picture: string }> = ({ picture }) => {
const c = failedCache.get(picture)
const [isFailed, setIsFailed] = useState(c !== undefined ? c : true)
@ -29,21 +26,5 @@ export const IconApp: FC<IIconApp> = ({ picture = '', alt, isRounded, isSmall, o
}
}, [picture])
return (
<StyledAppIcon isNotLoaded={isFailed} size={size} onClick={onClick} {...rest}>
{alt ? (
<StyledAppImg size={size} alt={alt} isSmall={isSmall} src={isFailed ? '' : picture}>
{isFailed && (
<div className="MuiAvatar-root MuiAvatar-square MuiAvatar-colorDefault">
{alt.substring(0, 1).toUpperCase()}
</div>
)}
</StyledAppImg>
) : (
<StyledAppImg size={size} alt={alt} isSmall={isSmall} src={isFailed ? '/' : picture}>
<ImageOutlinedIcon fontSize="inherit" />
</StyledAppImg>
)}
</StyledAppIcon>
)
return <div>IconApp</div>
}

View File

@ -1,53 +0,0 @@
import { APP_NSEC_SIZE } from '@/utils/consts'
const SIZE_VALUE = {
[APP_NSEC_SIZE.BIG]: 70,
[APP_NSEC_SIZE.LARGE]: 56,
[APP_NSEC_SIZE.MEDIUM]: 40,
[APP_NSEC_SIZE.SMALL]: 36,
[APP_NSEC_SIZE.EXTRA_SMALL]: 30,
}
export const APP_SIZE_VALUE = {
[APP_NSEC_SIZE.BIG]: {
height: SIZE_VALUE[APP_NSEC_SIZE.BIG],
minWidth: SIZE_VALUE[APP_NSEC_SIZE.BIG],
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.BIG],
},
[APP_NSEC_SIZE.LARGE]: {
height: SIZE_VALUE[APP_NSEC_SIZE.LARGE],
minWidth: SIZE_VALUE[APP_NSEC_SIZE.LARGE],
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.LARGE],
},
[APP_NSEC_SIZE.MEDIUM]: {
height: SIZE_VALUE[APP_NSEC_SIZE.MEDIUM],
minWidth: SIZE_VALUE[APP_NSEC_SIZE.MEDIUM],
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.MEDIUM],
},
[APP_NSEC_SIZE.SMALL]: {
height: SIZE_VALUE[APP_NSEC_SIZE.SMALL],
minWidth: SIZE_VALUE[APP_NSEC_SIZE.SMALL],
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.SMALL],
},
[APP_NSEC_SIZE.EXTRA_SMALL]: {
height: SIZE_VALUE[APP_NSEC_SIZE.EXTRA_SMALL],
minWidth: SIZE_VALUE[APP_NSEC_SIZE.EXTRA_SMALL],
maxWidth: SIZE_VALUE[APP_NSEC_SIZE.EXTRA_SMALL],
},
}
const FONT_SIZE_VALUE = {
[APP_NSEC_SIZE.BIG]: 24,
[APP_NSEC_SIZE.LARGE]: 20,
[APP_NSEC_SIZE.MEDIUM]: 16,
[APP_NSEC_SIZE.SMALL]: 12,
[APP_NSEC_SIZE.EXTRA_SMALL]: 10,
}
export const APP_NAME_FONT_SIZE_VALUE = {
[APP_NSEC_SIZE.LARGE]: FONT_SIZE_VALUE[APP_NSEC_SIZE.LARGE],
[APP_NSEC_SIZE.BIG]: FONT_SIZE_VALUE[APP_NSEC_SIZE.BIG],
[APP_NSEC_SIZE.MEDIUM]: FONT_SIZE_VALUE[APP_NSEC_SIZE.MEDIUM],
[APP_NSEC_SIZE.SMALL]: FONT_SIZE_VALUE[APP_NSEC_SIZE.SMALL],
[APP_NSEC_SIZE.EXTRA_SMALL]: FONT_SIZE_VALUE[APP_NSEC_SIZE.EXTRA_SMALL],
}

View File

@ -1,54 +0,0 @@
import { Avatar, Box, styled } from '@mui/material'
import { forwardRef } from 'react'
import { IAvatarStyled, IBoxStyled } from './types'
import { grey } from '@mui/material/colors'
import { AppNostrSize } from '@/types/app-nsec'
import { APP_NAME_FONT_SIZE_VALUE, APP_SIZE_VALUE } from './const'
import { APP_NSEC_SIZE } from '@/utils/consts'
const color = grey[500]
const getVariantApp = (isRounded: boolean, size: AppNostrSize) => {
if (isRounded) {
return {
height: 34,
minWidth: 34,
maxWidth: 34,
borderRadius: '7px',
}
}
return APP_SIZE_VALUE[size]
}
export const StyledAppIcon = styled(
forwardRef<HTMLAnchorElement, IBoxStyled>(function BoxDisplayName(props, ref) {
return <Box ref={ref} {...props} />
})
)(({ theme, isNotLoaded, isRounded = false, size = APP_NSEC_SIZE.MEDIUM }) => ({
position: 'relative',
borderRadius: theme.shape.borderRadius,
overflow: 'hidden',
...getVariantApp(isRounded, size),
transition: theme.transitions.create(['border-color', 'transition']),
backgroundColor: isNotLoaded ? color : theme.palette.background.default,
boxSizing: 'border-box',
':active': {
borderColor: 'rgba(255, 255, 255, 0.3)',
},
}))
export const StyledAppImg = styled(function BoxDisplayName(props: IAvatarStyled) {
return <Avatar variant="square" {...props} />
})(({ isSmall = false, size = APP_NSEC_SIZE.MEDIUM }) => ({
position: 'absolute',
left: 0,
top: 0,
height: '100%',
width: '100%',
fontWeight: '500',
fontSize: APP_NAME_FONT_SIZE_VALUE[size],
'.MuiAvatar-img': {
objectFit: isSmall ? 'scale-down' : 'cover',
},
}))

View File

@ -1,24 +0,0 @@
import { AppNostrSize } from '@/types/app-nsec'
import { AvatarProps, BoxProps } from '@mui/material'
export type IconAppProps = {
size?: AppNostrSize
isRounded?: boolean
isSmall?: boolean
onClick?: () => void
isNotLoaded?: boolean
}
export type IconAppBase = {
picture: string
alt?: string
}
export type IIconApp = Omit<IconAppProps, 'isNotLoaded'> & IconAppBase
export type IBoxStyled = IconAppProps & BoxProps
export type IAvatarStyled = {
size?: AppNostrSize
isSmall?: boolean
} & AvatarProps

View File

@ -1,34 +1,69 @@
import { ReactNode, forwardRef } from 'react'
import { FormHelperText, FormLabel, InputBase } from '@mui/material'
import { StyledInputContainer } from './styled'
import { AppInputProps } from './types'
import {
Box,
BoxProps,
FormHelperText,
FormHelperTextProps,
FormLabel,
InputBase,
InputBaseProps,
styled,
} from '@mui/material'
const renderItem = <T,>(item: T, value: ReactNode) => (item ? value : null)
export type AppInputProps = InputBaseProps & {
helperText?: string | ReactNode
helperTextProps?: FormHelperTextProps
containerProps?: BoxProps
label?: string
}
export const Input = forwardRef<HTMLInputElement, AppInputProps>(
({ helperText, containerProps, helperTextProps, label, ...props }, ref) => {
return (
<StyledInputContainer {...containerProps}>
{renderItem(
label,
{label ? (
<FormLabel className="label" htmlFor={props.id}>
{label}
</FormLabel>
)}
<InputBase
autoComplete="off"
{...props}
classes={{ error: 'error', root: 'input_root', input: 'input', disabled: 'disabled' }}
ref={ref}
/>
{renderItem(
helperText,
) : null}
<InputBase autoComplete="off" className="input" {...props} classes={{ error: 'error' }} ref={ref} />
{helperText ? (
<FormHelperText {...helperTextProps} className="helper_text">
{helperText}
</FormHelperText>
)}
) : null}
</StyledInputContainer>
)
}
)
const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({ theme }) => {
const isDark = theme.palette.mode === 'dark'
return {
width: '100%',
'& > .input': {
background: isDark ? '#000000A8' : '#000',
color: theme.palette.common.white,
padding: '0.75rem 1rem',
borderRadius: '1rem',
border: '0.3px solid #FFFFFF54',
fontSize: '0.875rem',
'& input::placeholder': {
color: '#fff',
},
'&.error': {
border: '0.3px solid ' + theme.palette.error.main,
},
},
'& > .helper_text': {
margin: '0.5rem 1rem 0',
color: theme.palette.text.primary,
},
'& > .label': {
margin: '0 1rem 0.5rem',
display: 'block',
color: theme.palette.primary.main,
fontSize: '0.875rem',
},
}
})

View File

@ -1,45 +0,0 @@
import { Box, BoxProps, styled } from '@mui/material'
export const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({ theme }) => {
const isDark = theme.palette.mode === 'dark'
return {
width: '100%',
'& > .input_root': {
background: isDark ? '#000000A8' : '#000',
color: theme.palette.common.white,
padding: '0.75rem 1rem',
borderRadius: '1rem',
border: '0.3px solid #FFFFFF54',
fontSize: '0.875rem',
'&.error': {
border: '0.3px solid ' + theme.palette.error.main,
},
},
'& .input:is(.disabled, &)': {
WebkitTextFillColor: '#ffffff80',
},
'& > .helper_text': {
margin: '0.5rem 0.5rem 0',
color: theme.palette.text.primary,
},
'& > .label': {
margin: '0 1rem 0.5rem',
display: 'block',
color: theme.palette.primary.main,
fontSize: '0.875rem',
},
'@media screen and (max-width: 320px)': {
'& > .input_root': {
padding: '0.5rem 0.75rem',
borderRadius: '0.75rem',
},
'& > .label': {
margin: '0 0.25rem 0.25rem',
fontSize: '0.75rem',
},
'& > .helper_text': {
fontSize: '0.75rem',
},
},
}
})

View File

@ -1,9 +0,0 @@
import { BoxProps, FormHelperTextProps, InputBaseProps } from '@mui/material'
import { ReactNode } from 'react'
export type AppInputProps = InputBaseProps & {
helperText?: string | ReactNode
helperTextProps?: FormHelperTextProps
containerProps?: BoxProps
label?: string
}

View File

@ -1,7 +1,8 @@
import { FC, useEffect, useState } from 'react'
import { Fade, Typography } from '@mui/material'
import { Fade, IconButton, Typography } from '@mui/material'
import CopyToClipboard from 'react-copy-to-clipboard'
import { StyledContainer, StyledCopyButton } from './styled'
import { CopyIcon } from '@/assets'
import { StyledContainer } from './styled'
type InputCopyButtonProps = {
value: string
@ -39,7 +40,9 @@ export const InputCopyButton: FC<InputCopyButtonProps> = ({ value, onCopy = () =
</Fade>
)}
<CopyToClipboard text={value} onCopy={handleCopy}>
<StyledCopyButton />
<IconButton color="inherit">
<CopyIcon />
</IconButton>
</CopyToClipboard>
</StyledContainer>
)

View File

@ -1,17 +1,7 @@
import { IconButton, Stack, StackProps, styled } from '@mui/material'
import { CopyIcon } from '@/assets'
import { Stack, StackProps, styled } from '@mui/material'
export const StyledContainer = styled((props: StackProps & { copied: number }) => (
<Stack {...props} direction={'row'} alignItems={'center'} />
))(({ theme, copied }) => ({
color: copied ? theme.palette.success.main : theme.palette.textSecondaryDecorate.main,
}))
export const StyledCopyButton = styled((props) => (
<IconButton color="inherit" {...props}>
<CopyIcon />
</IconButton>
))(() => ({
width: 40,
height: 40,
}))

View File

@ -2,7 +2,16 @@ import { combineReducers, configureStore } from '@reduxjs/toolkit'
import { contentSlice } from './reducers/content.slice'
import { uiSlice } from './reducers/ui.slice'
import { persistStore, persistReducer } from 'redux-persist'
import {
persistStore,
persistReducer,
// FLUSH,
// REGISTER,
// REHYDRATE,
// PAUSE,
// PERSIST,
// PURGE,
} from 'redux-persist'
import storage from 'redux-persist/lib/storage' // defaults to localStorage for web
import memoizeOne from 'memoize-one'
import isDeepEqual from 'lodash.isequal'

View File

@ -1,6 +0,0 @@
import { APP_NSEC_SIZE } from '@/utils/consts'
import { OverridableStringUnion } from './utils'
export type AppNostrSizeUnion = (typeof APP_NSEC_SIZE)[keyof typeof APP_NSEC_SIZE]
export type AppNostrSize = OverridableStringUnion<AppNostrSizeUnion>

View File

@ -10,7 +10,6 @@ export enum MODAL_PARAMS_KEYS {
CONFIRM_EVENT = 'confirm-event',
ACTIVITY = 'activity',
APP_DETAILS = 'app-details',
EDIT_NAME = 'edit-name',
}
export enum EXPLANATION_MODAL_KEYS {

View File

@ -1,10 +0,0 @@
export type OverridableStringUnion<T extends string> = GenerateStringUnion<DistributiveOmit<Record<T, true>>>
type GenerateStringUnion<T> = Extract<
{
[Key in keyof T]: true extends T[Key] ? Key : never
}[keyof T],
string
>
type DistributiveOmit<T> = T extends T ? T : never

View File

@ -9,10 +9,6 @@ export const MAX_POW = 19
export const KIND_RPC = 24133
export const RELOAD_STORAGE_KEY = 'reload'
export const REQ_TTL = 60000 // 1 min
export enum ACTION_TYPE {
BASIC = 'basic',
ADVANCED = 'advanced',
@ -20,18 +16,10 @@ export enum ACTION_TYPE {
}
export const ACTIONS: { [type: string]: string } = {
basic: 'Basic permissions',
basic: 'Basic permissions',
get_public_key: 'Get public key',
sign_event: 'Sign event',
connect: 'Connect',
nip04_encrypt: 'Encrypt message',
nip04_decrypt: 'Decrypt message',
}
export const APP_NSEC_SIZE = {
BIG: 'big',
LARGE: 'large',
MEDIUM: 'medium',
SMALL: 'small',
EXTRA_SMALL: 'extra-small',
} as const

View File

@ -3,7 +3,7 @@ import { format } from 'date-fns'
export const formatTimestampDate = (timestamp: number) => {
try {
const date = new Date(timestamp)
const formattedDate = format(date, 'dd-MM-yyyy HH:mm')
const formattedDate = format(date, "dd-MM-yyyy HH:mm")
return formattedDate
} catch (error) {
return ''

View File

@ -1,6 +1,6 @@
import { nip19 } from 'nostr-tools'
import { ACTIONS, ACTION_TYPE, DOMAIN, NIP46_RELAYS, NOAUTHD_URL } from '../consts'
import { DbHistory, DbPending, DbPerm } from '@/modules/db'
import { ACTIONS, ACTION_TYPE, DOMAIN, NIP46_RELAYS } from '../consts'
import { DbPending, DbPerm } from '@/modules/db'
import { MetaEvent } from '@/types/meta-event'
export async function call(cb: () => any) {
@ -16,7 +16,7 @@ export const getShortenNpub = (npub = '') => {
}
export const getProfileUsername = (profile: MetaEvent | null) => {
if (!profile) return undefined
if (!profile) return null
return profile?.info?.name || profile?.info?.display_name
}
@ -72,7 +72,6 @@ export function isPackagePerm(perm: string, reqPerm: string) {
case 'sign_event:10002':
case 'sign_event:30023':
case 'sign_event:10000':
case 'sign_event:27235':
return true
}
}
@ -98,21 +97,6 @@ export async function fetchNip05(value: string, origin?: string) {
}
}
export async function fetchNpubNames(npub: string) {
try {
const url = `${NOAUTHD_URL}/name?npub=${npub}`
const response = await fetch(url)
const names: {
names: string[]
} = await response.json()
return names.names
} catch (e) {
console.log('Failed to fetch names for', npub, 'error: ' + e)
return []
}
}
export const getDomain = (url: string) => {
try {
return new URL(url).hostname
@ -122,7 +106,7 @@ export const getDomain = (url: string) => {
}
export const getReferrerAppUrl = () => {
// console.log('referrer', window.document.referrer)
console.log('referrer', window.document.referrer)
if (!window.document.referrer) return ''
try {
const u = new URL(window.document.referrer.toLocaleLowerCase())
@ -135,7 +119,7 @@ export const getAppIconTitle = (name: string | undefined, appNpub: string) => {
return name ? name[0].toLocaleUpperCase() : appNpub.substring(4, 7)
}
export function getReqActionName(req: DbPending | DbHistory) {
export function getReqActionName(req: DbPending) {
const action = ACTIONS[req.method]
if (req.method === 'sign_event') {
const kind = getSignReqKind(req)

View File

@ -13,7 +13,6 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"downlevelIteration": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",