Compare commits
25 Commits
better-con
...
feature/ap
Author | SHA1 | Date | |
---|---|---|---|
|
ec544a0592 | ||
|
72d561f8c9 | ||
|
f408fd1b38 | ||
|
2b6a1e1e5d | ||
|
104404b04c | ||
|
e4fdb7794a | ||
|
1566592683 | ||
|
52b119b424 | ||
|
12afbaa76b | ||
|
4aa4f7f175 | ||
|
7aaea89f21 | ||
|
89fc5b0ae0 | ||
|
48c07ad1c0 | ||
|
caf8f9a82b | ||
|
449bdb79ce | ||
|
d16c3cd9b0 | ||
|
fe4705afc8 | ||
|
9d565ddbde | ||
|
c5c5843cb8 | ||
|
34bf3f7c12 | ||
|
d3ab9174e1 | ||
|
8faccc383b | ||
|
1305af6896 | ||
|
593fafd9f8 | ||
|
2ba1eaef65 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Noauth",
|
||||
"short_name": "Noauth Nostr key manager",
|
||||
"name": "Nsec.app",
|
||||
"short_name": "Nsec.app - Nostr key management tool",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
|
@@ -45,11 +45,7 @@ function App() {
|
||||
const apps = await dbi.listApps()
|
||||
dispatch(
|
||||
setApps({
|
||||
apps: apps.map((app) => ({
|
||||
...app,
|
||||
// MOCK IMAGE
|
||||
icon: 'https://nostr.band/android-chrome-192x192.png',
|
||||
})),
|
||||
apps,
|
||||
})
|
||||
)
|
||||
|
||||
|
173
src/components/Modal/ModalAppDetails/ModalAppDetails.tsx
Normal file
173
src/components/Modal/ModalAppDetails/ModalAppDetails.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Autocomplete, CircularProgress, Stack, Typography } from '@mui/material'
|
||||
import { StyledInput } from './styled'
|
||||
import { FormEvent, useEffect, useState } from 'react'
|
||||
import { isEmptyString } from '@/utils/helpers/helpers'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectApps } from '@/store'
|
||||
import { dbi } from '@/modules/db'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { setApps } from '@/store/reducers/content.slice'
|
||||
|
||||
export const ModalAppDetails = () => {
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.APP_DETAILS)
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.APP_DETAILS)
|
||||
|
||||
const { appNpub = '' } = useParams()
|
||||
const apps = useAppSelector(selectApps)
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
const [details, setDetails] = useState({
|
||||
url: '',
|
||||
name: '',
|
||||
icon: '',
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const currentApp = apps.find((app) => app.appNpub === appNpub)
|
||||
if (!currentApp) return
|
||||
|
||||
setDetails({
|
||||
icon: currentApp.icon || '',
|
||||
name: currentApp.name || '',
|
||||
url: currentApp.url || '',
|
||||
})
|
||||
|
||||
// eslint-disable-next-line
|
||||
}, [appNpub, isModalOpened])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isModalOpened) {
|
||||
// modal closed
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
}, [isModalOpened])
|
||||
|
||||
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||||
|
||||
if (isModalOpened && !isAppNpubExists) {
|
||||
handleCloseModal()
|
||||
return null
|
||||
}
|
||||
|
||||
const { icon, name, url } = details
|
||||
|
||||
const handleInputBlur = () => {
|
||||
if (isEmptyString(url)) return
|
||||
|
||||
try {
|
||||
const u = new URL(url)
|
||||
|
||||
if (isEmptyString(name)) setDetails((prev) => ({ ...prev, name: u.hostname }))
|
||||
if (isEmptyString(icon)) {
|
||||
const iconUrl = `https://${u.hostname}/favicon.ico`
|
||||
setDetails((prev) => ({ ...prev, icon: iconUrl }))
|
||||
}
|
||||
} catch {
|
||||
/* empty */
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputChange = (key: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setDetails((prevState) => {
|
||||
return { ...prevState, [key]: e.target.value }
|
||||
})
|
||||
}
|
||||
|
||||
const handleAutocompletInputChange = (e: unknown, value: string) => {
|
||||
setDetails((prevState) => {
|
||||
return { ...prevState, url: value }
|
||||
})
|
||||
}
|
||||
|
||||
const submitHandler = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (isLoading) return undefined
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const updatedApp = {
|
||||
url,
|
||||
name,
|
||||
icon,
|
||||
appNpub,
|
||||
}
|
||||
await dbi.updateApp(updatedApp)
|
||||
const apps = await dbi.listApps()
|
||||
dispatch(
|
||||
setApps({
|
||||
apps,
|
||||
})
|
||||
)
|
||||
notify(`App successfully updated!`, 'success')
|
||||
setIsLoading(false)
|
||||
handleCloseModal()
|
||||
} catch (error: any) {
|
||||
setIsLoading(false)
|
||||
notify(error?.message || 'Something went wrong!', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const isFormValid = !isEmptyString(url) && !isEmptyString(name) && !isEmptyString(icon)
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack gap={'1rem'} component={'form'} onSubmit={submitHandler}>
|
||||
<Stack alignItems={'center'}>
|
||||
<Typography fontWeight={600} variant="h5">
|
||||
App details
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Autocomplete
|
||||
options={[]}
|
||||
freeSolo
|
||||
onBlur={handleInputBlur}
|
||||
onInputChange={handleAutocompletInputChange}
|
||||
inputValue={details.url}
|
||||
renderInput={({ inputProps, disabled, id, InputProps }) => {
|
||||
return (
|
||||
<StyledInput
|
||||
{...InputProps}
|
||||
className="input"
|
||||
inputProps={inputProps}
|
||||
disabled={disabled}
|
||||
label="URL"
|
||||
fullWidth
|
||||
placeholder="Enter URL"
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label="Name"
|
||||
fullWidth
|
||||
placeholder="Enter app name"
|
||||
onChange={handleInputChange('name')}
|
||||
value={details.name}
|
||||
/>
|
||||
<Input
|
||||
label="Icon"
|
||||
fullWidth
|
||||
placeholder="Enter app icon url"
|
||||
onChange={handleInputChange('icon')}
|
||||
value={details.icon}
|
||||
/>
|
||||
|
||||
<Button varianttype="secondary" type="submit" fullWidth disabled={!isFormValid || isLoading}>
|
||||
Save changes {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
)
|
||||
}
|
11
src/components/Modal/ModalAppDetails/styled.tsx
Normal file
11
src/components/Modal/ModalAppDetails/styled.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AppInputProps, Input } from '@/shared/Input/Input'
|
||||
import { styled } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const StyledInput = styled(
|
||||
forwardRef<HTMLInputElement, AppInputProps>((props, ref) => <Input {...props} ref={ref} />)
|
||||
)(() => ({
|
||||
'& .MuiAutocomplete-endAdornment': {
|
||||
right: '1rem',
|
||||
},
|
||||
}))
|
@@ -1,55 +1,79 @@
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { call, getAppIconTitle, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { call, getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { Avatar, Box, Stack, Typography } from '@mui/material'
|
||||
import { useParams, useSearchParams } from 'react-router-dom'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectAppsByNpub } from '@/store'
|
||||
import { selectAppsByNpub, selectKeys, selectPendingsByNpub } from '@/store'
|
||||
import { StyledButton, StyledToggleButtonsGroup } from './styled'
|
||||
import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
||||
import { useState } from 'react'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { ACTION_TYPE } from '@/utils/consts'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
|
||||
export const ModalConfirmConnect = () => {
|
||||
const keys = useAppSelector(selectKeys)
|
||||
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const notify = useEnqueueSnackbar()
|
||||
const navigate = useNavigate()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_CONNECT)
|
||||
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const [searchParams] = useSearchParams()
|
||||
const paramNpub = searchParams.get('npub') || ''
|
||||
const { npub = paramNpub } = useParams<{ npub: string }>()
|
||||
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
|
||||
const pending = useAppSelector((state) => selectPendingsByNpub(state, npub))
|
||||
|
||||
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.BASIC)
|
||||
|
||||
const [searchParams] = useSearchParams()
|
||||
const appNpub = searchParams.get('appNpub') || ''
|
||||
const pendingReqId = searchParams.get('reqId') || ''
|
||||
const isPopup = searchParams.get('popup') === 'true'
|
||||
const token = searchParams.get('token') || ''
|
||||
|
||||
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
||||
const { name, icon = '' } = triggerApp || {}
|
||||
const appName = name || getShortenNpub(appNpub)
|
||||
const appAvatarTitle = getAppIconTitle(name, appNpub)
|
||||
const { name, url = '', icon = '' } = triggerApp || {}
|
||||
|
||||
let appUrl = url || searchParams.get('appUrl') || ''
|
||||
// console.log('referrer', window.document.referrer, appUrl)
|
||||
if (!appUrl && window.document.referrer) {
|
||||
try {
|
||||
const u = new URL(window.document.referrer)
|
||||
appUrl = u.origin
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const appDomain = getDomain(appUrl)
|
||||
const appName = name || appDomain || getShortenNpub(appNpub)
|
||||
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||
const appIcon = icon || (appDomain ? `https://${appDomain}/favicon.ico` : '')
|
||||
|
||||
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
sp.delete('popup')
|
||||
sp.delete('npub')
|
||||
sp.delete('appUrl')
|
||||
},
|
||||
})
|
||||
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||||
const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId)
|
||||
if (isModalOpened && (!isNpubExists || !isAppNpubExists || !isPendingReqIdExists)) {
|
||||
closeModalAfterRequest()
|
||||
return null
|
||||
}
|
||||
|
||||
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
||||
if (!value) return undefined
|
||||
return setSelectedActionType(value)
|
||||
}
|
||||
|
||||
// const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||
// onClose: async (sp) => {
|
||||
// sp.delete('appNpub')
|
||||
// sp.delete('reqId')
|
||||
// await swicCall('confirm', pendingReqId, false, false)
|
||||
// },
|
||||
// })
|
||||
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
},
|
||||
})
|
||||
|
||||
async function confirmPending(id: string, allow: boolean, remember: boolean, options?: any) {
|
||||
call(async () => {
|
||||
await swicCall('confirm', id, allow, remember, options)
|
||||
@@ -59,16 +83,53 @@ export const ModalConfirmConnect = () => {
|
||||
if (isPopup) window.close()
|
||||
}
|
||||
|
||||
const allow = () => {
|
||||
const options: any = {}
|
||||
if (selectedActionType === ACTION_TYPE.BASIC) options.perms = [ACTION_TYPE.BASIC]
|
||||
// else
|
||||
// options.perms = ['connect','get_public_key'];
|
||||
confirmPending(pendingReqId, true, true, options)
|
||||
const allow = async () => {
|
||||
let perms = ['connect', 'get_public_key']
|
||||
if (selectedActionType === ACTION_TYPE.BASIC) perms = [ACTION_TYPE.BASIC]
|
||||
|
||||
if (pendingReqId) {
|
||||
const options = { perms }
|
||||
await confirmPending(pendingReqId, true, true, options)
|
||||
} else {
|
||||
try {
|
||||
await swicCall('enablePush')
|
||||
console.log('enablePush done')
|
||||
} catch (e: any) {
|
||||
console.log('error', e)
|
||||
notify('Please enable Notifications in website settings!', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await swicCall('connectApp', { npub, appNpub, appUrl, perms })
|
||||
console.log('connectApp done', npub, appNpub, appUrl, perms)
|
||||
} catch (e: any) {
|
||||
notify(e.toString(), 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (token) {
|
||||
try {
|
||||
await swicCall('redeemToken', npub, token)
|
||||
console.log('redeemToken done')
|
||||
} catch (e) {
|
||||
console.log('error', e)
|
||||
notify('App did not reply. Please try to log in now.', 'error')
|
||||
navigate(`/key/${npub}`, { replace: true })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
notify('App connected! Closing...', 'success')
|
||||
|
||||
if (isPopup) setTimeout(() => window.close(), 3000)
|
||||
else navigate(`/key/${npub}`, { replace: true })
|
||||
}
|
||||
}
|
||||
|
||||
const disallow = () => {
|
||||
confirmPending(pendingReqId, false, true)
|
||||
if (pendingReqId) confirmPending(pendingReqId, false, true)
|
||||
else closeModalAfterRequest()
|
||||
}
|
||||
|
||||
if (isPopup) {
|
||||
@@ -80,10 +141,14 @@ export const ModalConfirmConnect = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title='Connection request' open={isModalOpened} withCloseButton={false}
|
||||
// withCloseButton={!isPopup} onClose={!isPopup ? handleCloseModal : undefined}
|
||||
>
|
||||
<Modal title="Connection request" open={isModalOpened} withCloseButton={false}>
|
||||
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
||||
{!pendingReqId && (
|
||||
<Typography variant="body1" color={'GrayText'}>
|
||||
You will be asked to <b>enable notifications</b>, this is needed for a reliable communication with Nostr
|
||||
apps.
|
||||
</Typography>
|
||||
)}
|
||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
@@ -91,10 +156,10 @@ export const ModalConfirmConnect = () => {
|
||||
width: 56,
|
||||
height: 56,
|
||||
}}
|
||||
src={icon}
|
||||
src={appIcon}
|
||||
>
|
||||
{appAvatarTitle}
|
||||
</Avatar>
|
||||
{appAvatarTitle}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h5" fontWeight={600}>
|
||||
{appName}
|
||||
@@ -109,14 +174,7 @@ export const ModalConfirmConnect = () => {
|
||||
value={ACTION_TYPE.BASIC}
|
||||
title="Basic permissions"
|
||||
description="Read your public key, sign notes, reactions, zaps, etc"
|
||||
// hasinfo
|
||||
/>
|
||||
{/* <ActionToggleButton
|
||||
value={ACTION_TYPE.ADVANCED}
|
||||
title='Advanced'
|
||||
description='Use for trusted apps only'
|
||||
hasinfo
|
||||
/> */}
|
||||
<ActionToggleButton
|
||||
value={ACTION_TYPE.CUSTOM}
|
||||
title="On demand"
|
||||
@@ -128,7 +186,6 @@ export const ModalConfirmConnect = () => {
|
||||
Disallow
|
||||
</StyledButton>
|
||||
<StyledButton fullWidth onClick={allow}>
|
||||
{/* Allow {selectedActionType} actions */}
|
||||
Connect
|
||||
</StyledButton>
|
||||
</Stack>
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { call, getAppIconTitle, getShortenNpub, getSignReqKind } from '@/utils/helpers/helpers'
|
||||
import { call, getAppIconTitle, getReqActionName, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { Avatar, Box, List, ListItem, ListItemIcon, ListItemText, Stack, Typography } from '@mui/material'
|
||||
import { useParams, useSearchParams } from 'react-router-dom'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectAppsByNpub } from '@/store'
|
||||
import { selectAppsByNpub, selectKeys } from '@/store'
|
||||
import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
||||
import { FC, useEffect, useMemo, useState } from 'react'
|
||||
import { StyledActionsListContainer, StyledButton, StyledToggleButtonsGroup } from './styled'
|
||||
@@ -13,7 +13,6 @@ import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
||||
import { DbPending } from '@/modules/db'
|
||||
import { ACTIONS } from '@/utils/consts'
|
||||
import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal'
|
||||
|
||||
enum ACTION_TYPE {
|
||||
@@ -35,13 +34,14 @@ type ModalConfirmEventProps = {
|
||||
type PendingRequest = DbPending & { checked: boolean }
|
||||
|
||||
export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs }) => {
|
||||
const keys = useAppSelector(selectKeys)
|
||||
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONFIRM_EVENT)
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
const appNpub = searchParams.get('appNpub') || ''
|
||||
const isPopup = searchParams.get('popup') === 'true'
|
||||
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const apps = useAppSelector((state) => selectAppsByNpub(state, npub))
|
||||
|
||||
@@ -54,10 +54,25 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
||||
setPendingRequests(currentAppPendingReqs.map((pr) => ({ ...pr, checked: true })))
|
||||
}, [currentAppPendingReqs])
|
||||
|
||||
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
},
|
||||
})
|
||||
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||||
|
||||
if (isModalOpened && (!isNpubExists || !isAppNpubExists)) {
|
||||
closeModalAfterRequest()
|
||||
return null
|
||||
}
|
||||
|
||||
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
||||
const { name, icon = '' } = triggerApp || {}
|
||||
const appName = name || getShortenNpub(appNpub)
|
||||
const appAvatarTitle = getAppIconTitle(name, appNpub)
|
||||
const appAvatarTitle = getAppIconTitle(name, appNpub)
|
||||
|
||||
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
||||
if (!value) return undefined
|
||||
@@ -66,21 +81,6 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
||||
|
||||
const selectedPendingRequests = pendingRequests.filter((pr) => pr.checked)
|
||||
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
selectedPendingRequests.forEach(async (req) => await swicCall('confirm', req.id, false, false))
|
||||
},
|
||||
})
|
||||
|
||||
const closeModalAfterRequest = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONFIRM_EVENT, {
|
||||
onClose: (sp) => {
|
||||
sp.delete('appNpub')
|
||||
sp.delete('reqId')
|
||||
},
|
||||
})
|
||||
|
||||
async function confirmPending(allow: boolean) {
|
||||
selectedPendingRequests.forEach((req) => {
|
||||
call(async () => {
|
||||
@@ -101,15 +101,6 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
||||
setPendingRequests(newPendingRequests)
|
||||
}
|
||||
|
||||
const getAction = (req: PendingRequest) => {
|
||||
const action = ACTIONS[req.method]
|
||||
if (req.method === 'sign_event') {
|
||||
const kind = getSignReqKind(req)
|
||||
if (kind !== undefined) return `${action} of kind ${kind}`
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
if (isPopup) {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
@@ -119,9 +110,7 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal title='Permission request' open={isModalOpened} withCloseButton={false}
|
||||
// withCloseButton={!isPopup} onClose={!isPopup ? handleCloseModal : undefined}
|
||||
>
|
||||
<Modal title="Permission request" open={isModalOpened} withCloseButton={false}>
|
||||
<Stack gap={'1rem'} paddingTop={'1rem'}>
|
||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} marginBottom={'1rem'}>
|
||||
<Avatar
|
||||
@@ -133,8 +122,8 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
||||
}}
|
||||
src={icon}
|
||||
>
|
||||
{appAvatarTitle}
|
||||
</Avatar>
|
||||
{appAvatarTitle}
|
||||
</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h5" fontWeight={600}>
|
||||
{appName}
|
||||
@@ -154,7 +143,7 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
||||
<ListItemIcon>
|
||||
<Checkbox checked={req.checked} onChange={handleChangeCheckbox(req.id)} />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{getAction(req)}</ListItemText>
|
||||
<ListItemText>{getReqActionName(req)}</ListItemText>
|
||||
</ListItem>
|
||||
)
|
||||
})}
|
||||
@@ -163,11 +152,6 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
||||
<StyledToggleButtonsGroup value={selectedActionType} onChange={handleActionTypeChange} exclusive>
|
||||
<ActionToggleButton value={ACTION_TYPE.ALWAYS} title="Always" />
|
||||
<ActionToggleButton value={ACTION_TYPE.ONCE} title="Just once" />
|
||||
{/* <ActionToggleButton
|
||||
value={ACTION_TYPE.ALLOW_ALL}
|
||||
title='Allow All Advanced Actions'
|
||||
hasinfo
|
||||
/> */}
|
||||
</StyledToggleButtonsGroup>
|
||||
|
||||
<Stack direction={'row'} gap={'1rem'}>
|
||||
|
@@ -5,16 +5,23 @@ import { Button } from '@/shared/Button/Button'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { selectKeys } from '@/store'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { getBunkerLink } from '@/utils/helpers/helpers'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
import { useRef } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
export const ModalConnectApp = () => {
|
||||
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||
const timerRef = useRef<NodeJS.Timeout>()
|
||||
const keys = useAppSelector(selectKeys)
|
||||
|
||||
const timerRef = useRef<NodeJS.Timeout>()
|
||||
const notify = useEnqueueSnackbar()
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const bunkerStr = getBunkerLink(npub)
|
||||
|
||||
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.CONNECT_APP, {
|
||||
onClose: () => {
|
||||
@@ -22,11 +29,11 @@ export const ModalConnectApp = () => {
|
||||
},
|
||||
})
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
|
||||
const bunkerStr = getBunkerLink(npub)
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
if (isModalOpened && !isNpubExists) {
|
||||
handleCloseModal()
|
||||
return null
|
||||
}
|
||||
|
||||
const handleShareBunker = async () => {
|
||||
const shareData = {
|
||||
@@ -62,7 +69,10 @@ export const ModalConnectApp = () => {
|
||||
value={bunkerStr}
|
||||
endAdornment={<InputCopyButton value={bunkerStr} onCopy={handleCopy} />}
|
||||
/>
|
||||
<AppLink title="What is this?" onClick={() => handleOpen(MODAL_PARAMS_KEYS.EXPLANATION)} />
|
||||
<AppLink
|
||||
title="What is this?"
|
||||
onClick={() => handleOpen(MODAL_PARAMS_KEYS.EXPLANATION, { search: { type: EXPLANATION_MODAL_KEYS.BUNKER } })}
|
||||
/>
|
||||
<Button fullWidth onClick={handleShareBunker}>
|
||||
Share it
|
||||
</Button>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { FC } from 'react'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
@@ -10,7 +10,7 @@ type ModalExplanationProps = {
|
||||
explanationText?: string
|
||||
}
|
||||
|
||||
export const ModalExplanation: FC<ModalExplanationProps> = ({ explanationText = '' }) => {
|
||||
export const ModalExplanation: FC<ModalExplanationProps> = () => {
|
||||
const { getModalOpened } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EXPLANATION)
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
@@ -18,21 +18,76 @@ export const ModalExplanation: FC<ModalExplanationProps> = ({ explanationText =
|
||||
const handleCloseModal = () => {
|
||||
searchParams.delete('type')
|
||||
searchParams.delete(MODAL_PARAMS_KEYS.EXPLANATION)
|
||||
setSearchParams(searchParams)
|
||||
setSearchParams(searchParams, { replace: true })
|
||||
}
|
||||
|
||||
const type = searchParams.get('type')
|
||||
|
||||
let title = ''
|
||||
let explanationText
|
||||
switch (type) {
|
||||
case EXPLANATION_MODAL_KEYS.NPUB: {
|
||||
title = 'What is NPUB?'
|
||||
explanationText = (
|
||||
<>
|
||||
NPUB is your Nostr PUBlic key.
|
||||
<br />
|
||||
<br />
|
||||
It is your global unique identifier on the Nostr network, and is derived from your private key.
|
||||
<br />
|
||||
<br />
|
||||
You can share your NPUB with other people so that they could unambiguously find you on the network.
|
||||
</>
|
||||
)
|
||||
break
|
||||
}
|
||||
case EXPLANATION_MODAL_KEYS.LOGIN: {
|
||||
title = 'What is Login?'
|
||||
explanationText = (
|
||||
<>
|
||||
Login (username) is your human-readable name on the Nostr network.
|
||||
<br />
|
||||
<br />
|
||||
Unlike your NPUB, which is a long string of random symbols, your login is a meaningful name tied to a website
|
||||
address (like name@nsec.app).
|
||||
<br />
|
||||
<br />
|
||||
Use your username to log in to Nostr apps.
|
||||
<br />
|
||||
<br />
|
||||
You can have many usernames all pointing to your NPUB. People also refer to these names as nostr-addresses or
|
||||
NIP05 names.
|
||||
</>
|
||||
)
|
||||
break
|
||||
}
|
||||
case EXPLANATION_MODAL_KEYS.BUNKER: {
|
||||
title = 'What is Bunker URL?'
|
||||
explanationText = (
|
||||
<>
|
||||
Bunker URL is a string used to connect to Nostr apps.
|
||||
<br />
|
||||
<br />
|
||||
Some apps require bunker URL to connect to your keys. Paste it to the app and then confirm a connection
|
||||
request.
|
||||
</>
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
title="What is this?"
|
||||
title={title}
|
||||
open={isModalOpened}
|
||||
onClose={handleCloseModal}
|
||||
withCloseButton={false}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
minHeight: '60%',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack height={'100%'}>
|
||||
<Stack height={'100%'} gap={2}>
|
||||
<Typography flex={1}>{explanationText}</Typography>
|
||||
<Button fullWidth onClick={handleCloseModal}>
|
||||
Got it!
|
||||
|
@@ -5,56 +5,151 @@ import { Button } from '@/shared/Button/Button'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
import React, { ChangeEvent, FormEvent, useState } from 'react'
|
||||
import { CircularProgress, 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'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { usePassword } from '@/hooks/usePassword'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { CheckmarkIcon } from '@/assets'
|
||||
|
||||
const FORM_DEFAULT_VALUES = {
|
||||
username: '',
|
||||
nsec: '',
|
||||
}
|
||||
|
||||
export const ModalImportKeys = () => {
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.IMPORT_KEYS)
|
||||
const { hidePassword, inputProps } = usePassword()
|
||||
const theme = useTheme()
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
register,
|
||||
formState: { errors },
|
||||
watch,
|
||||
} = useForm<FormInputType>({
|
||||
defaultValues: FORM_DEFAULT_VALUES,
|
||||
resolver: yupResolver(schema),
|
||||
mode: 'onSubmit',
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isAvailable, setIsAvailable] = useState(false)
|
||||
const enteredUsername = watch('username')
|
||||
const [debouncedUsername] = useDebounce(enteredUsername, 100)
|
||||
|
||||
const checkIsUsernameAvailable = useCallback(async () => {
|
||||
if (!debouncedUsername.trim().length) return undefined
|
||||
const npubNip05 = await fetchNip05(`${debouncedUsername}@${DOMAIN}`)
|
||||
|
||||
setIsAvailable(!npubNip05)
|
||||
}, [debouncedUsername])
|
||||
|
||||
useEffect(() => {
|
||||
checkIsUsernameAvailable()
|
||||
}, [checkIsUsernameAvailable])
|
||||
|
||||
const cleanUpStates = useCallback(() => {
|
||||
hidePassword()
|
||||
reset()
|
||||
setIsLoading(false)
|
||||
setIsAvailable(false)
|
||||
}, [reset, hidePassword])
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [enteredNsec, setEnteredNsec] = useState('')
|
||||
|
||||
const handleNsecChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setEnteredNsec(e.target.value)
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
const submitHandler = async (values: FormInputType) => {
|
||||
if (isLoading || !isAvailable) return undefined
|
||||
try {
|
||||
if (!enteredNsec.trim().length) return
|
||||
const enteredName = '' // FIXME get from input
|
||||
const k: any = await swicCall('importKey', enteredName, enteredNsec)
|
||||
const { nsec, username } = values
|
||||
setIsLoading(true)
|
||||
const k: any = await swicCall('importKey', username, nsec)
|
||||
notify('Key imported!', 'success')
|
||||
navigate(`/key/${k.npub}`)
|
||||
cleanUpStates()
|
||||
} catch (error: any) {
|
||||
notify(error.message, 'error')
|
||||
notify(error?.message || 'Something went wrong!', 'error')
|
||||
cleanUpStates()
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
isModalOpened && cleanUpStates()
|
||||
}
|
||||
}, [isModalOpened, cleanUpStates])
|
||||
|
||||
const getInputHelperText = () => {
|
||||
if (!enteredUsername) return "Don't worry, username can be changed later."
|
||||
if (!isAvailable) return 'Already taken'
|
||||
return (
|
||||
<>
|
||||
<CheckmarkIcon /> Available
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const inputHelperText = getInputHelperText()
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||
<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 keys
|
||||
Import key
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Input
|
||||
label="Enter a NSEC"
|
||||
placeholder="Your NSEC"
|
||||
value={enteredNsec}
|
||||
onChange={handleNsecChange}
|
||||
label="Choose a username"
|
||||
fullWidth
|
||||
type="password"
|
||||
placeholder="Enter a Username"
|
||||
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||
{...register('username')}
|
||||
error={!!errors.username}
|
||||
helperText={inputHelperText}
|
||||
helperTextProps={{
|
||||
sx: {
|
||||
'&.helper_text': {
|
||||
color:
|
||||
enteredUsername && isAvailable
|
||||
? theme.palette.success.main
|
||||
: enteredUsername && !isAvailable
|
||||
? theme.palette.error.main
|
||||
: theme.palette.textSecondaryDecorate.main,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button type="submit">Import nsec</Button>
|
||||
<Input
|
||||
label="Paste your private key"
|
||||
placeholder="nsec1..."
|
||||
fullWidth
|
||||
{...register('nsec')}
|
||||
error={!!errors.nsec}
|
||||
{...inputProps}
|
||||
helperText="Keys stay on your device."
|
||||
helperTextProps={{
|
||||
sx: {
|
||||
'&.helper_text': {
|
||||
color: theme.palette.textSecondaryDecorate.main,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
Import key {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
)
|
||||
|
8
src/components/Modal/ModalImportKeys/const.ts
Normal file
8
src/components/Modal/ModalImportKeys/const.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import * as yup from 'yup'
|
||||
|
||||
export const schema = yup.object().shape({
|
||||
username: yup.string().required(),
|
||||
nsec: yup.string().required(),
|
||||
})
|
||||
|
||||
export type FormInputType = yup.InferType<typeof schema>
|
@@ -35,7 +35,7 @@ export const ModalInitial = () => {
|
||||
|
||||
{showAdvancedContent && (
|
||||
<Fade in>
|
||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS)}>Import keys</Button>
|
||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS)}>Import key</Button>
|
||||
</Fade>
|
||||
)}
|
||||
</Stack>
|
||||
|
@@ -4,18 +4,23 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { IconButton, Stack, Typography } from '@mui/material'
|
||||
import { CircularProgress, Stack, Typography } from '@mui/material'
|
||||
import { StyledAppLogo } from './styled'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { FormInputType, schema } from './const'
|
||||
import { yupResolver } from '@hookform/resolvers/yup'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||
import { usePassword } from '@/hooks/usePassword'
|
||||
import { dbi } from '@/modules/db'
|
||||
|
||||
const FORM_DEFAULT_VALUES = {
|
||||
username: '',
|
||||
password: '',
|
||||
}
|
||||
|
||||
export const ModalLogin = () => {
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
@@ -23,8 +28,9 @@ export const ModalLogin = () => {
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.LOGIN)
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
const navigate = useNavigate()
|
||||
const { hidePassword, inputProps } = usePassword()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -32,27 +38,25 @@ export const ModalLogin = () => {
|
||||
register,
|
||||
formState: { errors },
|
||||
} = useForm<FormInputType>({
|
||||
defaultValues: {
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
defaultValues: FORM_DEFAULT_VALUES,
|
||||
resolver: yupResolver(schema),
|
||||
mode: 'onSubmit',
|
||||
})
|
||||
|
||||
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
||||
|
||||
const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState)
|
||||
|
||||
const cleanUpStates = useCallback(() => {
|
||||
setIsPasswordShown(false)
|
||||
hidePassword()
|
||||
reset()
|
||||
}, [reset])
|
||||
setIsLoading(false)
|
||||
}, [reset, hidePassword])
|
||||
|
||||
const submitHandler = async (values: FormInputType) => {
|
||||
if (isLoading) return undefined
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
let npub = values.username
|
||||
let name = ''
|
||||
|
||||
if (!npub.startsWith('npub1')) {
|
||||
name = npub
|
||||
if (!npub.includes('@')) {
|
||||
@@ -72,11 +76,13 @@ export const ModalLogin = () => {
|
||||
console.log('fetch', npub, name)
|
||||
const k: any = await swicCall('fetchKey', npub, passphrase, name)
|
||||
notify(`Fetched ${k.npub}`, 'success')
|
||||
dbi.addSynced(k.npub)
|
||||
cleanUpStates()
|
||||
navigate(`/key/${k.npub}`)
|
||||
} catch (error: any) {
|
||||
console.log('error', error)
|
||||
notify(error?.message || 'Something went wrong!', 'error')
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,16 +116,11 @@ export const ModalLogin = () => {
|
||||
fullWidth
|
||||
placeholder="Your password"
|
||||
{...register('password')}
|
||||
endAdornment={
|
||||
<IconButton size="small" onClick={handlePasswordTypeChange}>
|
||||
{isPasswordShown ? <VisibilityOffOutlinedIcon /> : <VisibilityOutlinedIcon />}
|
||||
</IconButton>
|
||||
}
|
||||
type={isPasswordShown ? 'text' : 'password'}
|
||||
{...inputProps}
|
||||
error={!!errors.password}
|
||||
/>
|
||||
<Button type="submit" fullWidth>
|
||||
Add account
|
||||
<Button type="submit" fullWidth disabled={isLoading}>
|
||||
Add account {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
@@ -1,15 +1,7 @@
|
||||
import * as yup from 'yup'
|
||||
|
||||
export const schema = yup.object().shape({
|
||||
username: yup
|
||||
.string()
|
||||
.test('Domain validation', 'The domain is required!', function (value) {
|
||||
if (!value || !value.trim().length) return false
|
||||
|
||||
const USERNAME_WITH_DOMAIN_REGEXP = new RegExp(/^[\w-.]+@([\w-]+\.)+[\w-]{2,8}$/g)
|
||||
return USERNAME_WITH_DOMAIN_REGEXP.test(value)
|
||||
})
|
||||
.required(),
|
||||
username: yup.string().required(),
|
||||
password: yup.string().required().min(4),
|
||||
})
|
||||
|
||||
|
@@ -2,19 +2,20 @@ import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Box, CircularProgress, IconButton, Stack, Typography } from '@mui/material'
|
||||
import { Box, CircularProgress, Stack, Typography } from '@mui/material'
|
||||
import { StyledButton, StyledSettingContainer, StyledSynchedText } from './styled'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { CheckmarkIcon } from '@/assets'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
||||
import { ChangeEvent, FC, useEffect, useState } from 'react'
|
||||
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { dbi } from '@/modules/db'
|
||||
import { usePassword } from '@/hooks/usePassword'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectKeys } from '@/store'
|
||||
|
||||
type ModalSettingsProps = {
|
||||
isSynced: boolean
|
||||
@@ -23,29 +24,44 @@ type ModalSettingsProps = {
|
||||
export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const keys = useAppSelector(selectKeys)
|
||||
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.SETTINGS)
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.SETTINGS)
|
||||
|
||||
const { hidePassword, inputProps } = usePassword()
|
||||
|
||||
const [enteredPassword, setEnteredPassword] = useState('')
|
||||
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
||||
const [isPasswordInvalid, setIsPasswordInvalid] = useState(false)
|
||||
|
||||
const [isChecked, setIsChecked] = useState(false)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
useEffect(() => setIsChecked(isSynced), [isModalOpened, isSynced])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isModalOpened) {
|
||||
// modal closed
|
||||
hidePassword()
|
||||
}
|
||||
}
|
||||
}, [hidePassword, isModalOpened])
|
||||
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
|
||||
if (isModalOpened && !isNpubExists) {
|
||||
handleCloseModal()
|
||||
return null
|
||||
}
|
||||
|
||||
const handlePasswordChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setIsPasswordInvalid(false)
|
||||
setEnteredPassword(e.target.value)
|
||||
}
|
||||
|
||||
const handlePasswordTypeChange = () => setIsPasswordShown((prevState) => !prevState)
|
||||
|
||||
const onClose = () => {
|
||||
handleCloseModal()
|
||||
setEnteredPassword('')
|
||||
@@ -95,16 +111,7 @@ export const ModalSettings: FC<ModalSettingsProps> = ({ isSynced }) => {
|
||||
</Box>
|
||||
<Input
|
||||
fullWidth
|
||||
endAdornment={
|
||||
<IconButton size="small" onClick={handlePasswordTypeChange}>
|
||||
{isPasswordShown ? (
|
||||
<VisibilityOffOutlinedIcon htmlColor="#6b6b6b" />
|
||||
) : (
|
||||
<VisibilityOutlinedIcon htmlColor="#6b6b6b" />
|
||||
)}
|
||||
</IconButton>
|
||||
}
|
||||
type={isPasswordShown ? 'text' : 'password'}
|
||||
{...inputProps}
|
||||
onChange={handlePasswordChange}
|
||||
value={enteredPassword}
|
||||
helperText={isPasswordInvalid ? 'Invalid password' : ''}
|
||||
|
@@ -2,15 +2,15 @@ import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Stack, Typography, useTheme } from '@mui/material'
|
||||
import React, { ChangeEvent, useState } from 'react'
|
||||
import { CircularProgress, 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'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { DOMAIN, NOAUTHD_URL } from '@/utils/consts'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||
|
||||
export const ModalSignUp = () => {
|
||||
@@ -25,6 +25,8 @@ export const ModalSignUp = () => {
|
||||
const [enteredValue, setEnteredValue] = useState('')
|
||||
const [isAvailable, setIsAvailable] = useState(false)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const handleInputChange = async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setEnteredValue(e.target.value)
|
||||
const name = e.target.value.trim()
|
||||
@@ -36,31 +38,47 @@ export const ModalSignUp = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const inputHelperText = enteredValue ? (
|
||||
isAvailable ? (
|
||||
const getInputHelperText = () => {
|
||||
if (!enteredValue) return "Don't worry, username can be changed later."
|
||||
if (!isAvailable) return 'Already taken'
|
||||
return (
|
||||
<>
|
||||
<CheckmarkIcon /> Available
|
||||
</>
|
||||
) : (
|
||||
<>Already taken</>
|
||||
)
|
||||
) : (
|
||||
"Don't worry, username can be changed later."
|
||||
)
|
||||
}
|
||||
|
||||
const inputHelperText = getInputHelperText()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (isLoading || !isAvailable) return undefined
|
||||
|
||||
const name = enteredValue.trim()
|
||||
if (!name.length) return
|
||||
e.preventDefault()
|
||||
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const k: any = await swicCall('generateKey', name)
|
||||
notify(`Account created for "${name}"`, 'success')
|
||||
navigate(`/key/${k.npub}`)
|
||||
setIsLoading(false)
|
||||
} catch (error: any) {
|
||||
notify(error.message, 'error')
|
||||
notify(error?.message || 'Something went wrong!', 'error')
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isModalOpened) {
|
||||
// modal closed
|
||||
setIsLoading(false)
|
||||
setIsAvailable(false)
|
||||
}
|
||||
}
|
||||
}, [isModalOpened])
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||
@@ -71,9 +89,9 @@ export const ModalSignUp = () => {
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Input
|
||||
label="Enter a Username"
|
||||
label="Username"
|
||||
fullWidth
|
||||
placeholder="Username"
|
||||
placeholder="Enter a Username"
|
||||
helperText={inputHelperText}
|
||||
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||
onChange={handleInputChange}
|
||||
@@ -91,8 +109,8 @@ export const ModalSignUp = () => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button fullWidth type="submit">
|
||||
Create account
|
||||
<Button fullWidth type="submit" disabled={isLoading}>
|
||||
Create account {isLoading && <CircularProgress sx={{ marginLeft: '0.5rem' }} size={'1rem'} />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
30
src/hooks/usePassword.tsx
Normal file
30
src/hooks/usePassword.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { IconButton } from '@mui/material'
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'
|
||||
|
||||
export const usePassword = () => {
|
||||
const [isPasswordShown, setIsPasswordShown] = useState(false)
|
||||
|
||||
const handlePasswordTypeChange = useCallback(() => setIsPasswordShown((prevState) => !prevState), [])
|
||||
|
||||
const hidePassword = useCallback(() => setIsPasswordShown(false), [])
|
||||
|
||||
const inputProps = useMemo(
|
||||
() => ({
|
||||
endAdornment: (
|
||||
<IconButton size="small" onClick={handlePasswordTypeChange}>
|
||||
{isPasswordShown ? (
|
||||
<VisibilityOffOutlinedIcon htmlColor="#6b6b6b" />
|
||||
) : (
|
||||
<VisibilityOutlinedIcon htmlColor="#6b6b6b" />
|
||||
)}
|
||||
</IconButton>
|
||||
),
|
||||
type: isPasswordShown ? 'text' : 'password',
|
||||
}),
|
||||
[handlePasswordTypeChange, isPasswordShown]
|
||||
)
|
||||
|
||||
return { inputProps, hidePassword }
|
||||
}
|
41
src/hooks/useProfile.ts
Normal file
41
src/hooks/useProfile.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { fetchProfile } from '@/modules/nostr'
|
||||
import { MetaEvent } from '@/types/meta-event'
|
||||
import { getProfileUsername, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectKeyByNpub } from '@/store'
|
||||
|
||||
const getFirstLetter = (text: string | undefined): string | null => {
|
||||
if (!text || text.trim().length === 0) return null
|
||||
return text.substring(0, 1).toUpperCase()
|
||||
}
|
||||
|
||||
export const useProfile = (npub: string) => {
|
||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||
const currentKey = useAppSelector((state) => selectKeyByNpub(state, npub))
|
||||
|
||||
const userName = getProfileUsername(profile) || currentKey?.name
|
||||
const userAvatar = profile?.info?.picture || ''
|
||||
const avatarTitle = getFirstLetter(userName)
|
||||
|
||||
const loadProfile = useCallback(async () => {
|
||||
if (!npub) return undefined
|
||||
try {
|
||||
const response = await fetchProfile(npub)
|
||||
setProfile(response)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error)
|
||||
}
|
||||
}, [npub])
|
||||
|
||||
useEffect(() => {
|
||||
loadProfile()
|
||||
}, [loadProfile])
|
||||
|
||||
return {
|
||||
profile,
|
||||
userName: userName || getShortenNpub(npub),
|
||||
userAvatar,
|
||||
avatarTitle,
|
||||
}
|
||||
}
|
@@ -1,53 +1,59 @@
|
||||
import { Avatar, Stack, Toolbar, Typography } from '@mui/material'
|
||||
import { AppLogo } from '../../assets'
|
||||
import { StyledAppBar, StyledAppName } from './styled'
|
||||
import { StyledAppBar, StyledAppName, StyledProfileContainer, StyledThemeButton } from './styled'
|
||||
import { Menu } from './components/Menu'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { MetaEvent } from '@/types/meta-event'
|
||||
import { fetchProfile } from '@/modules/nostr'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import { ProfileMenu } from './components/ProfileMenu'
|
||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { useProfile } from '@/hooks/useProfile'
|
||||
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'
|
||||
|
||||
export const Header = () => {
|
||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||
const navigate = useNavigate()
|
||||
const dispatch = useAppDispatch()
|
||||
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||
const showProfile = Boolean(npub)
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!npub) return setProfile(null)
|
||||
const handleNavigate = () => {
|
||||
navigate(`/key/${npub}`)
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchProfile(npub)
|
||||
setProfile(response as any)
|
||||
} catch (e) {
|
||||
return setProfile(null)
|
||||
}
|
||||
}, [npub])
|
||||
const isDarkMode = themeMode === 'dark'
|
||||
const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#000" />
|
||||
|
||||
useEffect(() => {
|
||||
load()
|
||||
}, [load])
|
||||
|
||||
const showProfile = Boolean(npub || profile)
|
||||
const userName = profile?.info?.name || getShortenNpub(npub)
|
||||
const userAvatar = profile?.info?.picture || ''
|
||||
const handleChangeMode = () => {
|
||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledAppBar position="fixed">
|
||||
<Toolbar sx={{ padding: '12px' }}>
|
||||
<Stack direction={'row'} justifyContent={'space-between'} alignItems={'center'} width={'100%'}>
|
||||
{showProfile ? (
|
||||
<Stack gap={'1rem'} direction={'row'} alignItems={'center'} flex={1}>
|
||||
<Avatar src={userAvatar} alt={userName} />
|
||||
<Typography fontWeight={600}>{userName}</Typography>
|
||||
</Stack>
|
||||
) : (
|
||||
{showProfile && (
|
||||
<StyledProfileContainer>
|
||||
<Avatar src={userAvatar} alt={userName} onClick={handleNavigate} className="avatar">
|
||||
{avatarTitle}
|
||||
</Avatar>
|
||||
<Typography fontWeight={600} onClick={handleNavigate} className="username">
|
||||
{userName}
|
||||
</Typography>
|
||||
</StyledProfileContainer>
|
||||
)}
|
||||
|
||||
{!showProfile && (
|
||||
<StyledAppName>
|
||||
<AppLogo />
|
||||
<span>Nsec.app</span>
|
||||
</StyledAppName>
|
||||
)}
|
||||
|
||||
<StyledThemeButton onClick={handleChangeMode}>{themeIcon}</StyledThemeButton>
|
||||
|
||||
{showProfile ? <ProfileMenu /> : <Menu />}
|
||||
</Stack>
|
||||
</Toolbar>
|
||||
|
31
src/layout/Header/components/ListItemProfile.tsx
Normal file
31
src/layout/Header/components/ListItemProfile.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { useProfile } from '@/hooks/useProfile'
|
||||
import { DbKey } from '@/modules/db'
|
||||
import { Avatar, ListItemIcon, MenuItem, Typography } from '@mui/material'
|
||||
import React, { FC } from 'react'
|
||||
|
||||
type ListItemProfileProps = {
|
||||
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>
|
||||
)
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
import { DbKey } from '@/modules/db'
|
||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { Avatar, ListItemIcon, MenuItem, Stack, Typography } from '@mui/material'
|
||||
import React, { FC } from 'react'
|
||||
import { Stack } from '@mui/material'
|
||||
import { FC } from 'react'
|
||||
import { ListItemProfile } from './ListItemProfile'
|
||||
|
||||
type ListProfilesProps = {
|
||||
keys: DbKey[]
|
||||
@@ -12,18 +12,7 @@ export const ListProfiles: FC<ListProfilesProps> = ({ keys = [], onClickItem })
|
||||
return (
|
||||
<Stack maxHeight={'10rem'} overflow={'auto'}>
|
||||
{keys.map((key) => {
|
||||
const userName = key?.profile?.info?.name || getShortenNpub(key.npub)
|
||||
const userAvatar = key?.profile?.info?.picture || ''
|
||||
return (
|
||||
<MenuItem sx={{ gap: '0.5rem' }} onClick={() => onClickItem(key)} key={key.npub}>
|
||||
<ListItemIcon>
|
||||
<Avatar src={userAvatar} alt={userName} sx={{ width: 36, height: 36 }} />
|
||||
</ListItemIcon>
|
||||
<Typography variant="body2" noWrap>
|
||||
{userName}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
)
|
||||
return <ListItemProfile {...key} key={key.npub} onClickItem={() => onClickItem(key)} />
|
||||
})}
|
||||
</Stack>
|
||||
)
|
||||
|
@@ -1,10 +1,7 @@
|
||||
import { Menu as MuiMenu } from '@mui/material'
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode'
|
||||
import LightModeIcon from '@mui/icons-material/LightMode'
|
||||
import LoginIcon from '@mui/icons-material/Login'
|
||||
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
||||
import { setThemeMode } from '@/store/reducers/ui.slice'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { MenuButton } from './styled'
|
||||
@@ -14,26 +11,17 @@ import MenuRoundedIcon from '@mui/icons-material/MenuRounded'
|
||||
import { selectKeys } from '@/store'
|
||||
|
||||
export const Menu = () => {
|
||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||
const keys = useAppSelector(selectKeys)
|
||||
const dispatch = useAppDispatch()
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
|
||||
const isDarkMode = themeMode === 'dark'
|
||||
const isNoKeys = !keys || keys.length === 0
|
||||
|
||||
const { anchorEl, handleClose, handleOpen: handleOpenMenu, open } = useOpenMenu()
|
||||
|
||||
const handleChangeMode = () => {
|
||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
||||
}
|
||||
const isNoKeys = !keys || keys.length === 0
|
||||
|
||||
const handleNavigateToAuth = () => {
|
||||
handleOpen(MODAL_PARAMS_KEYS.INITIAL)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#feb94a" />
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuButton onClick={handleOpenMenu}>
|
||||
@@ -52,7 +40,6 @@ export const Menu = () => {
|
||||
onClick={handleNavigateToAuth}
|
||||
title={isNoKeys ? 'Sign up' : 'Add account'}
|
||||
/>
|
||||
<MenuItem Icon={themeIcon} onClick={handleChangeMode} title="Change theme" />
|
||||
</MuiMenu>
|
||||
</>
|
||||
)
|
||||
|
@@ -9,11 +9,9 @@ import LoginIcon from '@mui/icons-material/Login'
|
||||
import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded'
|
||||
import HomeRoundedIcon from '@mui/icons-material/HomeRounded'
|
||||
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks/redux'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectKeys } from '@/store'
|
||||
import { setThemeMode } from '@/store/reducers/ui.slice'
|
||||
import DarkModeIcon from '@mui/icons-material/DarkMode'
|
||||
import LightModeIcon from '@mui/icons-material/LightMode'
|
||||
|
||||
import { ListProfiles } from './ListProfiles'
|
||||
import { DbKey } from '@/modules/db'
|
||||
|
||||
@@ -23,10 +21,7 @@ export const ProfileMenu = () => {
|
||||
|
||||
const keys = useAppSelector(selectKeys)
|
||||
const isNoKeys = !keys || keys.length === 0
|
||||
const themeMode = useAppSelector((state) => state.ui.themeMode)
|
||||
const isDarkMode = themeMode === 'dark'
|
||||
|
||||
const dispatch = useAppDispatch()
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleNavigateToAuth = () => {
|
||||
@@ -39,17 +34,11 @@ export const ProfileMenu = () => {
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const handleChangeMode = () => {
|
||||
dispatch(setThemeMode({ mode: isDarkMode ? 'light' : 'dark' }))
|
||||
}
|
||||
|
||||
const handleNavigateToKeyInnerPage = (key: DbKey) => {
|
||||
navigate('/key/' + key.npub)
|
||||
handleClose()
|
||||
}
|
||||
|
||||
const themeIcon = isDarkMode ? <DarkModeIcon htmlColor="#fff" /> : <LightModeIcon htmlColor="#feb94a" />
|
||||
|
||||
return (
|
||||
<>
|
||||
<MenuButton onClick={handleOpenMenu}>
|
||||
@@ -71,7 +60,6 @@ export const ProfileMenu = () => {
|
||||
onClick={handleNavigateToAuth}
|
||||
title={isNoKeys ? 'Sign up' : 'Add account'}
|
||||
/>
|
||||
<MenuItem Icon={themeIcon} onClick={handleChangeMode} title="Change theme" />
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { AppBar, Typography, TypographyProps, styled } from '@mui/material'
|
||||
import { AppBar, IconButton, Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export const StyledAppBar = styled(AppBar)(({ theme }) => {
|
||||
@@ -11,6 +11,7 @@ export const StyledAppBar = styled(AppBar)(({ theme }) => {
|
||||
maxWidth: 'inherit',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
borderRadius: '8px',
|
||||
}
|
||||
})
|
||||
|
||||
@@ -29,3 +30,20 @@ export const StyledAppName = styled((props: TypographyProps) => (
|
||||
lineHeight: '22.4px',
|
||||
marginLeft: '0.5rem',
|
||||
}))
|
||||
|
||||
export const StyledProfileContainer = styled((props: StackProps) => <Stack {...props} />)(() => ({
|
||||
gap: '1rem',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flex: 1,
|
||||
'& .avatar': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
'& .username': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
}))
|
||||
|
||||
export const StyledThemeButton = styled(IconButton)({
|
||||
margin: '0 0.5rem',
|
||||
})
|
||||
|
@@ -246,12 +246,12 @@ export class NoauthBackend {
|
||||
return Buffer.from(await this.swg.crypto.subtle.digest('SHA-256', Buffer.from(s))).toString('hex')
|
||||
}
|
||||
|
||||
private async fetchNpubName(npub: string) {
|
||||
private async fetchNpubName(npub: string) {
|
||||
const url = `${NOAUTHD_URL}/name?npub=${npub}`
|
||||
const r = await fetch(url)
|
||||
const d = await r.json()
|
||||
return d?.names?.length ? d.names[0] as string : ''
|
||||
}
|
||||
const d = await r.json()
|
||||
return d?.names?.length ? (d.names[0] as string) : ''
|
||||
}
|
||||
|
||||
private async sendPost({ url, method, headers, body }: { url: string; method: string; headers: any; body: string }) {
|
||||
const r = await fetch(url, {
|
||||
@@ -407,6 +407,23 @@ export class NoauthBackend {
|
||||
throw new Error('Too many requests, retry later')
|
||||
}
|
||||
|
||||
private async sendTokenToServer(npub: string, token: string) {
|
||||
const body = JSON.stringify({
|
||||
npub,
|
||||
token,
|
||||
})
|
||||
|
||||
const method = 'POST'
|
||||
const url = `${NOAUTHD_URL}/created`
|
||||
|
||||
return this.sendPostAuthd({
|
||||
npub,
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
})
|
||||
}
|
||||
|
||||
private notify() {
|
||||
// FIXME collect info from accessBuffer and confirmBuffer
|
||||
// and update the notifications
|
||||
@@ -420,8 +437,8 @@ export class NoauthBackend {
|
||||
const app = this.apps.find((a) => a.appNpub === r.req.appNpub)
|
||||
if (r.req.method !== 'connect' && !app) continue
|
||||
|
||||
// FIXME use Nsec.app icon!
|
||||
const icon = 'https://nostr.band/android-chrome-192x192.png'
|
||||
// FIXME check
|
||||
const icon = 'assets/icons/logo.svg'
|
||||
|
||||
const appName = app?.name || getShortenNpub(r.req.appNpub)
|
||||
// FIXME load profile?
|
||||
@@ -550,6 +567,50 @@ export class NoauthBackend {
|
||||
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({
|
||||
appNpub: appNpub,
|
||||
npub: npub,
|
||||
timestamp: Date.now(),
|
||||
name: appName,
|
||||
icon: appIcon,
|
||||
url: appUrl,
|
||||
})
|
||||
|
||||
// reload
|
||||
this.apps = await dbi.listApps()
|
||||
|
||||
// write new perms confirmed by user
|
||||
for (const p of perms) {
|
||||
await dbi.addPerm({
|
||||
id: Math.random().toString(36).substring(7),
|
||||
npub: npub,
|
||||
appNpub: appNpub,
|
||||
perm: p,
|
||||
value: '1',
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
// reload
|
||||
this.perms = await dbi.listPerms()
|
||||
}
|
||||
|
||||
private async allowPermitCallback({
|
||||
backend,
|
||||
npub,
|
||||
@@ -566,13 +627,13 @@ export class NoauthBackend {
|
||||
}
|
||||
|
||||
const appNpub = nip19.npubEncode(remotePubkey)
|
||||
const connected = !!this.apps.find(a => a.appNpub === appNpub)
|
||||
if (!connected && method !== 'connect') {
|
||||
const connected = !!this.apps.find((a) => a.appNpub === appNpub)
|
||||
if (!connected && method !== 'connect') {
|
||||
console.log('ignoring request before connect', method, id, appNpub, npub)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const req: DbPending = {
|
||||
const req: DbPending = {
|
||||
id,
|
||||
npub,
|
||||
appNpub,
|
||||
@@ -588,24 +649,24 @@ export class NoauthBackend {
|
||||
// confirm
|
||||
console.log(Date.now(), allow ? 'allowed' : 'disallowed', npub, method, options, params)
|
||||
|
||||
if (manual) {
|
||||
if (manual) {
|
||||
await dbi.confirmPending(id, allow)
|
||||
|
||||
// add app on 'allow connect'
|
||||
// add app on 'allow connect'
|
||||
if (method === 'connect' && allow) {
|
||||
// if (!(await dbi.getApp(req.appNpub))) {
|
||||
await dbi.addApp({
|
||||
appNpub: req.appNpub,
|
||||
npub: req.npub,
|
||||
timestamp: Date.now(),
|
||||
name: '',
|
||||
icon: '',
|
||||
url: '',
|
||||
})
|
||||
await dbi.addApp({
|
||||
appNpub: req.appNpub,
|
||||
npub: req.npub,
|
||||
timestamp: Date.now(),
|
||||
name: '',
|
||||
icon: '',
|
||||
url: '',
|
||||
})
|
||||
|
||||
// reload
|
||||
self.apps = await dbi.listApps()
|
||||
}
|
||||
// reload
|
||||
self.apps = await dbi.listApps()
|
||||
}
|
||||
} else {
|
||||
// just send to db w/o waiting for it
|
||||
dbi.addConfirmed({
|
||||
@@ -625,7 +686,7 @@ export class NoauthBackend {
|
||||
let newPerms = [getReqPerm(req)]
|
||||
if (allow && options && options.perms) newPerms = options.perms
|
||||
|
||||
// write new perms confirmed by user
|
||||
// write new perms confirmed by user
|
||||
for (const p of newPerms) {
|
||||
await dbi.addPerm({
|
||||
id: req.id,
|
||||
@@ -635,14 +696,14 @@ export class NoauthBackend {
|
||||
value: allow ? '1' : '0',
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// reload
|
||||
// reload
|
||||
this.perms = await dbi.listPerms()
|
||||
|
||||
// confirm pending requests that might now have
|
||||
// the proper perms
|
||||
const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub)
|
||||
// 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) {
|
||||
let perm = this.getPerm(r.req)
|
||||
@@ -790,6 +851,11 @@ export class NoauthBackend {
|
||||
return k
|
||||
}
|
||||
|
||||
private async redeemToken(npub: string, token: string) {
|
||||
console.log('redeeming token', npub, token)
|
||||
await this.sendTokenToServer(npub, token)
|
||||
}
|
||||
|
||||
private async importKey(name: string, nsec: string) {
|
||||
const k = await this.addKey({ name, nsec })
|
||||
this.updateUI()
|
||||
@@ -821,42 +887,42 @@ export class NoauthBackend {
|
||||
const key = this.enckeys.find((k) => k.npub === npub)
|
||||
if (key) return this.keyInfo(key)
|
||||
|
||||
let name = ''
|
||||
let existingName = true
|
||||
// check name - user might have provided external nip05,
|
||||
// or just his npub - we must fetch their name from our
|
||||
// server, and if not exists - try to assign one
|
||||
const npubName = await this.fetchNpubName(npub)
|
||||
if (npubName) {
|
||||
// already have name for this npub
|
||||
console.log("existing npub name", npub, npubName)
|
||||
name = npubName
|
||||
} else if (nip05.includes('@')) {
|
||||
// no name for them?
|
||||
const [nip05name, domain] = nip05.split('@')
|
||||
if (domain === DOMAIN) {
|
||||
// wtf? how did we learn their npub if
|
||||
// it's the name on our server but we can't fetch it?
|
||||
console.log("existing name", nip05name)
|
||||
name = nip05name
|
||||
} else {
|
||||
// try to take same name on our domain
|
||||
existingName = false
|
||||
name = nip05name
|
||||
let takenName = await fetchNip05(`${name}@${DOMAIN}`)
|
||||
if (takenName) {
|
||||
// already taken? try name_domain as name
|
||||
name = `${nip05name}_${domain}`
|
||||
takenName = await fetchNip05(`${name}@${DOMAIN}`)
|
||||
}
|
||||
if (takenName) {
|
||||
console.log("All names taken, leave without a name?")
|
||||
name = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
let name = ''
|
||||
let existingName = true
|
||||
// check name - user might have provided external nip05,
|
||||
// or just his npub - we must fetch their name from our
|
||||
// server, and if not exists - try to assign one
|
||||
const npubName = await this.fetchNpubName(npub)
|
||||
if (npubName) {
|
||||
// already have name for this npub
|
||||
console.log('existing npub name', npub, npubName)
|
||||
name = npubName
|
||||
} else if (nip05.includes('@')) {
|
||||
// no name for them?
|
||||
const [nip05name, domain] = nip05.split('@')
|
||||
if (domain === DOMAIN) {
|
||||
// wtf? how did we learn their npub if
|
||||
// it's the name on our server but we can't fetch it?
|
||||
console.log('existing name', nip05name)
|
||||
name = nip05name
|
||||
} else {
|
||||
// try to take same name on our domain
|
||||
existingName = false
|
||||
name = nip05name
|
||||
let takenName = await fetchNip05(`${name}@${DOMAIN}`)
|
||||
if (takenName) {
|
||||
// already taken? try name_domain as name
|
||||
name = `${nip05name}_${domain}`
|
||||
takenName = await fetchNip05(`${name}@${DOMAIN}`)
|
||||
}
|
||||
if (takenName) {
|
||||
console.log('All names taken, leave without a name?')
|
||||
name = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("fetch", { name, existingName })
|
||||
console.log('fetch', { name, existingName })
|
||||
|
||||
// add new key
|
||||
const nsec = await this.keysModule.decryptKeyPass({
|
||||
@@ -926,6 +992,8 @@ export class NoauthBackend {
|
||||
let result = undefined
|
||||
if (method === 'generateKey') {
|
||||
result = await this.generateKey(args[0])
|
||||
} else if (method === 'redeemToken') {
|
||||
result = await this.redeemToken(args[0], args[1])
|
||||
} else if (method === 'importKey') {
|
||||
result = await this.importKey(args[0], args[1])
|
||||
} else if (method === 'saveKey') {
|
||||
@@ -934,6 +1002,8 @@ export class NoauthBackend {
|
||||
result = await this.fetchKey(args[0], args[1], args[2])
|
||||
} else if (method === 'confirm') {
|
||||
result = await this.confirm(args[0], args[1], args[2], args[3])
|
||||
} else if (method === 'connectApp') {
|
||||
result = await this.connectApp(args[0])
|
||||
} else if (method === 'deleteApp') {
|
||||
result = await this.deleteApp(args[0])
|
||||
} else if (method === 'deletePerm') {
|
||||
|
@@ -103,6 +103,17 @@ export const dbi = {
|
||||
console.log(`db addApp error: ${error}`)
|
||||
}
|
||||
},
|
||||
updateApp: async (app: Omit<DbApp, 'npub' | 'timestamp'>) => {
|
||||
try {
|
||||
await db.apps.where({ appNpub: app.appNpub }).modify({
|
||||
name: app.name,
|
||||
icon: app.icon,
|
||||
url: app.url,
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(`db updateApp error: ${error}`)
|
||||
}
|
||||
},
|
||||
listApps: async (): Promise<DbApp[]> => {
|
||||
try {
|
||||
return await db.apps.toArray()
|
||||
|
@@ -1,11 +1,11 @@
|
||||
import { useParams } from 'react-router'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectAppByAppNpub, selectPermsByNpubAndAppNpub } from '@/store'
|
||||
import { selectAppByAppNpub, selectKeys, selectPermsByNpubAndAppNpub } from '@/store'
|
||||
import { Navigate, useNavigate } from 'react-router-dom'
|
||||
import { formatTimestampDate } from '@/utils/helpers/date'
|
||||
import { Box, Stack, Typography } from '@mui/material'
|
||||
import { Box, IconButton, Stack, Typography } from '@mui/material'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { ACTION_TYPE } from '@/utils/consts'
|
||||
import { Permissions } from './components/Permissions/Permissions'
|
||||
@@ -18,28 +18,35 @@ import { IOSBackButton } from '@/shared/IOSBackButton/IOSBackButton'
|
||||
import { ModalActivities } from './components/Activities/ModalActivities'
|
||||
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'
|
||||
|
||||
const AppPage = () => {
|
||||
const keys = useAppSelector(selectKeys)
|
||||
|
||||
const { appNpub = '', npub = '' } = useParams()
|
||||
const currentApp = useAppSelector((state) => selectAppByAppNpub(state, appNpub))
|
||||
const perms = useAppSelector((state) => selectPermsByNpubAndAppNpub(state, npub, appNpub))
|
||||
|
||||
const navigate = useNavigate()
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
const perms = useAppSelector((state) => selectPermsByNpubAndAppNpub(state, npub, appNpub))
|
||||
const currentApp = useAppSelector((state) => selectAppByAppNpub(state, appNpub))
|
||||
|
||||
const { open, handleClose, handleShow } = useToggleConfirm()
|
||||
const { handleOpen: handleOpenModal } = useModalSearchParams()
|
||||
|
||||
const connectPerm = perms.find((perm) => perm.perm === 'connect' || perm.perm === ACTION_TYPE.BASIC)
|
||||
|
||||
if (!currentApp) {
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
|
||||
if (!isNpubExists || !currentApp) {
|
||||
return <Navigate to={`/key/${npub}`} />
|
||||
}
|
||||
|
||||
const { icon = '', name = '' } = currentApp || {}
|
||||
const appName = name || getShortenNpub(appNpub)
|
||||
const { timestamp } = connectPerm || {}
|
||||
const { icon = '', name = '', url = '' } = currentApp || {}
|
||||
const appDomain = getDomain(url)
|
||||
const appName = name || appDomain || getShortenNpub(appNpub)
|
||||
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||
|
||||
const { timestamp } = connectPerm || {}
|
||||
const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected'
|
||||
|
||||
const handleDeleteApp = async () => {
|
||||
@@ -52,16 +59,23 @@ const AppPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleShowAppDetailsModal = () => handleOpenModal(MODAL_PARAMS_KEYS.APP_DETAILS)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack maxHeight={'100%'} overflow={'auto'} alignItems={'flex-start'} height={'100%'}>
|
||||
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
|
||||
<Stack marginBottom={'1rem'} direction={'row'} gap={'1rem'} width={'100%'}>
|
||||
<StyledAppIcon src={icon} />
|
||||
<StyledAppIcon src={icon}>{appAvatarTitle}</StyledAppIcon>
|
||||
<Box flex={'1'} overflow={'hidden'}>
|
||||
<Typography variant="h4" noWrap>
|
||||
{appName}
|
||||
</Typography>
|
||||
<Stack direction={'row'} alignItems={'center'} gap={'0.5rem'}>
|
||||
<Typography variant="h4" noWrap flex={1}>
|
||||
{appName}
|
||||
</Typography>
|
||||
<IconButton onClick={handleShowAppDetailsModal}>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Typography variant="body2" noWrap>
|
||||
{connectedOn}
|
||||
</Typography>
|
||||
@@ -89,6 +103,7 @@ const AppPage = () => {
|
||||
onClose={handleClose}
|
||||
/>
|
||||
<ModalActivities appNpub={appNpub} />
|
||||
<ModalAppDetails />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@@ -2,20 +2,20 @@ import { FC } from 'react'
|
||||
import { Box, IconButton, Typography } from '@mui/material'
|
||||
import { DbPerm } from '@/modules/db'
|
||||
import { formatTimestampDate } from '@/utils/helpers/date'
|
||||
import { ACTIONS } from '@/utils/consts'
|
||||
import { StyledPermissionItem } from './styled'
|
||||
import ClearRoundedIcon from '@mui/icons-material/ClearRounded'
|
||||
import DoneRoundedIcon from '@mui/icons-material/DoneRounded'
|
||||
import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded'
|
||||
import { ItemPermissionMenu } from './ItemPermissionMenu'
|
||||
import { useOpenMenu } from '@/hooks/useOpenMenu'
|
||||
import { getPermActionName } from '@/utils/helpers/helpers'
|
||||
|
||||
type ItemPermissionProps = {
|
||||
permission: DbPerm
|
||||
}
|
||||
|
||||
export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => {
|
||||
const { perm, value, timestamp, id } = permission || {}
|
||||
const { value, timestamp, id } = permission || {}
|
||||
|
||||
const { anchorEl, handleClose, handleOpen, open } = useOpenMenu()
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ItemPermission: FC<ItemPermissionProps> = ({ permission }) => {
|
||||
<StyledPermissionItem>
|
||||
<Box display={'flex'} flexDirection={'column'} gap={'0.5rem'} flex={1}>
|
||||
<Typography flex={1} fontWeight={700}>
|
||||
{ACTIONS[perm] || perm}
|
||||
{getPermActionName(permission)}
|
||||
</Typography>
|
||||
<Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
|
||||
</Box>
|
||||
|
119
src/pages/CreatePage/Create.Page.tsx
Normal file
119
src/pages/CreatePage/Create.Page.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
import { GetStartedButton, LearnMoreButton } from './styled'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { ModalConfirmConnect } from '@/components/Modal/ModalConfirmConnect/ModalConfirmConnect'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { useState } from 'react'
|
||||
|
||||
const CreatePage = () => {
|
||||
const notify = useEnqueueSnackbar()
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
const [created, setCreated] = useState(false)
|
||||
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
const name = searchParams.get('name') || ''
|
||||
const token = searchParams.get('token') || ''
|
||||
const appNpub = searchParams.get('appNpub') || ''
|
||||
const isValid = name && token && appNpub
|
||||
|
||||
const nip05 = `${name}@${DOMAIN}`
|
||||
|
||||
const handleLearnMore = () => {
|
||||
// @ts-ignore
|
||||
window.open(`https://${DOMAIN}`, '_blank').focus()
|
||||
}
|
||||
|
||||
const handleClickAddAccount = async () => {
|
||||
try {
|
||||
const key: any = await swicCall('generateKey', name)
|
||||
|
||||
let appUrl = ''
|
||||
if (window.document.referrer) {
|
||||
try {
|
||||
const u = new URL(window.document.referrer)
|
||||
appUrl = u.origin
|
||||
} catch {}
|
||||
}
|
||||
|
||||
console.log('Created', key.npub, 'app', appUrl)
|
||||
setCreated(true)
|
||||
|
||||
handleOpen(MODAL_PARAMS_KEYS.CONFIRM_CONNECT, {
|
||||
search: {
|
||||
npub: key.npub,
|
||||
appNpub,
|
||||
appUrl,
|
||||
token,
|
||||
// needed for this screen itself
|
||||
name,
|
||||
// will close after all done
|
||||
popup: 'true',
|
||||
},
|
||||
replace: true,
|
||||
})
|
||||
} catch (error: any) {
|
||||
notify(error.message || error.toString(), 'error')
|
||||
}
|
||||
}
|
||||
|
||||
if (!isValid) {
|
||||
return (
|
||||
<Stack maxHeight={'100%'} overflow={'auto'}>
|
||||
<Typography textAlign={'center'} variant="h6" paddingTop="1em">
|
||||
Bad parameters.
|
||||
</Typography>
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack maxHeight={'100%'} overflow={'auto'}>
|
||||
{created && (
|
||||
<>
|
||||
<Typography textAlign={'center'} variant="h4" paddingTop="0.5em">
|
||||
Account created!
|
||||
</Typography>
|
||||
<Typography textAlign={'center'} variant="body1" paddingTop="0.5em">
|
||||
User name: <b>{nip05}</b>
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
{!created && (
|
||||
<>
|
||||
<Typography textAlign={'center'} variant="h4" paddingTop="0.5em">
|
||||
Welcome to Nostr!
|
||||
</Typography>
|
||||
<Stack gap={'0.5rem'} overflow={'auto'}>
|
||||
<Typography textAlign={'left'} variant="h6" paddingTop="0.5em">
|
||||
Chosen name: <b>{nip05}</b>
|
||||
</Typography>
|
||||
<GetStartedButton onClick={handleClickAddAccount}>Create account</GetStartedButton>
|
||||
|
||||
<Typography textAlign={'left'} variant="h5" paddingTop="1em">
|
||||
What you need to know:
|
||||
</Typography>
|
||||
|
||||
<ol style={{ marginLeft: '1em' }}>
|
||||
<li>Nostr accounts are based on cryptographic keys.</li>
|
||||
<li>All your actions on Nostr will be signed by your keys.</li>
|
||||
<li>Nsec.app is one of many services to manage Nostr keys.</li>
|
||||
<li>When you create an account, a new key will be created.</li>
|
||||
<li>This key can later be used with other Nostr websites.</li>
|
||||
</ol>
|
||||
<LearnMoreButton onClick={handleLearnMore}>Learn more</LearnMoreButton>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<ModalConfirmConnect />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreatePage
|
26
src/pages/CreatePage/styled.tsx
Normal file
26
src/pages/CreatePage/styled.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { AppButtonProps, Button } from '@/shared/Button/Button'
|
||||
import { styled } from '@mui/material'
|
||||
import PersonAddAltRoundedIcon from '@mui/icons-material/PersonAddAltRounded'
|
||||
import PlayArrowOutlinedIcon from '@mui/icons-material/PlayArrowOutlined'
|
||||
import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'
|
||||
|
||||
export const AddAccountButton = styled((props: AppButtonProps) => (
|
||||
<Button {...props} startIcon={<PersonAddAltRoundedIcon />} />
|
||||
))(() => ({
|
||||
alignSelf: 'center',
|
||||
padding: '0.35rem 1rem',
|
||||
}))
|
||||
|
||||
export const GetStartedButton = styled((props: AppButtonProps) => (
|
||||
<Button {...props} startIcon={<PlayArrowOutlinedIcon />} />
|
||||
))(() => ({
|
||||
alignSelf: 'left',
|
||||
padding: '0.35rem 1rem',
|
||||
}))
|
||||
|
||||
export const LearnMoreButton = styled((props: AppButtonProps) => (
|
||||
<Button {...props} startIcon={<HelpOutlineOutlinedIcon />} />
|
||||
))(() => ({
|
||||
alignSelf: 'left',
|
||||
padding: '0.35rem 1rem',
|
||||
}))
|
@@ -18,7 +18,7 @@ const HomePage = () => {
|
||||
|
||||
const handleLearnMore = () => {
|
||||
// @ts-ignore
|
||||
window.open(`https://info.${DOMAIN}`, '_blank').focus()
|
||||
window.open(`https://${DOMAIN}`, '_blank').focus()
|
||||
}
|
||||
|
||||
return (
|
||||
|
@@ -1,26 +1,26 @@
|
||||
import { FC } from 'react'
|
||||
import { DbKey } from '../../../modules/db'
|
||||
import { Avatar, Stack, StackProps, Typography, TypographyProps, styled } from '@mui/material'
|
||||
import { getShortenNpub } from '../../../utils/helpers/helpers'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useProfile } from '@/hooks/useProfile'
|
||||
|
||||
type ItemKeyProps = DbKey
|
||||
|
||||
export const ItemKey: FC<ItemKeyProps> = (props) => {
|
||||
const { npub, profile } = props
|
||||
const { npub } = props
|
||||
const navigate = useNavigate()
|
||||
const { userName, userAvatar, avatarTitle } = useProfile(npub)
|
||||
|
||||
const handleNavigate = () => {
|
||||
navigate('/key/' + npub)
|
||||
}
|
||||
const { name = '', picture = '' } = profile?.info || {}
|
||||
const userName = name || getShortenNpub(npub)
|
||||
const userAvatar = picture || ''
|
||||
|
||||
return (
|
||||
<StyledKeyContainer onClick={handleNavigate}>
|
||||
<Stack direction={'row'} alignItems={'center'} gap="1rem">
|
||||
<Avatar src={userAvatar} alt={userName} />
|
||||
<Avatar src={userAvatar} alt={userName}>
|
||||
{avatarTitle}
|
||||
</Avatar>
|
||||
<StyledText variant="body1">{userName}</StyledText>
|
||||
</Stack>
|
||||
</StyledKeyContainer>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { useAppSelector } from '../../store/hooks/redux'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { Navigate, useParams } from 'react-router-dom'
|
||||
import { Stack } from '@mui/material'
|
||||
import { StyledIconButton } from './styled'
|
||||
import { SettingsIcon, ShareIcon } from '@/assets'
|
||||
@@ -11,7 +11,6 @@ import { ModalSettings } from '@/components/Modal/ModalSettings/ModalSettings'
|
||||
import { ModalExplanation } from '@/components/Modal/ModalExplanation/ModalExplanation'
|
||||
import { ModalConfirmConnect } from '@/components/Modal/ModalConfirmConnect/ModalConfirmConnect'
|
||||
import { ModalConfirmEvent } from '@/components/Modal/ModalConfirmEvent/ModalConfirmEvent'
|
||||
import { useProfile } from './hooks/useProfile'
|
||||
import { useBackgroundSigning } from './hooks/useBackgroundSigning'
|
||||
import { BackgroundSigningWarning } from './components/BackgroundSigningWarning'
|
||||
import UserValueSection from './components/UserValueSection'
|
||||
@@ -19,29 +18,32 @@ import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { checkNpubSyncQuerier } from './utils'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
const KeyPage = () => {
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const { keys, apps, pending, perms } = useAppSelector((state) => state.content)
|
||||
|
||||
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
|
||||
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
|
||||
// const { userNameWithPrefix } = useProfile(npub)
|
||||
const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning()
|
||||
|
||||
const key = keys.find((k) => k.npub === npub)
|
||||
let username = ''
|
||||
if (key?.name) {
|
||||
if (key.name.includes('@')) username = key.name
|
||||
else username = `${key?.name}@${DOMAIN}`
|
||||
}
|
||||
|
||||
const getUsername = useCallback(() => {
|
||||
if (!key || !key?.name) return ''
|
||||
if (key.name.includes('@')) return key.name
|
||||
return `${key?.name}@${DOMAIN}`
|
||||
}, [key])
|
||||
const username = getUsername()
|
||||
|
||||
const filteredApps = apps.filter((a) => a.npub === npub)
|
||||
const { prepareEventPendings } = useTriggerConfirmModal(npub, pending, perms)
|
||||
|
||||
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||
const isKeyExists = npub.trim().length && key
|
||||
if (!isKeyExists) return <Navigate to={`/home`} />
|
||||
|
||||
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
||||
|
||||
return (
|
||||
@@ -54,7 +56,7 @@ const KeyPage = () => {
|
||||
title="Your login"
|
||||
value={username}
|
||||
copyValue={username}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.LOGIN}
|
||||
/>
|
||||
<UserValueSection
|
||||
title="Your NPUB"
|
||||
|
@@ -15,7 +15,7 @@ type AppsProps = {
|
||||
npub: string
|
||||
}
|
||||
|
||||
export const Apps: FC<AppsProps> = ({ apps = [], npub = '' }) => {
|
||||
export const Apps: FC<AppsProps> = ({ apps = [] }) => {
|
||||
const notify = useEnqueueSnackbar()
|
||||
|
||||
// eslint-disable-next-line
|
||||
@@ -26,18 +26,22 @@ export const Apps: FC<AppsProps> = ({ apps = [], npub = '' }) => {
|
||||
})
|
||||
}
|
||||
|
||||
const openAppStore = () => {
|
||||
window.open('https://nostrapp.link', '_blank')
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flex={1} marginBottom={'1rem'} display={'flex'} flexDirection={'column'} overflow={'auto'}>
|
||||
<Stack direction={'row'} alignItems={'center'} justifyContent={'space-between'} marginBottom={'0.5rem'}>
|
||||
<SectionTitle>Connected apps</SectionTitle>
|
||||
<AppLink title="Discover Apps" />
|
||||
<AppLink title="Discover Apps" onClick={openAppStore} />
|
||||
</Stack>
|
||||
{!apps.length && (
|
||||
<StyledEmptyAppsBox>
|
||||
<Typography className="message" variant="h5" fontWeight={600} textAlign={'center'}>
|
||||
No connected apps
|
||||
</Typography>
|
||||
<Button>Discover Nostr Apps</Button>
|
||||
<Button onClick={openAppStore}>Discover Nostr Apps</Button>
|
||||
</StyledEmptyAppsBox>
|
||||
)}
|
||||
|
||||
|
@@ -2,14 +2,16 @@ import { DbApp } from '@/modules/db'
|
||||
import { Avatar, Stack, Typography } from '@mui/material'
|
||||
import { FC } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
// import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined'
|
||||
import { getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { getAppIconTitle, getDomain, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import { StyledItemAppContainer } from './styled'
|
||||
|
||||
type ItemAppProps = DbApp
|
||||
|
||||
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => {
|
||||
const appName = name || getShortenNpub(appNpub)
|
||||
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name, url }) => {
|
||||
const appDomain = getDomain(url)
|
||||
const appName = name || appDomain || getShortenNpub(appNpub)
|
||||
const appIcon = icon || `https://${appDomain}/favicon.ico`
|
||||
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||
return (
|
||||
<StyledItemAppContainer
|
||||
direction={'row'}
|
||||
@@ -19,7 +21,14 @@ export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name }) => {
|
||||
component={Link}
|
||||
to={`/key/${npub}/app/${appNpub}`}
|
||||
>
|
||||
<Avatar variant="square" sx={{ width: 56, height: 56 }} src={icon} alt={name} />
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
sx={{ width: 56, height: 56 }}
|
||||
src={appIcon}
|
||||
alt={appName}
|
||||
>
|
||||
{appAvatarTitle}
|
||||
</Avatar>
|
||||
<Stack>
|
||||
<Typography noWrap display={'block'} variant="body2">
|
||||
{appName}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { Input, InputProps } from '@/shared/Input/Input'
|
||||
import { Input, AppInputProps } from '@/shared/Input/Input'
|
||||
import { Stack, StackProps, styled } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
export const StyledInput = styled(
|
||||
forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
|
||||
forwardRef<HTMLInputElement, AppInputProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
|
@@ -1,31 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { fetchProfile } from '@/modules/nostr'
|
||||
import { MetaEvent } from '@/types/meta-event'
|
||||
import { getProfileUsername } from '@/utils/helpers/helpers'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
|
||||
export const useProfile = (npub: string) => {
|
||||
const [profile, setProfile] = useState<MetaEvent | null>(null)
|
||||
|
||||
const userName = getProfileUsername(profile, npub)
|
||||
// FIXME use nip05?
|
||||
const userNameWithPrefix = userName + '@' + DOMAIN
|
||||
|
||||
const loadProfile = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetchProfile(npub)
|
||||
setProfile(response)
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch profile:', error)
|
||||
}
|
||||
}, [npub])
|
||||
|
||||
useEffect(() => {
|
||||
loadProfile()
|
||||
}, [loadProfile])
|
||||
|
||||
return {
|
||||
profile,
|
||||
userNameWithPrefix,
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { Input, InputProps } from '@/shared/Input/Input'
|
||||
import { Input, AppInputProps } from '@/shared/Input/Input'
|
||||
import { Box, Button, ButtonProps, styled, Badge } from '@mui/material'
|
||||
import { forwardRef } from 'react'
|
||||
|
||||
@@ -53,7 +53,7 @@ export const StyledEmptyAppsBox = styled(Box)(({ theme }) => {
|
||||
})
|
||||
|
||||
export const StyledInput = styled(
|
||||
forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => {
|
||||
forwardRef<HTMLInputElement, AppInputProps>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { Suspense, lazy } from 'react'
|
||||
import { Route, Routes, Navigate } from 'react-router-dom'
|
||||
import HomePage from '../pages/HomePage/Home.Page'
|
||||
import WelcomePage from '../pages/Welcome.Page'
|
||||
import { Layout } from '../layout/Layout'
|
||||
import { CircularProgress, Stack } from '@mui/material'
|
||||
import CreatePage from '@/pages/CreatePage/Create.Page'
|
||||
|
||||
const KeyPage = lazy(() => import('../pages/KeyPage/Key.Page'))
|
||||
const ConfirmPage = lazy(() => import('../pages/Confirm.Page'))
|
||||
@@ -26,6 +26,7 @@ const AppRoutes = () => {
|
||||
<Route path="/key/:npub" element={<KeyPage />} />
|
||||
<Route path="/key/:npub/app/:appNpub" element={<AppPage />} />
|
||||
<Route path="/key/:npub/:req_id" element={<ConfirmPage />} />
|
||||
<Route path="/create" element={<CreatePage />} />
|
||||
</Route>
|
||||
<Route path="*" element={<Navigate to={'/home'} />} />
|
||||
</Routes>
|
||||
|
@@ -7,7 +7,7 @@ export type AppButtonProps = MuiButtonProps & {
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, AppButtonProps>(({ children, ...restProps }, ref) => {
|
||||
return (
|
||||
<StyledButton classes={{ root: 'button' }} {...restProps} ref={ref}>
|
||||
<StyledButton classes={{ root: 'button', disabled: 'disabled' }} {...restProps} ref={ref}>
|
||||
{children}
|
||||
</StyledButton>
|
||||
)
|
||||
@@ -27,19 +27,22 @@ const StyledButton = styled(
|
||||
background: theme.palette.backgroundSecondary.default,
|
||||
},
|
||||
color: theme.palette.text.primary,
|
||||
'&.disabled': {
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
}
|
||||
}
|
||||
return {
|
||||
...commonStyles,
|
||||
'&.button:is(:hover, :active, &)': {
|
||||
'&.button:is(:hover, :active, &, .disabled)': {
|
||||
background: theme.palette.primary.main,
|
||||
},
|
||||
color: theme.palette.text.secondary,
|
||||
':disabled': {
|
||||
'&.button:is(:hover, :active, &)': {
|
||||
background: theme.palette.backgroundSecondary.default,
|
||||
},
|
||||
color: theme.palette.backgroundSecondary.paper,
|
||||
'&.disabled': {
|
||||
color: theme.palette.text.secondary,
|
||||
opacity: 0.5,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
}
|
||||
})
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { forwardRef, useRef } from 'react'
|
||||
import { Input, InputProps } from '../Input/Input'
|
||||
import { useRef } from 'react'
|
||||
import { Input, AppInputProps } from '../Input/Input'
|
||||
|
||||
export type DebounceProps = {
|
||||
handleDebounce: (value: string) => void
|
||||
debounceTimeout: number
|
||||
}
|
||||
|
||||
export const DebounceInput = (props: InputProps & DebounceProps) => {
|
||||
export const DebounceInput = (props: AppInputProps & DebounceProps) => {
|
||||
const { handleDebounce, debounceTimeout, ...rest } = props
|
||||
|
||||
const timerRef = useRef<number>()
|
||||
|
@@ -10,14 +10,14 @@ import {
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
|
||||
export type InputProps = InputBaseProps & {
|
||||
export type AppInputProps = InputBaseProps & {
|
||||
helperText?: string | ReactNode
|
||||
helperTextProps?: FormHelperTextProps
|
||||
containerProps?: BoxProps
|
||||
label?: string
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
export const Input = forwardRef<HTMLInputElement, AppInputProps>(
|
||||
({ helperText, containerProps, helperTextProps, label, ...props }, ref) => {
|
||||
return (
|
||||
<StyledInputContainer {...containerProps}>
|
||||
@@ -26,7 +26,7 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
{label}
|
||||
</FormLabel>
|
||||
) : null}
|
||||
<InputBase className="input" {...props} classes={{ error: 'error' }} inputRef={ref} />
|
||||
<InputBase autoComplete="off" className="input" {...props} classes={{ error: 'error' }} ref={ref} />
|
||||
{helperText ? (
|
||||
<FormHelperText {...helperTextProps} className="helper_text">
|
||||
{helperText}
|
||||
|
@@ -20,7 +20,7 @@ const Transition = forwardRef(function Transition(
|
||||
|
||||
export const Modal: FC<ModalProps> = ({ children, title, onClose, withCloseButton = true, fixedHeight, ...props }) => {
|
||||
return (
|
||||
<StyledDialog fixedHeight={fixedHeight} {...props} onClose={onClose} TransitionComponent={Transition}>
|
||||
<StyledDialog fixedheight={fixedHeight} {...props} onClose={onClose} TransitionComponent={Transition}>
|
||||
{withCloseButton && (
|
||||
<StyledCloseButtonWrapper>
|
||||
<IconButton onClick={() => onClose && onClose({}, 'backdropClick')} className="close_btn">
|
||||
|
@@ -10,7 +10,7 @@ import {
|
||||
styled,
|
||||
} from '@mui/material'
|
||||
|
||||
export const StyledDialog = styled((props: DialogProps & { fixedHeight?: string }) => (
|
||||
export const StyledDialog = styled((props: DialogProps & { fixedheight?: string }) => (
|
||||
<Dialog
|
||||
{...props}
|
||||
classes={{
|
||||
@@ -26,8 +26,8 @@ export const StyledDialog = styled((props: DialogProps & { fixedHeight?: string
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
))(({ theme, fixedHeight = '' }) => {
|
||||
const fixedHeightStyles = fixedHeight ? { height: fixedHeight } : {}
|
||||
))(({ theme, fixedheight = '' }) => {
|
||||
const fixedHeightStyles = fixedheight ? { height: fixedheight } : {}
|
||||
return {
|
||||
'& .container': {
|
||||
alignItems: 'flex-end',
|
||||
|
@@ -43,6 +43,11 @@ export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
|
||||
export const selectKeys = (state: RootState) => state.content.keys
|
||||
export const selectApps = (state: RootState) => state.content.apps
|
||||
|
||||
export const selectKeyByNpub = (state: RootState, npub: string) => {
|
||||
return state.content.keys.find((key) => key.npub === npub)
|
||||
}
|
||||
|
||||
export const selectAppsByNpub = memoizeOne((state: RootState, npub: string) => {
|
||||
return state.content.apps.filter((app) => app.npub === npub)
|
||||
|
@@ -9,9 +9,11 @@ export enum MODAL_PARAMS_KEYS {
|
||||
CONFIRM_CONNECT = 'confirm-connect',
|
||||
CONFIRM_EVENT = 'confirm-event',
|
||||
ACTIVITY = 'activity',
|
||||
APP_DETAILS = 'app-details',
|
||||
}
|
||||
|
||||
export enum EXPLANATION_MODAL_KEYS {
|
||||
BUNKER = 'bunker',
|
||||
NPUB = 'npub',
|
||||
LOGIN = 'login',
|
||||
}
|
||||
|
@@ -16,6 +16,7 @@ export enum ACTION_TYPE {
|
||||
}
|
||||
|
||||
export const ACTIONS: { [type: string]: string } = {
|
||||
basic: 'Basic permissions',
|
||||
get_public_key: 'Get public key',
|
||||
sign_event: 'Sign event',
|
||||
connect: 'Connect',
|
||||
|
@@ -3,7 +3,7 @@ import { format } from 'date-fns'
|
||||
export const formatTimestampDate = (timestamp: number) => {
|
||||
try {
|
||||
const date = new Date(timestamp)
|
||||
const formattedDate = format(date, "HH:mm',' dd-MM-yyyy")
|
||||
const formattedDate = format(date, "dd-MM-yyyy HH:mm")
|
||||
return formattedDate
|
||||
} catch (error) {
|
||||
return ''
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { ACTION_TYPE, NIP46_RELAYS } from '../consts'
|
||||
import { DbPending } from '@/modules/db'
|
||||
import { ACTIONS, ACTION_TYPE, NIP46_RELAYS } from '../consts'
|
||||
import { DbPending, DbPerm } from '@/modules/db'
|
||||
import { MetaEvent } from '@/types/meta-event'
|
||||
|
||||
export async function call(cb: () => any) {
|
||||
@@ -15,14 +15,9 @@ export const getShortenNpub = (npub = '') => {
|
||||
return npub.substring(0, 10) + '...' + npub.slice(-4)
|
||||
}
|
||||
|
||||
export const getAppIconTitle = (name: string | undefined, appNpub: string) => {
|
||||
return name
|
||||
? name[0].toLocaleUpperCase()
|
||||
: appNpub.substring(4, 7);
|
||||
}
|
||||
|
||||
export const getProfileUsername = (profile: MetaEvent | null, npub: string) => {
|
||||
return profile?.info?.name || profile?.info?.display_name || getShortenNpub(npub)
|
||||
export const getProfileUsername = (profile: MetaEvent | null) => {
|
||||
if (!profile) return null
|
||||
return profile?.info?.name || profile?.info?.display_name
|
||||
}
|
||||
|
||||
export const getBunkerLink = (npub = '') => {
|
||||
@@ -85,7 +80,7 @@ export function isPackagePerm(perm: string, reqPerm: string) {
|
||||
|
||||
export async function fetchNip05(value: string, origin?: string) {
|
||||
try {
|
||||
const [username, domain] = value.split('@')
|
||||
const [username, domain] = value.toLocaleLowerCase().split('@')
|
||||
if (!origin) origin = `https://${domain}`
|
||||
const response = await fetch(`${origin}/.well-known/nostr.json?name=${username}`)
|
||||
const getNpub: {
|
||||
@@ -101,3 +96,38 @@ export async function fetchNip05(value: string, origin?: string) {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export const getDomain = (url: string) => {
|
||||
try {
|
||||
return new URL(url).hostname
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export const getAppIconTitle = (name: string | undefined, appNpub: string) => {
|
||||
return name ? name[0].toLocaleUpperCase() : appNpub.substring(4, 7)
|
||||
}
|
||||
|
||||
export function getReqActionName(req: DbPending) {
|
||||
const action = ACTIONS[req.method]
|
||||
if (req.method === 'sign_event') {
|
||||
const kind = getSignReqKind(req)
|
||||
if (kind !== undefined) return `${action} of kind ${kind}`
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
export function getPermActionName(req: DbPerm) {
|
||||
const method = req.perm.split(':')[0]
|
||||
const action = ACTIONS[method]
|
||||
if (method === 'sign_event') {
|
||||
const kind = req.perm.split(':')[1]
|
||||
if (kind !== undefined) return `${action} of kind ${kind}`
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
export const isEmptyString = (str = '') => {
|
||||
return str.trim().length === 0
|
||||
}
|
||||
|
Reference in New Issue
Block a user