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:
@@ -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`,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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 can’t 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)
|
||||
|
||||
@@ -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(
|
||||
_(
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.`,
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user