Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { tailwind } from '@/theme';
import { Icon } from '@/components-next/common';
import { Spinner } from '@/components-next/spinner';
import { MESSAGE_VARIANTS } from '@/constants';
import { errorMessage } from '@/utils/errorUtils';

const generateUniqueFileName = (url: string, originalFileName: string) => {
const hash = url.split('').reduce((acc, char) => {
Expand Down Expand Up @@ -41,9 +42,11 @@ export const FileBubblePreview = (props: FilePreviewProps) => {

const previewFile = () => {
try {
FileViewer.open(localFilePath).catch(e => Alert.alert(e));
FileViewer.open(localFilePath).catch(e =>
Alert.alert('Not able to preview file', errorMessage(e)),
);
} catch (e) {
Alert.alert('Not able to preview file' + e);
Alert.alert('Not able to preview file', errorMessage(e));
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { TAB_BAR_HEIGHT } from '@/constants';
import { tailwind } from '@/theme';
import i18n from '@/i18n';
import { errorMessage } from '@/utils/errorUtils';
import { ConversationStatus } from '@/types';
import { useChatWindowContext } from '@/context';
import { useAppDispatch, useAppSelector } from '@/hooks';
Expand Down Expand Up @@ -70,7 +71,7 @@ export const ConversationActions = () => {
url,
});
} catch (error) {
Alert.alert((error as Error).message);
Alert.alert(i18n.t('COMMON.ERROR_TITLE'), errorMessage(error));
}
};

Expand Down
24 changes: 24 additions & 0 deletions src/utils/errorUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,30 @@ interface ErrorHandler {
(e: Error, isFatal: boolean): void;
}

/**
* Coerces a caught value into a string for display.
*
* Rejections reach the app as Errors, as plain objects from native modules, or
* as bare strings. Alert passes its arguments across the bridge untouched, and
* Android's DialogModule reads them as strings, so a non-string title or
* message crashes the process.
*/
export const errorMessage = (error: unknown): string => {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'string') {
return error;
}
if (error && typeof error === 'object' && 'message' in error) {
const { message } = error as { message: unknown };
if (typeof message === 'string') {
return message;
}
}
return i18n.t('COMMON.ERROR');
};

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const errorHandler: ErrorHandler = (e, isFatal) => {
Sentry.captureException(e);
Expand Down
28 changes: 28 additions & 0 deletions src/utils/specs/errorUtils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { errorMessage } from '@/utils/errorUtils';

describe('#errorMessage', () => {
it('returns the message of an Error', () => {
expect(errorMessage(new Error('No app associated with this mime type'))).toBe(
'No app associated with this mime type',
);
});

it('returns a string unchanged', () => {
expect(errorMessage('File load error')).toBe('File load error');
});

it('returns the message of a native module rejection', () => {
expect(errorMessage({ code: 'ENOENT', message: 'File does not exist' })).toBe(
'File does not exist',
);
});

it('falls back when the object has no string message', () => {
expect(errorMessage({ message: { detail: 'nested' } })).toBe('Error');
});

it('falls back for null and undefined', () => {
expect(errorMessage(null)).toBe('Error');
expect(errorMessage(undefined)).toBe('Error');
});
});