Compare commits

...

23 Commits

Author SHA1 Message Date
artur
ba3775e6c6 Make username settings button red if name empty, add sections to username settings, UI fixes to username settings, remove qs params from username settings 2024-02-19 10:37:06 +03:00
Bekbolsun
4ad66c8711 add transfer name field 2024-02-16 19:47:25 +06:00
Bekbolsun
6a04c3ec4b Merge branch 'develop' of https://github.com/nostrband/noauth into feature/edit-name 2024-02-16 18:14:59 +06:00
Bekbolsun
6d72cf1f82 implement edit username logic in edit modal 2024-02-16 17:59:59 +06:00
Nostr.Band
2e522b79ad Merge pull request #84 from nostrband/main
Merge w/ main
2024-02-16 14:48:48 +03:00
Nostr.Band
453a16690f Merge pull request #83 from nostrband/feature/ignore
Add ignore logic to stop interfering with replies from other instances
2024-02-16 14:48:16 +03:00
artur
46336d817f Add ignore logic to stop interfering with replies from other instances 2024-02-16 14:46:36 +03:00
Nostr.Band
8ef8157c38 Merge pull request #81 from nostrband/feature/watcher
Feature/watcher
2024-02-16 13:34:29 +03:00
Nostr.Band
4f00a014d0 Merge pull request #80 from nostrband/feature/watcher
Feature/watcher
2024-02-16 13:33:50 +03:00
artur
a500a2e2a5 Merge w/ develop 2024-02-16 13:33:04 +03:00
artur
1e6bf8679c Fix isLoading reset in popup confirms 2024-02-16 13:30:04 +03:00
Nostr.Band
04373e7991 Merge pull request #79 from nostrband/develop
Show app npubs
2024-02-16 12:02:24 +03:00
Nostr.Band
6acd00ca3b Merge pull request #78 from nostrband/refactor/display-app-npub
show appNpub in apps list & in app details page
2024-02-16 11:45:44 +03:00
artur
87ec23c737 Added watcher, deletes pending if watcher has concurrent reply, fixing popup closing issues 2024-02-16 11:28:02 +03:00
Bekbolsun
d199dcf9f7 Merge branch 'feature/edit-name' of https://github.com/nostrband/noauth into feature/edit-name 2024-02-16 14:22:30 +06:00
artur
0f28c80a15 Add editName and transferName to backend 2024-02-16 09:47:46 +03:00
Nostr.Band
34b516a1e3 Merge pull request #71 from nostrband/develop
Many minor fixes in UI, spinners etc.
2024-02-15 09:28:45 +03:00
Nostr.Band
40f4a9922a Merge pull request #69 from nostrband/develop
Fix redirect to confirm connect w/ popup=true after login
2024-02-15 09:00:24 +03:00
Nostr.Band
4b1f7564e7 Merge pull request #68 from nostrband/develop
Add logic to confirm after login
2024-02-15 08:42:14 +03:00
Nostr.Band
83d5c013cf Merge pull request #65 from nostrband/develop
Show kind in sign-event in activity history, show import key without …
2024-02-14 11:40:45 +03:00
Nostr.Band
e96edf90fe Merge pull request #64 from nostrband/develop
Fix - close confirm event popup after confirmed
2024-02-14 10:51:12 +03:00
Nostr.Band
56e71219a5 Merge pull request #63 from nostrband/develop
Readme
2024-02-14 10:17:22 +03:00
Nostr.Band
67b6a3bfcf Merge pull request #62 from nostrband/develop
Develop
2024-02-14 09:58:06 +03:00
18 changed files with 640 additions and 176 deletions

View File

