about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2023-11-16 11:16:16 -0600
committerGitHub <noreply@github.com>2023-11-16 09:16:16 -0800
commita652b52b880c2967e5b70f6f7661891253e20150 (patch)
treee27cf4bba4a334229c53a41fc6a1fe77b772abb3 /src
parente6efeea7c07682c981998483bd49d7c01822911e (diff)
downloadvoidsky-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.ts35
-rw-r--r--src/state/queries/service.ts16
-rw-r--r--src/state/session/index.tsx39
-rw-r--r--src/view/com/modals/ChangeHandle.tsx165
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>