diff options
author | Eric Bailey <git@esb.lol> | 2023-11-16 11:16:16 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-11-16 09:16:16 -0800 |
commit | a652b52b880c2967e5b70f6f7661891253e20150 (patch) | |
tree | e27cf4bba4a334229c53a41fc6a1fe77b772abb3 /src | |
parent | e6efeea7c07682c981998483bd49d7c01822911e (diff) | |
download | voidsky-a652b52b880c2967e5b70f6f7661891253e20150.tar.zst |
Refactor ChangeHandle modal (#1929)
* Refactor ChangeHandle to use new methods * Better telemetry * Remove unused logic * Remove caching * Add error message * Persist service changes, don't fall back on change handle
Diffstat (limited to 'src')
-rw-r--r-- | src/state/queries/handle.ts | 35 | ||||
-rw-r--r-- | src/state/queries/service.ts | 16 | ||||
-rw-r--r-- | src/state/session/index.tsx | 39 | ||||
-rw-r--r-- | src/view/com/modals/ChangeHandle.tsx | 165 |
4 files changed, 154 insertions, 101 deletions
diff --git a/src/state/queries/handle.ts b/src/state/queries/handle.ts index 97e9b2107..4c3296587 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 000000000..df12d6cbc --- /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 b8422553c..aa45c7bbc 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<ApiContext>({ }) 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 c03ebafda..1a259b85e 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<string>('') +export type Props = {onChanged: () => void} + +export function Component(props: Props) { + const {currentAccount} = useSession() + const { + isLoading, + data: serviceInfo, + error: serviceInfoError, + } = useServiceQuery() + + return isLoading || !currentAccount ? ( + <View style={{padding: 18}}> + <ActivityIndicator /> + </View> + ) : serviceInfoError || !serviceInfo ? ( + <ErrorMessage message={cleanError(serviceInfoError)} /> + ) : ( + <Inner + {...props} + currentAccount={currentAccount} + serviceInfo={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<string>('') - const [isProcessing, setProcessing] = useState<boolean>(false) - const [retryDescribeTrigger, setRetryDescribeTrigger] = React.useState<any>( - {}, - ) - const [serviceDescription, setServiceDescription] = React.useState< - ServiceDescription | undefined - >(undefined) - const [userDomain, setUserDomain] = React.useState<string>('') const [isCustom, setCustom] = React.useState<boolean>(false) const [handle, setHandle] = React.useState<string>('') const [canSave, setCanSave] = React.useState<boolean>(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}) { <Trans>Change Handle</Trans> </Text> <View style={styles.titleRight}> - {isProcessing ? ( + {isUpdateHandlePending ? ( <ActivityIndicator /> - ) : error && !serviceDescription ? ( - <TouchableOpacity - testID="retryConnectButton" - onPress={onPressRetryConnect} - accessibilityRole="button" - accessibilityLabel={_(msg`Retry change handle`)} - accessibilityHint={`Retries handle change to ${handle}`}> - <Text type="xl-bold" style={[pal.link, s.pr5]}> - Retry - </Text> - </TouchableOpacity> ) : canSave ? ( <TouchableOpacity onPress={onPressSave} @@ -194,8 +180,9 @@ export function Component({onChanged}: {onChanged: () => void}) { {isCustom ? ( <CustomHandleForm + currentAccount={currentAccount} handle={handle} - isProcessing={isProcessing} + isProcessing={isUpdateHandlePending} canSave={canSave} onToggleCustom={onToggleCustom} setHandle={setHandle} @@ -206,7 +193,7 @@ export function Component({onChanged}: {onChanged: () => void}) { <ProvidedHandleForm handle={handle} userDomain={userDomain} - isProcessing={isProcessing} + isProcessing={isUpdateHandlePending} onToggleCustom={onToggleCustom} setHandle={setHandle} setCanSave={setCanSave} @@ -297,6 +284,7 @@ function ProvidedHandleForm({ * The form for using a custom domain */ function CustomHandleForm({ + currentAccount, handle, canSave, isProcessing, @@ -305,6 +293,7 @@ function CustomHandleForm({ onPressSave, setCanSave, }: { + currentAccount: SessionAccount handle: string canSave: boolean isProcessing: boolean @@ -313,7 +302,6 @@ function CustomHandleForm({ onPressSave: () => 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<string>('') const [isDNSForm, setDNSForm] = React.useState<boolean>(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({ </Text> <View style={[styles.dnsValue]}> <Text type="mono" style={[styles.monoText, pal.text]}> - did={store.me.did} + did={currentAccount.did} </Text> </View> </View> @@ -472,7 +461,7 @@ function CustomHandleForm({ <View style={[styles.valueContainer, pal.btn]}> <View style={[styles.dnsValue]}> <Text type="mono" style={[styles.monoText, pal.text]}> - {store.me.did} + {currentAccount.did} </Text> </View> </View> |