@@ -18,7 +18,7 @@ function App() {
const load = useCallback(async () => { const load = useCallback(async () => {
const keys: DbKey[] = await dbi.listKeys() const keys: DbKey[] = await dbi.listKeys()
console.log(keys, 'keys') // console.log(keys, 'keys')
dispatch(setKeys({ keys })) dispatch(setKeys({ keys }))
const loadProfiles = async () => { const loadProfiles = async () => {
@@ -65,7 +65,7 @@ function App() {
useEffect(() => { useEffect(() => {
ndk.connect().then(() => { ndk.connect().then(() => {
console.log('NDK connected', { ndk }) console.log('NDK connected')
setIsConnected(true) setIsConnected(true)
}) })
// eslint-disable-next-line // eslint-disable-next-line

View File

@@ -1,15 +1,22 @@
import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Modal } from '@/shared/Modal/Modal' import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { 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 { Avatar, Box, Stack, Typography } from '@mui/material'
import { useNavigate, useParams, useSearchParams } from 'react-router-dom' import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { useAppSelector } from '@/store/hooks/redux' import { useAppSelector } from '@/store/hooks/redux'
import { selectAppsByNpub, selectKeys, selectPendingsByNpub } from '@/store' import { selectAppsByNpub, selectKeys, selectPendingsByNpub } from '@/store'
import { StyledButton, StyledToggleButtonsGroup } from './styled' import { StyledButton, StyledToggleButtonsGroup } from './styled'
import { ActionToggleButton } from './сomponents/ActionToggleButton' import { ActionToggleButton } from './сomponents/ActionToggleButton'
import { useState } from 'react' import { useEffect, useState } from 'react'
import { swicCall } from '@/modules/swic' import { swicCall, swicWaitStarted } from '@/modules/swic'
import { ACTION_TYPE } from '@/utils/consts' import { ACTION_TYPE } from '@/utils/consts'
import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar' import { useEnqueueSnackbar } from '@/hooks/useEnqueueSnackbar'
@@ -28,6 +35,7 @@ export const ModalConfirmConnect = () => {
const pending = useAppSelector((state) => selectPendingsByNpub(state, npub)) const pending = useAppSelector((state) => selectPendingsByNpub(state, npub))
const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.BASIC) const [selectedActionType, setSelectedActionType] = useState<ACTION_TYPE>(ACTION_TYPE.BASIC)
const [isLoaded, setIsLoaded] = useState(false)
const appNpub = searchParams.get('appNpub') || '' const appNpub = searchParams.get('appNpub') || ''
const pendingReqId = searchParams.get('reqId') || '' const pendingReqId = searchParams.get('reqId') || ''
@@ -37,7 +45,7 @@ export const ModalConfirmConnect = () => {
const triggerApp = apps.find((app) => app.appNpub === appNpub) const triggerApp = apps.find((app) => app.appNpub === appNpub)
const { name, url = '', icon = '' } = triggerApp || {} const { name, url = '', icon = '' } = triggerApp || {}
const appUrl = url || searchParams.get('appUrl') || getReferrerAppUrl(); const appUrl = url || searchParams.get('appUrl') || getReferrerAppUrl()
const appDomain = getDomain(appUrl) const appDomain = getDomain(appUrl)
const appName = name || appDomain || getShortenNpub(appNpub) const appName = name || appDomain || getShortenNpub(appNpub)
const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub) const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub)
@@ -53,14 +61,44 @@ export const ModalConfirmConnect = () => {
}, },
}) })
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) // NOTE: when opened directly to this modal using authUrl,
// App doesn't exist yet! // we might not have pending requests visible yet bcs we haven't
// const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub) // loaded them yet, which means this modal will be closed with
const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId) // the logic below. So now if it's popup then we wait for SW
// console.log("pending", {isModalOpened, isPendingReqIdExists, isNpubExists, /*isAppNpubExists,*/ pendingReqId, pending}); // and then wait a little more to give it time to fetch
if (!isPopup && isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) { // pending reqs from db. Same logic implemented in confirm-event.
closeModalAfterRequest()
return null // FIXME move to a separate hook and reuse?
useEffect(() => {
if (isModalOpened) {
if (isPopup) {
console.log("waiting for sw")
// wait for SW to start
swicWaitStarted().then(() => {
// give it some time to load the pending reqs etc
console.log("waiting for sw done")
setTimeout(() => setIsLoaded(true), 500)
})
} else {
setIsLoaded(true)
}
} else {
setIsLoaded(false)
}
}, [isModalOpened, isPopup])
if (isLoaded) {
const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub)
// NOTE: app doesn't exist yet!
// const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub)
const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId)
// console.log("pending", {isModalOpened, isPendingReqIdExists, isNpubExists, /*isAppNpubExists,*/ pendingReqId, pending});
if (isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) {
if (isPopup) window.close()
else closeModalAfterRequest()
return null
}
} }
const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => { const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => {
@@ -179,7 +217,7 @@ export const ModalConfirmConnect = () => {
</StyledToggleButtonsGroup> </StyledToggleButtonsGroup>
<Stack direction={'row'} gap={'1rem'}> <Stack direction={'row'} gap={'1rem'}>
<StyledButton onClick={disallow} varianttype="secondary"> <StyledButton onClick={disallow} varianttype="secondary">
Disallow Ignore
</StyledButton> </StyledButton>
<StyledButton fullWidth onClick={allow}> <StyledButton fullWidth onClick={allow}>
Connect Connect

View File

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

View 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>
)
}

View 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,
}))

