From 0be2159efb8e1c0a8119016eaa37f0e614900717 Mon Sep 17 00:00:00 2001 From: artur Date: Wed, 14 Feb 2024 11:39:37 +0300 Subject: [PATCH 01/16] Show kind in sign-event in activity history, show import key without advanced section --- src/components/Modal/ModalInitial/ModalInitial.tsx | 5 +++-- src/pages/AppPage/components/Activities/ItemActivity.tsx | 8 +++++--- src/utils/helpers/helpers.ts | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/components/Modal/ModalInitial/ModalInitial.tsx b/src/components/Modal/ModalInitial/ModalInitial.tsx index 98fa617..835f0ec 100644 --- a/src/components/Modal/ModalInitial/ModalInitial.tsx +++ b/src/components/Modal/ModalInitial/ModalInitial.tsx @@ -31,13 +31,14 @@ export const ModalInitial = () => { - + + {/* {showAdvancedContent && ( - )} + )} */} ) diff --git a/src/pages/AppPage/components/Activities/ItemActivity.tsx b/src/pages/AppPage/components/Activities/ItemActivity.tsx index 3496878..1d16624 100644 --- a/src/pages/AppPage/components/Activities/ItemActivity.tsx +++ b/src/pages/AppPage/components/Activities/ItemActivity.tsx @@ -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' @@ -7,15 +7,17 @@ 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 = ({ allowed, method, timestamp }) => { +export const ItemActivity: FC = (req) => { + const { allowed, timestamp } = req return ( - {ACTIONS[method] || method} + {getReqActionName(req)} {formatTimestampDate(timestamp)} diff --git a/src/utils/helpers/helpers.ts b/src/utils/helpers/helpers.ts index 0c31a5a..3ffb3a2 100644 --- a/src/utils/helpers/helpers.ts +++ b/src/utils/helpers/helpers.ts @@ -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 { DbHistory, DbPending, DbPerm } from '@/modules/db' import { MetaEvent } from '@/types/meta-event' export async function call(cb: () => any) { @@ -120,7 +120,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) From 43e375efe913a0a980cb20a7b2e5b59b4a08486e Mon Sep 17 00:00:00 2001 From: artur Date: Wed, 14 Feb 2024 16:15:50 +0300 Subject: [PATCH 02/16] Add logic to confirm after login --- .../Modal/ModalLogin/ModalLogin.tsx | 28 +++++++- .../Modal/ModalSignUp/ModalSignUp.tsx | 5 +- src/modules/backend.ts | 68 ++++++++++++++----- src/modules/swic.ts | 36 ++++++---- src/pages/KeyPage/Key.Page.tsx | 11 ++- src/utils/helpers/helpers.ts | 17 ++++- tsconfig.json | 1 + 7 files changed, 130 insertions(+), 36 deletions(-) diff --git a/src/components/Modal/ModalLogin/ModalLogin.tsx b/src/components/Modal/ModalLogin/ModalLogin.tsx index ea490c8..fb82f7f 100644 --- a/src/components/Modal/ModalLogin/ModalLogin.tsx +++ b/src/components/Modal/ModalLogin/ModalLogin.tsx @@ -8,12 +8,12 @@ import { CircularProgress, 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' @@ -36,6 +36,7 @@ export const ModalLogin = () => { handleSubmit, reset, register, + setValue, formState: { errors }, } = useForm({ defaultValues: FORM_DEFAULT_VALUES, @@ -78,7 +79,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}`) + }, 300) } catch (error: any) { console.log('error', error) notify(error?.message || 'Something went wrong!', 'error') @@ -86,6 +90,24 @@ export const ModalLogin = () => { } } + const [searchParams] = useSearchParams() + useEffect(() => { + if (isModalOpened) { + const isPopup = searchParams.get('popup') === 'true' + 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, setValue]) + useEffect(() => { return () => { if (isModalOpened) { diff --git a/src/components/Modal/ModalSignUp/ModalSignUp.tsx b/src/components/Modal/ModalSignUp/ModalSignUp.tsx index b63c296..9031cc1 100644 --- a/src/components/Modal/ModalSignUp/ModalSignUp.tsx +++ b/src/components/Modal/ModalSignUp/ModalSignUp.tsx @@ -61,8 +61,11 @@ export const ModalSignUp = () => { setIsLoading(true) const k: any = await swicCall('generateKey', name) notify(`Account created for "${name}"`, 'success') - navigate(`/key/${k.npub}`) setIsLoading(false) + setTimeout(() => { + // give frontend time to read the new key first + navigate(`/key/${k.npub}`) + }, 300) } catch (error: any) { notify(error?.message || 'Something went wrong!', 'error') setIsLoading(false) diff --git a/src/modules/backend.ts b/src/modules/backend.ts index 07df5d5..d8b07e6 100644 --- a/src/modules/backend.ts +++ b/src/modules/backend.ts @@ -46,6 +46,12 @@ interface IAllowCallbackParams { params?: any } +class Nip46Backend extends NDKNip46Backend { + public async processEvent(event: NDKEvent) { + this.handleIncomingEvent(event) + } +} + class Nip04KeyHandlingStrategy implements IEventHandlingStrategy { private privkey: string private nip04 = new Nip04() @@ -137,10 +143,16 @@ export class NoauthBackend { private confirmBuffer: Pending[] = [] private accessBuffer: DbPending[] = [] private notifCallback: (() => void) | null = null + private pendingNpubEvents = new Map() + 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) => { @@ -568,22 +580,21 @@ export class NoauthBackend { } private async connectApp({ - npub, - appNpub, - appUrl, - perms, - appName = '', - appIcon = '' - }: { - npub: string, - appNpub: string, - appUrl: string, - appName?: string, - appIcon?: string, - perms: string[] - }) { - - await dbi.addApp({ + npub, + appNpub, + appUrl, + perms, + appName = '', + appIcon = '', + }: { + npub: string + appNpub: string + appUrl: string + appName?: string + appIcon?: string + perms: string[] + }) { + await dbi.addApp({ appNpub: appNpub, npub: npub, timestamp: Date.now(), @@ -772,7 +783,7 @@ export class NoauthBackend { ndk.connect() const signer = new NDKPrivateKeySigner(sk) // PrivateKeySigner - const backend = new NDKNip46Backend(ndk, signer, () => Promise.resolve(true)) + const backend = new Nip46Backend(ndk, signer, () => Promise.resolve(true)) this.keys.push({ npub, backend, signer, ndk, backoff }) // new method @@ -829,6 +840,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) { @@ -1011,6 +1043,8 @@ export class NoauthBackend { result = await this.deletePerm(args[0]) } 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) } diff --git a/src/modules/swic.ts b/src/modules/swic.ts index 725b9ad..f7bc994 100644 --- a/src/modules/swic.ts +++ b/src/modules/swic.ts @@ -5,6 +5,7 @@ export let swr: ServiceWorkerRegistration | null = null const reqs = new Map void; rej: (r: any) => void }>() let nextReqId = 1 let onRender: (() => void) | null = null +const queue: (() => Promise)[] = [] export async function swicRegister() { serviceWorkerRegistration.register({ @@ -17,14 +18,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) => { @@ -57,19 +61,25 @@ export async function swicCall(method: string, ...args: any[]) { nextReqId++ return new Promise((ok, rej) => { - if (!swr || !swr.active) { - rej(new Error('No active service worker')) - return + + const call = async () => { + if (!swr || !swr.active) { + rej(new Error('No active service worker')) + return + } + + reqs.set(id, { ok, rej }) + const msg = { + id, + method, + args: [...args], + } + console.log('sending to SW', msg) + swr.active.postMessage(msg) } - reqs.set(id, { ok, rej }) - const msg = { - id, - method, - args: [...args], - } - console.log('sending to SW', msg) - swr.active.postMessage(msg) + if (swr && swr.active) call() + else queue.push(call) }) } diff --git a/src/pages/KeyPage/Key.Page.tsx b/src/pages/KeyPage/Key.Page.tsx index c08c65f..3f1547f 100644 --- a/src/pages/KeyPage/Key.Page.tsx +++ b/src/pages/KeyPage/Key.Page.tsx @@ -1,5 +1,5 @@ import { useAppSelector } from '../../store/hooks/redux' -import { Navigate, useParams } from 'react-router-dom' +import { Navigate, useParams, useSearchParams } from 'react-router-dom' import { Stack } from '@mui/material' import { StyledIconButton } from './styled' import { SettingsIcon, ShareIcon } from '@/assets' @@ -23,6 +23,7 @@ import { useCallback } from 'react' const KeyPage = () => { const { npub = '' } = useParams<{ npub: string }>() const { keys, apps, pending, perms } = useAppSelector((state) => state.content) + const [searchParams] = useSearchParams() const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false) const { handleOpen } = useModalSearchParams() @@ -41,6 +42,14 @@ 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 + } if (!isKeyExists) return const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP) diff --git a/src/utils/helpers/helpers.ts b/src/utils/helpers/helpers.ts index 3ffb3a2..7f77b53 100644 --- a/src/utils/helpers/helpers.ts +++ b/src/utils/helpers/helpers.ts @@ -1,5 +1,5 @@ import { nip19 } from 'nostr-tools' -import { ACTIONS, ACTION_TYPE, DOMAIN, NIP46_RELAYS } from '../consts' +import { ACTIONS, ACTION_TYPE, DOMAIN, NIP46_RELAYS, NOAUTHD_URL } from '../consts' import { DbHistory, DbPending, DbPerm } from '@/modules/db' import { MetaEvent } from '@/types/meta-event' @@ -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 diff --git a/tsconfig.json b/tsconfig.json index f244861..e5bf543 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, + "downlevelIteration": true, "noEmit": true, "jsx": "react-jsx", "baseUrl": ".", From 2058b900ac3ab09e81e988bb9fb3b0225673a685 Mon Sep 17 00:00:00 2001 From: artur Date: Thu, 15 Feb 2024 08:58:49 +0300 Subject: [PATCH 03/16] Fix redirect to confirm connect w/ popup=true after login --- src/components/Modal/ModalLogin/ModalLogin.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/Modal/ModalLogin/ModalLogin.tsx b/src/components/Modal/ModalLogin/ModalLogin.tsx index fb82f7f..e800f6e 100644 --- a/src/components/Modal/ModalLogin/ModalLogin.tsx +++ b/src/components/Modal/ModalLogin/ModalLogin.tsx @@ -31,6 +31,8 @@ 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, @@ -81,7 +83,7 @@ export const ModalLogin = () => { cleanUpStates() setTimeout(() => { // give frontend time to read the new key first - navigate(`/key/${k.npub}`) + navigate(`/key/${k.npub}${isPopup ? '?popup=true' : ''}`) }, 300) } catch (error: any) { console.log('error', error) @@ -90,10 +92,8 @@ export const ModalLogin = () => { } } - const [searchParams] = useSearchParams() useEffect(() => { if (isModalOpened) { - const isPopup = searchParams.get('popup') === 'true' const npub = searchParams.get('npub') || '' const appNpub = searchParams.get('appNpub') || '' if (isPopup && isModalOpened) { From 04c425c32cbdc21b949d7f12919ebc8113d32acb Mon Sep 17 00:00:00 2001 From: Bekbolsun Date: Fri, 16 Feb 2024 14:20:51 +0600 Subject: [PATCH 04/16] show appNpub in apps list & in app details page --- .../Modal/ModalInitial/ModalInitial.tsx | 26 +------------------ src/pages/AppPage/App.Page.tsx | 21 ++++++++++----- src/pages/KeyPage/Key.Page.tsx | 16 +++++++++--- src/pages/KeyPage/components/ItemApp.tsx | 21 ++++++++------- src/pages/KeyPage/utils.ts | 3 ++- 5 files changed, 43 insertions(+), 44 deletions(-) diff --git a/src/components/Modal/ModalInitial/ModalInitial.tsx b/src/components/Modal/ModalInitial/ModalInitial.tsx index 835f0ec..7baf84c 100644 --- a/src/components/Modal/ModalInitial/ModalInitial.tsx +++ b/src/components/Modal/ModalInitial/ModalInitial.tsx @@ -1,44 +1,20 @@ -import React, { useEffect, useState } from 'react' import { useModalSearchParams } from '@/hooks/useModalSearchParams' import { Button } from '@/shared/Button/Button' import { Modal } from '@/shared/Modal/Modal' import { MODAL_PARAMS_KEYS } from '@/types/modal' -import { Fade, Stack } from '@mui/material' -import { AppLink } from '@/shared/AppLink/AppLink' +import { Stack } from '@mui/material' 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 handleShowAdvanced = () => { - setShowAdvancedContent(true) - } - - useEffect(() => { - return () => { - if (isModalOpened) { - setShowAdvancedContent(false) - } - } - }, [isModalOpened]) - return ( - {/* - - {showAdvancedContent && ( - - - - )} */} ) diff --git a/src/pages/AppPage/App.Page.tsx b/src/pages/AppPage/App.Page.tsx index 23c33f6..c293257 100644 --- a/src/pages/AppPage/App.Page.tsx +++ b/src/pages/AppPage/App.Page.tsx @@ -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 const { timestamp } = connectPerm || {} const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected' @@ -65,13 +67,20 @@ const AppPage = () => { <> navigate(`key/${npub}`)} /> - + {appAvatarTitle} - - - {appName} - + + + + {appName} + + {isAppNameExists && ( + + {shortAppNpub} + + )} + diff --git a/src/pages/KeyPage/Key.Page.tsx b/src/pages/KeyPage/Key.Page.tsx index 3f1547f..59a6053 100644 --- a/src/pages/KeyPage/Key.Page.tsx +++ b/src/pages/KeyPage/Key.Page.tsx @@ -18,14 +18,18 @@ 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 { useCallback, useState } from 'react' const KeyPage = () => { const { npub = '' } = useParams<{ npub: string }>() const { keys, apps, pending, perms } = useAppSelector((state) => state.content) const [searchParams] = useSearchParams() - const isSynced = useLiveQuery(checkNpubSyncQuerier(npub), [npub], false) + const [isCheckingSync, setIsChecking] = useState(true) + const handleStopChecking = () => setIsChecking(false) + + const isSynced = useLiveQuery(checkNpubSyncQuerier(npub, handleStopChecking), [npub], false) + const { handleOpen } = useModalSearchParams() const { handleEnableBackground, showWarning, isEnabling } = useBackgroundSigning() @@ -44,12 +48,14 @@ const KeyPage = () => { 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 } + if (!isKeyExists) return const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP) @@ -80,7 +86,11 @@ const KeyPage = () => { Connect app - + Settings diff --git a/src/pages/KeyPage/components/ItemApp.tsx b/src/pages/KeyPage/components/ItemApp.tsx index a016137..8ce2dea 100644 --- a/src/pages/KeyPage/components/ItemApp.tsx +++ b/src/pages/KeyPage/components/ItemApp.tsx @@ -9,9 +9,12 @@ type ItemAppProps = DbApp export const ItemApp: FC = ({ 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 appAvatarTitle = getAppIconTitle(name || appDomain, appNpub) + const isAppNameExists = !!name + return ( = ({ npub, appNpub, icon, name, url }) => component={Link} to={`/key/${npub}/app/${appNpub}`} > - + {appAvatarTitle} - + {appName} + {isAppNameExists && ( + + {shortAppNpub} + + )} Basic actions diff --git a/src/pages/KeyPage/utils.ts b/src/pages/KeyPage/utils.ts index f8a388f..090ca20 100644 --- a/src/pages/KeyPage/utils.ts +++ b/src/pages/KeyPage/utils.ts @@ -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 } From 87ec23c73753a00a80964e2f21ea9e04a6968233 Mon Sep 17 00:00:00 2001 From: artur Date: Fri, 16 Feb 2024 11:28:02 +0300 Subject: [PATCH 05/16] Added watcher, deletes pending if watcher has concurrent reply, fixing popup closing issues --- .../ModalConfirmConnect.tsx | 53 ++++++++++++++---- .../ModalConfirmEvent/ModalConfirmEvent.tsx | 38 ++++++++++--- .../Modal/ModalInitial/ModalInitial.tsx | 28 +++++----- .../Modal/ModalLogin/ModalLogin.tsx | 2 +- src/modules/backend.ts | 55 ++++++++++++++++++- src/modules/swic.ts | 9 ++- .../components/Activities/ItemActivity.tsx | 1 - src/pages/KeyPage/Key.Page.tsx | 2 +- src/utils/helpers/helpers.ts | 2 +- 9 files changed, 149 insertions(+), 41 deletions(-) diff --git a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx index 7e55f95..8f254ae 100644 --- a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx +++ b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx @@ -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.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,14 +61,35 @@ export const ModalConfirmConnect = () => { }, }) - const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) - // App doesn't exist yet! - // const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub) - const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId) - // console.log("pending", {isModalOpened, isPendingReqIdExists, isNpubExists, /*isAppNpubExists,*/ pendingReqId, pending}); - if (!isPopup && isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) { - closeModalAfterRequest() - return null + 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]) + + 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 + } + // reset + setIsLoaded(false) } const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => { diff --git a/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx b/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx index 22d6388..bc99ae3 100644 --- a/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx +++ b/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx @@ -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 = ({ confirmEventReqs const [selectedActionType, setSelectedActionType] = useState(ACTION_TYPE.ALWAYS) const [pendingRequests, setPendingRequests] = useState([]) + const [isLoaded, setIsLoaded] = useState(false) const currentAppPendingReqs = useMemo(() => confirmEventReqs[appNpub]?.pending || [], [confirmEventReqs, appNpub]) @@ -61,10 +62,26 @@ export const ModalConfirmEvent: FC = ({ confirmEventReqs }, }) + useEffect(() => { + if (isModalOpened) { + if (isPopup) { + // wait for SW to start + swicWaitStarted().then(() => { + // give it some time to load the pending reqs etc + setTimeout(() => setIsLoaded(true), 500) + }) + } else { + setIsLoaded(true) + } + } else { + setIsLoaded(false) + } + }, [isModalOpened]) + // 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 + // the logic 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 @@ -74,12 +91,17 @@ export const ModalConfirmEvent: FC = ({ confirmEventReqs // for the specified appNpub // FIXME is the same logic valid for Connect modal? - const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) - const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub) - // console.log("confirm event", { confirmEventReqs, isModalOpened, isNpubExists, isAppNpubExists }); - if (isModalOpened && (!currentAppPendingReqs.length || !isNpubExists || !isAppNpubExists)) { - closeModalAfterRequest() - return null + if (isLoaded) { + const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) + const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub) + // console.log("confirm event", { confirmEventReqs, isModalOpened, isNpubExists, isAppNpubExists }); + if (isModalOpened && (!currentAppPendingReqs.length || !isNpubExists || !isAppNpubExists)) { + if (isPopup) window.close() + else closeModalAfterRequest() + return null + } + // reset + setIsLoaded(false) } const triggerApp = apps.find((app) => app.appNpub === appNpub) diff --git a/src/components/Modal/ModalInitial/ModalInitial.tsx b/src/components/Modal/ModalInitial/ModalInitial.tsx index 835f0ec..7a2949f 100644 --- a/src/components/Modal/ModalInitial/ModalInitial.tsx +++ b/src/components/Modal/ModalInitial/ModalInitial.tsx @@ -1,10 +1,10 @@ -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() @@ -12,19 +12,19 @@ export const ModalInitial = () => { 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 ( diff --git a/src/components/Modal/ModalLogin/ModalLogin.tsx b/src/components/Modal/ModalLogin/ModalLogin.tsx index ad3d1ad..b7f90da 100644 --- a/src/components/Modal/ModalLogin/ModalLogin.tsx +++ b/src/components/Modal/ModalLogin/ModalLogin.tsx @@ -107,7 +107,7 @@ export const ModalLogin = () => { }) } } - }, [searchParams, isModalOpened, setValue]) + }, [searchParams, isModalOpened, isPopup, setValue]) useEffect(() => { return () => { diff --git a/src/modules/backend.ts b/src/modules/backend.ts index d8b07e6..fc709b8 100644 --- a/src/modules/backend.ts +++ b/src/modules/backend.ts @@ -7,6 +7,9 @@ import NDK, { 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' @@ -28,6 +31,7 @@ interface Key { backoff: number signer: NDKSigner backend: NDKNip46Backend + watcher: Watcher } interface Pending { @@ -46,6 +50,46 @@ interface IAllowCallbackParams { 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 { public async processEvent(event: NDKEvent) { this.handleIncomingEvent(event) @@ -784,7 +828,11 @@ export class NoauthBackend { const signer = new NDKPrivateKeySigner(sk) // PrivateKeySigner const backend = new Nip46Backend(ndk, signer, () => 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 backend.handlers['get_nip04_key'] = new Nip04KeyHandlingStrategy(sk) @@ -802,6 +850,7 @@ export class NoauthBackend { // start backend.start() + watcher.start() console.log('started', npub) // backoff reset on successfull connection @@ -825,11 +874,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) diff --git a/src/modules/swic.ts b/src/modules/swic.ts index f7bc994..2cc6e56 100644 --- a/src/modules/swic.ts +++ b/src/modules/swic.ts @@ -5,7 +5,7 @@ export let swr: ServiceWorkerRegistration | null = null const reqs = new Map void; rej: (r: any) => void }>() let nextReqId = 1 let onRender: (() => void) | null = null -const queue: (() => Promise)[] = [] +const queue: (() => Promise | void)[] = [] export async function swicRegister() { serviceWorkerRegistration.register({ @@ -36,6 +36,13 @@ export async function swicRegister() { }) } +export function swicWaitStarted() { + return new Promise(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) diff --git a/src/pages/AppPage/components/Activities/ItemActivity.tsx b/src/pages/AppPage/components/Activities/ItemActivity.tsx index 1d16624..56e4f8d 100644 --- a/src/pages/AppPage/components/Activities/ItemActivity.tsx +++ b/src/pages/AppPage/components/Activities/ItemActivity.tsx @@ -6,7 +6,6 @@ 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 diff --git a/src/pages/KeyPage/Key.Page.tsx b/src/pages/KeyPage/Key.Page.tsx index 3f1547f..6894366 100644 --- a/src/pages/KeyPage/Key.Page.tsx +++ b/src/pages/KeyPage/Key.Page.tsx @@ -43,7 +43,7 @@ const KeyPage = () => { const isKeyExists = npub.trim().length && key const isPopup = searchParams.get('popup') === 'true' - console.log({ isKeyExists, isPopup }) + // console.log({ isKeyExists, isPopup }) if (isPopup && !isKeyExists) { searchParams.set('login', 'true') searchParams.set('npub', npub) diff --git a/src/utils/helpers/helpers.ts b/src/utils/helpers/helpers.ts index 5d67e9a..19fd358 100644 --- a/src/utils/helpers/helpers.ts +++ b/src/utils/helpers/helpers.ts @@ -121,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()) From 6186f3dd3df03ed46a00eb70ff49478707f15179 Mon Sep 17 00:00:00 2001 From: artur Date: Fri, 16 Feb 2024 11:44:50 +0300 Subject: [PATCH 06/16] Make url optional, move name to top on app detail modal --- .../Modal/ModalAppDetails/ModalAppDetails.tsx | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx b/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx index 4d69582..1e562b1 100644 --- a/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx +++ b/src/components/Modal/ModalAppDetails/ModalAppDetails.tsx @@ -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 ( @@ -130,6 +131,13 @@ export const ModalAppDetails = () => { + { ) }} /> - Date: Fri, 16 Feb 2024 13:30:04 +0300 Subject: [PATCH 07/16] Fix isLoading reset in popup confirms --- src/App.tsx | 2 +- .../ModalConfirmConnect/ModalConfirmConnect.tsx | 17 +++++++++++++---- .../ModalConfirmEvent/ModalConfirmEvent.tsx | 17 +---------------- src/hooks/useModalSearchParams.ts | 2 +- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 4f18330..8ad25ac 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 diff --git a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx index 8f254ae..fd72265 100644 --- a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx +++ b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx @@ -61,12 +61,23 @@ 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 { @@ -75,7 +86,7 @@ export const ModalConfirmConnect = () => { } else { setIsLoaded(false) } - }, [isModalOpened]) + }, [isModalOpened, isPopup]) if (isLoaded) { const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) @@ -88,8 +99,6 @@ export const ModalConfirmConnect = () => { else closeModalAfterRequest() return null } - // reset - setIsLoaded(false) } const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => { @@ -208,7 +217,7 @@ export const ModalConfirmConnect = () => { - Disallow + Ignore Connect diff --git a/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx b/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx index bc99ae3..cc0f01f 100644 --- a/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx +++ b/src/components/Modal/ModalConfirmEvent/ModalConfirmEvent.tsx @@ -76,20 +76,7 @@ export const ModalConfirmEvent: FC = ({ confirmEventReqs } else { setIsLoaded(false) } - }, [isModalOpened]) - - // 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 logic 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? + }, [isModalOpened, isPopup]) if (isLoaded) { const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) @@ -100,8 +87,6 @@ export const ModalConfirmEvent: FC = ({ confirmEventReqs else closeModalAfterRequest() return null } - // reset - setIsLoaded(false) } const triggerApp = apps.find((app) => app.appNpub === appNpub) diff --git a/src/hooks/useModalSearchParams.ts b/src/hooks/useModalSearchParams.ts index f935c15..f1cbf3e 100644 --- a/src/hooks/useModalSearchParams.ts +++ b/src/hooks/useModalSearchParams.ts @@ -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 }) } From 46336d817fe372ceaffbef3942bcb12ff660ec65 Mon Sep 17 00:00:00 2001 From: artur Date: Fri, 16 Feb 2024 14:46:36 +0300 Subject: [PATCH 08/16] Add ignore logic to stop interfering with replies from other instances --- src/App.tsx | 2 +- src/modules/backend.ts | 336 ++++++++++++++++++++------------- src/pages/KeyPage/Key.Page.tsx | 2 +- 3 files changed, 205 insertions(+), 135 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8ad25ac..b382f04 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 () => { diff --git a/src/modules/backend.ts b/src/modules/backend.ts index fc709b8..e99dadf 100644 --- a/src/modules/backend.ts +++ b/src/modules/backend.ts @@ -1,8 +1,7 @@ -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, @@ -12,13 +11,20 @@ import NDK, { 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 @@ -36,7 +42,7 @@ interface Key { interface Pending { req: DbPending - cb: (allow: boolean, remember: boolean, options?: any) => void + cb: (allow: DECISION, remember: boolean, options?: any) => void notified?: boolean } @@ -63,23 +69,25 @@ class Watcher { } 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 = 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 }) + 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 }) + 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) }) @@ -91,91 +99,128 @@ class Watcher { } class Nip46Backend extends NDKNip46Backend { + private allowCb: (params: IAllowCallbackParams) => Promise + private npub: string = '' + + public constructor(ndk: NDK, signer: NDKSigner, allowCb: (params: IAllowCallbackParams) => Promise) { + 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) } -} -class Nip04KeyHandlingStrategy implements IEventHandlingStrategy { - private privkey: string - private nip04 = new Nip04() + protected async handleIncomingEvent(event: NDKEvent) { + const { id, method, params } = (await this.rpc.parseEvent(event)) as any + const remotePubkey = event.pubkey + let response: string | undefined - constructor(privkey: string) { - this.privkey = privkey - } + this.debug('incoming event', { id, method, params }) - 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 + // validate signature explicitly + if (!verifySignature(event.rawEvent() as Event)) { + this.debug('invalid signature', event.rawEvent()) + return } - 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 - - constructor( - backend: NDKNip46Backend, - npub: string, - method: string, - body: IEventHandlingStrategy, - allowCb: (params: IAllowCallbackParams) => Promise - ) { - 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 { - console.log(Date.now(), 'handle', { - method: this.method, - id, - remotePubkey, - params, - }) - const allow = await this.allowCb({ - backend: this.backend, + 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 { +// 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 @@ -190,7 +235,7 @@ export class NoauthBackend { private pendingNpubEvents = new Map() private ndk = new NDK({ explicitRelayUrls: NIP46_RELAYS, - enableOutboxModel: false + enableOutboxModel: false, }) public constructor(swg: ServiceWorkerGlobalScope) { @@ -610,7 +655,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) @@ -619,8 +664,18 @@ export class NoauthBackend { // non-exact next if (!perm) perm = appPerms.find((p) => isPackagePerm(p.perm, reqPerm)) - console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms) - return perm?.value || '' + if (perm) { + console.log('req', req, 'perm', reqPerm, 'value', perm, appPerms) + return perm.value === '1' ? DECISION.ALLOW : DECISION.DISALLOW + } + + const conn = appPerms.find((p) => p.perm === 'connect') + if (conn && conn.value === '0') { + console.log('req', req, 'perm', reqPerm, 'ignore by connect disallow') + return DECISION.IGNORE + } + + return DECISION.ASK } private async connectApp({ @@ -673,19 +728,19 @@ export class NoauthBackend { method, remotePubkey, params, - }: IAllowCallbackParams): Promise { + }: IAllowCallbackParams): Promise { // 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 = { @@ -700,9 +755,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) @@ -755,35 +822,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) @@ -794,7 +866,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 @@ -827,7 +899,7 @@ export class NoauthBackend { ndk.connect() 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) const watcher = new Watcher(ndk, signer, (id) => { // drop pending request dbi.removePending(id).then(() => this.updateUI()) @@ -835,18 +907,16 @@ export class NoauthBackend { 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() @@ -907,11 +977,11 @@ export class NoauthBackend { 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()]); + '#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) { @@ -1028,7 +1098,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) } } diff --git a/src/pages/KeyPage/Key.Page.tsx b/src/pages/KeyPage/Key.Page.tsx index 59a6053..9761f93 100644 --- a/src/pages/KeyPage/Key.Page.tsx +++ b/src/pages/KeyPage/Key.Page.tsx @@ -47,7 +47,7 @@ const KeyPage = () => { const isKeyExists = npub.trim().length && key const isPopup = searchParams.get('popup') === 'true' - console.log({ isKeyExists, isPopup }) + // console.log({ isKeyExists, isPopup }) if (isPopup && !isKeyExists) { searchParams.set('login', 'true') From 3813cef605ad0d3e061c6f9d3969f7f9698ff959 Mon Sep 17 00:00:00 2001 From: artur Date: Fri, 16 Feb 2024 14:55:35 +0300 Subject: [PATCH 09/16] Don't stop signup if enable-push failed --- .../Modal/ModalConfirmConnect/ModalConfirmConnect.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx index fd72265..afaf459 100644 --- a/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx +++ b/src/components/Modal/ModalConfirmConnect/ModalConfirmConnect.tsx @@ -123,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') @@ -131,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 { From 6d72cf1f82fafeca72761ac18c536eed784a07b0 Mon Sep 17 00:00:00 2001 From: Bekbolsun Date: Fri, 16 Feb 2024 17:59:59 +0600 Subject: [PATCH 10/16] implement edit username logic in edit modal --- .../Modal/ModalEditName/ModalEditName.tsx | 138 ++++++++++++++++++ .../Modal/ModalLogin/ModalLogin.tsx | 3 +- src/pages/KeyPage/Key.Page.tsx | 25 +++- .../KeyPage/components/UserValueSection.tsx | 7 +- src/shared/Button/Button.tsx | 4 +- src/shared/Input/Input.tsx | 15 +- src/types/modal.ts | 1 + 7 files changed, 178 insertions(+), 15 deletions(-) create mode 100644 src/components/Modal/ModalEditName/ModalEditName.tsx diff --git a/src/components/Modal/ModalEditName/ModalEditName.tsx b/src/components/Modal/ModalEditName/ModalEditName.tsx new file mode 100644 index 0000000..136fea9 --- /dev/null +++ b/src/components/Modal/ModalEditName/ModalEditName.tsx @@ -0,0 +1,138 @@ +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 { useSearchParams } from 'react-router-dom' +import { useDebounce } from 'use-debounce' + +export const ModalEditName = () => { + const keys = useAppSelector(selectKeys) + const notify = useEnqueueSnackbar() + + const [searchParams] = useSearchParams() + const name = searchParams.get('name') || '' + const npub = searchParams.get('npub') || '' + + 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 [isAvailable, setIsAvailable] = useState(true) + const [isChecking, setIsChecking] = useState(false) + + const [isLoading, setIsLoading] = useState(false) + + const checkIsUsernameAvailable = useCallback(async () => { + if (!debouncedName.trim().length) return undefined + try { + setIsChecking(true) + const npubNip05 = await fetchNip05(`${debouncedName}@${DOMAIN}`) + setIsAvailable(!npubNip05) + setIsChecking(false) + } catch (error) { + setIsAvailable(true) + setIsChecking(false) + } + }, [debouncedName]) + + useEffect(() => { + checkIsUsernameAvailable() + }, [checkIsUsernameAvailable]) + + useEffect(() => { + setEnteredName(name) + return () => { + if (isModalOpened) setEnteredName('') + } + // eslint-disable-next-line + }, [isModalOpened]) + + const handleNameChange = (e: ChangeEvent) => setEnteredName(e.target.value) + + const getInputHelperText = () => { + if (!debouncedName.trim().length || isNameEqual) return '' + if (isChecking) return 'Loading...' + if (!isAvailable) return 'Already taken' + return ( + + Available + + ) + } + 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 || isChecking || isLoading || !enteredName.trim().length + + const handleEditName = async () => { + try { + setIsLoading(true) + await swicCall('editName', npub, debouncedName) + notify('Username successfully editted!', 'success') + setEnteredName(debouncedName) + setIsLoading(false) + } catch (error) { + setIsLoading(false) + } + } + + return ( + + + + @{DOMAIN}} + helperText={inputHelperText} + onChange={handleNameChange} + value={enteredName} + helperTextProps={{ + sx: { + '&.helper_text': { + color: getHelperTextColor(), + }, + }, + }} + /> + + + + + + + + + ) +} diff --git a/src/components/Modal/ModalLogin/ModalLogin.tsx b/src/components/Modal/ModalLogin/ModalLogin.tsx index ad3d1ad..ec02647 100644 --- a/src/components/Modal/ModalLogin/ModalLogin.tsx +++ b/src/components/Modal/ModalLogin/ModalLogin.tsx @@ -100,13 +100,14 @@ export const ModalLogin = () => { if (isPopup && isModalOpened) { swicCall('fetchPendingRequests', npub, appNpub) - fetchNpubNames(npub).then(names => { + fetchNpubNames(npub).then((names) => { if (names.length) { setValue('username', `${names[0]}@${DOMAIN}`) } }) } } + // eslint-disable-next-line }, [searchParams, isModalOpened, setValue]) useEffect(() => { diff --git a/src/pages/KeyPage/Key.Page.tsx b/src/pages/KeyPage/Key.Page.tsx index 3f1547f..3244e97 100644 --- a/src/pages/KeyPage/Key.Page.tsx +++ b/src/pages/KeyPage/Key.Page.tsx @@ -1,6 +1,6 @@ import { useAppSelector } from '../../store/hooks/redux' 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 { SettingsIcon, ShareIcon } from '@/assets' import { Apps } from './components/Apps' @@ -19,6 +19,9 @@ 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 }>() @@ -54,6 +57,13 @@ const KeyPage = () => { const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP) const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS) + const handleOpenEditNameModal = () => + handleOpen(MODAL_PARAMS_KEYS.EDIT_NAME, { + search: { + name: key.name || '', + npub, + }, + }) return ( <> @@ -64,13 +74,20 @@ const KeyPage = () => { + + + + + + } explanationType={EXPLANATION_MODAL_KEYS.LOGIN} /> } explanationType={EXPLANATION_MODAL_KEYS.NPUB} /> @@ -88,11 +105,13 @@ const KeyPage = () => { + + ) } diff --git a/src/pages/KeyPage/components/UserValueSection.tsx b/src/pages/KeyPage/components/UserValueSection.tsx index a3520ac..935a665 100644 --- a/src/pages/KeyPage/components/UserValueSection.tsx +++ b/src/pages/KeyPage/components/UserValueSection.tsx @@ -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 = ({ title, value, explanationType, copyValue }) => { +const UserValueSection: FC = ({ title, value, explanationType, endAdornment }) => { const { handleOpen } = useModalSearchParams() const handleOpenExplanationModal = (type: EXPLANATION_MODAL_KEYS) => { @@ -30,7 +29,7 @@ const UserValueSection: FC = ({ title, value, explanation {title} handleOpenExplanationModal(explanationType)} /> - } /> + ) } diff --git a/src/shared/Button/Button.tsx b/src/shared/Button/Button.tsx index 683ecde..97886d8 100644 --- a/src/shared/Button/Button.tsx +++ b/src/shared/Button/Button.tsx @@ -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', }, } diff --git a/src/shared/Input/Input.tsx b/src/shared/Input/Input.tsx index 9cb6f74..d65a39a 100644 --- a/src/shared/Input/Input.tsx +++ b/src/shared/Input/Input.tsx @@ -26,7 +26,12 @@ export const Input = forwardRef( {label} ) : null} - + {helperText ? ( {helperText} @@ -41,20 +46,20 @@ const StyledInputContainer = styled((props: BoxProps) => )(({ 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', color: theme.palette.text.primary, diff --git a/src/types/modal.ts b/src/types/modal.ts index bf50959..b99c479 100644 --- a/src/types/modal.ts +++ b/src/types/modal.ts @@ -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 { From a60fcd65b59f744ae6d04a79aeddc4a02ce80c63 Mon Sep 17 00:00:00 2001 From: artur Date: Fri, 16 Feb 2024 15:29:06 +0300 Subject: [PATCH 11/16] Show app npub if app only has url --- src/pages/AppPage/App.Page.tsx | 2 +- src/pages/KeyPage/components/ItemApp.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/AppPage/App.Page.tsx b/src/pages/AppPage/App.Page.tsx index c293257..3410255 100644 --- a/src/pages/AppPage/App.Page.tsx +++ b/src/pages/AppPage/App.Page.tsx @@ -46,7 +46,7 @@ const AppPage = () => { const shortAppNpub = getShortenNpub(appNpub) const appName = name || appDomain || shortAppNpub const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub) - const isAppNameExists = !!name + const isAppNameExists = !!name || !!appDomain const { timestamp } = connectPerm || {} const connectedOn = connectPerm && timestamp ? `Connected at ${formatTimestampDate(timestamp)}` : 'Not connected' diff --git a/src/pages/KeyPage/components/ItemApp.tsx b/src/pages/KeyPage/components/ItemApp.tsx index 8ce2dea..586aefa 100644 --- a/src/pages/KeyPage/components/ItemApp.tsx +++ b/src/pages/KeyPage/components/ItemApp.tsx @@ -13,7 +13,7 @@ export const ItemApp: FC = ({ npub, appNpub, icon, name, url }) => const appName = name || appDomain || shortAppNpub const appIcon = icon || `https://${appDomain}/favicon.ico` const appAvatarTitle = getAppIconTitle(name || appDomain, appNpub) - const isAppNameExists = !!name + const isAppNameExists = !!name || !!appDomain return ( Date: Fri, 16 Feb 2024 19:47:25 +0600 Subject: [PATCH 12/16] add transfer name field --- .../Modal/ModalEditName/ModalEditName.tsx | 51 ++++++++++++++++--- src/modules/backend.ts | 22 ++++---- 2 files changed, 53 insertions(+), 20 deletions(-) diff --git a/src/components/Modal/ModalEditName/ModalEditName.tsx b/src/components/Modal/ModalEditName/ModalEditName.tsx index 136fea9..6dbc0ae 100644 --- a/src/components/Modal/ModalEditName/ModalEditName.tsx +++ b/src/components/Modal/ModalEditName/ModalEditName.tsx @@ -20,7 +20,7 @@ export const ModalEditName = () => { const keys = useAppSelector(selectKeys) const notify = useEnqueueSnackbar() - const [searchParams] = useSearchParams() + const [searchParams, setSearchParams] = useSearchParams() const name = searchParams.get('name') || '' const npub = searchParams.get('npub') || '' @@ -28,16 +28,24 @@ export const ModalEditName = () => { const { getModalOpened, createHandleCloseReplace } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.EDIT_NAME) - const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.EDIT_NAME) + const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.EDIT_NAME, { + onClose: (search) => { + search.delete('name') + search.delete('npub') + }, + }) 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 @@ -66,6 +74,8 @@ export const ModalEditName = () => { const handleNameChange = (e: ChangeEvent) => setEnteredName(e.target.value) + const handleReceiverNpubChange = (e: ChangeEvent) => setReceiverNpub(e.target.value) + const getInputHelperText = () => { if (!debouncedName.trim().length || isNameEqual) return '' if (isChecking) return 'Loading...' @@ -90,17 +100,34 @@ export const ModalEditName = () => { return null } - const isEditButtonDisabled = isNameEqual || isChecking || isLoading || !enteredName.trim().length + const isEditButtonDisabled = isNameEqual || !isAvailable || isChecking || isLoading || !enteredName.trim().length + const isTransferButtonDisabled = !enteredName.trim().length || !receiverNpub.trim().length || isTransferLoading const handleEditName = async () => { + if (isEditButtonDisabled) return try { setIsLoading(true) - await swicCall('editName', npub, debouncedName) + await swicCall('editName', npub, enteredName) notify('Username successfully editted!', 'success') - setEnteredName(debouncedName) setIsLoading(false) - } catch (error) { + searchParams.set('name', enteredName) + setSearchParams(searchParams) + } 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('Npub successfully transfered!', 'success') + setIsTransferLoading(false) + } catch (error: any) { + setIsTransferLoading(false) + notify(error?.message || 'Failed to transfer npub!', 'error') } } @@ -129,8 +156,16 @@ export const ModalEditName = () => { - - + + diff --git a/src/modules/backend.ts b/src/modules/backend.ts index 626dd64..3a03fdc 100644 --- a/src/modules/backend.ts +++ b/src/modules/backend.ts @@ -192,7 +192,7 @@ class Nip46Backend extends NDKNip46Backend { // } // } -// FIXME why do we need it? Just to print +// FIXME why do we need it? Just to print // class EventHandlingStrategyWrapper implements IEventHandlingStrategy { // readonly backend: NDKNip46Backend // readonly method: string @@ -525,13 +525,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' @@ -803,7 +801,7 @@ export class NoauthBackend { return // noop case DECISION.ALLOW: case DECISION.DISALLOW: - // fall through + // fall through } const allow = decision === DECISION.ALLOW @@ -1154,8 +1152,8 @@ 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) { await this.sendDeleteNameToServer(npub, key.name) } @@ -1168,10 +1166,10 @@ export class NoauthBackend { } 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") + 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 = '' From b98339e1777f7285a2a47c6258712f34b9fe0d81 Mon Sep 17 00:00:00 2001 From: Bekbolsun Date: Fri, 16 Feb 2024 20:09:33 +0600 Subject: [PATCH 13/16] add hints --- src/components/Modal/ModalLogin/ModalLogin.tsx | 15 +++++++++++---- src/components/Modal/ModalSignUp/ModalSignUp.tsx | 11 ++++++++--- src/shared/Input/Input.tsx | 2 +- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/components/Modal/ModalLogin/ModalLogin.tsx b/src/components/Modal/ModalLogin/ModalLogin.tsx index b7f90da..37190ae 100644 --- a/src/components/Modal/ModalLogin/ModalLogin.tsx +++ b/src/components/Modal/ModalLogin/ModalLogin.tsx @@ -100,7 +100,7 @@ export const ModalLogin = () => { if (isPopup && isModalOpened) { swicCall('fetchPendingRequests', npub, appNpub) - fetchNpubNames(npub).then(names => { + fetchNpubNames(npub).then((names) => { if (names.length) { setValue('username', `${names[0]}@${DOMAIN}`) } @@ -141,10 +141,17 @@ export const ModalLogin = () => { {...register('password')} {...inputProps} error={!!errors.password} + helperText={'Password you set in Cloud Sync settings'} /> - + + + + + Import your keys from another nsec.app instance + + ) diff --git a/src/components/Modal/ModalSignUp/ModalSignUp.tsx b/src/components/Modal/ModalSignUp/ModalSignUp.tsx index 327ea06..b867d84 100644 --- a/src/components/Modal/ModalSignUp/ModalSignUp.tsx +++ b/src/components/Modal/ModalSignUp/ModalSignUp.tsx @@ -113,9 +113,14 @@ export const ModalSignUp = () => { }, }} /> - + + + + New keys will be generated for you + + ) diff --git a/src/shared/Input/Input.tsx b/src/shared/Input/Input.tsx index 9cb6f74..f63ae57 100644 --- a/src/shared/Input/Input.tsx +++ b/src/shared/Input/Input.tsx @@ -56,7 +56,7 @@ const StyledInputContainer = styled((props: BoxProps) => )(({ }, }, '& > .helper_text': { - margin: '0.5rem 1rem 0', + margin: '0.5rem 0.5rem 0', color: theme.palette.text.primary, }, '& > .label': { From ba3775e6c6781937bff8f7cc088d64bef7722ab2 Mon Sep 17 00:00:00 2001 From: artur Date: Mon, 19 Feb 2024 10:37:06 +0300 Subject: [PATCH 14/16] Make username settings button red if name empty, add sections to username settings, UI fixes to username settings, remove qs params from username settings --- .../Modal/ModalEditName/ModalEditName.tsx | 51 ++++++++++--------- src/components/Modal/ModalEditName/styled.tsx | 10 ++++ src/pages/KeyPage/Key.Page.tsx | 10 +--- 3 files changed, 38 insertions(+), 33 deletions(-) create mode 100644 src/components/Modal/ModalEditName/styled.tsx diff --git a/src/components/Modal/ModalEditName/ModalEditName.tsx b/src/components/Modal/ModalEditName/ModalEditName.tsx index 6dbc0ae..09d43d0 100644 --- a/src/components/Modal/ModalEditName/ModalEditName.tsx +++ b/src/components/Modal/ModalEditName/ModalEditName.tsx @@ -13,27 +13,24 @@ 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 { useSearchParams } from 'react-router-dom' +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 [searchParams, setSearchParams] = useSearchParams() - const name = searchParams.get('name') || '' - const npub = searchParams.get('npub') || '' + 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, { - onClose: (search) => { - search.delete('name') - search.delete('npub') - }, - }) + const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.EDIT_NAME) const [enteredName, setEnteredName] = useState('') const [debouncedName] = useDebounce(enteredName, 300) @@ -52,7 +49,7 @@ export const ModalEditName = () => { try { setIsChecking(true) const npubNip05 = await fetchNip05(`${debouncedName}@${DOMAIN}`) - setIsAvailable(!npubNip05) + setIsAvailable(!npubNip05 || npubNip05 === npub) setIsChecking(false) } catch (error) { setIsAvailable(true) @@ -67,7 +64,10 @@ export const ModalEditName = () => { useEffect(() => { setEnteredName(name) return () => { - if (isModalOpened) setEnteredName('') + if (isModalOpened) { + setEnteredName('') + setReceiverNpub('') + } } // eslint-disable-next-line }, [isModalOpened]) @@ -101,17 +101,15 @@ export const ModalEditName = () => { } const isEditButtonDisabled = isNameEqual || !isAvailable || isChecking || isLoading || !enteredName.trim().length - const isTransferButtonDisabled = !enteredName.trim().length || !receiverNpub.trim().length || isTransferLoading + const isTransferButtonDisabled = !name.length || !receiverNpub.trim().length || isTransferLoading const handleEditName = async () => { if (isEditButtonDisabled) return try { setIsLoading(true) await swicCall('editName', npub, enteredName) - notify('Username successfully editted!', 'success') + notify('Username updated!', 'success') setIsLoading(false) - searchParams.set('name', enteredName) - setSearchParams(searchParams) } catch (error: any) { setIsLoading(false) notify(error?.message || 'Failed to edit username!', 'error') @@ -123,20 +121,22 @@ export const ModalEditName = () => { try { setIsTransferLoading(true) await swicCall('transferName', npub, enteredName, receiverNpub) - notify('Npub successfully transfered!', 'success') + notify('Username transferred!', 'success') setIsTransferLoading(false) + setEnteredName('') } catch (error: any) { setIsTransferLoading(false) - notify(error?.message || 'Failed to transfer npub!', 'error') + notify(error?.message || 'Failed to transfer username!', 'error') } } return ( - + - + + Change name @{DOMAIN}} @@ -152,10 +152,11 @@ export const ModalEditName = () => { }} /> - - + + + Transfer name { - + ) diff --git a/src/components/Modal/ModalEditName/styled.tsx b/src/components/Modal/ModalEditName/styled.tsx new file mode 100644 index 0000000..0f0e021 --- /dev/null +++ b/src/components/Modal/ModalEditName/styled.tsx @@ -0,0 +1,10 @@ +import { Stack, StackProps, styled } from "@mui/material"; + +export const StyledSettingContainer = styled((props: StackProps) => ( + +))(({ theme }) => ({ + padding: '1rem', + borderRadius: '1rem', + background: theme.palette.background.default, + color: theme.palette.text.primary, +})) \ No newline at end of file diff --git a/src/pages/KeyPage/Key.Page.tsx b/src/pages/KeyPage/Key.Page.tsx index 40aa53c..7b98275 100644 --- a/src/pages/KeyPage/Key.Page.tsx +++ b/src/pages/KeyPage/Key.Page.tsx @@ -63,13 +63,7 @@ const KeyPage = () => { const handleOpenConnectAppModal = () => handleOpen(MODAL_PARAMS_KEYS.CONNECT_APP) const handleOpenSettingsModal = () => handleOpen(MODAL_PARAMS_KEYS.SETTINGS) - const handleOpenEditNameModal = () => - handleOpen(MODAL_PARAMS_KEYS.EDIT_NAME, { - search: { - name: key.name || '', - npub, - }, - }) + const handleOpenEditNameModal = () => handleOpen(MODAL_PARAMS_KEYS.EDIT_NAME) return ( <> @@ -82,7 +76,7 @@ const KeyPage = () => { value={username} endAdornment={ - + From 6d4a8b4f64840320afff4a99519ddb25dfac73ef Mon Sep 17 00:00:00 2001 From: artur Date: Mon, 19 Feb 2024 11:01:34 +0300 Subject: [PATCH 15/16] Remove logos from signup modals, move signup hints to the top of modals, fix signup hints --- .../Modal/ModalImportKeys/ModalImportKeys.tsx | 11 ++++++----- src/components/Modal/ModalLogin/ModalLogin.tsx | 14 ++++++-------- src/components/Modal/ModalSignUp/ModalSignUp.tsx | 12 +++++------- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx b/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx index 9595b87..d8e4dda 100644 --- a/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx +++ b/src/components/Modal/ModalImportKeys/ModalImportKeys.tsx @@ -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 ( - - - - + + + Import key + + Bring your existing Nostr keys to Nsec.app + { }, [isModalOpened, cleanUpStates]) return ( - - - - + + + Login + + Sync keys from the cloud to this device + { - - Import your keys from another nsec.app instance - diff --git a/src/components/Modal/ModalSignUp/ModalSignUp.tsx b/src/components/Modal/ModalSignUp/ModalSignUp.tsx index b867d84..f3165fc 100644 --- a/src/components/Modal/ModalSignUp/ModalSignUp.tsx +++ b/src/components/Modal/ModalSignUp/ModalSignUp.tsx @@ -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' @@ -84,13 +83,15 @@ export const ModalSignUp = () => { }, [isModalOpened]) return ( - + - - + Sign up + + Generate new Nostr keys + { - - New keys will be generated for you - From 06fa8ffbd749ef1d0ce0b89a9c73f4bf4f10781e Mon Sep 17 00:00:00 2001 From: artur Date: Mon, 19 Feb 2024 14:10:15 +0300 Subject: [PATCH 16/16] Leave name empty if failed to assign at addKey --- .../Modal/ModalSignUp/ModalSignUp.tsx | 5 +++- src/modules/backend.ts | 24 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/components/Modal/ModalSignUp/ModalSignUp.tsx b/src/components/Modal/ModalSignUp/ModalSignUp.tsx index f3165fc..d9cff4c 100644 --- a/src/components/Modal/ModalSignUp/ModalSignUp.tsx +++ b/src/components/Modal/ModalSignUp/ModalSignUp.tsx @@ -60,7 +60,10 @@ export const ModalSignUp = () => { try { setIsLoading(true) const k: any = await swicCall('generateKey', name) - notify(`Account created for "${name}"`, 'success') + 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 diff --git a/src/modules/backend.ts b/src/modules/backend.ts index 3a03fdc..2bbcc1e 100644 --- a/src/modules/backend.ts +++ b/src/modules/backend.ts @@ -28,6 +28,7 @@ enum DECISION { export interface KeyInfo { npub: string nip05?: string + name?: string locked: boolean } @@ -366,7 +367,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() @@ -501,7 +502,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 } } @@ -637,6 +638,7 @@ export class NoauthBackend { return { npub: k.npub, nip05: k.nip05, + name: k.name, locked: this.isLocked(k.npub), } } @@ -677,11 +679,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) - await this.sendNameToServer(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() @@ -1155,7 +1162,12 @@ export class NoauthBackend { 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) + 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)