migrate the contact queries and phone verification to the appview client

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-08-03 17:54:04 +03:00
parent 8a87a5fdea
commit 1a6bfd8a6a
5 changed files with 132 additions and 126 deletions
+20 -20
View File
@@ -2,7 +2,6 @@ import {useState} from 'react'
import {Keyboard, View} from 'react-native'
import {KeyboardAvoidingView} from 'react-native-keyboard-controller'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import {AppBskyContactStartPhoneVerification} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
@@ -14,8 +13,9 @@ import {
getDefaultCountry,
} from '#/lib/international-telephone-codes'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {matchXrpcError} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {OnboardingPosition} from '#/screens/Onboarding/Layout'
import {
android,
@@ -34,6 +34,7 @@ import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {useGeolocation} from '#/geolocation'
import {app} from '#/lexicons'
import {isFindContactsFeatureEnabled} from '../country-allowlist'
import {
constructFullPhoneNumber,
@@ -56,7 +57,7 @@ export function PhoneInput({
const {_} = useLingui()
const ax = useAnalytics()
const t = useTheme()
const agent = useAgent()
const client = useAppviewClient()
const location = useGeolocation()
const [countryCode, setCountryCode] = useState(
() => state.phoneCountryCode ?? getDefaultCountry(location),
@@ -78,7 +79,7 @@ export function PhoneInput({
phoneNumber: string
}) => {
// sends a onetime code to the user's phone number
await agent.app.bsky.contact.startPhoneVerification({
await client.call(app.bsky.contact.startPhoneVerification, {
phone: constructFullPhoneNumber(phoneCountryCode, phoneNumber),
})
},
@@ -102,23 +103,22 @@ export function PhoneInput({
msg`A network error occurred. Please check your internet connection`,
),
)
} else if (
err instanceof
AppBskyContactStartPhoneVerification.RateLimitExceededError
) {
setError(_(msg`Rate limit exceeded. Please try again later.`))
} else if (
err instanceof AppBskyContactStartPhoneVerification.InvalidPhoneError
) {
setError(
_(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
)
} else {
logger.error('Verify phone number failed', {safeMessage: err})
setError(_(msg`An error occurred. ${cleanError(err)}`))
return
}
switch (matchXrpcError(err, app.bsky.contact.startPhoneVerification)) {
case 'RateLimitExceeded':
setError(_(msg`Rate limit exceeded. Please try again later.`))
return
case 'InvalidPhone':
setError(
_(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
)
return
}
logger.error('Verify phone number failed', {safeMessage: err})
setError(_(msg`An error occurred. ${cleanError(err)}`))
},
})
@@ -1,9 +1,5 @@
import {useEffect, useMemo, useState} from 'react'
import {Text as NestedText, View} from 'react-native'
import {
AppBskyContactStartPhoneVerification,
AppBskyContactVerifyPhone,
} from '@atproto/api'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Trans} from '@lingui/react/macro'
@@ -11,8 +7,9 @@ import {useMutation} from '@tanstack/react-query'
import {clamp} from '#/lib/numbers'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {matchXrpcError} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {OnboardingPosition} from '#/screens/Onboarding/Layout'
import {atoms as a, useGutters, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
@@ -25,6 +22,7 @@ import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
import {OTPInput} from '../components/OTPInput'
import {constructFullPhoneNumber, prettyPhoneNumber} from '../phone-number'
import {type Action, type State, useOnPressBackButton} from '../state'
@@ -43,7 +41,7 @@ export function VerifyNumber({
const t = useTheme()
const {_} = useLingui()
const ax = useAnalytics()
const agent = useAgent()
const client = useAppviewClient()
const gutters = useGutters([0, 'wide'])
const [otpCode, setOtpCode] = useState('')
@@ -72,8 +70,11 @@ export function VerifyNumber({
isSuccess,
} = useMutation({
mutationFn: async (code: string) => {
const res = await agent.app.bsky.contact.verifyPhone({code, phone})
return res.data.token
const data = await client.call(app.bsky.contact.verifyPhone, {
code,
phone,
})
return data.token
},
onSuccess: async token => {
// let the success state show for a moment
@@ -99,44 +100,47 @@ export function VerifyNumber({
msg`A network error occurred. Please check your internet connection.`,
),
})
} else if (err instanceof AppBskyContactVerifyPhone.InvalidCodeError) {
setError({
retryable: true,
isResendError: true,
message: _(msg`This code is invalid. Resend to get a new code.`),
})
} else if (err instanceof AppBskyContactVerifyPhone.InvalidPhoneError) {
setError({
retryable: false,
isResendError: false,
message: _(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
})
} else if (
err instanceof AppBskyContactVerifyPhone.RateLimitExceededError
) {
setError({
retryable: true,
isResendError: false,
message: _(
msg`Too many attempts. Please wait a few minutes and try again.`,
),
})
} else {
logger.error('Verify phone number failed', {safeMessage: err})
setError({
retryable: true,
isResendError: false,
message: _(msg`An error occurred. ${cleanError(err)}`),
})
return
}
switch (matchXrpcError(err, app.bsky.contact.verifyPhone)) {
case 'InvalidCode':
setError({
retryable: true,
isResendError: true,
message: _(msg`This code is invalid. Resend to get a new code.`),
})
return
case 'InvalidPhone':
setError({
retryable: false,
isResendError: false,
message: _(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
})
return
case 'RateLimitExceeded':
setError({
retryable: true,
isResendError: false,
message: _(
msg`Too many attempts. Please wait a few minutes and try again.`,
),
})
return
}
logger.error('Verify phone number failed', {safeMessage: err})
setError({
retryable: true,
isResendError: false,
message: _(msg`An error occurred. ${cleanError(err)}`),
})
},
})
const {mutate: resendCode, isPending: isResendingCode} = useMutation({
mutationFn: async () => {
await agent.app.bsky.contact.startPhoneVerification({phone: phone})
await client.call(app.bsky.contact.startPhoneVerification, {phone: phone})
},
onSuccess: () => {
dispatch({type: 'RESEND_VERIFICATION_CODE'})
@@ -155,35 +159,34 @@ export function VerifyNumber({
msg`A network error occurred. Please check your internet connection.`,
),
})
} else if (
err instanceof AppBskyContactStartPhoneVerification.InvalidPhoneError
) {
setError({
retryable: false,
isResendError: true,
message: _(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
})
} else if (
err instanceof
AppBskyContactStartPhoneVerification.RateLimitExceededError
) {
setError({
retryable: true,
isResendError: true,
message: _(
msg`Too many codes sent. Please wait a few minutes and try again.`,
),
})
} else {
logger.error('Resend failed', {safeMessage: err})
setError({
retryable: true,
isResendError: true,
message: _(msg`An error occurred. ${cleanError(err)}`),
})
return
}
switch (matchXrpcError(err, app.bsky.contact.startPhoneVerification)) {
case 'InvalidPhone':
setError({
retryable: false,
isResendError: true,
message: _(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
})
return
case 'RateLimitExceeded':
setError({
retryable: true,
isResendError: true,
message: _(
msg`Too many codes sent. Please wait a few minutes and try again.`,
),
})
return
}
logger.error('Resend failed', {safeMessage: err})
setError({
retryable: true,
isResendError: true,
message: _(msg`An error occurred. ${cleanError(err)}`),
})
},
})
@@ -3,6 +3,7 @@ import {View} from 'react-native'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import * as SMS from 'expo-sms'
import {type ModerationOpts} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro'
@@ -20,7 +21,7 @@ import {
optimisticRemoveMatch,
useMatchesPassthroughQuery,
} from '#/state/queries/find-contacts'
import {useAgent, useSession} from '#/state/session'
import {useAgent, useAppviewClient, useSession} from '#/state/session'
import {List, type ListMethods} from '#/view/com/util/List'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {OnboardingPosition} from '#/screens/Onboarding/Layout'
@@ -41,6 +42,7 @@ import * as ProfileCard from '#/components/ProfileCard'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
import type * as bsky from '#/types/bsky'
import {InviteInfo} from '../components/InviteInfo'
import {type Action, type Contact, type Match, type State} from '../state'
@@ -90,6 +92,7 @@ export function ViewMatches({
const moderationOpts = useModerationOpts()
const queryClient = useQueryClient()
const agent = useAgent()
const client = useAppviewClient()
const insets = useSafeAreaInsets()
const listRef = useRef<ListMethods>(null)
@@ -218,7 +221,9 @@ export function ViewMatches({
const {mutate: dismissMatch} = useMutation({
mutationFn: async (did: string) => {
await agent.app.bsky.contact.dismissMatch({subject: did})
await client.call(app.bsky.contact.dismissMatch, {
subject: did as DidString,
})
},
onMutate: did => {
ax.metric('contacts:matches:dismiss', {entryPoint: context})
+15 -14
View File
@@ -1,11 +1,8 @@
import {useCallback, useEffect, useState} from 'react'
import {type ListRenderItemInfo, View} from 'react-native'
import * as Contacts from 'expo-contacts'
import {
type AppBskyContactDefs,
type AppBskyContactGetSyncStatus,
type ModerationOpts,
} from '@atproto/api'
import {type AppBskyContactDefs, type ModerationOpts} from '@atproto/api'
import {type DidString} from '@atproto/syntax'
import {msg} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {Plural, Trans} from '@lingui/react/macro'
@@ -32,7 +29,7 @@ import {
useContactsMatchesQuery,
useContactsSyncStatusQuery,
} from '#/state/queries/find-contacts'
import {useAgent, useSession} from '#/state/session'
import {useAgent, useAppviewClient, useSession} from '#/state/session'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {List} from '#/view/com/util/List'
import {atoms as a, tokens, useGutters, useTheme} from '#/alf'
@@ -52,6 +49,7 @@ import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {IS_NATIVE} from '#/env'
import {InviteFriendsDialog} from '#/features/inviteFriends'
import {app} from '#/lexicons'
import type * as bsky from '#/types/bsky'
import {bulkWriteFollows} from '../Onboarding/util'
@@ -194,7 +192,7 @@ function SyncStatus({
refetchStatus: () => Promise<any>
}) {
const ax = useAnalytics()
const agent = useAgent()
const client = useAppviewClient()
const queryClient = useQueryClient()
const {_} = useLingui()
const moderationOpts = useModerationOpts()
@@ -219,7 +217,9 @@ function SyncStatus({
const {mutate: dismissMatch} = useMutation({
mutationFn: async (did: string) => {
await agent.app.bsky.contact.dismissMatch({subject: did})
await client.call(app.bsky.contact.dismissMatch, {
subject: did as DidString,
})
},
onMutate: async (did: string) => {
ax.metric('contacts:settings:dismiss', {})
@@ -371,6 +371,7 @@ function StatusHeader({
const {_} = useLingui()
const ax = useAnalytics()
const agent = useAgent()
const client = useAppviewClient()
const queryClient = useQueryClient()
const {currentAccount} = useSession()
@@ -384,12 +385,12 @@ function StatusHeader({
let cursor: string | undefined
do {
const page = await agent.app.bsky.contact.getMatches({
const page = await client.call(app.bsky.contact.getMatches, {
limit: 100,
cursor,
})
cursor = page.data.cursor
for (const profile of page.data.matches) {
cursor = page.cursor
for (const profile of page.matches) {
if (
profile.did !== currentAccount?.did &&
!isBlockedOrBlocking(profile) &&
@@ -487,17 +488,17 @@ function StatusFooter({syncedAt}: {syncedAt: string}) {
const {_, i18n} = useLingui()
const t = useTheme()
const ax = useAnalytics()
const agent = useAgent()
const client = useAppviewClient()
const queryClient = useQueryClient()
const {mutate: removeData, isPending} = useMutation({
mutationFn: async () => {
await agent.app.bsky.contact.removeData({})
await client.call(app.bsky.contact.removeData, {})
},
onMutate: () => ax.metric('contacts:settings:removeData', {}),
onSuccess: () => {
Toast.show(_(msg`Contacts removed`))
queryClient.setQueryData<AppBskyContactGetSyncStatus.OutputSchema>(
queryClient.setQueryData<app.bsky.contact.getSyncStatus.$OutputBody>(
findContactsStatusQueryKey,
{syncStatus: undefined},
)
+19 -22
View File
@@ -1,4 +1,3 @@
import {type AppBskyContactGetMatches} from '@atproto/api'
import {
type InfiniteData,
type QueryClient,
@@ -6,8 +5,9 @@ import {
useQuery,
} from '@tanstack/react-query'
import {useAgent} from '#/state/session'
import {useAppviewClient} from '#/state/session'
import {type Match} from '#/components/contacts/state'
import {app} from '#/lexicons'
import type * as bsky from '#/types/bsky'
import {STALE} from '.'
@@ -15,13 +15,12 @@ const RQ_KEY_ROOT = 'find-contacts'
export const findContactsStatusQueryKey = [RQ_KEY_ROOT, 'sync-status']
export function useContactsSyncStatusQuery() {
const agent = useAgent()
const client = useAppviewClient()
return useQuery({
queryKey: findContactsStatusQueryKey,
queryFn: async () => {
const status = await agent.app.bsky.contact.getSyncStatus()
return status.data
return await client.call(app.bsky.contact.getSyncStatus, {})
},
staleTime: STALE.SECONDS.THIRTY,
})
@@ -30,15 +29,14 @@ export function useContactsSyncStatusQuery() {
export const findContactsGetMatchesQueryKey = [RQ_KEY_ROOT, 'matches']
export function useContactsMatchesQuery() {
const agent = useAgent()
const client = useAppviewClient()
return useInfiniteQuery({
queryKey: findContactsGetMatchesQueryKey,
queryFn: async ({pageParam}) => {
const matches = await agent.app.bsky.contact.getMatches({
return await client.call(app.bsky.contact.getMatches, {
cursor: pageParam,
})
return matches.data
},
initialPageParam: undefined as string | undefined,
getNextPageParam: lastPage => lastPage.cursor,
@@ -47,20 +45,19 @@ export function useContactsMatchesQuery() {
}
export function optimisticRemoveMatch(queryClient: QueryClient, did: string) {
queryClient.setQueryData<InfiniteData<AppBskyContactGetMatches.OutputSchema>>(
findContactsGetMatchesQueryKey,
old => {
if (!old) return old
queryClient.setQueryData<
InfiniteData<app.bsky.contact.getMatches.$OutputBody>
>(findContactsGetMatchesQueryKey, old => {
if (!old) return old
return {
...old,
pages: old.pages.map(page => ({
...page,
matches: page.matches.filter(match => match.did !== did),
})),
}
},
)
return {
...old,
pages: old.pages.map(page => ({
...page,
matches: page.matches.filter(match => match.did !== did),
})),
}
})
}
export const findContactsMatchesPassthroughQueryKey = (dids: string[]) => [
@@ -95,7 +92,7 @@ export function* findAllProfilesInQueryData(
did: string,
): Generator<bsky.profile.AnyProfileView, void> {
const queryDatas = queryClient.getQueriesData<
InfiniteData<AppBskyContactGetMatches.OutputSchema>
InfiniteData<app.bsky.contact.getMatches.$OutputBody>
>({
queryKey: findContactsGetMatchesQueryKey,
})