View File

@@ -1,14 +1,30 @@
// import { useEffect } from 'react'
import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { useModalSearchParams } from '@/hooks/useModalSearchParams'
import { Button } from '@/shared/Button/Button' import { Button } from '@/shared/Button/Button'
import { Modal } from '@/shared/Modal/Modal' import { Modal } from '@/shared/Modal/Modal'
import { MODAL_PARAMS_KEYS } from '@/types/modal' import { MODAL_PARAMS_KEYS } from '@/types/modal'
import { Stack } from '@mui/material' import { Stack } from '@mui/material'
// import { AppLink } from '@/shared/AppLink/AppLink'
export const ModalInitial = () => { export const ModalInitial = () => {
const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams() const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams()
const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL) const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL)
const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.INITIAL) const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.INITIAL)
// const [showAdvancedContent, setShowAdvancedContent] = useState(false)
// const handleShowAdvanced = () => {
// setShowAdvancedContent(true)
// }
// useEffect(() => {
// return () => {
// if (isModalOpened) {
// setShowAdvancedContent(false)
// }
// }
// }, [isModalOpened])
return ( return (
<Modal open={isModalOpened} onClose={handleCloseModal}> <Modal open={isModalOpened} onClose={handleCloseModal}>
<Stack paddingTop={'0.5rem'} gap={'1rem'}> <Stack paddingTop={'0.5rem'} gap={'1rem'}>

View File

@@ -100,14 +100,14 @@ export const ModalLogin = () => {
if (isPopup && isModalOpened) { if (isPopup && isModalOpened) {
swicCall('fetchPendingRequests', npub, appNpub) swicCall('fetchPendingRequests', npub, appNpub)
fetchNpubNames(npub).then(names => { fetchNpubNames(npub).then((names) => {
if (names.length) { if (names.length) {
setValue('username', `${names[0]}@${DOMAIN}`) setValue('username', `${names[0]}@${DOMAIN}`)
} }
}) })
} }
} }
}, [searchParams, isModalOpened, setValue]) }, [searchParams, isModalOpened, isPopup, setValue])
useEffect(() => { useEffect(() => {
return () => { return () => {

View File

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

View File

@@ -1,21 +1,30 @@
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 { DbApp, dbi, DbKey, DbPending, DbPerm } from './db'
import { Keys } from './keys' import { Keys } from './keys'
import NDK, { import NDK, {
IEventHandlingStrategy,
NDKEvent, NDKEvent,
NDKNip46Backend, NDKNip46Backend,
NDKPrivateKeySigner, NDKPrivateKeySigner,
NDKSigner, NDKSigner,
NDKSubscription,
NDKSubscriptionCacheUsage,
NDKUser,
} from '@nostr-dev-kit/ndk' } from '@nostr-dev-kit/ndk'
import { NOAUTHD_URL, WEB_PUSH_PUBKEY, NIP46_RELAYS, MIN_POW, MAX_POW, KIND_RPC, DOMAIN } from '../utils/consts' 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 { fetchNip05, getReqPerm, getShortenNpub, isPackagePerm } from '@/utils/helpers/helpers'
import { NostrPowEvent, minePow } from './pow' import { NostrPowEvent, minePow } from './pow'
//import { PrivateKeySigner } from './signer' //import { PrivateKeySigner } from './signer'
//const PERF_TEST = false //const PERF_TEST = false
enum DECISION {
ASK = '',
ALLOW = 'allow',
DISALLOW = 'disallow',
IGNORE = 'ignore',
}
export interface KeyInfo { export interface KeyInfo {
npub: string npub: string
nip05?: string nip05?: string
@@ -28,11 +37,12 @@ interface Key {
backoff: number backoff: number
signer: NDKSigner signer: NDKSigner
backend: NDKNip46Backend backend: NDKNip46Backend
watcher: Watcher
} }
interface Pending { interface Pending {
req: DbPending req: DbPending
cb: (allow: boolean, remember: boolean, options?: any) => void cb: (allow: DECISION, remember: boolean, options?: any) => void
notified?: boolean notified?: boolean
} }
@@ -46,92 +56,171 @@ interface IAllowCallbackParams {
params?: any params?: any
} }
class Watcher {
private ndk: NDK
private signer: NDKSigner
private onReply: (id: string) => void
private sub?: NDKSubscription
constructor(ndk: NDK, signer: NDKSigner, onReply: (id: string) => void) {
this.ndk = ndk
this.signer = signer
this.onReply = onReply
}
async start() {
this.sub = this.ndk.subscribe(
{
kinds: [KIND_RPC],
authors: [(await this.signer.user()).pubkey],
since: Math.floor(Date.now() / 1000 - 10),
},
{
closeOnEose: false,
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY,
}
)
this.sub.on('event', async (e: NDKEvent) => {
const peer = e.tags.find((t) => t.length >= 2 && t[0] === 'p')
console.log('watcher got event', { e, peer })
if (!peer) return
const decryptedContent = await this.signer.decrypt(new NDKUser({ pubkey: peer[1] }), e.content)
const parsedContent = JSON.parse(decryptedContent)
const { id, method, params, result, error } = parsedContent
console.log('watcher got', { peer, id, method, params, result, error })
if (method || result === 'auth_url') return
this.onReply(id)
})
}
stop() {
this.sub!.stop()
}
}
class Nip46Backend extends NDKNip46Backend { 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) { public async processEvent(event: NDKEvent) {
this.handleIncomingEvent(event) this.handleIncomingEvent(event)
} }
}
class Nip04KeyHandlingStrategy implements IEventHandlingStrategy { protected async handleIncomingEvent(event: NDKEvent) {
private privkey: string const { id, method, params } = (await this.rpc.parseEvent(event)) as any
private nip04 = new Nip04() const remotePubkey = event.pubkey
let response: string | undefined
constructor(privkey: string) { this.debug('incoming event', { id, method, params })
this.privkey = privkey
}
private async getKey(backend: NDKNip46Backend, id: string, remotePubkey: string, recipientPubkey: string) { // validate signature explicitly
if ( if (!verifySignature(event.rawEvent() as Event)) {
!(await backend.pubkeyAllowed({ this.debug('invalid signature', event.rawEvent())
id, return
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') const decision = await this.allowCb({
} backend: this,
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,
})
const allow = await this.allowCb({
backend: this.backend,
npub: this.npub, npub: this.npub,
id, id,
method: this.method, method,
remotePubkey, remotePubkey,
params, params,
}) })
if (!allow) return undefined console.log(Date.now(), 'handle', { method, id, decision, remotePubkey, params })
return this.body.handle(backend, id, remotePubkey, params).then((r) => { if (decision === DECISION.IGNORE) return
console.log(Date.now(), 'req', id, 'method', this.method, 'result', r)
return r 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 { export class NoauthBackend {
readonly swg: ServiceWorkerGlobalScope readonly swg: ServiceWorkerGlobalScope
private keysModule: Keys private keysModule: Keys
@@ -146,7 +235,7 @@ export class NoauthBackend {
private pendingNpubEvents = new Map<string, NDKEvent[]>() private pendingNpubEvents = new Map<string, NDKEvent[]>()
private ndk = new NDK({ private ndk = new NDK({
explicitRelayUrls: NIP46_RELAYS, explicitRelayUrls: NIP46_RELAYS,
enableOutboxModel: false enableOutboxModel: false,
}) })
public constructor(swg: ServiceWorkerGlobalScope) { public constructor(swg: ServiceWorkerGlobalScope) {
@@ -419,6 +508,41 @@ export class NoauthBackend {
throw new Error('Too many requests, retry later') throw new Error('Too many requests, retry later')
} }
private async sendDeleteNameToServer(npub: string, name: string) {
const body = JSON.stringify({
npub,
name,
})
const method = 'DELETE'
const url = `${NOAUTHD_URL}/name`
return this.sendPostAuthd({
npub,
url,
method,
body,
})
}
private async sendTransferNameToServer(npub: string, name: string, newNpub: string) {
const body = JSON.stringify({
npub,
name,
newNpub,
})
const method = 'PUT'
const url = `${NOAUTHD_URL}/name`
return this.sendPostAuthd({
npub,
url,
method,
body,
})
}
private async sendTokenToServer(npub: string, token: string) { private async sendTokenToServer(npub: string, token: string) {
const body = JSON.stringify({ const body = JSON.stringify({
npub, npub,
@@ -566,7 +690,7 @@ export class NoauthBackend {
return this.keyInfo(dbKey) return this.keyInfo(dbKey)
} }
private getPerm(req: DbPending): string { private getDecision(req: DbPending): DECISION {
const reqPerm = getReqPerm(req) const reqPerm = getReqPerm(req)
const appPerms = this.perms.filter((p) => p.npub === req.npub && p.appNpub === req.appNpub) const appPerms = this.perms.filter((p) => p.npub === req.npub && p.appNpub === req.appNpub)
@@ -575,8 +699,18 @@ export class NoauthBackend {
// non-exact next // non-exact next
if (!perm) perm = appPerms.find((p) => isPackagePerm(p.perm, reqPerm)) if (!perm) perm = appPerms.find((p) => isPackagePerm(p.perm, reqPerm))
console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms) if (perm) {
return perm?.value || '' console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms)
return perm.value === '1' ? DECISION.ALLOW : DECISION.DISALLOW
}
const conn = appPerms.find((p) => p.perm === 'connect')
if (conn && conn.value === '0') {
console.log('req', req, 'perm', reqPerm, 'ignore by connect disallow')
return DECISION.IGNORE
}
return DECISION.ASK
} }
private async connectApp({ private async connectApp({
@@ -629,19 +763,19 @@ export class NoauthBackend {
method, method,
remotePubkey, remotePubkey,
params, params,
}: IAllowCallbackParams): Promise<boolean> { }: IAllowCallbackParams): Promise<DECISION> {
// same reqs usually come on reconnects // same reqs usually come on reconnects
if (this.doneReqIds.includes(id)) { if (this.doneReqIds.includes(id)) {
console.log('request already done', id) console.log('request already done', id)
// FIXME maybe repeat the reply, but without the Notification? // FIXME maybe repeat the reply, but without the Notification?
return false return DECISION.IGNORE
} }
const appNpub = nip19.npubEncode(remotePubkey) const appNpub = nip19.npubEncode(remotePubkey)
const connected = !!this.apps.find((a) => a.appNpub === appNpub) const connected = !!this.apps.find((a) => a.appNpub === appNpub)
if (!connected && method !== 'connect') { if (!connected && method !== 'connect') {
console.log('ignoring request before connect', method, id, appNpub, npub) console.log('ignoring request before connect', method, id, appNpub, npub)
return false return DECISION.IGNORE
} }
const req: DbPending = { const req: DbPending = {
@@ -656,9 +790,21 @@ export class NoauthBackend {
const self = this const self = this
return new Promise(async (ok) => { return new Promise(async (ok) => {
// called when it's decided whether to allow this or not // 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 // 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) { if (manual) {
await dbi.confirmPending(id, allow) await dbi.confirmPending(id, allow)
@@ -711,35 +857,40 @@ export class NoauthBackend {
// reload // reload
this.perms = await dbi.listPerms() 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 // notify UI that it was confirmed
// if (!PERF_TEST) // if (!PERF_TEST)
this.updateUI() this.updateUI()
// return to let nip46 flow proceed // after replying to this req check pending
ok(allow) // 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 // check perms
const perm = this.getPerm(req) const dec = this.getDecision(req)
console.log(Date.now(), 'perm', req.id, perm) console.log(Date.now(), 'decision', req.id, dec)
// have perm? // have perm?
if (perm) { if (dec !== DECISION.ASK) {
// reply immediately // reply immediately
onAllow(false, perm === '1', false) onAllow(false, dec, false)
} else { } else {
// put pending req to db // put pending req to db
await dbi.addPending(req) await dbi.addPending(req)
@@ -750,7 +901,7 @@ export class NoauthBackend {
// put to a list of pending requests // put to a list of pending requests
this.confirmBuffer.push({ this.confirmBuffer.push({
req, req,
cb: (allow, remember, options) => onAllow(true, allow, remember, options), cb: (decision, remember, options) => onAllow(true, decision, remember, options),
}) })
// OAuth flow // OAuth flow
@@ -783,25 +934,28 @@ export class NoauthBackend {
ndk.connect() ndk.connect()
const signer = new NDKPrivateKeySigner(sk) // PrivateKeySigner const signer = new NDKPrivateKeySigner(sk) // PrivateKeySigner
const backend = new Nip46Backend(ndk, signer, () => Promise.resolve(true)) const backend = new Nip46Backend(ndk, signer, this.allowPermitCallback.bind(this)) // , () => Promise.resolve(true)
this.keys.push({ npub, backend, signer, ndk, backoff }) 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 // new method
backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk) // backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk)
// assign our own permission callback // // assign our own permission callback
for (const method in backend.handlers) { // for (const method in backend.handlers) {
backend.handlers[method] = new EventHandlingStrategyWrapper( // backend.handlers[method] = new EventHandlingStrategyWrapper(
backend, // backend,
npub, // method,
method, // backend.handlers[method]
backend.handlers[method], // )
this.allowPermitCallback.bind(this) // }
)
}
// start // start
backend.start() backend.start()
watcher.start()
console.log('started', npub) console.log('started', npub)
// backoff reset on successfull connection // backoff reset on successfull connection
@@ -825,11 +979,13 @@ export class NoauthBackend {
const bo = self.keys.find((k) => k.npub === npub)?.backoff || 1000 const bo = self.keys.find((k) => k.npub === npub)?.backoff || 1000
setTimeout(() => { setTimeout(() => {
console.log(new Date(), 'reconnect relays for key', npub, 'backoff', bo) console.log(new Date(), 'reconnect relays for key', npub, 'backoff', bo)
// @ts-ignore
for (const r of ndk.pool.relays.values()) r.disconnect() for (const r of ndk.pool.relays.values()) r.disconnect()
// make sure it no longer activates // make sure it no longer activates
backend.handlers = {} backend.handlers = {}
// stop watching
watcher.stop()
self.keys = self.keys.filter((k) => k.npub !== npub) self.keys = self.keys.filter((k) => k.npub !== npub)
self.startKey({ npub, sk, backoff: Math.min(bo * 2, 60000) }) self.startKey({ npub, sk, backoff: Math.min(bo * 2, 60000) })
}, bo) }, bo)
@@ -856,11 +1012,11 @@ export class NoauthBackend {
const events = await this.ndk.fetchEvents({ const events = await this.ndk.fetchEvents({
kinds: [KIND_RPC], kinds: [KIND_RPC],
"#p": [pubkey as string], '#p': [pubkey as string],
authors: [appPubkey as string] authors: [appPubkey as string],
}); })
console.log("fetched pending for", npub, events.size) console.log('fetched pending for', npub, events.size)
this.pendingNpubEvents.set(npub, [...events.values()]); this.pendingNpubEvents.set(npub, [...events.values()])
} }
public async unlock(npub: string) { public async unlock(npub: string) {
@@ -977,7 +1133,7 @@ export class NoauthBackend {
this.updateUI() this.updateUI()
} else { } else {
console.log('confirming req', id, allow, remember, options) console.log('confirming req', id, allow, remember, options)
req.cb(allow, remember, options) req.cb(allow ? DECISION.ALLOW : DECISION.DISALLOW, remember, options)
} }
} }
@@ -995,6 +1151,31 @@ export class NoauthBackend {
this.updateUI() this.updateUI()
} }
private async editName(npub: string, name: string) {
const key = this.enckeys.find((k) => k.npub == npub)
if (!key) throw new Error('Npub not found')
if (key.name) {
await this.sendDeleteNameToServer(npub, key.name)
}
if (name) {
await this.sendNameToServer(npub, name)
}
await dbi.editName(npub, name)
key.name = name
this.updateUI()
}
private async transferName(npub: string, name: string, newNpub: string) {
const key = this.enckeys.find((k) => k.npub == npub)
if (!key) throw new Error('Npub not found')
if (!name) throw new Error('Empty name')
if (key.name !== name) throw new Error('Name changed, please reload')
await this.sendTransferNameToServer(npub, key.name, newNpub)
await dbi.editName(npub, '')
key.name = ''
this.updateUI()
}
private async enablePush(): Promise<boolean> { private async enablePush(): Promise<boolean> {
const options = { const options = {
userVisibleOnly: true, userVisibleOnly: true,
@@ -1041,6 +1222,10 @@ export class NoauthBackend {
result = await this.deleteApp(args[0]) result = await this.deleteApp(args[0])
} else if (method === 'deletePerm') { } else if (method === 'deletePerm') {
result = await this.deletePerm(args[0]) result = await this.deletePerm(args[0])
} else if (method === 'editName') {
result = await this.editName(args[0], args[1])
} else if (method === 'transferName') {
result = await this.transferName(args[0], args[1], args[2])
} else if (method === 'enablePush') { } else if (method === 'enablePush') {
result = await this.enablePush() result = await this.enablePush()
} else if (method === 'fetchPendingRequests') { } else if (method === 'fetchPendingRequests') {

View File

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

View File

@@ -5,7 +5,7 @@ export let swr: ServiceWorkerRegistration | null = null
const reqs = new Map<number, { ok: (r: any) => void; rej: (r: any) => void }>() const reqs = new Map<number, { ok: (r: any) => void; rej: (r: any) => void }>()
let nextReqId = 1 let nextReqId = 1
let onRender: (() => void) | null = null let onRender: (() => void) | null = null
const queue: (() => Promise<void>)[] = [] const queue: (() => Promise<void> | void)[] = []
export async function swicRegister() { export async function swicRegister() {
serviceWorkerRegistration.register({ serviceWorkerRegistration.register({
@@ -36,6 +36,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) { function onMessage(data: any) {
const { id, result, error } = data const { id, result, error } = data
console.log('SW message', id, result, error) console.log('SW message', id, result, error)

View File

@@ -6,7 +6,6 @@ import { formatTimestampDate } from '@/utils/helpers/date'
import ClearRoundedIcon from '@mui/icons-material/ClearRounded' import ClearRoundedIcon from '@mui/icons-material/ClearRounded'
import DoneRoundedIcon from '@mui/icons-material/DoneRounded' import DoneRoundedIcon from '@mui/icons-material/DoneRounded'
import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded' import MoreVertRoundedIcon from '@mui/icons-material/MoreVertRounded'
import { ACTIONS } from '@/utils/consts'
import { getReqActionName } from '@/utils/helpers/helpers' import { getReqActionName } from '@/utils/helpers/helpers'
type ItemActivityProps = DbHistory type ItemActivityProps = DbHistory

View File

@@ -1,6 +1,7 @@
import { useCallback, useState } from 'react'
import { useAppSelector } from '../../store/hooks/redux' import { useAppSelector } from '../../store/hooks/redux'
import { Navigate, useParams, useSearchParams } from 'react-router-dom' import { Navigate, useParams, useSearchParams } from 'react-router-dom'
import { Stack } from '@mui/material' import { Box, IconButton, Stack } from '@mui/material'
import { StyledIconButton } from './styled' import { StyledIconButton } from './styled'
import { SettingsIcon, ShareIcon } from '@/assets' import { SettingsIcon, ShareIcon } from '@/assets'
import { Apps } from './components/Apps' import { Apps } from './components/Apps'
@@ -18,7 +19,9 @@ import { useTriggerConfirmModal } from './hooks/useTriggerConfirmModal'
import { useLiveQuery } from 'dexie-react-hooks' import { useLiveQuery } from 'dexie-react-hooks'
import { checkNpubSyncQuerier } from './utils' import { checkNpubSyncQuerier } from './utils'
import { DOMAIN } from '@/utils/consts' import { DOMAIN } from '@/utils/consts'
import { useCallback, useState } 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 KeyPage = () => {
const { npub = '' } = useParams<{ npub: string }>() const { npub = '' } = useParams<{ npub: string }>()
@@ -47,7 +50,7 @@ const KeyPage = () => {
const isKeyExists = npub.trim().length && key const isKeyExists = npub.trim().length && key
const isPopup = searchParams.get('popup') === 'true' const isPopup = searchParams.get('popup') === 'true'
console.log({ isKeyExists, isPopup }) // console.log({ isKeyExists, isPopup })
if (isPopup && !isKeyExists) { if (isPopup && !isKeyExists) {
searchParams.set('login', 'true') searchParams.set('login', 'true')
@@ -60,6 +63,7 @@ const KeyPage = () => {
const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP) const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP)
const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS) const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS)
const handleOpenEditNameModal = () => handleOpen(MODAL_PARAMS_KEYS.EDIT_NAME)
return ( return (
<> <>
@@ -70,13 +74,20 @@ const KeyPage = () => {
<UserValueSection <UserValueSection
title="Your login" title="Your login"
value={username} 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} explanationType={EXPLANATION_MODAL_KEYS.LOGIN}
/> />
<UserValueSection <UserValueSection
title="Your NPUB" title="Your NPUB"
value={npub} value={npub}
copyValue={npub} endAdornment={<InputCopyButton value={npub} />}
explanationType={EXPLANATION_MODAL_KEYS.NPUB} explanationType={EXPLANATION_MODAL_KEYS.NPUB}
/> />
@@ -98,11 +109,13 @@ const KeyPage = () => {
<Apps apps={filteredApps} npub={npub} /> <Apps apps={filteredApps} npub={npub} />
</Stack> </Stack>
<ModalConnectApp /> <ModalConnectApp />
<ModalSettings isSynced={isSynced} /> <ModalSettings isSynced={isSynced} />
<ModalExplanation /> <ModalExplanation />
<ModalConfirmConnect /> <ModalConfirmConnect />
<ModalConfirmEvent confirmEventReqs={prepareEventPendings} /> <ModalConfirmEvent confirmEventReqs={prepareEventPendings} />
<ModalEditName />
</> </>
) )
} }

View File

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

View File

@@ -39,9 +39,9 @@ const StyledButton = styled(
background: theme.palette.primary.main, background: theme.palette.primary.main,
}, },
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
'&.disabled': { '&.button.disabled': {
color: theme.palette.text.secondary, color: theme.palette.text.secondary,
background: `${theme.palette.primary.main}50`, background: `${theme.palette.primary.main}75`,
cursor: 'not-allowed', cursor: 'not-allowed',
}, },
} }

View File

@@ -26,7 +26,12 @@ export const Input = forwardRef<HTMLInputElement, AppInputProps>(
{label} {label}
</FormLabel> </FormLabel>
) : null} ) : 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 ? ( {helperText ? (
<FormHelperText {...helperTextProps} className="helper_text"> <FormHelperText {...helperTextProps} className="helper_text">
{helperText} {helperText}
@@ -41,20 +46,20 @@ const StyledInputContainer = styled((props: BoxProps) => <Box {...props} />)(({
const isDark = theme.palette.mode === 'dark' const isDark = theme.palette.mode === 'dark'
return { return {
width: '100%', width: '100%',
'& > .input': { '& > .input_root': {
background: isDark ? '#000000A8' : '#000', background: isDark ? '#000000A8' : '#000',
color: theme.palette.common.white, color: theme.palette.common.white,
padding: '0.75rem 1rem', padding: '0.75rem 1rem',
borderRadius: '1rem', borderRadius: '1rem',
border: '0.3px solid #FFFFFF54', border: '0.3px solid #FFFFFF54',
fontSize: '0.875rem', fontSize: '0.875rem',
'& input::placeholder': {
color: '#fff',
},
'&.error': { '&.error': {
border: '0.3px solid ' + theme.palette.error.main, border: '0.3px solid ' + theme.palette.error.main,
}, },
}, },
'& .input:is(.disabled, &)': {
WebkitTextFillColor: '#ffffff80',
},
'& > .helper_text': { '& > .helper_text': {
margin: '0.5rem 1rem 0', margin: '0.5rem 1rem 0',
color: theme.palette.text.primary, color: theme.palette.text.primary,

View File

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

View File

@@ -121,7 +121,7 @@ export const getDomain = (url: string) => {
} }
export const getReferrerAppUrl = () => { export const getReferrerAppUrl = () => {
console.log('referrer', window.document.referrer) // console.log('referrer', window.document.referrer)
if (!window.document.referrer) return '' if (!window.document.referrer) return ''
try { try {
const u = new URL(window.document.referrer.toLocaleLowerCase()) const u = new URL(window.document.referrer.toLocaleLowerCase())