diff --git a/src/state/queries/handle.ts b/src/state/queries/handle.ts index 97e9b210..4c329658 100644 --- a/src/state/queries/handle.ts +++ b/src/state/queries/handle.ts @@ -1,9 +1,10 @@ import React from 'react' -import {useQueryClient} from '@tanstack/react-query' +import {useQueryClient, useMutation} from '@tanstack/react-query' import {useSession} from '#/state/session' const fetchHandleQueryKey = (handleOrDid: string) => ['handle', handleOrDid] +const fetchDidQueryKey = (handleOrDid: string) => ['did', handleOrDid] export function useFetchHandle() { const {agent} = useSession() @@ -23,3 +24,35 @@ export function useFetchHandle() { [agent, queryClient], ) } + +export function useUpdateHandleMutation() { + const {agent} = useSession() + + return useMutation({ + mutationFn: async ({handle}: {handle: string}) => { + await agent.updateHandle({handle}) + }, + }) +} + +export function useFetchDid() { + const {agent} = useSession() + const queryClient = useQueryClient() + + return React.useCallback( + async (handleOrDid: string) => { + return queryClient.fetchQuery({ + queryKey: fetchDidQueryKey(handleOrDid), + queryFn: async () => { + let identifier = handleOrDid + if (!identifier.startsWith('did:')) { + const res = await agent.resolveHandle({handle: identifier}) + identifier = res.data.did + } + return identifier + }, + }) + }, + [agent, queryClient], + ) +} diff --git a/src/state/queries/service.ts b/src/state/queries/service.ts new file mode 100644 index 00000000..df12d6cb --- /dev/null +++ b/src/state/queries/service.ts @@ -0,0 +1,16 @@ +import {useQuery} from '@tanstack/react-query' + +import {useSession} from '#/state/session' + +export const RQKEY = (serviceUrl: string) => ['service', serviceUrl] + +export function useServiceQuery() { + const {agent} = useSession() + return useQuery({ + queryKey: RQKEY(agent.service.toString()), + queryFn: async () => { + const res = await agent.com.atproto.server.describeServer() + return res.data + }, + }) +} diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index b8422553..aa45c7bb 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -14,8 +14,8 @@ export type SessionState = { agent: BskyAgent isInitialLoad: boolean isSwitchingAccounts: boolean - accounts: persisted.PersistedAccount[] - currentAccount: persisted.PersistedAccount | undefined + accounts: SessionAccount[] + currentAccount: SessionAccount | undefined } export type StateContext = SessionState & { hasSession: boolean @@ -70,15 +70,15 @@ const ApiContext = React.createContext({ }) function createPersistSessionHandler( - account: persisted.PersistedAccount, + account: SessionAccount, persistSessionCallback: (props: { expired: boolean - refreshedAccount: persisted.PersistedAccount + refreshedAccount: SessionAccount }) => void, ): AtpPersistSessionHandler { return function persistSession(event, session) { const expired = !(event === 'create' || event === 'update') - const refreshedAccount = { + const refreshedAccount: SessionAccount = { service: account.service, did: session?.did || account.did, handle: session?.handle || account.handle, @@ -128,7 +128,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ) const upsertAccount = React.useCallback( - (account: persisted.PersistedAccount, expired = false) => { + (account: SessionAccount, expired = false) => { setStateAndPersist(s => { return { ...s, @@ -164,8 +164,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { throw new Error(`session: createAccount failed to establish a session`) } - const account: persisted.PersistedAccount = { - service, + const account: SessionAccount = { + service: agent.service.toString(), did: agent.session.did, handle: agent.session.handle, email: agent.session.email!, // TODO this is always defined? @@ -215,8 +215,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { throw new Error(`session: login failed to establish a session`) } - const account: persisted.PersistedAccount = { - service, + const account: SessionAccount = { + service: agent.service.toString(), did: agent.session.did, handle: agent.session.handle, email: agent.session.email!, // TODO this is always defined? @@ -293,9 +293,24 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }), ) + if (!agent.session) { + throw new Error(`session: initSession failed to establish a session`) + } + + // ensure changes in handle/email etc are captured on reload + const freshAccount: SessionAccount = { + service: agent.service.toString(), + did: agent.session.did, + handle: agent.session.handle, + email: agent.session.email!, // TODO this is always defined? + emailConfirmed: agent.session.emailConfirmed || false, + refreshJwt: agent.session.refreshJwt, + accessJwt: agent.session.accessJwt, + } + setState(s => ({...s, agent})) - upsertAccount(account) - emitSessionLoaded(account, agent) + upsertAccount(freshAccount) + emitSessionLoaded(freshAccount, agent) }, [upsertAccount], ) diff --git a/src/view/com/modals/ChangeHandle.tsx b/src/view/com/modals/ChangeHandle.tsx index c03ebafd..1a259b85 100644 --- a/src/view/com/modals/ChangeHandle.tsx +++ b/src/view/com/modals/ChangeHandle.tsx @@ -1,5 +1,6 @@ import React, {useState} from 'react' import Clipboard from '@react-native-clipboard/clipboard' +import {ComAtprotoServerDescribeServer} from '@atproto/api' import * as Toast from '../util/Toast' import { ActivityIndicator, @@ -13,8 +14,6 @@ import {Text} from '../util/text/Text' import {Button} from '../util/forms/Button' import {SelectableBtn} from '../util/forms/SelectableBtn' import {ErrorMessage} from '../util/error/ErrorMessage' -import {useStores} from 'state/index' -import {ServiceDescription} from 'state/models/session' import {s} from 'lib/styles' import {createFullHandle, makeValidHandle} from 'lib/strings/handles' import {usePalette} from 'lib/hooks/usePalette' @@ -25,77 +24,66 @@ import {logger} from '#/logger' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useModalControls} from '#/state/modals' +import {useServiceQuery} from '#/state/queries/service' +import {useUpdateHandleMutation, useFetchDid} from '#/state/queries/handle' +import {useSession, useSessionApi, SessionAccount} from '#/state/session' export const snapPoints = ['100%'] -export function Component({onChanged}: {onChanged: () => void}) { - const store = useStores() - const [error, setError] = useState('') +export type Props = {onChanged: () => void} + +export function Component(props: Props) { + const {currentAccount} = useSession() + const { + isLoading, + data: serviceInfo, + error: serviceInfoError, + } = useServiceQuery() + + return isLoading || !currentAccount ? ( + + + + ) : serviceInfoError || !serviceInfo ? ( + + ) : ( + + ) +} + +export function Inner({ + currentAccount, + serviceInfo, + onChanged, +}: Props & { + currentAccount: SessionAccount + serviceInfo: ComAtprotoServerDescribeServer.OutputSchema +}) { + const {_} = useLingui() const pal = usePalette('default') const {track} = useAnalytics() - const {_} = useLingui() + const {updateCurrentAccount} = useSessionApi() const {closeModal} = useModalControls() + const {mutateAsync: updateHandle, isPending: isUpdateHandlePending} = + useUpdateHandleMutation() + + const [error, setError] = useState('') - const [isProcessing, setProcessing] = useState(false) - const [retryDescribeTrigger, setRetryDescribeTrigger] = React.useState( - {}, - ) - const [serviceDescription, setServiceDescription] = React.useState< - ServiceDescription | undefined - >(undefined) - const [userDomain, setUserDomain] = React.useState('') const [isCustom, setCustom] = React.useState(false) const [handle, setHandle] = React.useState('') const [canSave, setCanSave] = React.useState(false) - // init - // = - React.useEffect(() => { - let aborted = false - setError('') - setServiceDescription(undefined) - setProcessing(true) - - // load the service description so we can properly provision handles - store.session.describeService(String(store.agent.service)).then( - desc => { - if (aborted) { - return - } - setServiceDescription(desc) - setUserDomain(desc.availableUserDomains[0]) - setProcessing(false) - }, - err => { - if (aborted) { - return - } - setProcessing(false) - logger.warn( - `Failed to fetch service description for ${String( - store.agent.service, - )}`, - {error: err}, - ) - setError( - 'Unable to contact your service. Please check your Internet connection.', - ) - }, - ) - return () => { - aborted = true - } - }, [store.agent.service, store.session, retryDescribeTrigger]) + const userDomain = serviceInfo.availableUserDomains?.[0] // events // = const onPressCancel = React.useCallback(() => { closeModal() }, [closeModal]) - const onPressRetryConnect = React.useCallback( - () => setRetryDescribeTrigger({}), - [setRetryDescribeTrigger], - ) const onToggleCustom = React.useCallback(() => { // toggle between a provided domain vs a custom one setHandle('') @@ -106,13 +94,22 @@ export function Component({onChanged}: {onChanged: () => void}) { ) }, [setCustom, isCustom, track]) const onPressSave = React.useCallback(async () => { - setError('') - setProcessing(true) + if (!userDomain) { + logger.error(`ChangeHandle: userDomain is undefined`, { + service: serviceInfo, + }) + setError(`The service you've selected has no domains configured.`) + return + } + try { track('EditHandle:SetNewHandle') const newHandle = isCustom ? handle : createFullHandle(handle, userDomain) logger.debug(`Updating handle to ${newHandle}`) - await store.agent.updateHandle({ + await updateHandle({ + handle: newHandle, + }) + updateCurrentAccount({ handle: newHandle, }) closeModal() @@ -121,18 +118,18 @@ export function Component({onChanged}: {onChanged: () => void}) { setError(cleanError(err)) logger.error('Failed to update handle', {handle, error: err}) } finally { - setProcessing(false) } }, [ setError, - setProcessing, handle, userDomain, - store, isCustom, onChanged, track, closeModal, + updateCurrentAccount, + updateHandle, + serviceInfo, ]) // rendering @@ -159,19 +156,8 @@ export function Component({onChanged}: {onChanged: () => void}) { Change Handle - {isProcessing ? ( + {isUpdateHandlePending ? ( - ) : error && !serviceDescription ? ( - - - Retry - - ) : canSave ? ( void}) { {isCustom ? ( void}) { void setCanSave: (v: boolean) => void }) { - const store = useStores() const pal = usePalette('default') const palSecondary = usePalette('secondary') const palError = usePalette('error') @@ -322,12 +310,15 @@ function CustomHandleForm({ const [isVerifying, setIsVerifying] = React.useState(false) const [error, setError] = React.useState('') const [isDNSForm, setDNSForm] = React.useState(true) + const fetchDid = useFetchDid() // events // = const onPressCopy = React.useCallback(() => { - Clipboard.setString(isDNSForm ? `did=${store.me.did}` : store.me.did) + Clipboard.setString( + isDNSForm ? `did=${currentAccount.did}` : currentAccount.did, + ) Toast.show('Copied to clipboard') - }, [store.me.did, isDNSForm]) + }, [currentAccount, isDNSForm]) const onChangeHandle = React.useCallback( (v: string) => { setHandle(v) @@ -342,13 +333,11 @@ function CustomHandleForm({ try { setIsVerifying(true) setError('') - const res = await store.agent.com.atproto.identity.resolveHandle({ - handle, - }) - if (res.data.did === store.me.did) { + const did = await fetchDid(handle) + if (did === currentAccount.did) { setCanSave(true) } else { - setError(`Incorrect DID returned (got ${res.data.did})`) + setError(`Incorrect DID returned (got ${did})`) } } catch (err: any) { setError(cleanError(err)) @@ -358,13 +347,13 @@ function CustomHandleForm({ } }, [ handle, - store.me.did, + currentAccount, setIsVerifying, setCanSave, setError, canSave, onPressSave, - store.agent, + fetchDid, ]) // rendering @@ -442,7 +431,7 @@ function CustomHandleForm({ - did={store.me.did} + did={currentAccount.did} @@ -472,7 +461,7 @@ function CustomHandleForm({ - {store.me.did} + {currentAccount.did}