From 1a6bfd8a6a42e2e111ff89397469a9df2d1e4527 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 3 Aug 2026 17:54:04 +0300 Subject: [PATCH] migrate the contact queries and phone verification to the appview client Co-Authored-By: Claude Fable 5 --- .../contacts/screens/PhoneInput.tsx | 40 ++--- .../contacts/screens/VerifyNumber.tsx | 139 +++++++++--------- .../contacts/screens/ViewMatches.tsx | 9 +- src/screens/Settings/FindContactsSettings.tsx | 29 ++-- src/state/queries/find-contacts.ts | 41 +++--- 5 files changed, 132 insertions(+), 126 deletions(-) diff --git a/src/components/contacts/screens/PhoneInput.tsx b/src/components/contacts/screens/PhoneInput.tsx index 6904789a6e..95d63055c0 100644 --- a/src/components/contacts/screens/PhoneInput.tsx +++ b/src/components/contacts/screens/PhoneInput.tsx @@ -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)}`)) }, }) diff --git a/src/components/contacts/screens/VerifyNumber.tsx b/src/components/contacts/screens/VerifyNumber.tsx index f9d062f691..0bd5369397 100644 --- a/src/components/contacts/screens/VerifyNumber.tsx +++ b/src/components/contacts/screens/VerifyNumber.tsx @@ -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)}`), + }) }, }) diff --git a/src/components/contacts/screens/ViewMatches.tsx b/src/components/contacts/screens/ViewMatches.tsx index 3543c604f5..dab3a79c6b 100644 --- a/src/components/contacts/screens/ViewMatches.tsx +++ b/src/components/contacts/screens/ViewMatches.tsx @@ -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(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}) diff --git a/src/screens/Settings/FindContactsSettings.tsx b/src/screens/Settings/FindContactsSettings.tsx index 57f1edde90..6104fd12b7 100644 --- a/src/screens/Settings/FindContactsSettings.tsx +++ b/src/screens/Settings/FindContactsSettings.tsx @@ -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 }) { 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( + queryClient.setQueryData( findContactsStatusQueryKey, {syncStatus: undefined}, ) diff --git a/src/state/queries/find-contacts.ts b/src/state/queries/find-contacts.ts index b1eb6c9c5e..e462e6a6c1 100644 --- a/src/state/queries/find-contacts.ts +++ b/src/state/queries/find-contacts.ts @@ -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>( - findContactsGetMatchesQueryKey, - old => { - if (!old) return old + queryClient.setQueryData< + InfiniteData + >(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 { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: findContactsGetMatchesQueryKey, })