From e61a90a0cfe3e3438e35cc4696edb7861eda5854 Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Fri, 9 Oct 2026 22:14:59 +0530 Subject: [PATCH] fix: show readable server errors in the error alert REST errors keep their bracketed code in the alert and DDP errors have no data, so the alert is blank. The helper now reads the error code and text from either shape, shows the translation when the app has one for the code, and otherwise shows the server's text without the bracketed code. --- .../methods/helpers/__tests__/info.test.ts | 98 +++++++++++++++++++ app/lib/methods/helpers/info.ts | 15 ++- 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 app/lib/methods/helpers/__tests__/info.test.ts diff --git a/app/lib/methods/helpers/__tests__/info.test.ts b/app/lib/methods/helpers/__tests__/info.test.ts new file mode 100644 index 00000000000..c60665992e4 --- /dev/null +++ b/app/lib/methods/helpers/__tests__/info.test.ts @@ -0,0 +1,98 @@ +import { Alert } from 'react-native'; + +import { showErrorAlertWithEMessage } from '~/lib/methods/helpers/info'; +import { TwoFactorCancelledError } from '~/lib/services/twoFactor/twoFactorCancelled'; + +jest.mock('~/i18n', () => ({ + __esModule: true, + default: { + t: (key: string, options?: { seconds?: string }) => (options?.seconds ? `${key} ${options.seconds}` : `translated:${key}`), + isTranslated: (key: string) => key === 'error-not-allowed' || key === 'Some_translated_key' + } +})); + +const alertMessage = () => (Alert.alert as jest.Mock).mock.calls[0][1]; + +describe('showErrorAlertWithEMessage', () => { + beforeEach(() => { + jest.spyOn(Alert, 'alert').mockImplementation(() => {}); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('does not alert for a cancelled two-factor prompt', () => { + showErrorAlertWithEMessage(new TwoFactorCancelledError()); + expect(Alert.alert).not.toHaveBeenCalled(); + }); + + it('shows the server reason of a REST error without its bracketed code', () => { + showErrorAlertWithEMessage({ data: { error: 'User not found [error-user-not-found]', errorType: 'error-user-not-found' } }); + expect(alertMessage()).toBe('User not found'); + }); + + it('shows the translation of a REST error whose code is translated', () => { + showErrorAlertWithEMessage({ data: { error: 'Not allowed [error-not-allowed]', errorType: 'error-not-allowed' } }); + expect(alertMessage()).toBe('translated:error-not-allowed'); + }); + + it('keeps a REST error message that has no bracketed code', () => { + showErrorAlertWithEMessage({ data: { error: 'User is banned' } }); + expect(alertMessage()).toBe('User is banned'); + }); + + it('keeps a bracketed suffix that differs from the error code', () => { + showErrorAlertWithEMessage({ data: { error: 'Failed [other-code]', errorType: 'error-user-not-found' } }); + expect(alertMessage()).toBe('Failed [other-code]'); + }); + + it('translates a plain REST message that is itself a translation key', () => { + showErrorAlertWithEMessage({ data: { error: 'Some_translated_key' } }); + expect(alertMessage()).toBe('translated:Some_translated_key'); + }); + + it('shows the reason of a DDP error whose code has no translation', () => { + showErrorAlertWithEMessage({ + error: 'error-invalid-user', + reason: 'Invalid user', + message: 'Invalid user [error-invalid-user]' + }); + expect(alertMessage()).toBe('Invalid user'); + }); + + it('shows the translation of a DDP error whose code is translated', () => { + showErrorAlertWithEMessage({ + error: 'error-not-allowed', + reason: 'Rejected by the server', + message: 'Rejected by the server [error-not-allowed]' + }); + expect(alertMessage()).toBe('translated:error-not-allowed'); + }); + + it('shows the reason of a DDP error whose code is numeric', () => { + showErrorAlertWithEMessage({ error: 404, reason: "Method 'x' not found", message: "Method 'x' not found [404]" }); + expect(alertMessage()).toBe("Method 'x' not found"); + }); + + it('shows the message of a DDP error without a reason', () => { + showErrorAlertWithEMessage({ error: 'error-invalid-room', message: 'Invalid room [error-invalid-room]' }); + expect(alertMessage()).toBe('Invalid room'); + }); + + it('shows the rate limit message with the seconds to wait', () => { + showErrorAlertWithEMessage({ + data: { + error: + 'Error, too many requests. Please slow down. You must wait 12 seconds before trying this endpoint again. [error-too-many-requests]', + errorType: 'error-too-many-requests' + } + }); + expect(alertMessage()).toBe('error-too-many-requests 12'); + }); + + it('forwards the title', () => { + showErrorAlertWithEMessage({ data: { error: 'User is banned' } }, 'Oops'); + expect((Alert.alert as jest.Mock).mock.calls[0][0]).toBe('Oops'); + }); +}); diff --git a/app/lib/methods/helpers/info.ts b/app/lib/methods/helpers/info.ts index 5f0c262324b..55b0b6b34e4 100644 --- a/app/lib/methods/helpers/info.ts +++ b/app/lib/methods/helpers/info.ts @@ -6,17 +6,28 @@ import { isTwoFactorCancelled } from '~/lib/services/twoFactor/twoFactorCancelle export const showErrorAlert = (message: string, title?: string, onPress = () => {}): void => Alert.alert(title || '', message, [{ text: 'OK', onPress }], { cancelable: true }); +const getErrorCode = (e: any): string | undefined => e?.data?.errorType || (typeof e?.error === 'string' ? e.error : undefined); + +const withoutErrorCode = (message: string, code?: string): string => { + const suffix = ` [${code}]`; + return code && message?.endsWith(suffix) ? message.slice(0, -suffix.length) : message; +}; + export const showErrorAlertWithEMessage = (e: any, title?: string): void => { if (isTwoFactorCancelled(e)) { return; } - let errorMessage: string = e?.data?.error; + const code = getErrorCode(e); + let errorMessage: string = e?.data?.error || e?.reason || e?.message; if (errorMessage?.includes('[error-too-many-requests]')) { const seconds = errorMessage.replace(/\D/g, ''); errorMessage = I18n.t('error-too-many-requests', { seconds }); + } else if (code && I18n.isTranslated(code)) { + errorMessage = I18n.t(code); } else { - errorMessage = I18n.isTranslated(errorMessage) ? I18n.t(errorMessage) : errorMessage; + const withoutCode = withoutErrorCode(errorMessage, code); + errorMessage = I18n.isTranslated(withoutCode) ? I18n.t(withoutCode) : withoutCode; } showErrorAlert(errorMessage, title);