restore compile-time error matching with isXrpcErrorOf

The migration replaced typed error classes (e instanceof
SomeNsid.SomeError) with getErrorName(e) === 'X' string comparisons,
where a typo'd or undeclared name compiles silently. isXrpcErrorOf
(method, e, name) constrains name to InferMethodError<M> - the errors
tuple the generated lexicon schema declares - restoring the old
compile-time guarantee (verified: a typo'd name fails typecheck).
47 comparison sites across 16 files migrated.

Deliberately left on getErrorName, with comments:
- LoginForm AuthFactorTokenRequired: a LexAuthFactorError from
  PasswordSession.login, not an XrpcError.
- AppealForm AlreadyAppealed: createReport declares NO errors in its
  lexicon; the server sends it anyway (undeclared behavior worth
  raising upstream).
- PostFeedErrorMessage Block detection: generic over feed descriptors,
  source method genuinely ambiguous.
- isErrorMaybeAppPasswordPermissions: generic InvalidToken + message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-07-17 19:21:45 +03:00
parent f1afc3c47c
commit 934d2e2170
21 changed files with 311 additions and 69 deletions
@@ -13,7 +13,7 @@ import {
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
import {useTLDs} from '#/lib/hooks/useTLDs'
import {isEmailMaybeInvalid} from '#/lib/strings/email'
import {getErrorName, isXrpcError} from '#/lib/xrpc-error'
import {isXrpcError, isXrpcErrorOf} from '#/lib/xrpc-error'
import {type AppLanguage} from '#/locale/languages'
import {useLanguagePrefs} from '#/state/preferences'
import {useSession} from '#/state/session'
@@ -33,6 +33,7 @@ import {Text} from '#/components/Typography'
import {useAgeAssurance} from '#/ageAssurance'
import {useBeginAgeAssurance} from '#/ageAssurance/useBeginAgeAssurance'
import {useAnalytics} from '#/analytics'
import {app} from '#/lexicons'
export {useDialogControl} from '#/components/Dialog/context'
@@ -140,13 +141,14 @@ function Inner() {
)
if (isXrpcError(e)) {
const errorName = getErrorName(e)
if (errorName === 'InvalidEmail') {
if (isXrpcErrorOf(app.bsky.ageassurance.begin, e, 'InvalidEmail')) {
error = _(
msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`,
)
ax.metric('ageAssurance:initDialogError', {code: 'InvalidEmail'})
} else if (errorName === 'DidTooLong') {
} else if (
isXrpcErrorOf(app.bsky.ageassurance.begin, e, 'DidTooLong')
) {
error = (
<>
<Trans>
@@ -13,7 +13,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query'
import {uploadBlob} from '#/lib/api'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {findContactsStatusQueryKey} from '#/state/queries/find-contacts'
import {useAppviewClient, usePdsClient} from '#/state/session'
@@ -149,14 +149,18 @@ export function GetContacts({
),
{type: 'error'},
)
} else if (getErrorName(err) === 'TooManyContacts') {
} else if (
isXrpcErrorOf(app.bsky.contact.importContacts, err, 'TooManyContacts')
) {
Toast.show(
_(
msg`Too many contacts - you've exceeded the number of contacts you can import to find your friends`,
),
{type: 'error'},
)
} else if (getErrorName(err) === 'InvalidToken') {
} else if (
isXrpcErrorOf(app.bsky.contact.importContacts, err, 'InvalidToken')
) {
Toast.show(
_(
msg`Could not upload contacts. You need to re-verify your phone number to proceed`,
+15 -3
View File
@@ -13,7 +13,7 @@ import {
getDefaultCountry,
} from '#/lib/international-telephone-codes'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {useAppviewClient} from '#/state/session'
import {OnboardingPosition} from '#/screens/Onboarding/Layout'
@@ -103,9 +103,21 @@ export function PhoneInput({
msg`A network error occurred. Please check your internet connection`,
),
)
} else if (getErrorName(err) === 'RateLimitExceeded') {
} else if (
isXrpcErrorOf(
app.bsky.contact.startPhoneVerification,
err,
'RateLimitExceeded',
)
) {
setError(_(msg`Rate limit exceeded. Please try again later.`))
} else if (getErrorName(err) === 'InvalidPhone') {
} else if (
isXrpcErrorOf(
app.bsky.contact.startPhoneVerification,
err,
'InvalidPhone',
)
) {
setError(
_(
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
@@ -7,7 +7,7 @@ import {useMutation} from '@tanstack/react-query'
import {clamp} from '#/lib/numbers'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {useAppviewClient} from '#/state/session'
import {OnboardingPosition} from '#/screens/Onboarding/Layout'
@@ -100,13 +100,17 @@ export function VerifyNumber({
msg`A network error occurred. Please check your internet connection.`,
),
})
} else if (getErrorName(err) === 'InvalidCode') {
} else if (
isXrpcErrorOf(app.bsky.contact.verifyPhone, err, 'InvalidCode')
) {
setError({
retryable: true,
isResendError: true,
message: _(msg`This code is invalid. Resend to get a new code.`),
})
} else if (getErrorName(err) === 'InvalidPhone') {
} else if (
isXrpcErrorOf(app.bsky.contact.verifyPhone, err, 'InvalidPhone')
) {
setError({
retryable: false,
isResendError: false,
@@ -114,7 +118,9 @@ export function VerifyNumber({
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
})
} else if (getErrorName(err) === 'RateLimitExceeded') {
} else if (
isXrpcErrorOf(app.bsky.contact.verifyPhone, err, 'RateLimitExceeded')
) {
setError({
retryable: true,
isResendError: false,
@@ -156,7 +162,13 @@ export function VerifyNumber({
msg`A network error occurred. Please check your internet connection.`,
),
})
} else if (getErrorName(err) === 'InvalidPhone') {
} else if (
isXrpcErrorOf(
app.bsky.contact.startPhoneVerification,
err,
'InvalidPhone',
)
) {
setError({
retryable: false,
isResendError: true,
@@ -164,7 +176,13 @@ export function VerifyNumber({
msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`,
),
})
} else if (getErrorName(err) === 'RateLimitExceeded') {
} else if (
isXrpcErrorOf(
app.bsky.contact.startPhoneVerification,
err,
'RateLimitExceeded',
)
) {
setError({
retryable: true,
isResendError: true,
+8 -3
View File
@@ -3,12 +3,13 @@ import {StackActions, useNavigation} from '@react-navigation/native'
import {type NavigationProp} from '#/lib/routes/types'
import {isNetworkError} from '#/lib/strings/errors'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
import {type DialogOuterProps} from '#/components/Dialog'
import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {IS_NATIVE} from '#/env'
import {chat} from '#/lexicons'
export function LeaveConvoPrompt({
control,
@@ -36,9 +37,13 @@ export function LeaveConvoPrompt({
let errorMessage = l`Could not leave chat`
if (isNetworkError(error)) {
errorMessage = l`A network error occurred. Please check your internet connection.`
} else if (getErrorName(error) === 'InvalidConvo') {
} else if (
isXrpcErrorOf(chat.bsky.convo.leaveConvo, error, 'InvalidConvo')
) {
errorMessage = l`Conversation not found.`
} else if (getErrorName(error) === 'OwnerCannotLeave') {
} else if (
isXrpcErrorOf(chat.bsky.convo.leaveConvo, error, 'OwnerCannotLeave')
) {
errorMessage = l`Owner must lock the group before leaving.`
}
Toast.show(errorMessage, {type: 'error'})
+55 -12
View File
@@ -3,7 +3,7 @@ import {Trans, useLingui} from '@lingui/react/macro'
import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification'
import {isNetworkError} from '#/lib/strings/errors'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {useCreateGroupChat} from '#/state/queries/messages/create-group-chat'
import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members'
@@ -16,6 +16,7 @@ import {InitiateChatFlow} from '#/components/dms/InitiateChatFlow'
import {MessagePlus_Stroke2_Corner0_Rounded as NewChatIcon} from '#/components/icons/Message'
import * as Toast from '#/components/Toast'
import {useAnalytics} from '#/analytics'
import {chat} from '#/lexicons'
export function NewChat({
control,
@@ -51,15 +52,41 @@ export function NewChat({
let errorMessage = l`An issue occurred starting the chat, please try again.`
if (isNetworkError(error)) {
errorMessage = l`A network error occurred. Please check your internet connection.`
} else if (getErrorName(error) === 'AccountSuspended') {
} else if (
isXrpcErrorOf(
chat.bsky.convo.getConvoForMembers,
error,
'AccountSuspended',
)
) {
errorMessage = l`Suspended accounts cannot participate in chat.`
} else if (getErrorName(error) === 'BlockedActor') {
} else if (
isXrpcErrorOf(chat.bsky.convo.getConvoForMembers, error, 'BlockedActor')
) {
errorMessage = l`This user has blocked you and cannot be messaged.`
} else if (getErrorName(error) === 'MessagesDisabled') {
} else if (
isXrpcErrorOf(
chat.bsky.convo.getConvoForMembers,
error,
'MessagesDisabled',
)
) {
errorMessage = l`This user has disabled chat and cannot be messaged.`
} else if (getErrorName(error) === 'NotFollowedBySender') {
} else if (
isXrpcErrorOf(
chat.bsky.convo.getConvoForMembers,
error,
'NotFollowedBySender',
)
) {
errorMessage = l`Chat recipient is not followed by the sender.`
} else if (getErrorName(error) === 'RecipientNotFound') {
} else if (
isXrpcErrorOf(
chat.bsky.convo.getConvoForMembers,
error,
'RecipientNotFound',
)
) {
errorMessage = l`Unable to find the selected recipient.`
}
Toast.show(errorMessage, {
@@ -78,17 +105,33 @@ export function NewChat({
let errorMessage = l`An issue occurred starting the group chat, please try again.`
if (isNetworkError(error)) {
errorMessage = l`A network error occurred. Please check your internet connection.`
} else if (getErrorName(error) === 'AccountSuspended') {
} else if (
isXrpcErrorOf(chat.bsky.group.createGroup, error, 'AccountSuspended')
) {
errorMessage = l`Suspended accounts cannot participate in a group chat.`
} else if (getErrorName(error) === 'BlockedActor') {
} else if (
isXrpcErrorOf(chat.bsky.group.createGroup, error, 'BlockedActor')
) {
errorMessage = l`One of the selected recipients has blocked you and cannot be messaged.`
} else if (getErrorName(error) === 'NewAccountCannotCreateGroup') {
} else if (
isXrpcErrorOf(
chat.bsky.group.createGroup,
error,
'NewAccountCannotCreateGroup',
)
) {
errorMessage = l`You cannot create a group chat yet.`
} else if (getErrorName(error) === 'NotFollowedBySender') {
} else if (
isXrpcErrorOf(chat.bsky.group.createGroup, error, 'NotFollowedBySender')
) {
errorMessage = l`A selected recipient is not followed by the sender.`
} else if (getErrorName(error) === 'RecipientNotFound') {
} else if (
isXrpcErrorOf(chat.bsky.group.createGroup, error, 'RecipientNotFound')
) {
errorMessage = l`Unable to find a selected recipient.`
} else if (getErrorName(error) === 'UserForbidsGroups') {
} else if (
isXrpcErrorOf(chat.bsky.group.createGroup, error, 'UserForbidsGroups')
) {
errorMessage = l`One of the selected recipients does not allow group chats.`
}
Toast.show(errorMessage, {
+30 -8
View File
@@ -11,7 +11,7 @@ import {makeProfileLink} from '#/lib/routes/links'
import {type NavigationProp} from '#/lib/routes/types'
import {isNetworkError} from '#/lib/strings/errors'
import {sanitizeHandle} from '#/lib/strings/handles'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {
@@ -145,15 +145,29 @@ function GroupChatJoinDialogContent({code}: {code?: string}) {
let errorMessage = l`Failed to join the group chat. Please try again.`
if (isNetworkError(error)) {
errorMessage = l`There was a problem with your internet connection, please try again`
} else if (getErrorName(error) === 'ConvoLocked') {
} else if (
isXrpcErrorOf(chat.bsky.group.requestJoin, error, 'ConvoLocked')
) {
errorMessage = l`This conversation is locked.`
} else if (getErrorName(error) === 'FollowRequired') {
} else if (
isXrpcErrorOf(chat.bsky.group.requestJoin, error, 'FollowRequired')
) {
errorMessage = l`Only followers can join this group chat.`
} else if (getErrorName(error) === 'InvalidCode') {
} else if (
isXrpcErrorOf(chat.bsky.group.requestJoin, error, 'InvalidCode')
) {
errorMessage = l`Invalid group chat code.`
} else if (getErrorName(error) === 'LinkDisabled') {
} else if (
isXrpcErrorOf(chat.bsky.group.requestJoin, error, 'LinkDisabled')
) {
errorMessage = l`This invite link has been disabled.`
} else if (getErrorName(error) === 'MemberLimitReached') {
} else if (
isXrpcErrorOf(
chat.bsky.group.requestJoin,
error,
'MemberLimitReached',
)
) {
errorMessage = l`The member limit has been reached.`
const preview = data?.joinLinkPreviews[0]
if (
@@ -164,7 +178,9 @@ function GroupChatJoinDialogContent({code}: {code?: string}) {
convoId: preview.convo.id,
})
}
} else if (getErrorName(error) === 'UserKicked') {
} else if (
isXrpcErrorOf(chat.bsky.group.requestJoin, error, 'UserKicked')
) {
errorMessage = l`You have been previously removed from this group and cant join it using this link.`
}
Toast.show(errorMessage)
@@ -185,7 +201,13 @@ function GroupChatJoinDialogContent({code}: {code?: string}) {
let errorMessage = l`Failed to rescind your request. Please try again.`
if (isNetworkError(error)) {
errorMessage = l`There was a problem with your internet connection, please try again`
} else if (getErrorName(error) === 'InvalidJoinRequest') {
} else if (
isXrpcErrorOf(
chat.bsky.group.withdrawJoinRequest,
error,
'InvalidJoinRequest',
)
) {
errorMessage = l`Invalid rescind request.`
}
Toast.show(errorMessage)
+1
View File
@@ -66,6 +66,7 @@ export function AppealForm({
)
},
onError: err => {
/* not a declared error of com.atproto.moderation.createReport (which declares none); server sends it anyway */
if (isXrpcError(err) && getErrorName(err) === 'AlreadyAppealed') {
setError(
_(
+18 -6
View File
@@ -4,7 +4,7 @@ import {Trans, useLingui} from '@lingui/react/macro'
import {useQueryClient} from '@tanstack/react-query'
import {isNetworkError} from '#/lib/strings/errors'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {type Shadow} from '#/state/cache/types'
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
@@ -23,7 +23,7 @@ import {parseConvoView} from '#/components/dms/util'
import {Loader} from '#/components/Loader'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {type chat} from '#/lexicons'
import {chat} from '#/lexicons'
import {type AnyProfileView} from '#/types/bsky/profile'
type Item = chat.bsky.convo.defs.ConvoView
@@ -279,9 +279,13 @@ function MutualGroupChat({
let errorMessage = l`Could not leave chat.`
if (isNetworkError(error)) {
errorMessage = l`A network error occurred. Please check your internet connection.`
} else if (getErrorName(error) === 'InvalidConvo') {
} else if (
isXrpcErrorOf(chat.bsky.convo.leaveConvo, error, 'InvalidConvo')
) {
errorMessage = l`Chat not found.`
} else if (getErrorName(error) === 'OwnerCannotLeave') {
} else if (
isXrpcErrorOf(chat.bsky.convo.leaveConvo, error, 'OwnerCannotLeave')
) {
errorMessage = l`Chat owners cannot leave a group chat.`
}
Toast.show(errorMessage, {type: 'error'})
@@ -303,9 +307,17 @@ function MutualGroupChat({
let errorMessage = l`Could not remove member.`
if (isNetworkError(error)) {
errorMessage = l`A network error occurred. Please check your internet connection.`
} else if (getErrorName(error) === 'InvalidConvo') {
} else if (
isXrpcErrorOf(chat.bsky.group.removeMembers, error, 'InvalidConvo')
) {
errorMessage = l`Chat not found.`
} else if (getErrorName(error) === 'InsufficientRole') {
} else if (
isXrpcErrorOf(
chat.bsky.group.removeMembers,
error,
'InsufficientRole',
)
) {
errorMessage = l`You must be a chat owner to remove a member.`
}
Toast.show(errorMessage, {type: 'error'})
+3 -2
View File
@@ -16,7 +16,7 @@ import {isAfter, parseISO} from 'date-fns'
import {uploadBlob} from '#/lib/api'
import {imageToThumb} from '#/lib/api/resolve'
import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {useAppConfig} from '#/state/appConfig'
import {
updateProfileShadow,
@@ -291,7 +291,8 @@ export function useUpsertLiveStatusMutation(
await retry(upsert, {
maxRetries: 5,
retryable: e => getErrorName(e) === 'InvalidSwap',
retryable: e =>
isXrpcErrorOf(com.atproto.repo.putRecord, e, 'InvalidSwap'),
})
return {
+40
View File
@@ -6,7 +6,9 @@ import {
getErrorName,
getErrorStatus,
isXrpcError,
isXrpcErrorOf,
} from '#/lib/xrpc-error'
import {chat} from '#/lexicons'
import {isErrorMaybeAppPasswordPermissions, isNetworkError} from '../errors'
/**
@@ -60,6 +62,44 @@ describe('getErrorName', () => {
})
})
describe('isXrpcErrorOf', () => {
it('matches a declared error code for the method', () => {
expect(
isXrpcErrorOf(
chat.bsky.convo.acceptConvo,
lexError(400, 'InvalidConvo'),
'InvalidConvo',
),
).toBe(true)
})
it('does not match a different error code or a non-XRPC value', () => {
expect(
isXrpcErrorOf(
chat.bsky.convo.getMessages,
lexError(400, 'InvalidConvo'),
'InvalidConvo',
),
).toBe(true)
expect(
isXrpcErrorOf(
chat.bsky.convo.acceptConvo,
new Error('boom'),
'InvalidConvo',
),
).toBe(false)
})
it('constrains the error name to the method declared errors at compile time', () => {
isXrpcErrorOf(
chat.bsky.convo.acceptConvo,
lexError(400, 'InvalidConvo'),
// @ts-expect-error 'Bogus' is not a declared error of acceptConvo
'Bogus',
)
})
})
describe('getErrorHeader', () => {
it('reads a header from the lex Headers object', () => {
const e = lexError(429, 'RateLimitExceeded', {'ratelimit-reset': '123'})
+28 -1
View File
@@ -1,4 +1,12 @@
import {LexError, XrpcError, XrpcResponseError} from '@atproto/lex'
import {
type InferMethodError,
LexError,
type Main,
type Procedure,
type Query,
XrpcError,
XrpcResponseError,
} from '@atproto/lex'
/**
* True for an XRPC error from a lex `Client` (`@atproto/lex-client` `XrpcError`,
@@ -30,6 +38,25 @@ export function getErrorName(e: unknown): string | undefined {
return e instanceof LexError ? e.error : undefined
}
/**
* True when `e` is an XRPC error whose lexicon error code is `name`, with
* `name` compile-time constrained to the errors DECLARED by `method`'s
* lexicon. The typed replacement for the old generated error classes
* (`e instanceof SomeNsid.SomeError`): a typo'd or undeclared name is a type
* error instead of a silent never-match.
*
* `method` accepts the same value passed to `client.call` - either the
* generated method namespace (`chat.bsky.convo.acceptConvo`) or its `.main`
* schema - via lex's `Main<M>`.
*/
export function isXrpcErrorOf<M extends Procedure | Query>(
method: Main<M>,
e: unknown,
name: NoInfer<InferMethodError<M>>,
): boolean {
return isXrpcError(e) && e.error === name
}
/**
* Read a response header off an XRPC error. Only `XrpcResponseError` carries a
* response; its `.headers` is a WHATWG `Headers` object.
+1
View File
@@ -140,6 +140,7 @@ export const LoginForm = ({
} catch (err) {
const errMsg = String(err)
setIsProcessing(false)
/* matches a LexAuthFactorError from PasswordSession.login (not an XrpcError), so getErrorName - gated on LexError, not isXrpcError - is required here */
if (getErrorName(err) === 'AuthFactorTokenRequired') {
setIsAuthFactorTokenNeeded(true)
} else {
@@ -14,7 +14,7 @@ import {
type NativeStackScreenProps,
type NavigationProp,
} from '#/lib/routes/types'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useConvoQuery} from '#/state/queries/messages/conversation'
@@ -430,7 +430,9 @@ function SettingsHeader({
if (lock) {
logger.error('Failed to lock group chat', {message: e})
Toast.show(l`Failed to lock group chat`, {type: 'error'})
} else if (getErrorName(e) === 'ConvoLockedByModeration') {
} else if (
isXrpcErrorOf(chat.bsky.convo.unlockConvo, e, 'ConvoLockedByModeration')
) {
Toast.show(l`This chat is locked by a moderation action`, {
type: 'error',
})
+37 -7
View File
@@ -11,7 +11,7 @@ import {
type NativeStackScreenProps,
type NavigationProp,
} from '#/lib/routes/types'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {ConvoProvider, useConvo} from '#/state/messages/convo'
import {ConvoStatus} from '#/state/messages/convo/types'
@@ -39,7 +39,7 @@ import * as ProfileCard from '#/components/ProfileCard'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {useAnalytics} from '#/analytics'
import {type chat} from '#/lexicons'
import {chat} from '#/lexicons'
import type * as bsky from '#/types/bsky'
import {InviteLinkDialog} from './components/InviteLinkDialog'
@@ -187,11 +187,29 @@ function JoinRequestsList({
let errorMessage = l`Failed to accept join request`
if (isNetworkError(error)) {
errorMessage = l`A network error occurred. Please check your internet connection.`
} else if (getErrorName(error) === 'InvalidConvo') {
} else if (
isXrpcErrorOf(
chat.bsky.group.approveJoinRequest,
error,
'InvalidConvo',
)
) {
errorMessage = l`Conversation not found.`
} else if (getErrorName(error) === 'InsufficientRole') {
} else if (
isXrpcErrorOf(
chat.bsky.group.approveJoinRequest,
error,
'InsufficientRole',
)
) {
errorMessage = l`Only admins can accept join requests.`
} else if (getErrorName(error) === 'MemberLimitReached') {
} else if (
isXrpcErrorOf(
chat.bsky.group.approveJoinRequest,
error,
'MemberLimitReached',
)
) {
errorMessage = l`The member limit has been reached.`
}
Toast.show(errorMessage, {type: 'error'})
@@ -213,9 +231,21 @@ function JoinRequestsList({
let errorMessage = l`Failed to reject join request`
if (isNetworkError(error)) {
errorMessage = l`A network error occurred. Please check your internet connection.`
} else if (getErrorName(error) === 'InvalidConvo') {
} else if (
isXrpcErrorOf(
chat.bsky.group.rejectJoinRequest,
error,
'InvalidConvo',
)
) {
errorMessage = l`Conversation not found.`
} else if (getErrorName(error) === 'InsufficientRole') {
} else if (
isXrpcErrorOf(
chat.bsky.group.rejectJoinRequest,
error,
'InsufficientRole',
)
) {
errorMessage = l`Only admins can reject join requests.`
}
Toast.show(errorMessage, {type: 'error'})
@@ -4,7 +4,7 @@ import {useNavigation} from '@react-navigation/native'
import {HITSLOP_10} from '#/lib/constants'
import {type NavigationProp} from '#/lib/routes/types'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
import {useLockConvo} from '#/state/queries/messages/lock-conversation'
@@ -15,6 +15,7 @@ import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock'
import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {chat} from '#/lexicons'
import {LeaveChatPrompt} from '../ConversationSettings/prompts'
import {ChatFooter} from './ChatFooter'
@@ -41,7 +42,9 @@ export function ChatLocked({
Toast.show(l({message: 'Group chat unlocked', context: 'toast'}))
},
onError: e => {
if (getErrorName(e) === 'ConvoLockedByModeration') {
if (
isXrpcErrorOf(chat.bsky.convo.unlockConvo, e, 'ConvoLockedByModeration')
) {
Toast.show(l`This chat is locked by a moderation action`, {
type: 'error',
})
@@ -2,7 +2,7 @@ import {View} from 'react-native'
import {Trans, useLingui} from '@lingui/react/macro'
import {isNetworkError} from '#/lib/strings/errors'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {useWithdrawJoinGroupChatRequest} from '#/state/queries/messages/withdraw-join-group-chat'
import {TimeElapsed} from '#/view/com/util/TimeElapsed'
import {atoms as a, useTheme, web} from '#/alf'
@@ -11,7 +11,7 @@ import {createStaticClick, Link} from '#/components/Link'
import * as Prompt from '#/components/Prompt'
import * as Toast from '#/components/Toast'
import {Text} from '#/components/Typography'
import {type chat} from '#/lexicons'
import {chat} from '#/lexicons'
export function OutgoingRequestListItem({
convo: convoView,
@@ -32,7 +32,13 @@ export function OutgoingRequestListItem({
let errorMessage = l`Failed to rescind your request. Please try again.`
if (isNetworkError(error)) {
errorMessage = l`There was a problem with your internet connection, please try again`
} else if (getErrorName(error) === 'InvalidJoinRequest') {
} else if (
isXrpcErrorOf(
chat.bsky.group.withdrawJoinRequest,
error,
'InvalidJoinRequest',
)
) {
errorMessage = l`Invalid rescind request.`
}
Toast.show(errorMessage)
+9 -3
View File
@@ -7,11 +7,11 @@ import {DEFAULT_SERVICE} from '#/lib/constants'
import {cleanError, isNetworkError} from '#/lib/strings/errors'
import {createFullHandle} from '#/lib/strings/handles'
import {getAge} from '#/lib/strings/time'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {useSessionApi} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
import {type AnalyticsContextType, useAnalytics} from '#/analytics'
import {type com} from '#/lexicons'
import {com} from '#/lexicons'
export type ServiceDescription = com.atproto.server.describeServer.$OutputBody
@@ -355,7 +355,13 @@ export function useSubmitSignup() {
} catch (err) {
const e = err as Error
let errMsg = e.toString()
if (getErrorName(e) === 'InvalidInviteCode') {
if (
isXrpcErrorOf(
com.atproto.server.createAccount,
e,
'InvalidInviteCode',
)
) {
dispatch({
type: 'setError',
value: l`Invite code not accepted. Check that you input it correctly and try again.`,
+2 -2
View File
@@ -73,7 +73,7 @@ import {useCallOnce} from '#/lib/once'
import {type NavigationProp} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {colors} from '#/lib/styles'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {logger} from '#/logger'
import {useDialogStateControlContext} from '#/state/dialogs'
import {emitPostCreated} from '#/state/events'
@@ -745,7 +745,7 @@ export const ComposePost = ({
const getDraftSaveError = useCallback(
(e: unknown): string => {
if (getErrorName(e) === 'DraftLimitReached') {
if (isXrpcErrorOf(app.bsky.draft.createDraft, e, 'DraftLimitReached')) {
return l`You've reached the maximum number of drafts`
}
return l`Failed to save draft`
@@ -5,7 +5,7 @@ import {
} from '@tanstack/react-query'
import {isNetworkError} from '#/lib/strings/errors'
import {getErrorName} from '#/lib/xrpc-error'
import {isXrpcErrorOf} from '#/lib/xrpc-error'
import {useAppviewClient} from '#/state/session'
import {type ComposerState} from '#/view/com/composer/state/composer'
import {useAnalytics} from '#/analytics'
@@ -209,7 +209,9 @@ export function useSaveDraftMutation() {
},
onError: error => {
// Check for draft limit error
if (getErrorName(error) === 'DraftLimitReached') {
if (
isXrpcErrorOf(app.bsky.draft.createDraft, error, 'DraftLimitReached')
) {
logger.error('Draft limit reached', {safeMessage: error.message})
// Error will be handled by caller
} else if (!isNetworkError(error)) {
@@ -239,6 +239,11 @@ function detectKnownError(
if (!error) {
return undefined
}
/*
* Generic helper over an arbitrary feed error (any feed descriptor). Both
* names are declared by app.bsky.feed.getAuthorFeed and .getActorLikes, so
* the source method is ambiguous - kept as an untyped getErrorName check.
*/
if (
getErrorName(error) === 'BlockedActor' ||
getErrorName(error) === 'BlockedByActor'