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 7e55f95..fd72265 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,44 @@ 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 + // NOTE: when opened directly to this modal using authUrl, + // we might not have pending requests visible yet bcs we haven't + // loaded them yet, which means this modal will be closed with + // the logic below. So now if it's popup then we wait for SW + // and then wait a little more to give it time to fetch + // pending reqs from db. Same logic implemented in confirm-event. + + // FIXME move to a separate hook and reuse? + + useEffect(() => { + if (isModalOpened) { + if (isPopup) { + console.log("waiting for sw") + // wait for SW to start + swicWaitStarted().then(() => { + // give it some time to load the pending reqs etc + console.log("waiting for sw done") + setTimeout(() => setIsLoaded(true), 500) + }) + } else { + setIsLoaded(true) + } + } else { + setIsLoaded(false) + } + }, [isModalOpened, isPopup]) + + if (isLoaded) { + const isNpubExists = npub.trim().length && keys.some((key) => key.npub === npub) + // NOTE: app doesn't exist yet! + // const isAppNpubExists = appNpub.trim().length && apps.some((app) => app.appNpub === appNpub) + const isPendingReqIdExists = pendingReqId.trim().length && pending.some((p) => p.id === pendingReqId) + // console.log("pending", {isModalOpened, isPendingReqIdExists, isNpubExists, /*isAppNpubExists,*/ pendingReqId, pending}); + if (isModalOpened && (!isNpubExists /*|| !isAppNpubExists*/ || (pendingReqId && !isPendingReqIdExists))) { + if (isPopup) window.close() + else closeModalAfterRequest() + return null + } } const handleActionTypeChange = (_: any, value: ACTION_TYPE | null) => { @@ -179,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 22d6388..cc0f01f 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,25 +62,31 @@ export const ModalConfirmEvent: FC = ({ confirmEventReqs }, }) - // FIXME: when opened directly to this modal using authUrl, - // we might not have pending requests visible yet bcs we haven't - // loaded them yet, which means this modal will be closed with - // the login below. It's fine if only one app has sent pending - // requests atm, bcs the modal would re-appear as soon as we load - // the requests. But if there are several pending reqs from other - // apps then popup might show a different one! Which is very - // contrary to what user expects. So: - // - if isPopup - dont close the modal with logic below - // - show some 'loading' indicator until we've got some requests - // for the specified appNpub - // FIXME is the same logic valid for Connect modal? + useEffect(() => { + if (isModalOpened) { + if (isPopup) { + // wait for SW to start + swicWaitStarted().then(() => { + // give it some time to load the pending reqs etc + setTimeout(() => setIsLoaded(true), 500) + }) + } else { + setIsLoaded(true) + } + } else { + setIsLoaded(false) + } + }, [isModalOpened, isPopup]) - 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 + } } 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 7baf84c..7d6a4ec 100644 --- a/src/components/Modal/ModalInitial/ModalInitial.tsx +++ b/src/components/Modal/ModalInitial/ModalInitial.tsx @@ -1,14 +1,30 @@ +// 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 { Stack } from '@mui/material' +// import { AppLink } from '@/shared/AppLink/AppLink' export const ModalInitial = () => { const { getModalOpened, createHandleCloseReplace, handleOpen } = useModalSearchParams() const isModalOpened = getModalOpened(MODAL_PARAMS_KEYS.INITIAL) const handleCloseModal = createHandleCloseReplace(MODAL_PARAMS_KEYS.INITIAL) + // const [showAdvancedContent, setShowAdvancedContent] = useState(false) + + // const handleShowAdvanced = () => { + // setShowAdvancedContent(true) + // } + + // 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/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 }) } 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/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())