Merge w/ develop
This commit is contained in:
commit
e3feb8b5a2
@ -18,7 +18,7 @@ function App() {
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const keys: DbKey[] = await dbi.listKeys()
|
||||
console.log(keys, 'keys')
|
||||
// console.log(keys, 'keys')
|
||||
|
||||
dispatch(setKeys({ keys }))
|
||||
const loadProfiles = async () => {
|
||||
@ -65,7 +65,7 @@ function App() {
|
||||
|
||||
useEffect(() => {
|
||||
ndk.connect().then(() => {
|
||||
console.log('NDK connected', { ndk })
|
||||
console.log('NDK connected')
|
||||
setIsConnected(true)
|
||||
})
|
||||
// eslint-disable-next-line
|
||||
|
@ -68,6 +68,7 @@ export const ModalAppDetails = () => {
|
||||
if (isEmptyString(url)) return
|
||||
|
||||
try {
|
||||
|
||||
const u = new URL(url)
|
||||
|
||||
if (isEmptyString(name)) setDetails((prev) => ({ ...prev, name: u.hostname }))
|
||||
@ -119,7 +120,7 @@ export const ModalAppDetails = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const isFormValid = !isEmptyString(url) && !isEmptyString(name)
|
||||
const isFormValid = !isEmptyString(name)
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
@ -130,6 +131,13 @@ export const ModalAppDetails = () => {
|
||||
</Typography>
|
||||
</Stack>
|
||||
|
||||
<Input
|
||||
label="Name"
|
||||
fullWidth
|
||||
placeholder="Enter app name"
|
||||
onChange={handleInputChange('name')}
|
||||
value={details.name}
|
||||
/>
|
||||
<Autocomplete
|
||||
options={[]}
|
||||
freeSolo
|
||||
@ -150,13 +158,6 @@ export const ModalAppDetails = () => {
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
label="Name"
|
||||
fullWidth
|
||||
placeholder="Enter app name"
|
||||
onChange={handleInputChange('name')}
|
||||
value={details.name}
|
||||
/>
|
||||
<Input
|
||||
label="Icon"
|
||||
fullWidth
|
||||
|
@ -1,15 +1,22 @@
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { askNotificationPermission, call, getAppIconTitle, getDomain, getReferrerAppUrl, getShortenNpub } from '@/utils/helpers/helpers'
|
||||
import {
|
||||
askNotificationPermission,
|
||||
call,
|
||||
getAppIconTitle,
|
||||
getDomain,
|
||||
getReferrerAppUrl,
|
||||
getShortenNpub,
|
||||
} from '@/utils/helpers/helpers'
|
||||
import { Avatar, Box, Stack, Typography } from '@mui/material'
|
||||
import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { selectAppsByNpub, selectKeys, selectPendingsByNpub } from '@/store'
|
||||
import { StyledButton, StyledToggleButtonsGroup } from './styled'
|
||||
import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
||||
import { useState } from 'react'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { swicCall, swicWaitStarted } from '@/modules/swic'
|
||||
import { ACTION_TYPE } from '@/utils/consts'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
|
||||
@ -28,6 +35,7 @@ export const ModalConfirmConnect = () => {
|
||||
const pending = useAppSelector((state) => selectPendingsByNpub(state, npub))
|
||||
|
||||
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.BASIC)
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
|
||||
const appNpub = searchParams.get('appNpub') || ''
|
||||
const pendingReqId = searchParams.get('reqId') || ''
|
||||
@ -37,7 +45,7 @@ export const ModalConfirmConnect = () => {
|
||||
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
||||
const { name, url = '', icon = '' } = triggerApp || {}
|
||||
|
||||
const appUrl = url || searchParams.get('appUrl') || getReferrerAppUrl();
|
||||
const appUrl = url || searchParams.get('appUrl') || getReferrerAppUrl()
|
||||
const appDomain = getDomain(appUrl)
|
||||
const appName = name || appDomain || getShortenNpub(appNpub)
|
||||
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||
@ -53,15 +61,45 @@ export const ModalConfirmConnect = () => {
|
||||
},
|
||||
})
|
||||
|
||||
// NOTE: when opened directly to this modal using authUrl,
|
||||
// we might not have pending requests visible yet bcs we haven't
|
||||
// loaded them yet, which means this modal will be closed with
|
||||
// the logic below. So now if it's popup then we wait for SW
|
||||
// and then wait a little more to give it time to fetch
|
||||
// pending reqs from db. Same logic implemented in confirm-event.
|
||||
|
||||
// FIXME move to a separate hook and reuse?
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalOpened) {
|
||||
if (isPopup) {
|
||||
console.log("waiting for sw")
|
||||
// wait for SW to start
|
||||
swicWaitStarted().then(() => {
|
||||
// give it some time to load the pending reqs etc
|
||||
console.log("waiting for sw done")
|
||||
setTimeout(() => setIsLoaded(true), 500)
|
||||
})
|
||||
} else {
|
||||
setIsLoaded(true)
|
||||
}
|
||||
} else {
|
||||
setIsLoaded(false)
|
||||
}
|
||||
}, [isModalOpened, isPopup])
|
||||
|
||||
if (isLoaded) {
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
// App doesn't exist yet!
|
||||
// NOTE: app doesn't exist yet!
|
||||
// const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||||
const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId)
|
||||
// console.log("pending", {isModalOpened, isPendingReqIdExists, isNpubExists, /*isAppNpubExists,*/ pendingReqId, pending});
|
||||
if (!isPopup && isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) {
|
||||
closeModalAfterRequest()
|
||||
if (isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) {
|
||||
if (isPopup) window.close()
|
||||
else closeModalAfterRequest()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
|
||||
if (!value) return undefined
|
||||
@ -85,6 +123,7 @@ export const ModalConfirmConnect = () => {
|
||||
const options = { perms, appUrl }
|
||||
await confirmPending(pendingReqId, true, true, options)
|
||||
} else {
|
||||
|
||||
try {
|
||||
await askNotificationPermission()
|
||||
const result = await swicCall('enablePush')
|
||||
@ -93,7 +132,7 @@ export const ModalConfirmConnect = () => {
|
||||
} catch (e: any) {
|
||||
console.log('error', e)
|
||||
notify('Please enable Notifications in website settings!', 'error')
|
||||
return
|
||||
// keep going
|
||||
}
|
||||
|
||||
try {
|
||||
@ -179,7 +218,7 @@ export const ModalConfirmConnect = () => {
|
||||
</StyledToggleButtonsGroup>
|
||||
<Stack direction={'row'} gap={'1rem'}>
|
||||
<StyledButton onClick={disallow} varianttype="secondary">
|
||||
Disallow
|
||||
Ignore
|
||||
</StyledButton>
|
||||
<StyledButton fullWidth onClick={allow}>
|
||||
Connect
|
||||
|
@ -10,7 +10,7 @@ import { ActionToggleButton } from './сomponents/ActionToggleButton'
|
||||
import { FC, useEffect, useMemo, useState } from 'react'
|
||||
import { StyledActionsListContainer, StyledButton, StyledToggleButtonsGroup } from './styled'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { swicCall, swicWaitStarted } from '@/modules/swic'
|
||||
import { Checkbox } from '@/shared/Checkbox/Checkbox'
|
||||
import { DbPending } from '@/modules/db'
|
||||
import { IPendingsByAppNpub } from '@/pages/KeyPage/hooks/useTriggerConfirmModal'
|
||||
@ -47,6 +47,7 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
||||
|
||||
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.ALWAYS)
|
||||
const [pendingRequests, setPendingRequests] = useState<PendingRequest[]>([])
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
|
||||
const currentAppPendingReqs = useMemo(() => confirmEventReqs[appNpub]?.pending || [], [confirmEventReqs, appNpub])
|
||||
|
||||
@ -61,26 +62,32 @@ export const ModalConfirmEvent: FC<ModalConfirmEventProps> = ({ confirmEventReqs
|
||||
},
|
||||
})
|
||||
|
||||
// FIXME: when opened directly to this modal using authUrl,
|
||||
// we might not have pending requests visible yet bcs we haven't
|
||||
// loaded them yet, which means this modal will be closed with
|
||||
// the login below. It's fine if only one app has sent pending
|
||||
// requests atm, bcs the modal would re-appear as soon as we load
|
||||
// the requests. But if there are several pending reqs from other
|
||||
// apps then popup might show a different one! Which is very
|
||||
// contrary to what user expects. So:
|
||||
// - if isPopup - dont close the modal with logic below
|
||||
// - show some 'loading' indicator until we've got some requests
|
||||
// for the specified appNpub
|
||||
// FIXME is the same logic valid for Connect modal?
|
||||
useEffect(() => {
|
||||
if (isModalOpened) {
|
||||
if (isPopup) {
|
||||
// wait for SW to start
|
||||
swicWaitStarted().then(() => {
|
||||
// give it some time to load the pending reqs etc
|
||||
setTimeout(() => setIsLoaded(true), 500)
|
||||
})
|
||||
} else {
|
||||
setIsLoaded(true)
|
||||
}
|
||||
} else {
|
||||
setIsLoaded(false)
|
||||
}
|
||||
}, [isModalOpened, isPopup])
|
||||
|
||||
if (isLoaded) {
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
|
||||
// console.log("confirm event", { confirmEventReqs, isModalOpened, isNpubExists, isAppNpubExists });
|
||||
if (isModalOpened && (!currentAppPendingReqs.length || !isNpubExists || !isAppNpubExists)) {
|
||||
closeModalAfterRequest()
|
||||
if (isPopup) window.close()
|
||||
else closeModalAfterRequest()
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const triggerApp = apps.find((app) => app.appNpub === appNpub)
|
||||
const { name, icon = '' } = triggerApp || {}
|
||||
|
174
src/components/Modal/ModalEditName/ModalEditName.tsx
Normal file
174
src/components/Modal/ModalEditName/ModalEditName.tsx
Normal file
@ -0,0 +1,174 @@
|
||||
import { CheckmarkIcon } from '@/assets'
|
||||
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { swicCall } from '@/modules/swic'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { selectKeys } from '@/store'
|
||||
import { useAppSelector } from '@/store/hooks/redux'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { fetchNip05 } from '@/utils/helpers/helpers'
|
||||
import { Stack, Typography, useTheme } from '@mui/material'
|
||||
import { ChangeEvent, Fragment, useCallback, useEffect, useState } from 'react'
|
||||
import { useParams, useSearchParams } from 'react-router-dom'
|
||||
import { useDebounce } from 'use-debounce'
|
||||
import { StyledSettingContainer } from './styled'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
|
||||
export const ModalEditName = () => {
|
||||
const keys = useAppSelector(selectKeys)
|
||||
const notify = useEnqueueSnackbar()
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
|
||||
const key = keys.find((k) => k.npub === npub)
|
||||
const name = key?.name || ''
|
||||
|
||||
const { palette } = useTheme()
|
||||
|
||||
const { getModalOpened, createHandleCloseReplace } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EDIT_NAME)
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.EDIT_NAME)
|
||||
|
||||
const [enteredName, setEnteredName] = useState('')
|
||||
const [debouncedName] = useDebounce(enteredName, 300)
|
||||
const isNameEqual = debouncedName === name
|
||||
|
||||
const [receiverNpub, setReceiverNpub] = useState('')
|
||||
|
||||
const [isAvailable, setIsAvailable] = useState(true)
|
||||
const [isChecking, setIsChecking] = useState(false)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isTransferLoading, setIsTransferLoading] = useState(false)
|
||||
|
||||
const checkIsUsernameAvailable = useCallback(async () => {
|
||||
if (!debouncedName.trim().length) return undefined
|
||||
try {
|
||||
setIsChecking(true)
|
||||
const npubNip05 = await fetchNip05(`${debouncedName}@${DOMAIN}`)
|
||||
setIsAvailable(!npubNip05 || npubNip05 === npub)
|
||||
setIsChecking(false)
|
||||
} catch (error) {
|
||||
setIsAvailable(true)
|
||||
setIsChecking(false)
|
||||
}
|
||||
}, [debouncedName])
|
||||
|
||||
useEffect(() => {
|
||||
checkIsUsernameAvailable()
|
||||
}, [checkIsUsernameAvailable])
|
||||
|
||||
useEffect(() => {
|
||||
setEnteredName(name)
|
||||
return () => {
|
||||
if (isModalOpened) {
|
||||
setEnteredName('')
|
||||
setReceiverNpub('')
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line
|
||||
}, [isModalOpened])
|
||||
|
||||
const handleNameChange = (e: ChangeEvent<HTMLInputElement>) => setEnteredName(e.target.value)
|
||||
|
||||
const handleReceiverNpubChange = (e: ChangeEvent<HTMLInputElement>) => setReceiverNpub(e.target.value)
|
||||
|
||||
const getInputHelperText = () => {
|
||||
if (!debouncedName.trim().length || isNameEqual) return ''
|
||||
if (isChecking) return 'Loading...'
|
||||
if (!isAvailable) return 'Already taken'
|
||||
return (
|
||||
<Fragment>
|
||||
<CheckmarkIcon /> Available
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
const inputHelperText = getInputHelperText()
|
||||
|
||||
const getHelperTextColor = useCallback(() => {
|
||||
if (!debouncedName || isChecking || isNameEqual) return palette.textSecondaryDecorate.main
|
||||
return isAvailable ? palette.success.main : palette.error.main
|
||||
// deps
|
||||
}, [debouncedName, isAvailable, isChecking, isNameEqual, palette])
|
||||
|
||||
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
|
||||
if (isModalOpened && !isNpubExists) {
|
||||
handleCloseModal()
|
||||
return null
|
||||
}
|
||||
|
||||
const isEditButtonDisabled = isNameEqual || !isAvailable || isChecking || isLoading || !enteredName.trim().length
|
||||
const isTransferButtonDisabled = !name.length || !receiverNpub.trim().length || isTransferLoading
|
||||
|
||||
const handleEditName = async () => {
|
||||
if (isEditButtonDisabled) return
|
||||
try {
|
||||
setIsLoading(true)
|
||||
await swicCall('editName', npub, enteredName)
|
||||
notify('Username updated!', 'success')
|
||||
setIsLoading(false)
|
||||
} catch (error: any) {
|
||||
setIsLoading(false)
|
||||
notify(error?.message || 'Failed to edit username!', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const handleTransferName = async () => {
|
||||
if (isTransferButtonDisabled) return
|
||||
try {
|
||||
setIsTransferLoading(true)
|
||||
await swicCall('transferName', npub, enteredName, receiverNpub)
|
||||
notify('Username transferred!', 'success')
|
||||
setIsTransferLoading(false)
|
||||
setEnteredName('')
|
||||
} catch (error: any) {
|
||||
setIsTransferLoading(false)
|
||||
notify(error?.message || 'Failed to transfer username!', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} title="Username Settings" onClose={handleCloseModal}>
|
||||
<Stack gap={'1rem'}>
|
||||
<StyledSettingContainer>
|
||||
<SectionTitle>Change name</SectionTitle>
|
||||
<Input
|
||||
label="User name"
|
||||
fullWidth
|
||||
placeholder="Enter a Username"
|
||||
endAdornment={<Typography color={'#FFFFFFA8'}>@{DOMAIN}</Typography>}
|
||||
helperText={inputHelperText}
|
||||
onChange={handleNameChange}
|
||||
value={enteredName}
|
||||
helperTextProps={{
|
||||
sx: {
|
||||
'&.helper_text': {
|
||||
color: getHelperTextColor(),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Button fullWidth disabled={isEditButtonDisabled} onClick={handleEditName}>
|
||||
Save name {isLoading && <LoadingSpinner />}
|
||||
</Button>
|
||||
</StyledSettingContainer>
|
||||
<StyledSettingContainer>
|
||||
<SectionTitle>Transfer name</SectionTitle>
|
||||
<Input
|
||||
label="Receiver npub"
|
||||
fullWidth
|
||||
placeholder="npub1..."
|
||||
onChange={handleReceiverNpubChange}
|
||||
value={receiverNpub}
|
||||
/>
|
||||
<Button fullWidth onClick={handleTransferName} disabled={isTransferButtonDisabled}>
|
||||
Transfer name
|
||||
</Button>
|
||||
</StyledSettingContainer>
|
||||
</Stack>
|
||||
</Modal>
|
||||
)
|
||||
}
|
10
src/components/Modal/ModalEditName/styled.tsx
Normal file
10
src/components/Modal/ModalEditName/styled.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { Stack, StackProps, styled } from "@mui/material";
|
||||
|
||||
export const StyledSettingContainer = styled((props: StackProps) => (
|
||||
<Stack gap={'0.75rem'} component={'form'} {...props} />
|
||||
))(({ theme }) => ({
|
||||
padding: '1rem',
|
||||
borderRadius: '1rem',
|
||||
background: theme.palette.background.default,
|
||||
color: theme.palette.text.primary,
|
||||
}))
|
@ -6,7 +6,6 @@ import { Input } from '@/shared/Input/Input'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Stack, Typography, useTheme } from '@mui/material'
|
||||
import { StyledAppLogo } from './styled'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useForm } from 'react-hook-form'
|
||||
import { FormInputType, schema } from './const'
|
||||
@ -149,13 +148,15 @@ export const ModalImportKeys = () => {
|
||||
const nsecHelperText = getNsecHelperText()
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
||||
<StyledAppLogo />
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
|
||||
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
||||
<Stack gap={'0.2rem'} padding={'0 1rem'} alignSelf={'flex-start'}>
|
||||
<Typography fontWeight={600} variant="h5">
|
||||
Import key
|
||||
</Typography>
|
||||
<Typography noWrap variant="body2" color={'GrayText'}>
|
||||
Bring your existing Nostr keys to Nsec.app
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Input
|
||||
label="Choose a username"
|
||||
|
@ -1,43 +1,36 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
// import { useEffect } from 'react'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Fade, Stack } from '@mui/material'
|
||||
import { AppLink } from '@/shared/AppLink/AppLink'
|
||||
import { Stack } from '@mui/material'
|
||||
// import { AppLink } from '@/shared/AppLink/AppLink'
|
||||
|
||||
export const ModalInitial = () => {
|
||||
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
|
||||
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL)
|
||||
|
||||
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.INITIAL)
|
||||
|
||||
const [showAdvancedContent, setShowAdvancedContent] = useState(false)
|
||||
// const [showAdvancedContent, setShowAdvancedContent] = useState(false)
|
||||
|
||||
const handleShowAdvanced = () => {
|
||||
setShowAdvancedContent(true)
|
||||
}
|
||||
// const handleShowAdvanced = () => {
|
||||
// setShowAdvancedContent(true)
|
||||
// }
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isModalOpened) {
|
||||
setShowAdvancedContent(false)
|
||||
}
|
||||
}
|
||||
}, [isModalOpened])
|
||||
// useEffect(() => {
|
||||
// return () => {
|
||||
// if (isModalOpened) {
|
||||
// setShowAdvancedContent(false)
|
||||
// }
|
||||
// }
|
||||
// }, [isModalOpened])
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack paddingTop={'0.5rem'} gap={'1rem'}>
|
||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.SIGN_UP)}>Sign up</Button>
|
||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.LOGIN)}>Login</Button>
|
||||
<AppLink title="Advanced" alignSelf={'center'} onClick={handleShowAdvanced} />
|
||||
|
||||
{showAdvancedContent && (
|
||||
<Fade in>
|
||||
<Button onClick={() => handleOpen(MODAL_PARAMS_KEYS.IMPORT_KEYS)}>Import key</Button>
|
||||
</Fade>
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
)
|
||||
|
@ -5,15 +5,14 @@ import { swicCall } from '@/modules/swic'
|
||||
import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Stack, Typography } from '@mui/material'
|
||||
import { StyledAppLogo } from './styled'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useNavigate, useSearchParams } 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 { fetchNip05, fetchNpubNames } from '@/utils/helpers/helpers'
|
||||
import { usePassword } from '@/hooks/usePassword'
|
||||
import { dbi } from '@/modules/db'
|
||||
import { LoadingSpinner } from '@/shared/LoadingSpinner/LoadingSpinner'
|
||||
@ -32,11 +31,14 @@ export const ModalLogin = () => {
|
||||
const navigate = useNavigate()
|
||||
const { hidePassword, inputProps } = usePassword()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [searchParams] = useSearchParams()
|
||||
const isPopup = searchParams.get('popup') === 'true'
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
register,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<FormInputType>({
|
||||
defaultValues: FORM_DEFAULT_VALUES,
|
||||
@ -79,7 +81,10 @@ export const ModalLogin = () => {
|
||||
notify(`Fetched ${k.npub}`, 'success')
|
||||
dbi.addSynced(k.npub)
|
||||
cleanUpStates()
|
||||
navigate(`/key/${k.npub}`)
|
||||
setTimeout(() => {
|
||||
// give frontend time to read the new key first
|
||||
navigate(`/key/${k.npub}${isPopup ? '?popup=true' : ''}`)
|
||||
}, 300)
|
||||
} catch (error: any) {
|
||||
console.log('error', error)
|
||||
notify(error?.message || 'Something went wrong!', 'error')
|
||||
@ -87,6 +92,22 @@ export const ModalLogin = () => {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isModalOpened) {
|
||||
const npub = searchParams.get('npub') || ''
|
||||
const appNpub = searchParams.get('appNpub') || ''
|
||||
if (isPopup && isModalOpened) {
|
||||
swicCall('fetchPendingRequests', npub, appNpub)
|
||||
|
||||
fetchNpubNames(npub).then((names) => {
|
||||
if (names.length) {
|
||||
setValue('username', `${names[0]}@${DOMAIN}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}, [searchParams, isModalOpened, isPopup, setValue])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isModalOpened) {
|
||||
@ -97,13 +118,15 @@ export const ModalLogin = () => {
|
||||
}, [isModalOpened, cleanUpStates])
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Stack gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
||||
<StyledAppLogo />
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
|
||||
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit(submitHandler)}>
|
||||
<Stack gap={'0.2rem'} padding={'0 1rem'} alignSelf={'flex-start'}>
|
||||
<Typography fontWeight={600} variant="h5">
|
||||
Login
|
||||
</Typography>
|
||||
<Typography noWrap variant="body2" color={'GrayText'}>
|
||||
Sync keys from the cloud to this device
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Input
|
||||
label="Username or nip05 or npub"
|
||||
@ -119,11 +142,15 @@ export const ModalLogin = () => {
|
||||
{...register('password')}
|
||||
{...inputProps}
|
||||
error={!!errors.password}
|
||||
helperText={'Password you set in Cloud Sync settings'}
|
||||
/>
|
||||
|
||||
<Stack gap={'0.5rem'}>
|
||||
<Button type="submit" fullWidth disabled={isLoading}>
|
||||
Add account {isLoading && <LoadingSpinner />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import { Modal } from '@/shared/Modal/Modal'
|
||||
import { MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { Stack, Typography, useTheme } from '@mui/material'
|
||||
import React, { ChangeEvent, useEffect, useState } from 'react'
|
||||
import { StyledAppLogo } from './styled'
|
||||
import { Input } from '@/shared/Input/Input'
|
||||
import { Button } from '@/shared/Button/Button'
|
||||
import { CheckmarkIcon } from '@/assets'
|
||||
@ -61,9 +60,15 @@ export const ModalSignUp = () => {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const k: any = await swicCall('generateKey', name)
|
||||
notify(`Account created for "${name}"`, 'success')
|
||||
navigate(`/key/${k.npub}`)
|
||||
if (k.name)
|
||||
notify(`Account created for "${k.name}"`, 'success')
|
||||
else
|
||||
notify(`Failed to assign name "${name}", try again`, 'error')
|
||||
setIsLoading(false)
|
||||
setTimeout(() => {
|
||||
// give frontend time to read the new key first
|
||||
navigate(`/key/${k.npub}`)
|
||||
}, 300)
|
||||
} catch (error: any) {
|
||||
notify(error?.message || 'Something went wrong!', 'error')
|
||||
setIsLoading(false)
|
||||
@ -81,13 +86,15 @@ export const ModalSignUp = () => {
|
||||
}, [isModalOpened])
|
||||
|
||||
return (
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal}>
|
||||
<Modal open={isModalOpened} onClose={handleCloseModal} withCloseButton={false}>
|
||||
<Stack paddingTop={'1rem'} gap={'1rem'} component={'form'} onSubmit={handleSubmit}>
|
||||
<Stack direction={'row'} gap={'1rem'} alignItems={'center'} alignSelf={'flex-start'}>
|
||||
<StyledAppLogo />
|
||||
<Stack gap={'0.2rem'} padding={'0 1rem'} alignSelf={'flex-start'}>
|
||||
<Typography fontWeight={600} variant="h5">
|
||||
Sign up
|
||||
</Typography>
|
||||
<Typography noWrap variant="body2" color={'GrayText'}>
|
||||
Generate new Nostr keys
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Input
|
||||
label="Username"
|
||||
@ -110,10 +117,12 @@ export const ModalSignUp = () => {
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Stack gap={'0.5rem'}>
|
||||
<Button fullWidth type="submit" disabled={isLoading}>
|
||||
Create account {isLoading && <LoadingSpinner />}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ export const useModalSearchParams = () => {
|
||||
const enumKey = getEnumParam(modal)
|
||||
searchParams.delete(enumKey)
|
||||
extraOptions?.onClose && extraOptions?.onClose(searchParams)
|
||||
console.log({ searchParams })
|
||||
// console.log({ searchParams })
|
||||
setSearchParams(searchParams, { replace: !!extraOptions?.replace })
|
||||
}
|
||||
|
||||
|
@ -1,24 +1,34 @@
|
||||
import { generatePrivateKey, getPublicKey, nip19 } from 'nostr-tools'
|
||||
import { Event, generatePrivateKey, getPublicKey, nip19, verifySignature } from 'nostr-tools'
|
||||
import { DbApp, dbi, DbKey, DbPending, DbPerm } from './db'
|
||||
import { Keys } from './keys'
|
||||
import NDK, {
|
||||
IEventHandlingStrategy,
|
||||
NDKEvent,
|
||||
NDKNip46Backend,
|
||||
NDKPrivateKeySigner,
|
||||
NDKSigner,
|
||||
NDKSubscription,
|
||||
NDKSubscriptionCacheUsage,
|
||||
NDKUser,
|
||||
} from '@nostr-dev-kit/ndk'
|
||||
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN } from '../utils/consts'
|
||||
import { Nip04 } from './nip04'
|
||||
// import { Nip04 } from './nip04'
|
||||
import { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
|
||||
import { NostrPowEvent, minePow } from './pow'
|
||||
//import { PrivateKeySigner } from './signer'
|
||||
|
||||
//const PERF_TEST = false
|
||||
|
||||
enum DECISION {
|
||||
ASK = '',
|
||||
ALLOW = 'allow',
|
||||
DISALLOW = 'disallow',
|
||||
IGNORE = 'ignore',
|
||||
}
|
||||
|
||||
export interface KeyInfo {
|
||||
npub: string
|
||||
nip05?: string
|
||||
name?: string
|
||||
locked: boolean
|
||||
}
|
||||
|
||||
@ -28,11 +38,12 @@ interface Key {
|
||||
backoff: number
|
||||
signer: NDKSigner
|
||||
backend: NDKNip46Backend
|
||||
watcher: Watcher
|
||||
}
|
||||
|
||||
interface Pending {
|
||||
req: DbPending
|
||||
cb: (allow: boolean, remember: boolean, options?: any) => void
|
||||
cb: (allow: DECISION, remember: boolean, options?: any) => void
|
||||
notified?: boolean
|
||||
}
|
||||
|
||||
@ -46,86 +57,171 @@ interface IAllowCallbackParams {
|
||||
params?: any
|
||||
}
|
||||
|
||||
class Nip04KeyHandlingStrategy implements IEventHandlingStrategy {
|
||||
private privkey: string
|
||||
private nip04 = new Nip04()
|
||||
class Watcher {
|
||||
private ndk: NDK
|
||||
private signer: NDKSigner
|
||||
private onReply: (id: string) => void
|
||||
private sub?: NDKSubscription
|
||||
|
||||
constructor(privkey: string) {
|
||||
this.privkey = privkey
|
||||
constructor(ndk: NDK, signer: NDKSigner, onReply: (id: string) => void) {
|
||||
this.ndk = ndk
|
||||
this.signer = signer
|
||||
this.onReply = onReply
|
||||
}
|
||||
|
||||
private async getKey(backend: NDKNip46Backend, id: string, remotePubkey: string, recipientPubkey: string) {
|
||||
if (
|
||||
!(await backend.pubkeyAllowed({
|
||||
id,
|
||||
pubkey: remotePubkey,
|
||||
// @ts-ignore
|
||||
method: 'get_nip04_key',
|
||||
params: recipientPubkey,
|
||||
}))
|
||||
) {
|
||||
backend.debug(`get_nip04_key request from ${remotePubkey} rejected`)
|
||||
return undefined
|
||||
async start() {
|
||||
this.sub = this.ndk.subscribe(
|
||||
{
|
||||
kinds: [KIND_RPC],
|
||||
authors: [(await this.signer.user()).pubkey],
|
||||
since: Math.floor(Date.now() / 1000 - 10),
|
||||
},
|
||||
{
|
||||
closeOnEose: false,
|
||||
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
|
||||
}
|
||||
|
||||
return Buffer.from(this.nip04.createKey(this.privkey, recipientPubkey)).toString('hex')
|
||||
}
|
||||
|
||||
async handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]) {
|
||||
const [recipientPubkey] = params
|
||||
return await this.getKey(backend, id, remotePubkey, recipientPubkey)
|
||||
}
|
||||
}
|
||||
|
||||
class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
|
||||
readonly backend: NDKNip46Backend
|
||||
readonly npub: string
|
||||
readonly method: string
|
||||
private body: IEventHandlingStrategy
|
||||
private allowCb: (params: IAllowCallbackParams) => Promise<boolean>
|
||||
|
||||
constructor(
|
||||
backend: NDKNip46Backend,
|
||||
npub: string,
|
||||
method: string,
|
||||
body: IEventHandlingStrategy,
|
||||
allowCb: (params: IAllowCallbackParams) => Promise<boolean>
|
||||
) {
|
||||
this.backend = backend
|
||||
this.npub = npub
|
||||
this.method = method
|
||||
this.body = body
|
||||
this.allowCb = allowCb
|
||||
}
|
||||
|
||||
async handle(
|
||||
backend: NDKNip46Backend,
|
||||
id: string,
|
||||
remotePubkey: string,
|
||||
params: string[]
|
||||
): Promise<string | undefined> {
|
||||
console.log(Date.now(), 'handle', {
|
||||
method: this.method,
|
||||
id,
|
||||
remotePubkey,
|
||||
params,
|
||||
)
|
||||
this.sub.on('event', async (e: NDKEvent) => {
|
||||
const peer = e.tags.find((t) => t.length >= 2 && t[0] === 'p')
|
||||
console.log('watcher got event', { e, peer })
|
||||
if (!peer) return
|
||||
const decryptedContent = await this.signer.decrypt(new NDKUser({ pubkey: peer[1] }), e.content)
|
||||
const parsedContent = JSON.parse(decryptedContent)
|
||||
const { id, method, params, result, error } = parsedContent
|
||||
console.log('watcher got', { peer, id, method, params, result, error })
|
||||
if (method || result === 'auth_url') return
|
||||
this.onReply(id)
|
||||
})
|
||||
const allow = await this.allowCb({
|
||||
backend: this.backend,
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.sub!.stop()
|
||||
}
|
||||
}
|
||||
|
||||
class Nip46Backend extends NDKNip46Backend {
|
||||
private allowCb: (params: IAllowCallbackParams) => Promise<DECISION>
|
||||
private npub: string = ''
|
||||
|
||||
public constructor(ndk: NDK, signer: NDKSigner, allowCb: (params: IAllowCallbackParams) => Promise<DECISION>) {
|
||||
super(ndk, signer, () => Promise.resolve(true))
|
||||
this.allowCb = allowCb
|
||||
signer.user().then((u) => (this.npub = nip19.npubEncode(u.pubkey)))
|
||||
}
|
||||
|
||||
public async processEvent(event: NDKEvent) {
|
||||
this.handleIncomingEvent(event)
|
||||
}
|
||||
|
||||
protected async handleIncomingEvent(event: NDKEvent) {
|
||||
const { id, method, params } = (await this.rpc.parseEvent(event)) as any
|
||||
const remotePubkey = event.pubkey
|
||||
let response: string | undefined
|
||||
|
||||
this.debug('incoming event', { id, method, params })
|
||||
|
||||
// validate signature explicitly
|
||||
if (!verifySignature(event.rawEvent() as Event)) {
|
||||
this.debug('invalid signature', event.rawEvent())
|
||||
return
|
||||
}
|
||||
|
||||
const decision = await this.allowCb({
|
||||
backend: this,
|
||||
npub: this.npub,
|
||||
id,
|
||||
method: this.method,
|
||||
method,
|
||||
remotePubkey,
|
||||
params,
|
||||
})
|
||||
if (!allow) return undefined
|
||||
return this.body.handle(backend, id, remotePubkey, params).then((r) => {
|
||||
console.log(Date.now(), 'req', id, 'method', this.method, 'result', r)
|
||||
return r
|
||||
})
|
||||
console.log(Date.now(), 'handle', { method, id, decision, remotePubkey, params })
|
||||
if (decision === DECISION.IGNORE) return
|
||||
|
||||
const allow = decision === DECISION.ALLOW
|
||||
const strategy = this.handlers[method]
|
||||
if (allow) {
|
||||
if (strategy) {
|
||||
try {
|
||||
response = await strategy.handle(this, id, remotePubkey, params)
|
||||
console.log(Date.now(), 'req', id, 'method', method, 'result', response)
|
||||
} catch (e: any) {
|
||||
this.debug('error handling event', e, { id, method, params })
|
||||
this.rpc.sendResponse(id, remotePubkey, 'error', undefined, e.message)
|
||||
}
|
||||
} else {
|
||||
this.debug('unsupported method', { method, params })
|
||||
}
|
||||
}
|
||||
|
||||
if (response) {
|
||||
this.debug(`sending response to ${remotePubkey}`, response)
|
||||
this.rpc.sendResponse(id, remotePubkey, response)
|
||||
} else {
|
||||
this.rpc.sendResponse(id, remotePubkey, 'error', undefined, 'Not authorized')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// class Nip04KeyHandlingStrategy implements IEventHandlingStrategy {
|
||||
// private privkey: string
|
||||
// private nip04 = new Nip04()
|
||||
|
||||
// constructor(privkey: string) {
|
||||
// this.privkey = privkey
|
||||
// }
|
||||
|
||||
// private async getKey(backend: NDKNip46Backend, id: string, remotePubkey: string, recipientPubkey: string) {
|
||||
// if (
|
||||
// !(await backend.pubkeyAllowed({
|
||||
// id,
|
||||
// pubkey: remotePubkey,
|
||||
// // @ts-ignore
|
||||
// method: 'get_nip04_key',
|
||||
// params: recipientPubkey,
|
||||
// }))
|
||||
// ) {
|
||||
// backend.debug(`get_nip04_key request from ${remotePubkey} rejected`)
|
||||
// return undefined
|
||||
// }
|
||||
|
||||
// return Buffer.from(this.nip04.createKey(this.privkey, recipientPubkey)).toString('hex')
|
||||
// }
|
||||
|
||||
// async handle(backend: NDKNip46Backend, id: string, remotePubkey: string, params: string[]) {
|
||||
// const [recipientPubkey] = params
|
||||
// return await this.getKey(backend, id, remotePubkey, recipientPubkey)
|
||||
// }
|
||||
// }
|
||||
|
||||
// FIXME why do we need it? Just to print
|
||||
// class EventHandlingStrategyWrapper implements IEventHandlingStrategy {
|
||||
// readonly backend: NDKNip46Backend
|
||||
// readonly method: string
|
||||
// private body: IEventHandlingStrategy
|
||||
|
||||
// constructor(
|
||||
// backend: NDKNip46Backend,
|
||||
// method: string,
|
||||
// body: IEventHandlingStrategy
|
||||
// ) {
|
||||
// this.backend = backend
|
||||
// this.method = method
|
||||
// this.body = body
|
||||
// }
|
||||
|
||||
// async handle(
|
||||
// backend: NDKNip46Backend,
|
||||
// id: string,
|
||||
// remotePubkey: string,
|
||||
// params: string[]
|
||||
// ): Promise<string | undefined> {
|
||||
// return this.body.handle(backend, id, remotePubkey, params).then((r) => {
|
||||
// console.log(Date.now(), 'req', id, 'method', this.method, 'result', r)
|
||||
// return r
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
export class NoauthBackend {
|
||||
readonly swg: ServiceWorkerGlobalScope
|
||||
private keysModule: Keys
|
||||
@ -137,10 +233,16 @@ export class NoauthBackend {
|
||||
private confirmBuffer: Pending[] = []
|
||||
private accessBuffer: DbPending[] = []
|
||||
private notifCallback: (() => void) | null = null
|
||||
private pendingNpubEvents = new Map<string, NDKEvent[]>()
|
||||
private ndk = new NDK({
|
||||
explicitRelayUrls: NIP46_RELAYS,
|
||||
enableOutboxModel: false,
|
||||
})
|
||||
|
||||
public constructor(swg: ServiceWorkerGlobalScope) {
|
||||
this.swg = swg
|
||||
this.keysModule = new Keys(swg.crypto.subtle)
|
||||
this.ndk.connect()
|
||||
|
||||
const self = this
|
||||
swg.addEventListener('activate', (event) => {
|
||||
@ -260,7 +362,7 @@ export class NoauthBackend {
|
||||
if (r.status !== 200 && r.status !== 201) {
|
||||
console.log('Fetch error', url, method, r.status)
|
||||
const body = await r.json()
|
||||
throw new Error('Failed to fetch ' + url, { cause: body })
|
||||
throw new Error('Failed to fetch ' + url, { cause: { body, status: r.status } })
|
||||
}
|
||||
|
||||
return await r.json()
|
||||
@ -395,7 +497,7 @@ export class NoauthBackend {
|
||||
})
|
||||
} catch (e: any) {
|
||||
console.log('error', e.cause)
|
||||
if (e.cause && e.cause.minPow > pow) pow = e.cause.minPow
|
||||
if (e.cause && e.cause.body && e.cause.body.minPow > pow) pow = e.cause.body.minPow
|
||||
else throw e
|
||||
}
|
||||
}
|
||||
@ -419,13 +521,11 @@ export class NoauthBackend {
|
||||
})
|
||||
}
|
||||
|
||||
private async sendTransferNameToServer(
|
||||
npub: string, name: string, newNpub: string
|
||||
) {
|
||||
private async sendTransferNameToServer(npub: string, name: string, newNpub: string) {
|
||||
const body = JSON.stringify({
|
||||
npub,
|
||||
name,
|
||||
newNpub
|
||||
newNpub,
|
||||
})
|
||||
|
||||
const method = 'PUT'
|
||||
@ -533,6 +633,7 @@ export class NoauthBackend {
|
||||
return {
|
||||
npub: k.npub,
|
||||
nip05: k.nip05,
|
||||
name: k.name,
|
||||
locked: this.isLocked(k.npub),
|
||||
}
|
||||
}
|
||||
@ -573,11 +674,16 @@ export class NoauthBackend {
|
||||
await this.startKey({ npub, sk })
|
||||
|
||||
// assign nip05 before adding the key
|
||||
// FIXME set name to db and if this call to 'send' fails
|
||||
// then retry later
|
||||
if (!existingName && name && !name.includes('@')) {
|
||||
console.log('adding key', npub, name)
|
||||
try {
|
||||
await this.sendNameToServer(npub, name)
|
||||
} catch (e) {
|
||||
console.log('create name failed', e)
|
||||
// clear it
|
||||
await dbi.editName(npub, '')
|
||||
dbKey.name = ''
|
||||
}
|
||||
}
|
||||
|
||||
const sub = await this.swg.registration.pushManager.getSubscription()
|
||||
@ -586,7 +692,7 @@ export class NoauthBackend {
|
||||
return this.keyInfo(dbKey)
|
||||
}
|
||||
|
||||
private getPerm(req: DbPending): string {
|
||||
private getDecision(req: DbPending): DECISION {
|
||||
const reqPerm = getReqPerm(req)
|
||||
const appPerms = this.perms.filter((p) => p.npub === req.npub && p.appNpub === req.appNpub)
|
||||
|
||||
@ -595,8 +701,18 @@ export class NoauthBackend {
|
||||
// non-exact next
|
||||
if (!perm) perm = appPerms.find((p) => isPackagePerm(p.perm, reqPerm))
|
||||
|
||||
if (perm) {
|
||||
console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms)
|
||||
return perm?.value || ''
|
||||
return perm.value === '1' ? DECISION.ALLOW : DECISION.DISALLOW
|
||||
}
|
||||
|
||||
const conn = appPerms.find((p) => p.perm === 'connect')
|
||||
if (conn && conn.value === '0') {
|
||||
console.log('req', req, 'perm', reqPerm, 'ignore by connect disallow')
|
||||
return DECISION.IGNORE
|
||||
}
|
||||
|
||||
return DECISION.ASK
|
||||
}
|
||||
|
||||
private async connectApp({
|
||||
@ -605,16 +721,15 @@ export class NoauthBackend {
|
||||
appUrl,
|
||||
perms,
|
||||
appName = '',
|
||||
appIcon = ''
|
||||
appIcon = '',
|
||||
}: {
|
||||
npub: string,
|
||||
appNpub: string,
|
||||
appUrl: string,
|
||||
appName?: string,
|
||||
appIcon?: string,
|
||||
npub: string
|
||||
appNpub: string
|
||||
appUrl: string
|
||||
appName?: string
|
||||
appIcon?: string
|
||||
perms: string[]
|
||||
}) {
|
||||
|
||||
await dbi.addApp({
|
||||
appNpub: appNpub,
|
||||
npub: npub,
|
||||
@ -650,19 +765,19 @@ export class NoauthBackend {
|
||||
method,
|
||||
remotePubkey,
|
||||
params,
|
||||
}: IAllowCallbackParams): Promise<boolean> {
|
||||
}: IAllowCallbackParams): Promise<DECISION> {
|
||||
// same reqs usually come on reconnects
|
||||
if (this.doneReqIds.includes(id)) {
|
||||
console.log('request already done', id)
|
||||
// FIXME maybe repeat the reply, but without the Notification?
|
||||
return false
|
||||
return DECISION.IGNORE
|
||||
}
|
||||
|
||||
const appNpub = nip19.npubEncode(remotePubkey)
|
||||
const connected = !!this.apps.find((a) => a.appNpub === appNpub)
|
||||
if (!connected && method !== 'connect') {
|
||||
console.log('ignoring request before connect', method, id, appNpub, npub)
|
||||
return false
|
||||
return DECISION.IGNORE
|
||||
}
|
||||
|
||||
const req: DbPending = {
|
||||
@ -677,9 +792,21 @@ export class NoauthBackend {
|
||||
const self = this
|
||||
return new Promise(async (ok) => {
|
||||
// called when it's decided whether to allow this or not
|
||||
const onAllow = async (manual: boolean, allow: boolean, remember: boolean, options?: any) => {
|
||||
const onAllow = async (manual: boolean, decision: DECISION, remember: boolean, options?: any) => {
|
||||
// confirm
|
||||
console.log(Date.now(), allow ? 'allowed' : 'disallowed', npub, method, options, params)
|
||||
console.log(Date.now(), decision, npub, method, options, params)
|
||||
|
||||
switch (decision) {
|
||||
case DECISION.ASK:
|
||||
throw new Error('Make a decision!')
|
||||
case DECISION.IGNORE:
|
||||
return // noop
|
||||
case DECISION.ALLOW:
|
||||
case DECISION.DISALLOW:
|
||||
// fall through
|
||||
}
|
||||
|
||||
const allow = decision === DECISION.ALLOW
|
||||
|
||||
if (manual) {
|
||||
await dbi.confirmPending(id, allow)
|
||||
@ -732,35 +859,40 @@ export class NoauthBackend {
|
||||
|
||||
// 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)
|
||||
console.log('updated perms', this.perms, 'otherReqs', otherReqs, 'connected', connected)
|
||||
for (const r of otherReqs) {
|
||||
let perm = this.getPerm(r.req)
|
||||
if (perm) {
|
||||
r.cb(perm === '1', false)
|
||||
}
|
||||
}
|
||||
}
|
||||
// release this promise to send reply
|
||||
// to this req
|
||||
ok(decision)
|
||||
|
||||
// notify UI that it was confirmed
|
||||
// if (!PERF_TEST)
|
||||
this.updateUI()
|
||||
|
||||
// return to let nip46 flow proceed
|
||||
ok(allow)
|
||||
// after replying to this req check pending
|
||||
// reqs maybe they can be replied right away
|
||||
if (remember) {
|
||||
// confirm pending requests that might now have
|
||||
// the proper perms
|
||||
const otherReqs = self.confirmBuffer.filter((r) => r.req.appNpub === req.appNpub)
|
||||
console.log('updated perms', this.perms, 'otherReqs', otherReqs, 'connected', connected)
|
||||
for (const r of otherReqs) {
|
||||
const dec = this.getDecision(r.req)
|
||||
if (dec !== DECISION.ASK) {
|
||||
r.cb(dec, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check perms
|
||||
const perm = this.getPerm(req)
|
||||
console.log(Date.now(), 'perm', req.id, perm)
|
||||
const dec = this.getDecision(req)
|
||||
console.log(Date.now(), 'decision', req.id, dec)
|
||||
|
||||
// have perm?
|
||||
if (perm) {
|
||||
if (dec !== DECISION.ASK) {
|
||||
// reply immediately
|
||||
onAllow(false, perm === '1', false)
|
||||
onAllow(false, dec, false)
|
||||
} else {
|
||||
// put pending req to db
|
||||
await dbi.addPending(req)
|
||||
@ -771,7 +903,7 @@ export class NoauthBackend {
|
||||
// put to a list of pending requests
|
||||
this.confirmBuffer.push({
|
||||
req,
|
||||
cb: (allow, remember, options) => onAllow(true, allow, remember, options),
|
||||
cb: (decision, remember, options) => onAllow(true, decision, remember, options),
|
||||
})
|
||||
|
||||
// OAuth flow
|
||||
@ -804,25 +936,28 @@ export class NoauthBackend {
|
||||
ndk.connect()
|
||||
|
||||
const signer = new NDKPrivateKeySigner(sk) // PrivateKeySigner
|
||||
const backend = new NDKNip46Backend(ndk, signer, () => Promise.resolve(true))
|
||||
this.keys.push({ npub, backend, signer, ndk, backoff })
|
||||
const backend = new Nip46Backend(ndk, signer, this.allowPermitCallback.bind(this)) // , () => Promise.resolve(true)
|
||||
const watcher = new Watcher(ndk, signer, (id) => {
|
||||
// drop pending request
|
||||
dbi.removePending(id).then(() => this.updateUI())
|
||||
})
|
||||
this.keys.push({ npub, backend, signer, ndk, backoff, watcher })
|
||||
|
||||
// new method
|
||||
backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk)
|
||||
// backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk)
|
||||
|
||||
// assign our own permission callback
|
||||
for (const method in backend.handlers) {
|
||||
backend.handlers[method] = new EventHandlingStrategyWrapper(
|
||||
backend,
|
||||
npub,
|
||||
method,
|
||||
backend.handlers[method],
|
||||
this.allowPermitCallback.bind(this)
|
||||
)
|
||||
}
|
||||
// // assign our own permission callback
|
||||
// for (const method in backend.handlers) {
|
||||
// backend.handlers[method] = new EventHandlingStrategyWrapper(
|
||||
// backend,
|
||||
// method,
|
||||
// backend.handlers[method]
|
||||
// )
|
||||
// }
|
||||
|
||||
// start
|
||||
backend.start()
|
||||
watcher.start()
|
||||
console.log('started', npub)
|
||||
|
||||
// backoff reset on successfull connection
|
||||
@ -846,11 +981,13 @@ export class NoauthBackend {
|
||||
const bo = self.keys.find((k) => k.npub === npub)?.backoff || 1000
|
||||
setTimeout(() => {
|
||||
console.log(new Date(), 'reconnect relays for key', npub, 'backoff', bo)
|
||||
// @ts-ignore
|
||||
for (const r of ndk.pool.relays.values()) r.disconnect()
|
||||
// make sure it no longer activates
|
||||
backend.handlers = {}
|
||||
|
||||
// stop watching
|
||||
watcher.stop()
|
||||
|
||||
self.keys = self.keys.filter((k) => k.npub !== npub)
|
||||
self.startKey({ npub, sk, backoff: Math.min(bo * 2, 60000) })
|
||||
}, bo)
|
||||
@ -861,6 +998,27 @@ export class NoauthBackend {
|
||||
r.on('connect', onConnect)
|
||||
r.on('disconnect', onDisconnect)
|
||||
}
|
||||
|
||||
const pendingEvents = this.pendingNpubEvents.get(npub)
|
||||
if (pendingEvents) {
|
||||
this.pendingNpubEvents.delete(npub)
|
||||
for (const e of pendingEvents) {
|
||||
backend.processEvent(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchPendingRequests(npub: string, appNpub: string) {
|
||||
const { data: pubkey } = nip19.decode(npub)
|
||||
const { data: appPubkey } = nip19.decode(appNpub)
|
||||
|
||||
const events = await this.ndk.fetchEvents({
|
||||
kinds: [KIND_RPC],
|
||||
'#p': [pubkey as string],
|
||||
authors: [appPubkey as string],
|
||||
})
|
||||
console.log('fetched pending for', npub, events.size)
|
||||
this.pendingNpubEvents.set(npub, [...events.values()])
|
||||
}
|
||||
|
||||
public async unlock(npub: string) {
|
||||
@ -977,7 +1135,7 @@ export class NoauthBackend {
|
||||
this.updateUI()
|
||||
} else {
|
||||
console.log('confirming req', id, allow, remember, options)
|
||||
req.cb(allow, remember, options)
|
||||
req.cb(allow ? DECISION.ALLOW : DECISION.DISALLOW, remember, options)
|
||||
}
|
||||
}
|
||||
|
||||
@ -996,10 +1154,15 @@ export class NoauthBackend {
|
||||
}
|
||||
|
||||
private async editName(npub: string, name: string) {
|
||||
const key = this.enckeys.find(k => k.npub === npub)
|
||||
if (!key) throw new Error("Npub not found");
|
||||
const key = this.enckeys.find((k) => k.npub == npub)
|
||||
if (!key) throw new Error('Npub not found')
|
||||
if (key.name) {
|
||||
try {
|
||||
await this.sendDeleteNameToServer(npub, key.name)
|
||||
} catch (e: any) {
|
||||
if (e.cause && e.cause.status !== 404) throw e
|
||||
console.log("Deleted name didn't exist")
|
||||
}
|
||||
}
|
||||
if (name) {
|
||||
await this.sendNameToServer(npub, name)
|
||||
@ -1072,6 +1235,8 @@ export class NoauthBackend {
|
||||
result = await this.transferName(args[0], args[1], args[2])
|
||||
} else if (method === 'enablePush') {
|
||||
result = await this.enablePush()
|
||||
} else if (method === 'fetchPendingRequests') {
|
||||
result = await this.fetchPendingRequests(args[0], args[1])
|
||||
} else {
|
||||
console.log('unknown method from UI ', method)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ const reqs = new Map<number, { ok: (r: any) => void; rej: (r: any) => void }>()
|
||||
let nextReqId = 1
|
||||
let onRender: (() => void) | null = null
|
||||
let onReload: (() => void) | null = null
|
||||
const queue: (() => Promise<void> | void)[] = []
|
||||
|
||||
export async function swicRegister() {
|
||||
serviceWorkerRegistration.register({
|
||||
@ -23,14 +24,17 @@ export async function swicRegister() {
|
||||
}
|
||||
})
|
||||
|
||||
navigator.serviceWorker.ready.then((r) => {
|
||||
console.log('sw ready')
|
||||
navigator.serviceWorker.ready.then(async (r) => {
|
||||
console.log('sw ready, queue', queue.length)
|
||||
swr = r
|
||||
if (navigator.serviceWorker.controller) {
|
||||
console.log(`This page is currently controlled by: ${navigator.serviceWorker.controller}`)
|
||||
} else {
|
||||
console.log('This page is not currently controlled by a service worker.')
|
||||
}
|
||||
|
||||
while (queue.length)
|
||||
await (queue.shift()!)()
|
||||
})
|
||||
|
||||
navigator.serviceWorker.addEventListener('message', (event) => {
|
||||
@ -38,6 +42,13 @@ export async function swicRegister() {
|
||||
})
|
||||
}
|
||||
|
||||
export function swicWaitStarted() {
|
||||
return new Promise<void>(ok => {
|
||||
if (swr && swr.active) ok()
|
||||
else queue.push(ok)
|
||||
})
|
||||
}
|
||||
|
||||
function onMessage(data: any) {
|
||||
const { id, result, error } = data
|
||||
console.log('SW message', id, result, error)
|
||||
@ -67,6 +78,8 @@ export async function swicCall(method: string, ...args: any[]) {
|
||||
nextReqId++
|
||||
|
||||
return new Promise((ok, rej) => {
|
||||
|
||||
const call = async () => {
|
||||
if (!swr || !swr.active) {
|
||||
rej(new Error('No active service worker'))
|
||||
return
|
||||
@ -80,6 +93,10 @@ export async function swicCall(method: string, ...args: any[]) {
|
||||
}
|
||||
console.log('sending to SW', msg)
|
||||
swr.active.postMessage(msg)
|
||||
}
|
||||
|
||||
if (swr && swr.active) call()
|
||||
else queue.push(call)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -43,8 +43,10 @@ const AppPage = () => {
|
||||
|
||||
const { icon = '', name = '', url = '' } = currentApp || {}
|
||||
const appDomain = getDomain(url)
|
||||
const appName = name || appDomain || getShortenNpub(appNpub)
|
||||
const shortAppNpub = getShortenNpub(appNpub)
|
||||
const appName = name || appDomain || shortAppNpub
|
||||
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||
const isAppNameExists = !!name || !!appDomain
|
||||
|
||||
const { timestamp } = connectPerm || {}
|
||||
const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected'
|
||||
@ -65,13 +67,20 @@ const AppPage = () => {
|
||||
<>
|
||||
<Stack maxHeight={'100%'} overflow={'auto'} alignItems={'flex-start'} height={'100%'}>
|
||||
<IOSBackButton onNavigate={() => navigate(`key/${npub}`)} />
|
||||
<Stack marginBottom={'1rem'} direction={'row'} gap={'1rem'} width={'100%'}>
|
||||
<Stack marginBottom={'1rem'} direction={'row'} gap={'1rem'} width={'100%'} alignItems={'center'}>
|
||||
<StyledAppIcon src={icon}>{appAvatarTitle}</StyledAppIcon>
|
||||
<Box flex={'1'} overflow={'hidden'}>
|
||||
<Stack direction={'row'} alignItems={'center'} gap={'0.5rem'}>
|
||||
<Typography variant="h4" noWrap flex={1}>
|
||||
<Stack direction={'row'} alignItems={'flex-start'} gap={'0.5rem'} marginBottom={'0.5rem'}>
|
||||
<Box display={'flex'} flexDirection={'column'} flex={1}>
|
||||
<Typography variant="h4" noWrap>
|
||||
{appName}
|
||||
</Typography>
|
||||
{isAppNameExists && (
|
||||
<Typography noWrap display={'block'} variant="body1" color={'GrayText'}>
|
||||
{shortAppNpub}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<IconButton onClick={handleShowAppDetailsModal}>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { FC } from 'react'
|
||||
import { FC } from 'react'
|
||||
import { DbHistory } from '@/modules/db'
|
||||
import { Box, IconButton, Typography } from '@mui/material'
|
||||
import { StyledActivityItem } from './styled'
|
||||
@ -6,16 +6,17 @@ import { formatTimestampDate } from '@/utils/helpers/date'
|
||||
import ClearRoundedIcon from '@mui/icons-material/ClearRounded'
|
||||
import DoneRoundedIcon from '@mui/icons-material/DoneRounded'
|
||||
import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded'
|
||||
import { ACTIONS } from '@/utils/consts'
|
||||
import { getReqActionName } from '@/utils/helpers/helpers'
|
||||
|
||||
type ItemActivityProps = DbHistory
|
||||
|
||||
export const ItemActivity: FC<ItemActivityProps> = ({ allowed, method, timestamp }) => {
|
||||
export const ItemActivity: FC<ItemActivityProps> = (req) => {
|
||||
const { allowed, timestamp } = req
|
||||
return (
|
||||
<StyledActivityItem>
|
||||
<Box display={'flex'} flexDirection={'column'} gap={'0.5rem'} flex={1}>
|
||||
<Typography flex={1} fontWeight={700}>
|
||||
{ACTIONS[method] || method}
|
||||
{getReqActionName(req)}
|
||||
</Typography>
|
||||
<Typography variant="body2">{formatTimestampDate(timestamp)}</Typography>
|
||||
</Box>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useAppSelector } from '../../store/hooks/redux'
|
||||
import { Navigate, useParams } from 'react-router-dom'
|
||||
import { Stack } from '@mui/material'
|
||||
import { Navigate, useParams, useSearchParams } from 'react-router-dom'
|
||||
import { Box, IconButton, Stack } from '@mui/material'
|
||||
import { StyledIconButton } from './styled'
|
||||
import { SettingsIcon, ShareIcon } from '@/assets'
|
||||
import { Apps } from './components/Apps'
|
||||
@ -18,13 +19,20 @@ import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
|
||||
import { useLiveQuery } from 'dexie-react-hooks'
|
||||
import { checkNpubSyncQuerier } from './utils'
|
||||
import { DOMAIN } from '@/utils/consts'
|
||||
import { useCallback } from 'react'
|
||||
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
||||
import MoreHorizRoundedIcon from '@mui/icons-material/MoreHorizRounded'
|
||||
import { ModalEditName } from '@/components/Modal/ModalEditName/ModalEditName'
|
||||
|
||||
const KeyPage = () => {
|
||||
const { npub = '' } = useParams<{ npub: string }>()
|
||||
const { keys, apps, pending, perms } = useAppSelector((state) => state.content)
|
||||
const [searchParams] = useSearchParams()
|
||||
|
||||
const [isCheckingSync, setIsChecking] = useState(true)
|
||||
const handleStopChecking = () => setIsChecking(false)
|
||||
|
||||
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub, handleStopChecking), [npub], false)
|
||||
|
||||
const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false)
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning()
|
||||
|
||||
@ -41,10 +49,21 @@ const KeyPage = () => {
|
||||
const { prepareEventPendings } = useTriggerConfirmModal(npub, pending, perms)
|
||||
|
||||
const isKeyExists = npub.trim().length && key
|
||||
const isPopup = searchParams.get('popup') === 'true'
|
||||
// console.log({ isKeyExists, isPopup })
|
||||
|
||||
if (isPopup && !isKeyExists) {
|
||||
searchParams.set('login', 'true')
|
||||
searchParams.set('npub', npub)
|
||||
const url = `/home?${searchParams.toString()}`
|
||||
return <Navigate to={url} />
|
||||
}
|
||||
|
||||
if (!isKeyExists) return <Navigate to={`/home`} />
|
||||
|
||||
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
|
||||
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
|
||||
const handleOpenEditNameModal = () => handleOpen(MODAL_PARAMS_KEYS.EDIT_NAME)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -55,13 +74,20 @@ const KeyPage = () => {
|
||||
<UserValueSection
|
||||
title="Your login"
|
||||
value={username}
|
||||
copyValue={username}
|
||||
endAdornment={
|
||||
<Box display={'flex'} alignItems={'center'} gap={'0.25rem'}>
|
||||
<IconButton onClick={handleOpenEditNameModal} color={username ? 'default' : 'error'}>
|
||||
<MoreHorizRoundedIcon />
|
||||
</IconButton>
|
||||
<InputCopyButton value={username} />
|
||||
</Box>
|
||||
}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.LOGIN}
|
||||
/>
|
||||
<UserValueSection
|
||||
title="Your NPUB"
|
||||
value={npub}
|
||||
copyValue={npub}
|
||||
endAdornment={<InputCopyButton value={npub} />}
|
||||
explanationType={EXPLANATION_MODAL_KEYS.NPUB}
|
||||
/>
|
||||
|
||||
@ -71,7 +97,11 @@ const KeyPage = () => {
|
||||
Connect app
|
||||
</StyledIconButton>
|
||||
|
||||
<StyledIconButton bgcolor_variant="secondary" onClick={handleOpenSettingsModal} withBadge={!isSynced}>
|
||||
<StyledIconButton
|
||||
bgcolor_variant="secondary"
|
||||
onClick={handleOpenSettingsModal}
|
||||
withBadge={!isCheckingSync && !isSynced}
|
||||
>
|
||||
<SettingsIcon />
|
||||
Settings
|
||||
</StyledIconButton>
|
||||
@ -79,11 +109,13 @@ const KeyPage = () => {
|
||||
|
||||
<Apps apps={filteredApps} npub={npub} />
|
||||
</Stack>
|
||||
|
||||
<ModalConnectApp />
|
||||
<ModalSettings isSynced={isSynced} />
|
||||
<ModalExplanation />
|
||||
<ModalConfirmConnect />
|
||||
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
|
||||
<ModalEditName />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -9,9 +9,12 @@ type ItemAppProps = DbApp
|
||||
|
||||
export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name, url }) => {
|
||||
const appDomain = getDomain(url)
|
||||
const appName = name || appDomain || getShortenNpub(appNpub)
|
||||
const shortAppNpub = getShortenNpub(appNpub)
|
||||
const appName = name || appDomain || shortAppNpub
|
||||
const appIcon = icon || `https://${appDomain}/favicon.ico`
|
||||
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
|
||||
const isAppNameExists = !!name || !!appDomain
|
||||
|
||||
return (
|
||||
<StyledItemAppContainer
|
||||
direction={'row'}
|
||||
@ -21,18 +24,18 @@ export const ItemApp: FC<ItemAppProps> = ({ npub, appNpub, icon, name, url }) =>
|
||||
component={Link}
|
||||
to={`/key/${npub}/app/${appNpub}`}
|
||||
>
|
||||
<Avatar
|
||||
variant="rounded"
|
||||
sx={{ width: 56, height: 56 }}
|
||||
src={appIcon}
|
||||
alt={appName}
|
||||
>
|
||||
<Avatar variant="rounded" sx={{ width: 56, height: 56 }} src={appIcon} alt={appName}>
|
||||
{appAvatarTitle}
|
||||
</Avatar>
|
||||
<Stack>
|
||||
<Typography noWrap display={'block'} variant="body2">
|
||||
<Typography noWrap display={'block'} variant="body1">
|
||||
{appName}
|
||||
</Typography>
|
||||
{isAppNameExists && (
|
||||
<Typography noWrap display={'block'} variant="body2" color={'GrayText'}>
|
||||
{shortAppNpub}
|
||||
</Typography>
|
||||
)}
|
||||
<Typography noWrap display={'block'} variant="caption" color={'GrayText'}>
|
||||
Basic actions
|
||||
</Typography>
|
||||
|
@ -3,7 +3,6 @@ import { Box, Stack } from '@mui/material'
|
||||
import { EXPLANATION_MODAL_KEYS, MODAL_PARAMS_KEYS } from '@/types/modal'
|
||||
import { SectionTitle } from '@/shared/SectionTitle/SectionTitle'
|
||||
import { AppLink } from '@/shared/AppLink/AppLink'
|
||||
import { InputCopyButton } from '@/shared/InputCopyButton/InputCopyButton'
|
||||
import { StyledInput } from '../styled'
|
||||
import { useModalSearchParams } from '@/hooks/useModalSearchParams'
|
||||
|
||||
@ -11,10 +10,10 @@ type UserValueSectionProps = {
|
||||
title: string
|
||||
value: string
|
||||
explanationType: EXPLANATION_MODAL_KEYS
|
||||
copyValue: string
|
||||
endAdornment?: React.ReactNode
|
||||
}
|
||||
|
||||
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, copyValue }) => {
|
||||
const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanationType, endAdornment }) => {
|
||||
const { handleOpen } = useModalSearchParams()
|
||||
|
||||
const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => {
|
||||
@ -30,7 +29,7 @@ const UserValueSection: FC<UserValueSectionProps> = ({ title, value, explanation
|
||||
<SectionTitle>{title}</SectionTitle>
|
||||
<AppLink title="What is this?" onClick={() => handleOpenExplanationModal(explanationType)} />
|
||||
</Stack>
|
||||
<StyledInput value={value} readOnly endAdornment={<InputCopyButton value={copyValue} />} />
|
||||
<StyledInput value={value} readOnly endAdornment={endAdornment} />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { db } from '@/modules/db'
|
||||
|
||||
export const checkNpubSyncQuerier = (npub: string) => async () => {
|
||||
export const checkNpubSyncQuerier = (npub: string, onResolve: () => void) => async () => {
|
||||
const count = await db.syncHistory.where('npub').equals(npub).count()
|
||||
if (!count) onResolve()
|
||||
return count > 0
|
||||
}
|
||||
|
@ -39,9 +39,9 @@ const StyledButton = styled(
|
||||
background: theme.palette.primary.main,
|
||||
},
|
||||
color: theme.palette.text.secondary,
|
||||
'&.disabled': {
|
||||
'&.button.disabled': {
|
||||
color: theme.palette.text.secondary,
|
||||
background: `${theme.palette.primary.main}50`,
|
||||
background: `${theme.palette.primary.main}75`,
|
||||
cursor: 'not-allowed',
|
||||
},
|
||||
}
|
||||
|
@ -26,7 +26,12 @@ export const Input = forwardRef<HTMLInputElement, AppInputProps>(
|
||||
{label}
|
||||
</FormLabel>
|
||||
) : null}
|
||||
<InputBase autoComplete="off" className="input" {...props} classes={{ error: 'error' }} ref={ref} />
|
||||
<InputBase
|
||||
autoComplete="off"
|
||||
{...props}
|
||||
classes={{ error: 'error', root: 'input_root', input: 'input', disabled: 'disabled' }}
|
||||
ref={ref}
|
||||
/>
|
||||
{helperText ? (
|
||||
<FormHelperText {...helperTextProps} className="helper_text">
|
||||
{helperText}
|
||||
@ -41,22 +46,22 @@ const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({
|
||||
const isDark = theme.palette.mode === 'dark'
|
||||
return {
|
||||
width: '100%',
|
||||
'& > .input': {
|
||||
'& > .input_root': {
|
||||
background: isDark ? '#000000A8' : '#000',
|
||||
color: theme.palette.common.white,
|
||||
padding: '0.75rem 1rem',
|
||||
borderRadius: '1rem',
|
||||
border: '0.3px solid #FFFFFF54',
|
||||
fontSize: '0.875rem',
|
||||
'& input::placeholder': {
|
||||
color: '#fff',
|
||||
},
|
||||
'&.error': {
|
||||
border: '0.3px solid ' + theme.palette.error.main,
|
||||
},
|
||||
},
|
||||
'& .input:is(.disabled, &)': {
|
||||
WebkitTextFillColor: '#ffffff80',
|
||||
},
|
||||
'& > .helper_text': {
|
||||
margin: '0.5rem 1rem 0',
|
||||
margin: '0.5rem 0.5rem 0',
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
'& > .label': {
|
||||
|
@ -10,6 +10,7 @@ export enum MODAL_PARAMS_KEYS {
|
||||
CONFIRM_EVENT = 'confirm-event',
|
||||
ACTIVITY = 'activity',
|
||||
APP_DETAILS = 'app-details',
|
||||
EDIT_NAME = 'edit-name',
|
||||
}
|
||||
|
||||
export enum EXPLANATION_MODAL_KEYS {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { nip19 } from 'nostr-tools'
|
||||
import { ACTIONS, ACTION_TYPE, DOMAIN, NIP46_RELAYS } from '../consts'
|
||||
import { DbPending, DbPerm } from '@/modules/db'
|
||||
import { ACTIONS, ACTION_TYPE, DOMAIN, NIP46_RELAYS, NOAUTHD_URL } from '../consts'
|
||||
import { DbHistory, DbPending, DbPerm } from '@/modules/db'
|
||||
import { MetaEvent } from '@/types/meta-event'
|
||||
|
||||
export async function call(cb: () => any) {
|
||||
@ -97,6 +97,21 @@ export async function fetchNip05(value: string, origin?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchNpubNames(npub: string) {
|
||||
try {
|
||||
const url = `${NOAUTHD_URL}/name?npub=${npub}`
|
||||
const response = await fetch(url)
|
||||
const names: {
|
||||
names: string[]
|
||||
} = await response.json()
|
||||
|
||||
return names.names
|
||||
} catch (e) {
|
||||
console.log('Failed to fetch names for', npub, 'error: ' + e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const getDomain = (url: string) => {
|
||||
try {
|
||||
return new URL(url).hostname
|
||||
@ -106,7 +121,7 @@ export const getDomain = (url: string) => {
|
||||
}
|
||||
|
||||
export const getReferrerAppUrl = () => {
|
||||
console.log('referrer', window.document.referrer)
|
||||
// console.log('referrer', window.document.referrer)
|
||||
if (!window.document.referrer) return ''
|
||||
try {
|
||||
const u = new URL(window.document.referrer.toLocaleLowerCase())
|
||||
@ -119,7 +134,7 @@ export const getAppIconTitle = (name: string | undefined, appNpub: string) => {
|
||||
return name ? name[0].toLocaleUpperCase() : appNpub.substring(4, 7)
|
||||
}
|
||||
|
||||
export function getReqActionName(req: DbPending) {
|
||||
export function getReqActionName(req: DbPending | DbHistory) {
|
||||
const action = ACTIONS[req.method]
|
||||
if (req.method === 'sign_event') {
|
||||
const kind = getSignReqKind(req)
|
||||
|
@ -13,6 +13,7 @@
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"downlevelIteration": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": ".",
|
||||
|
Loading…
x
Reference in New Issue
Block a user