From 5c8dda8aed2ec06e271e611547cc77d98618f4ab Mon Sep 17 00:00:00 2001 From: Brenden Manquen Date: Tue, 3 Feb 2026 08:11:24 -0500 Subject: [PATCH] fix(ui): ensure bible reader loads with user settings from localStorage --- .changeset/honest-dragons-lead.md | 7 +++ .../src/components/bible-reader.stories.tsx | 56 +++++++++++++++++++ packages/ui/src/components/bible-reader.tsx | 44 ++++++++++----- 3 files changed, 93 insertions(+), 14 deletions(-) create mode 100644 .changeset/honest-dragons-lead.md diff --git a/.changeset/honest-dragons-lead.md b/.changeset/honest-dragons-lead.md new file mode 100644 index 00000000..68908f77 --- /dev/null +++ b/.changeset/honest-dragons-lead.md @@ -0,0 +1,7 @@ +--- +'@youversion/platform-react-ui': patch +'@youversion/platform-core': patch +'@youversion/platform-react-hooks': patch +--- + +Fix user settings from localStorage not loading in the bible reader diff --git a/packages/ui/src/components/bible-reader.stories.tsx b/packages/ui/src/components/bible-reader.stories.tsx index cf67319c..d4fee283 100644 --- a/packages/ui/src/components/bible-reader.stories.tsx +++ b/packages/ui/src/components/bible-reader.stories.tsx @@ -497,6 +497,62 @@ export const AuthenticatedWithAvatar: Story = { }, }; +export const LoadsSavedPreferencesFromLocalStorage: Story = { + tags: ['integration'], + args: { + versionId: 111, + book: 'JHN', + chapter: '1', + background: 'light', + }, + beforeEach: () => { + localStorage.clear(); + // Pre-populate localStorage with saved preferences + localStorage.setItem('youversion-platform:reader:font-size', '18'); + localStorage.setItem('youversion-platform:reader:font-family', 'Source Serif'); + }, + render: (args) => ( +
+ + + + +
+ ), + play: async ({ canvasElement }) => { + await waitFor( + async () => { + const verseContainer = canvasElement.querySelector('[data-slot="yv-bible-renderer"]'); + await expect(verseContainer).toBeInTheDocument(); + }, + { timeout: 5000 }, + ); + + // Verify the saved settings were applied via CSS variables + const verseContainer = canvasElement.querySelector( + '[data-slot="yv-bible-renderer"]', + )!; + await expect(verseContainer.style.getPropertyValue('--yv-reader-font-size')).toBe('18px'); + await expect(verseContainer.style.getPropertyValue('--yv-reader-font-family')).toBe( + 'Source Serif', + ); + + // Open settings and verify the correct font family button is active + const settingsButton = screen.getByRole('button', { name: /settings/i }); + await userEvent.click(settingsButton); + + await waitFor(async () => { + await expect(await screen.findByText('Reader Settings')).toBeInTheDocument(); + }); + + const sourceSerifButton = screen.getByRole('button', { name: /source serif/i }); + await expect(sourceSerifButton).toHaveClass('yv:bg-black'); + + const interButton = screen.getByRole('button', { name: /inter/i }); + await expect(interButton).not.toHaveClass('yv:bg-black'); + }, +}; + export const AuthenticatedWithoutAvatar: Story = { tags: ['integration'], args: { diff --git a/packages/ui/src/components/bible-reader.tsx b/packages/ui/src/components/bible-reader.tsx index b1847274..28a910e6 100644 --- a/packages/ui/src/components/bible-reader.tsx +++ b/packages/ui/src/components/bible-reader.tsx @@ -1,6 +1,14 @@ 'use client'; -import { createContext, useContext, useMemo, useState, useEffect, type ReactNode } from 'react'; +import { + createContext, + useContext, + useMemo, + useState, + useEffect, + useLayoutEffect, + type ReactNode, +} from 'react'; import { useControllableState } from '@radix-ui/react-use-controllable-state'; import { useBooks, useVersion, useTheme, useYVAuth } from '@youversion/platform-react-hooks'; import { BibleChapterPicker } from './bible-chapter-picker'; @@ -96,26 +104,34 @@ function Root({ onChange: onVersionChange, }); - const [currentFontSize, setCurrentFontSize] = useState(() => { - const validatedFontSize = - fontSize > MAX_FONT_SIZE || fontSize < MIN_FONT_SIZE ? DEFAULT_FONT_SIZE : fontSize; - if (typeof window === 'undefined') return validatedFontSize; - const size = localStorage.getItem('youversion-platform:reader:font-size'); - return size ? parseInt(size) : validatedFontSize; - }); + const validatedFontSize = + fontSize > MAX_FONT_SIZE || fontSize < MIN_FONT_SIZE ? DEFAULT_FONT_SIZE : fontSize; - const [currentFontFamily, setCurrentFontFamily] = useState(() => { - if (typeof window === 'undefined') return fontFamily; - return localStorage.getItem('youversion-platform:reader:font-family') || fontFamily; - }); + const [currentFontSize, setCurrentFontSize] = useState(validatedFontSize); + const [currentFontFamily, setCurrentFontFamily] = useState(fontFamily); + + // Load saved preferences from localStorage before paint (avoids flash of default values) + useLayoutEffect(() => { + const savedFontSize = localStorage.getItem('youversion-platform:reader:font-size'); + if (savedFontSize) { + const parsed = parseInt(savedFontSize); + if (parsed >= MIN_FONT_SIZE && parsed <= MAX_FONT_SIZE) { + setCurrentFontSize(parsed); + } + } + + const savedFontFamily = localStorage.getItem('youversion-platform:reader:font-family'); + if (savedFontFamily) { + setCurrentFontFamily(savedFontFamily); + } + }, []); + // Save preferences to localStorage when they change useEffect(() => { - if (typeof window === 'undefined') return; localStorage.setItem('youversion-platform:reader:font-size', currentFontSize.toString()); }, [currentFontSize]); useEffect(() => { - if (typeof window === 'undefined') return; localStorage.setItem('youversion-platform:reader:font-family', currentFontFamily); }, [currentFontFamily]);