diff --git a/src/components/contacts/screens/GetContacts.tsx b/src/components/contacts/screens/GetContacts.tsx index d84e0d1ab0..de5d6cf98b 100644 --- a/src/components/contacts/screens/GetContacts.tsx +++ b/src/components/contacts/screens/GetContacts.tsx @@ -2,24 +2,21 @@ import {useContext} from 'react' import {Alert, View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import * as Contacts from 'expo-contacts' -import type AtpAgent from '@atproto/api' -import { - type AppBskyActorProfile, - AppBskyContactImportContacts, - type Un$Typed, -} from '@atproto/api' +import {type Un$Typed} from '@atproto/lex' import {type Client} from '@atproto/lex' +import {toDatetimeString} from '@atproto/syntax' +import {upsertProfile} from '@bsky.app/sdk' import {msg, t} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useMutation, useQueryClient} from '@tanstack/react-query' import {uploadBlob} from '#/lib/api' -import {toLegacyBlobRef} from '#/lib/api/legacy-blob' import {cleanError, isNetworkError} from '#/lib/strings/errors' +import {matchXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' import {findContactsStatusQueryKey} from '#/state/queries/find-contacts' -import {useAgent, usePdsClient} from '#/state/session' +import {useAppviewClient, usePdsClient} from '#/state/session' import { Context as OnboardingContext, type OnboardingAction, @@ -32,6 +29,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 { contactsWithPhoneNumbersOnly, filterMatchedNumbers, @@ -56,8 +54,8 @@ export function GetContacts({ }) { const {_} = useLingui() const ax = useAnalytics() - const agent = useAgent() const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() const insets = useSafeAreaInsets() const gutters = useGutters([0, 'wide']) const queryClient = useQueryClient() @@ -75,7 +73,7 @@ export function GetContacts({ */ if (context === 'Onboarding' && maybeOnboardingContext) { try { - await createProfileRecord(agent, pdsClient, maybeOnboardingContext) + await createProfileRecord(pdsClient, maybeOnboardingContext) } catch (error) { logger.debug('Error creating profile record:', {safeMessage: error}) } @@ -88,13 +86,13 @@ export function GetContacts({ ) if (phoneNumbers.length > 0) { - const res = await agent.app.bsky.contact.importContacts({ + const res = await appviewClient.call(app.bsky.contact.importContacts, { token: state.token, contacts: phoneNumbers.slice(0, MAX_UPLOAD_COUNT), }) return { - matches: res.data.matchesAndContactIndexes, + matches: res.matchesAndContactIndexes, indexToContactId, } } else { @@ -151,29 +149,30 @@ export function GetContacts({ ), {type: 'error'}, ) - } else if ( - err instanceof AppBskyContactImportContacts.TooManyContactsError - ) { - Toast.show( - _( - msg`Too many contacts - you've exceeded the number of contacts you can import to find your friends`, - ), - {type: 'error'}, - ) - } else if ( - err instanceof AppBskyContactImportContacts.InvalidTokenError - ) { - Toast.show( - _( - msg`Could not upload contacts. You need to re-verify your phone number to proceed`, - ), - {type: 'error'}, - ) - } else { - logger.error('Error uploading contacts', {safeMessage: err}) - Toast.show(_(msg`Could not upload contacts. ${cleanError(err)}`), { - type: 'error', - }) + return + } + switch (matchXrpcError(err, app.bsky.contact.importContacts)) { + case 'TooManyContacts': + Toast.show( + _( + msg`Too many contacts - you've exceeded the number of contacts you can import to find your friends`, + ), + {type: 'error'}, + ) + break + case 'InvalidToken': + Toast.show( + _( + msg`Could not upload contacts. You need to re-verify your phone number to proceed`, + ), + {type: 'error'}, + ) + break + default: + logger.error('Error uploading contacts', {safeMessage: err}) + Toast.show(_(msg`Could not upload contacts. ${cleanError(err)}`), { + type: 'error', + }) } }, }) @@ -328,7 +327,6 @@ function showPermissionDeniedAlert() { * Copied from `#/screens/Onboarding/StepFinished/index.tsx` */ async function createProfileRecord( - agent: AtpAgent, pdsClient: Client, onboardingContext: { state: OnboardingState @@ -342,19 +340,21 @@ async function createProfileRecord( ? uploadBlob(pdsClient, imageUri, imageMime) : undefined - await agent.upsertProfile(async existing => { - let next: Un$Typed = existing ?? {} + await pdsClient.call(upsertProfile, async existing => { + let next: Un$Typed = existing ?? {} if (blobPromise) { const res = await blobPromise if (res.blob) { - next.avatar = toLegacyBlobRef(res.blob) + next.avatar = res.blob } } next.displayName = '' - next.createdAt = new Date().toISOString() + if (!next.createdAt) { + next.createdAt = toDatetimeString(new Date()) + } return next }) } diff --git a/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx b/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx index ceb07fb0e0..e6d14b2405 100644 --- a/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx +++ b/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx @@ -6,7 +6,6 @@ import { type AtIdentifierString, AtUri, type AtUriString, - type DidString, toDatetimeString, } from '@atproto/syntax' import {msg} from '@lingui/core/macro' @@ -86,8 +85,7 @@ export function CreateListFromStarterPackDialog({ items.map(item => { const listitemRecord: $Typed = { $type: 'app.bsky.graph.listitem', - // the list view is still legacy-typed, so its strings are unbranded - subject: item.subject.did as DidString, + subject: item.subject.did, list: listUri as AtUriString, createdAt: toDatetimeString(new Date()), } diff --git a/src/lib/api/legacy-blob.ts b/src/lib/api/legacy-blob.ts deleted file mode 100644 index b60acadfe4..0000000000 --- a/src/lib/api/legacy-blob.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {BlobRef} from '@atproto/api' -import {type BlobRef as LexBlobRef} from '@atproto/lex' - -/** - * Bridge a lex blob ref (the plain-JSON `{$type: 'blob', ref, mimeType, size}` - * that {@link uploadBlob} now returns) back to the legacy `BlobRef` class - * instance. - * - * Only needed where a blob is handed to a legacy agent write: the legacy - * lexicon blob validator checks `value instanceof BlobRef`, so a plain lex - * blob fails validation, and the legacy serializer would put the wrong shape - * on the wire. Drop each call as its write moves to the lex client. - */ -export function toLegacyBlobRef(blob: LexBlobRef): BlobRef { - return BlobRef.fromJsonRef(blob as Parameters[0]) -} diff --git a/src/screens/Onboarding/StepFinished/index.tsx b/src/screens/Onboarding/StepFinished/index.tsx index 706a160003..9eb22efc70 100644 --- a/src/screens/Onboarding/StepFinished/index.tsx +++ b/src/screens/Onboarding/StepFinished/index.tsx @@ -1,21 +1,19 @@ import {useCallback, useState} from 'react' import {View} from 'react-native' -import { - type AppBskyActorDefs, - type AppBskyActorProfile, - type AppBskyGraphDefs, - AppBskyGraphStarterpack, - type Un$Typed, -} from '@atproto/api' import {TID} from '@atproto/common-web' -import {type AtUriString} from '@atproto/syntax' +import {type Un$Typed} from '@atproto/lex' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' +import { + overwriteSavedFeeds, + setInterestsPref, + upsertProfile, +} from '@bsky.app/sdk' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {uploadBlob} from '#/lib/api' -import {toLegacyBlobRef} from '#/lib/api/legacy-blob' import { BSKY_APP_ACCOUNT_DID, DISCOVER_SAVED_FEED, @@ -28,7 +26,7 @@ import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-p import {getAllListMembers} from '#/state/queries/list-members' import {preferencesQueryKey} from '#/state/queries/preferences' import {RQKEY as profileRQKey} from '#/state/queries/profile' -import {useAgent, useAppviewClient, usePdsClient} from '#/state/session' +import {useAppviewClient, usePdsClient} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import { useActiveStarterPack, @@ -50,6 +48,7 @@ import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/ico import {Loader} from '#/components/Loader' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' import {ValuePropositionPager} from './ValuePropositionPager' @@ -59,9 +58,8 @@ export function StepFinished() { const onboardDispatch = useOnboardingDispatch() const [saving, setSaving] = useState(false) const queryClient = useQueryClient() - const agent = useAgent() - const appviewClient = useAppviewClient() const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() const requestNotificationsPermission = useRequestNotificationsPermission() const activeStarterPack = useActiveStarterPack() const setActiveStarterPack = useSetActiveStarterPack() @@ -71,15 +69,15 @@ export function StepFinished() { const finishOnboarding = useCallback(async () => { setSaving(true) - let starterPack: AppBskyGraphDefs.StarterPackView | undefined - let listItems: AppBskyGraphDefs.ListItemView[] | undefined + let starterPack: app.bsky.graph.defs.StarterPackView | undefined + let listItems: app.bsky.graph.defs.ListItemView[] | undefined if (activeStarterPack?.uri) { try { - const spRes = await agent.app.bsky.graph.getStarterPack({ - starterPack: activeStarterPack.uri, + const spRes = await appviewClient.call(app.bsky.graph.getStarterPack, { + starterPack: activeStarterPack.uri as AtUriString, }) - starterPack = spRes.data.starterPack + starterPack = spRes.starterPack } catch (e) { logger.error('Failed to fetch starter pack', {safeMessage: e}) // don't tell the user, just get them through onboarding. @@ -109,19 +107,15 @@ export function StepFinished() { appviewClient, [BSKY_APP_ACCOUNT_DID, ...(listItems?.map(i => i.subject.did) ?? [])], starterPack - ? // the starter pack view is still legacy-typed - { - uri: starterPack.uri as AtUriString, - cid: starterPack.cid, - } + ? {uri: starterPack.uri, cid: starterPack.cid} : undefined, ), (async () => { // Interests need to get saved first, then we can write the feeds to prefs - await agent.setInterestsPref({tags: selectedInterests}) + await pdsClient.call(setInterestsPref, {tags: selectedInterests}) // Default feeds that every user should have pinned when landing in the app - const feedsToSave: AppBskyActorDefs.SavedFeed[] = [ + const feedsToSave: app.bsky.actor.defs.SavedFeed[] = [ { ...DISCOVER_SAVED_FEED, id: TID.nextStr(), @@ -140,7 +134,7 @@ export function StepFinished() { if (starterPack && starterPack.feeds?.length) { feedsToSave.push( ...starterPack.feeds.map(f => ({ - type: 'feed', + type: 'feed' as const, value: f.uri, pinned: true, id: TID.nextStr(), @@ -148,7 +142,7 @@ export function StepFinished() { ) } - await agent.overwriteSavedFeeds(feedsToSave) + await pdsClient.call(overwriteSavedFeeds, feedsToSave) })(), (async () => { const {imageUri, imageMime} = profileStepResults @@ -157,13 +151,13 @@ export function StepFinished() { ? uploadBlob(pdsClient, imageUri, imageMime) : undefined - await agent.upsertProfile(async existing => { - let next: Un$Typed = existing ?? {} + await pdsClient.call(upsertProfile, async existing => { + let next: Un$Typed = existing ?? {} if (blobPromise) { const res = await blobPromise if (res.blob) { - next.avatar = toLegacyBlobRef(res.blob) + next.avatar = res.blob } } @@ -177,7 +171,7 @@ export function StepFinished() { next.displayName = '' if (!next.createdAt) { - next.createdAt = new Date().toISOString() + next.createdAt = toDatetimeString(new Date()) } return next }) @@ -204,7 +198,7 @@ export function StepFinished() { queryKey: preferencesQueryKey, }), queryClient.invalidateQueries({ - queryKey: profileRQKey(agent.session?.did ?? ''), + queryKey: profileRQKey(pdsClient.did ?? ''), }), ]).catch(e => { logger.error(e) @@ -221,10 +215,7 @@ export function StepFinished() { usedStarterPack: Boolean(starterPack), starterPackName: starterPack && - bsky.dangerousIsType( - starterPack.record, - AppBskyGraphStarterpack.isRecord, - ) + bsky.isType(app.bsky.graph.starterpack, starterPack.record) ? starterPack.record.name : undefined, starterPackCreator: starterPack?.creator.did, @@ -242,9 +233,8 @@ export function StepFinished() { }, [ ax, queryClient, - agent, - appviewClient, pdsClient, + appviewClient, dispatch, onboardDispatch, activeStarterPack, diff --git a/src/state/queries/list-members.ts b/src/state/queries/list-members.ts index 6e70ff4027..d466b92484 100644 --- a/src/state/queries/list-members.ts +++ b/src/state/queries/list-members.ts @@ -61,7 +61,7 @@ export function useAllListMembersQuery(uri?: string) { export async function getAllListMembers(client: Client, uri: string) { let hasMore = true let cursor: string | undefined - const listItems: AppBskyGraphDefs.ListItemView[] = [] + const listItems: app.bsky.graph.defs.ListItemView[] = [] // We want to cap this at 6 pages, just for anything weird happening with the api let i = 0 while (hasMore && i < 6) { diff --git a/src/state/session/create-account.ts b/src/state/session/create-account.ts index b44bed4dbd..29dfabacfc 100644 --- a/src/state/session/create-account.ts +++ b/src/state/session/create-account.ts @@ -1,7 +1,12 @@ -import {type AppBskyActorProfile, type Un$Typed} from '@atproto/api' import {TID} from '@atproto/common-web' import {type Client} from '@atproto/lex' import {PasswordSession} from '@atproto/lex-password-session' +import {toDatetimeString} from '@atproto/syntax' +import { + overwriteSavedFeeds, + setPersonalDetails, + upsertProfile, +} from '@bsky.app/sdk' import {networkRetry} from '#/lib/async/retry' import { @@ -21,7 +26,7 @@ import { } from '#/ageAssurance/data' import {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state' import {features} from '#/analytics' -import {type BskyAppAgent} from './bridge-agent' +import {type app} from '#/lexicons' import {agentToPdsClient} from './clients' import {configureModerationForAccount} from './moderation' import { @@ -88,7 +93,7 @@ export async function createSessionBundleAndCreateAccount( const gates = features.refresh({strategy: 'prefer-fresh-gates'}) configureModerationForAccount(bundle.agent, earlyAccount) - const createdAt = new Date().toISOString() + const createdAt = toDatetimeString(new Date()) const birthdate = birthDate.toISOString() /* @@ -104,18 +109,16 @@ export async function createSessionBundleAndCreateAccount( const aa = prefetchAgeAssuranceServerData({agent: bundle.agent}) const isProd = Boolean(IS_PROD_SERVICE(service)) + // Post-signup writes all target the account's own repo and actor store. + const pdsClient = agentToPdsClient(bundle.agent) const postSignupTasks: Promise[] = [ - savePersonalDetails(bundle.agent, birthdate), - initializeProfile(bundle.agent, {handle, createdAt, isProd}), + savePersonalDetails(pdsClient, birthDate), + initializeProfile(pdsClient, {handle, createdAt, isProd}), ] if (isProd) { postSignupTasks.push( - initializeSavedFeeds(bundle.agent), - restrictChatAfterAgeAssurance( - aa, - agentToPdsClient(bundle.agent), - earlyAccount.did, - ), + initializeSavedFeeds(pdsClient), + restrictChatAfterAgeAssurance(aa, pdsClient, earlyAccount.did), ) } // Post-signup writes are not required to enter onboarding. @@ -170,41 +173,41 @@ function snapshotNewAccount( } } -function savePersonalDetails(agent: BskyAppAgent, birthDate: string) { +function savePersonalDetails(client: Client, birthDate: Date) { return retryPostSignupTask('set birthDate', 3, () => - agent.setPersonalDetails({birthDate}), + client.call(setPersonalDetails, {birthDate}), ) } function initializeProfile( - agent: BskyAppAgent, + client: Client, { handle, createdAt, isProd, }: { handle: string - createdAt: string + createdAt: ReturnType isProd: boolean }, ) { return retryPostSignupTask('set initial profile', 3, () => - agent.upsertProfile(prev => { - const next: Un$Typed = prev || {} + client.call(upsertProfile, prev => { + const next: Partial = prev || {} if (isProd) { next.displayName = handle next.createdAt = createdAt } else { - next.createdAt = prev?.createdAt || new Date().toISOString() + next.createdAt = prev?.createdAt || toDatetimeString(new Date()) } return next }), ) } -function initializeSavedFeeds(agent: BskyAppAgent) { +function initializeSavedFeeds(client: Client) { return retryPostSignupTask('set initial feeds', 1, () => - agent.overwriteSavedFeeds([ + client.call(overwriteSavedFeeds, [ {...DISCOVER_SAVED_FEED, id: TID.nextStr()}, {...TIMELINE_SAVED_FEED, id: TID.nextStr()}, ]),