From 56df5d89d92daef7588b3353a416c59ea4cb08a2 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 29 Apr 2024 12:49:06 -0500 Subject: [PATCH] Load the Nostr signer before calling verify_credentials --- src/api/hooks/nostr/useSignerStream.ts | 17 +++++++++++++++ src/contexts/nostr-context.tsx | 29 +++++++++++++++++++++----- src/features/ui/index.tsx | 2 -- src/hooks/useAuthToken.ts | 24 +++++++++++++++++++++ src/init/soapbox-load.tsx | 10 ++++++--- 5 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 src/hooks/useAuthToken.ts diff --git a/src/api/hooks/nostr/useSignerStream.ts b/src/api/hooks/nostr/useSignerStream.ts index c36b49b0d..44aa2ce18 100644 --- a/src/api/hooks/nostr/useSignerStream.ts +++ b/src/api/hooks/nostr/useSignerStream.ts @@ -15,6 +15,11 @@ interface NostrConnectResponse { error?: string; } +let onOpen: () => void; +const open = new Promise((resolve) => { + onOpen = resolve; +}); + function useSignerStream() { const { relay, pubkey, signer } = useNostr(); @@ -143,6 +148,18 @@ function useSignerStream() { }; }, [relay, pubkey, signer]); + + useEffect(() => { + if (relay) { + if (relay.socket.readyState === WebSocket.OPEN) { + onOpen(); + } else { + relay.socket.addEventListener('open', onOpen, { once: true }); + } + } + }, [relay]); + + return { open }; } export { useSignerStream }; diff --git a/src/contexts/nostr-context.tsx b/src/contexts/nostr-context.tsx index f06cd01bb..2365020d7 100644 --- a/src/contexts/nostr-context.tsx +++ b/src/contexts/nostr-context.tsx @@ -1,12 +1,13 @@ -import { NRelay, NRelay1, NostrSigner } from '@soapbox/nspec'; +import { NRelay1, NostrSigner } from '@soapbox/nspec'; +import { getPublicKey, nip19 } from 'nostr-tools'; import React, { createContext, useContext, useState, useEffect, useMemo } from 'react'; import { NKeys } from 'soapbox/features/nostr/keys'; -import { useOwnAccount } from 'soapbox/hooks'; +import { useAuthToken } from 'soapbox/hooks/useAuthToken'; import { useInstance } from 'soapbox/hooks/useInstance'; interface NostrContextType { - relay?: NRelay; + relay?: NRelay1; pubkey?: string; signer?: NostrSigner; } @@ -21,11 +22,29 @@ export const NostrProvider: React.FC = ({ children }) => { const instance = useInstance(); const [relay, setRelay] = useState(); - const { account } = useOwnAccount(); + const token = useAuthToken(); const url = instance.nostr?.relay; const pubkey = instance.nostr?.pubkey; - const accountPubkey = account?.nostr.pubkey; + + let accountPubkey: string | undefined; + + try { + const result = nip19.decode(token!); + switch (result.type) { + case 'npub': + accountPubkey = result.data; + break; + case 'nsec': + accountPubkey = getPublicKey(result.data); + break; + case 'nprofile': + accountPubkey = result.data.pubkey; + break; + } + } catch (e) { + // Ignore + } const signer = useMemo( () => (accountPubkey ? NKeys.get(accountPubkey) : undefined) ?? window.nostr, diff --git a/src/features/ui/index.tsx b/src/features/ui/index.tsx index f31315c02..8759abe2f 100644 --- a/src/features/ui/index.tsx +++ b/src/features/ui/index.tsx @@ -13,7 +13,6 @@ import { fetchScheduledStatuses } from 'soapbox/actions/scheduled-statuses'; import { fetchSuggestionsForTimeline } from 'soapbox/actions/suggestions'; import { expandHomeTimeline } from 'soapbox/actions/timelines'; import { useUserStream } from 'soapbox/api/hooks'; -import { useSignerStream } from 'soapbox/api/hooks/nostr/useSignerStream'; import SidebarNavigation from 'soapbox/components/sidebar-navigation'; import ThumbNavigation from 'soapbox/components/thumb-navigation'; import { Layout } from 'soapbox/components/ui'; @@ -459,7 +458,6 @@ const UI: React.FC = ({ children }) => { }, []); useUserStream(); - useSignerStream(); // The user has logged in useEffect(() => { diff --git a/src/hooks/useAuthToken.ts b/src/hooks/useAuthToken.ts new file mode 100644 index 000000000..269d40fe4 --- /dev/null +++ b/src/hooks/useAuthToken.ts @@ -0,0 +1,24 @@ +import { selectAccount } from 'soapbox/selectors'; +import { RootState } from 'soapbox/store'; +import { getAuthUserId, getAuthUserUrl } from 'soapbox/utils/auth'; + +import { useAppSelector } from './useAppSelector'; + +export function useAuthToken(): string | undefined { + const getMeId = (state: RootState) => state.me || getAuthUserId(state); + + const getMeUrl = (state: RootState) => { + const accountId = getMeId(state); + if (accountId) { + return selectAccount(state, accountId)?.url || getAuthUserUrl(state); + } + }; + + const getMeToken = (state: RootState) => { + // Fallback for upgrading IDs to URLs + const accountUrl = getMeUrl(state) || state.auth.me; + return state.auth.users.get(accountUrl!)?.access_token; + }; + + return useAppSelector((state) => getMeToken(state)); +} \ No newline at end of file diff --git a/src/init/soapbox-load.tsx b/src/init/soapbox-load.tsx index cc9b883f1..8b2ed4ea1 100644 --- a/src/init/soapbox-load.tsx +++ b/src/init/soapbox-load.tsx @@ -4,6 +4,7 @@ import { IntlProvider } from 'react-intl'; import { fetchInstance } from 'soapbox/actions/instance'; import { fetchMe } from 'soapbox/actions/me'; import { loadSoapboxConfig } from 'soapbox/actions/soapbox'; +import { useSignerStream } from 'soapbox/api/hooks/nostr/useSignerStream'; import LoadingScreen from 'soapbox/components/loading-screen'; import { useAppSelector, @@ -14,13 +15,14 @@ import { import MESSAGES from 'soapbox/messages'; /** Load initial data from the backend */ -const loadInitial = () => { +const loadInitial = (open: Promise) => { // @ts-ignore return async(dispatch, getState) => { // Await for authenticated fetch + await dispatch(fetchInstance()); + await open; await dispatch(fetchMe()); // Await for feature detection - await dispatch(fetchInstance()); // Await for configuration await dispatch(loadSoapboxConfig()); }; @@ -43,6 +45,8 @@ const SoapboxLoad: React.FC = ({ children }) => { const [localeLoading, setLocaleLoading] = useState(true); const [isLoaded, setIsLoaded] = useState(false); + const { open } = useSignerStream(); + /** Whether to display a loading indicator. */ const showLoading = [ me === null, @@ -62,7 +66,7 @@ const SoapboxLoad: React.FC = ({ children }) => { // Load initial data from the API useEffect(() => { - dispatch(loadInitial()).then(() => { + dispatch(loadInitial(open)).then(() => { setIsLoaded(true); }).catch(() => { setIsLoaded(true);