From d2198705608db2210fec839395e6e292c8968d4a Mon Sep 17 00:00:00 2001 From: David Fedor Date: Thu, 26 Mar 2026 12:11:42 -0500 Subject: [PATCH 1/3] fix(ui): show specific Bible passage error messages Map BibleTextView errors to clearer user-facing messages for missing passages, missing or invalid app keys, forbidden access, rate limits, unreachable server/timeouts, and 5xx responses. Extract the error-message mapping into a dedicated UI helper and extend component tests to cover the new cases. --- .../ui/src/components/bible-card.stories.tsx | 2 +- packages/ui/src/components/verse.test.tsx | 165 +++++++++++++++++- packages/ui/src/components/verse.tsx | 11 +- packages/ui/src/lib/bible-text-error.ts | 60 +++++++ 4 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 packages/ui/src/lib/bible-text-error.ts diff --git a/packages/ui/src/components/bible-card.stories.tsx b/packages/ui/src/components/bible-card.stories.tsx index 91708e3c..cef2605e 100644 --- a/packages/ui/src/components/bible-card.stories.tsx +++ b/packages/ui/src/components/bible-card.stories.tsx @@ -163,7 +163,7 @@ export const Error: Story = { await waitFor(async () => { await expect(canvas.getByRole('heading', { level: 2, name: /error/i })).toBeInTheDocument(); const errorMessages = canvas.getAllByText( - 'Your previously selected Bible verse is unavailable.', + 'The Bible service is having trouble right now. Please try again in a moment.', ); await expect(errorMessages.length).toBeGreaterThan(0); }); diff --git a/packages/ui/src/components/verse.test.tsx b/packages/ui/src/components/verse.test.tsx index 2b36238e..54e8ebc7 100644 --- a/packages/ui/src/components/verse.test.tsx +++ b/packages/ui/src/components/verse.test.tsx @@ -1,7 +1,7 @@ /** * @vitest-environment jsdom */ -import { describe, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi, afterEach } from 'vitest'; import { render, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { Verse, BibleTextView, type BibleTextViewPassageState } from './verse'; @@ -694,3 +694,166 @@ describe('BibleTextView - Refetch loading behavior', () => { }); }); }); + +describe('BibleTextView - Error messaging', () => { + const originalNavigator = globalThis.navigator; + + function createError(message: string, status?: number): Error { + return Object.assign(new Error(message), status === undefined ? {} : { status }); + } + + afterEach(() => { + Object.defineProperty(globalThis, 'navigator', { + configurable: true, + value: originalNavigator, + }); + }); + + it('should show a passage-specific message for 404 errors', async () => { + const { getByRole } = render( + , + ); + + await waitFor(() => { + expect(getByRole('alert')).toHaveTextContent( + "This passage isn't available in the selected Bible version.", + ); + }); + }); + + it('should show an app key message for 401 errors', async () => { + const { getByRole } = render( + , + ); + + await waitFor(() => { + expect(getByRole('alert')).toHaveTextContent( + "This Bible content couldn't be loaded because the app key is missing or invalid.", + ); + }); + }); + + it('should show a forbidden message for 403 errors', async () => { + const { getByRole } = render( + , + ); + + await waitFor(() => { + expect(getByRole('alert')).toHaveTextContent( + "This app isn't allowed to access this Bible content.", + ); + }); + }); + + it('should show an offline message when navigator reports offline', async () => { + Object.defineProperty(globalThis, 'navigator', { + configurable: true, + value: { + ...originalNavigator, + onLine: false, + }, + }); + + const { getByRole } = render( + , + ); + + await waitFor(() => { + expect(getByRole('alert')).toHaveTextContent( + "The Bible server couldn't be reached. Check your connection and try again.", + ); + }); + }); + + it('should show an unreachable-server message for request timeouts', async () => { + const { getByRole } = render( + , + ); + + await waitFor(() => { + expect(getByRole('alert')).toHaveTextContent( + "The Bible server couldn't be reached. Check your connection and try again.", + ); + }); + }); + + it('should show a rate-limit message for 429 errors', async () => { + const { getByRole } = render( + , + ); + + await waitFor(() => { + expect(getByRole('alert')).toHaveTextContent( + 'The Bible service is receiving too many requests right now. Please wait a moment and try again.', + ); + }); + }); + + it('should show a service message for 5xx errors', async () => { + const { getByRole } = render( + , + ); + + await waitFor(() => { + expect(getByRole('alert')).toHaveTextContent( + 'The Bible service is having trouble right now. Please try again in a moment.', + ); + }); + }); +}); diff --git a/packages/ui/src/components/verse.tsx b/packages/ui/src/components/verse.tsx index 77dcff6e..93a3186d 100644 --- a/packages/ui/src/components/verse.tsx +++ b/packages/ui/src/components/verse.tsx @@ -15,6 +15,7 @@ import { ExclamationCircle } from '@/components/icons/exclamation-circle'; import { Footnote } from '@/components/icons/footnote'; import { LoaderIcon } from '@/components/icons/loader'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { getBibleTextErrorMessage } from '@/lib/bible-text-error'; import { cn } from '@/lib/utils'; import { type FontFamily } from '@/lib/verse-html-utils'; import { transformBibleHtml } from '@youversion/platform-core/browser'; @@ -142,13 +143,11 @@ const VerseFootnoteButton = memo(function VerseFootnoteButton({ ); }); -const VERSE_UNAVAILABLE_MESSAGE = 'Your previously selected Bible verse is unavailable.'; - /** * Displays a verse-unavailable error message with a circular exclamation * icon and descriptive text. */ -function VerseUnavailableMessage(): React.ReactElement { +function VerseUnavailableMessage({ message }: { message: string }): React.ReactElement { return (
-

- {VERSE_UNAVAILABLE_MESSAGE} -

+

{message}

); } @@ -470,7 +467,7 @@ export const BibleTextView = forwardRef( if (currentError) { return (
- +
); } diff --git a/packages/ui/src/lib/bible-text-error.ts b/packages/ui/src/lib/bible-text-error.ts new file mode 100644 index 00000000..e2f77e90 --- /dev/null +++ b/packages/ui/src/lib/bible-text-error.ts @@ -0,0 +1,60 @@ +type BibleTextError = Error & { + status?: number; +}; + +const PASSAGE_NOT_FOUND_MESSAGE = "This passage isn't available in the selected Bible version."; +const INVALID_APP_KEY_MESSAGE = + "This Bible content couldn't be loaded because the app key is missing or invalid."; +const FORBIDDEN_MESSAGE = "This app isn't allowed to access this Bible content."; +const UNREACHABLE_SERVER_MESSAGE = + "The Bible server couldn't be reached. Check your connection and try again."; +const RATE_LIMITED_MESSAGE = + 'The Bible service is receiving too many requests right now. Please wait a moment and try again.'; +const SERVER_ERROR_MESSAGE = + 'The Bible service is having trouble right now. Please try again in a moment.'; +const GENERIC_ERROR_MESSAGE = "We couldn't load this Bible passage. Please try again."; + +function isUnreachableServerError(message: string): boolean { + return ( + message.includes('failed to fetch') || + message.includes('networkerror') || + message.includes('network request failed') || + message.includes('load failed') || + message.includes('request timeout') + ); +} + +export function getBibleTextErrorMessage(error: BibleTextError): string { + const status = error.status; + const message = error.message.toLowerCase(); + + if (status === 401) { + return INVALID_APP_KEY_MESSAGE; + } + + if (status === 403) { + return FORBIDDEN_MESSAGE; + } + + if (status === 404 || message.includes('not found')) { + return PASSAGE_NOT_FOUND_MESSAGE; + } + + if (status === 429) { + return RATE_LIMITED_MESSAGE; + } + + if (typeof navigator !== 'undefined' && navigator.onLine === false) { + return UNREACHABLE_SERVER_MESSAGE; + } + + if (isUnreachableServerError(message)) { + return UNREACHABLE_SERVER_MESSAGE; + } + + if (status !== undefined && status >= 500) { + return SERVER_ERROR_MESSAGE; + } + + return GENERIC_ERROR_MESSAGE; +} From e8a0c911e19701f07cd008d033332621cab49975 Mon Sep 17 00:00:00 2001 From: David Fedor Date: Thu, 26 Mar 2026 13:25:52 -0500 Subject: [PATCH 2/3] Fix Bible text error precedence regression --- packages/ui/src/components/verse.test.tsx | 22 +++++++++++++++++++++- packages/ui/src/lib/bible-text-error.ts | 16 ++++++++-------- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/packages/ui/src/components/verse.test.tsx b/packages/ui/src/components/verse.test.tsx index 54e8ebc7..e4900314 100644 --- a/packages/ui/src/components/verse.test.tsx +++ b/packages/ui/src/components/verse.test.tsx @@ -785,7 +785,7 @@ describe('BibleTextView - Error messaging', () => { passageState={{ passage: null, loading: false, - error: createError('Failed to fetch'), + error: createError('Unexpected connection state'), }} />, ); @@ -856,4 +856,24 @@ describe('BibleTextView - Error messaging', () => { ); }); }); + + it('should prioritize 5xx errors over "not found" text in the message', async () => { + const { getByRole } = render( + , + ); + + await waitFor(() => { + expect(getByRole('alert')).toHaveTextContent( + 'The Bible service is having trouble right now. Please try again in a moment.', + ); + }); + }); }); diff --git a/packages/ui/src/lib/bible-text-error.ts b/packages/ui/src/lib/bible-text-error.ts index e2f77e90..27688c52 100644 --- a/packages/ui/src/lib/bible-text-error.ts +++ b/packages/ui/src/lib/bible-text-error.ts @@ -36,14 +36,18 @@ export function getBibleTextErrorMessage(error: BibleTextError): string { return FORBIDDEN_MESSAGE; } - if (status === 404 || message.includes('not found')) { - return PASSAGE_NOT_FOUND_MESSAGE; - } - if (status === 429) { return RATE_LIMITED_MESSAGE; } + if (status !== undefined && status >= 500) { + return SERVER_ERROR_MESSAGE; + } + + if (status === 404 || message.includes('not found')) { + return PASSAGE_NOT_FOUND_MESSAGE; + } + if (typeof navigator !== 'undefined' && navigator.onLine === false) { return UNREACHABLE_SERVER_MESSAGE; } @@ -52,9 +56,5 @@ export function getBibleTextErrorMessage(error: BibleTextError): string { return UNREACHABLE_SERVER_MESSAGE; } - if (status !== undefined && status >= 500) { - return SERVER_ERROR_MESSAGE; - } - return GENERIC_ERROR_MESSAGE; } From 6fcfbc610d9f2353dd14e1437c6bb603740fa931 Mon Sep 17 00:00:00 2001 From: David Fedor Date: Tue, 31 Mar 2026 09:29:53 -0500 Subject: [PATCH 3/3] chore: add changeset --- .changeset/fresh-towns-marry.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/fresh-towns-marry.md diff --git a/.changeset/fresh-towns-marry.md b/.changeset/fresh-towns-marry.md new file mode 100644 index 00000000..959b34ba --- /dev/null +++ b/.changeset/fresh-towns-marry.md @@ -0,0 +1,7 @@ +--- +'@youversion/platform-react-hooks': patch +'@youversion/platform-core': patch +'@youversion/platform-react-ui': patch +--- + +fix(ui): show specific Bible passage error messages