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 handlezio/stable
parent
e6efeea7c0
commit
a652b52b88
|
@ -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],
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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],
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue