diff --git a/packages/core/src/highlights/highlight-queue-drain-host.tsx b/packages/core/src/highlights/highlight-queue-drain-host.tsx index a23981cf..ea010d9d 100644 --- a/packages/core/src/highlights/highlight-queue-drain-host.tsx +++ b/packages/core/src/highlights/highlight-queue-drain-host.tsx @@ -28,15 +28,12 @@ export default function HighlightQueueDrainHost() { // scope from this same context afterwards. The forced retry uses the same // accessor with `{ force: true }` so a silent mint failure cannot look like // a freshly minted token. - const ensureFreshToken = useMemo( - () => - getAccessToken === null - ? null - : async () => { - await getAccessToken() - }, - [getAccessToken], - ) + const ensureFreshToken = + getAccessToken === null + ? null + : async () => { + await getAccessToken() + } const authRef = useRef({ userId, accessToken, ensureFreshToken, getAccessToken }) // Declared before the effects that read it: effects run in order, so the drain diff --git a/packages/ui/src/dom/bible-reader.tsx b/packages/ui/src/dom/bible-reader.tsx index 77f933e4..02677f0a 100644 --- a/packages/ui/src/dom/bible-reader.tsx +++ b/packages/ui/src/dom/bible-reader.tsx @@ -9,7 +9,7 @@ import type { } from '@youversion/platform-react-ui' import { BibleReader } from '@youversion/platform-react-ui' import type { ComponentType, ReactNode } from 'react' -import { useEffect, useMemo } from 'react' +import { useEffect } from 'react' import type { StyleProp, ViewStyle } from 'react-native' import { applySDKConfig, clearAuthResidue } from '../lib/dom-apply' @@ -158,18 +158,14 @@ export default function BibleReaderDOM(props: BibleReaderDOMProps) { // native action can only be async — so fire and forget. Catch rather than // `void`: expo's `marshal` rejects when the consumer's handler throws, and an // unattached rejection surfaces as an unhandled rejection in the DOM. - const handleVerseSelect = useMemo( - () => - onVerseSelect - ? (selection: BibleReaderVerseSelection) => { - // `Promise.resolve` so a sync handler (no bridge) can't throw on `.catch`. - Promise.resolve(onVerseSelect(selection)).catch((error: unknown) => { - console.error('[YouVersion SDK] onVerseSelect handler rejected:', error) - }) - } - : undefined, - [onVerseSelect], - ) + const handleVerseSelect = onVerseSelect + ? (selection: BibleReaderVerseSelection) => { + // `Promise.resolve` so a sync handler (no bridge) can't throw on `.catch`. + Promise.resolve(onVerseSelect(selection)).catch((error: unknown) => { + console.error('[YouVersion SDK] onVerseSelect handler rejected:', error) + }) + } + : undefined // fontFamily crosses the bridge as a quote-free token; resolve it back to the // canonical CSS stack the Web SDK expects. See lib/reader-fonts.ts. diff --git a/packages/ui/src/native/bible-card.tsx b/packages/ui/src/native/bible-card.tsx index 6a1c271a..f5aa2348 100644 --- a/packages/ui/src/native/bible-card.tsx +++ b/packages/ui/src/native/bible-card.tsx @@ -1,7 +1,7 @@ import { useControllableState } from '@radix-ui/react-use-controllable-state' import { useYouVersion, type Highlight } from '@youversion/platform-react-native-expo-core' import type { BibleVersionPickerPressData, FootnoteData } from '@youversion/platform-react-ui' -import { useCallback, useState } from 'react' +import { useState } from 'react' import { Platform } from 'react-native' import { useShallow } from 'zustand/react/shallow' import type { BibleCardProps as BibleCardDOMProps } from '../dom/bible-card' @@ -188,32 +188,23 @@ export function BibleCard({ const [footnoteData, setFootnoteData] = useState(null) const [isVersionPickerOpen, setIsVersionPickerOpen] = useState(false) - const handleVersionChange = useCallback( - async (newVersionId: number) => { - setVersionId(newVersionId) - }, - [setVersionId], - ) + const handleVersionChange = async (newVersionId: number) => { + setVersionId(newVersionId) + } - const handleVersionPickerPress = useCallback( - async (_data: BibleVersionPickerPressData) => { - if (Platform.OS === 'web') return - if (!showVersionPicker) return - if (consumerOnVersionPickerPress) { - await consumerOnVersionPickerPress(_data) - } else { - setIsVersionPickerOpen(true) - } - }, - [consumerOnVersionPickerPress, setIsVersionPickerOpen, showVersionPicker], - ) + const handleVersionPickerPress = async (_data: BibleVersionPickerPressData) => { + if (Platform.OS === 'web') return + if (!showVersionPicker) return + if (consumerOnVersionPickerPress) { + await consumerOnVersionPickerPress(_data) + } else { + setIsVersionPickerOpen(true) + } + } - const handleFootnotePress = useCallback( - async (data: FootnoteData) => { - setFootnoteData(data) - }, - [setFootnoteData], - ) + const handleFootnotePress = async (data: FootnoteData) => { + setFootnoteData(data) + } const onFootnotePress = Platform.OS !== 'web' ? (consumerOnFootnotePress ?? handleFootnotePress) : undefined diff --git a/packages/ui/src/native/bible-chapter-picker-sheet.tsx b/packages/ui/src/native/bible-chapter-picker-sheet.tsx index e15c8c6a..2006f096 100644 --- a/packages/ui/src/native/bible-chapter-picker-sheet.tsx +++ b/packages/ui/src/native/bible-chapter-picker-sheet.tsx @@ -1,6 +1,6 @@ import { useYouVersion } from '@youversion/platform-react-native-expo-core' import type { BibleChapterPickerSelectData } from '@youversion/platform-react-ui' -import { useCallback, useState } from 'react' +import { useState } from 'react' import { useSdkTranslation } from '../i18n/use-sdk-translation' import { Platform, StyleSheet, View, useWindowDimensions } from 'react-native' import ChapterPickerContentDOM from '../dom/chapter-picker-content' @@ -46,9 +46,9 @@ export function BibleChapterPickerSheet({ // rather than an effect — see https://react.dev/learn/you-might-not-need-an-effect. const [resetKey, setResetKey] = useState(0) const [dismissKeyboardNonce, setDismissKeyboardNonce] = useState(0) - const handleDismissKeyboardStart = useCallback(() => { + const handleDismissKeyboardStart = () => { setDismissKeyboardNonce((n) => n + 1) - }, []) + } const handleClose = () => { setResetKey((k) => k + 1) diff --git a/packages/ui/src/native/bible-reader-settings-sheet.tsx b/packages/ui/src/native/bible-reader-settings-sheet.tsx index cd283c6a..3a0f370b 100644 --- a/packages/ui/src/native/bible-reader-settings-sheet.tsx +++ b/packages/ui/src/native/bible-reader-settings-sheet.tsx @@ -1,6 +1,5 @@ import { useYouVersion } from '@youversion/platform-react-native-expo-core' import { createBibleThemeSettingsContentHandlers } from '@youversion/platform-react-ui' -import { useMemo } from 'react' import BibleReaderSettingsDOM from '../dom/bible-reader-settings' import { withSheetDomDefaults } from '../lib/embed-dom-props' import { encodeFontFamilyForDom } from '../lib/reader-fonts' @@ -22,18 +21,15 @@ export function BibleReaderSettingsSheet({ const { setFontFamily, setFontSize, setLineSpacing, fontSize, fontFamily, lineSpacing } = useReaderSettingsStore() - const { onFontIncreased, onFontDecreased, onFontSelected, onChangeLineSpacing } = useMemo( - () => - createBibleThemeSettingsContentHandlers({ - getFontSize: () => useReaderSettingsStore.getState().fontSize, - getFontFamily: () => useReaderSettingsStore.getState().fontFamily, - getLineSpacing: () => useReaderSettingsStore.getState().lineSpacing, - setFontSize, - setFontFamily, - setLineSpacing, - }), - [setFontSize, setFontFamily, setLineSpacing], - ) + const { onFontIncreased, onFontDecreased, onFontSelected, onChangeLineSpacing } = + createBibleThemeSettingsContentHandlers({ + getFontSize: () => useReaderSettingsStore.getState().fontSize, + getFontFamily: () => useReaderSettingsStore.getState().fontFamily, + getLineSpacing: () => useReaderSettingsStore.getState().lineSpacing, + setFontSize, + setFontFamily, + setLineSpacing, + }) return ( diff --git a/packages/ui/src/native/bible-reader.tsx b/packages/ui/src/native/bible-reader.tsx index b07b692c..d360d03e 100644 --- a/packages/ui/src/native/bible-reader.tsx +++ b/packages/ui/src/native/bible-reader.tsx @@ -323,24 +323,21 @@ export function BibleReader({ setPrompt(NO_PROMPT) } - const handleVerseSelect = useCallback( - async (next: BibleReaderVerseSelection) => { - // A new non-empty selection is the user picking other verses. Drop a - // held tap so settle cannot prompt for the old ones. An empty payload is - // our own `closeVerseActions` after the swatch press — keep the hold. - if (next.verses.length > 0 && prompt.kind === 'none' && pendingIntentRef.current !== null) { - pendingIntentRef.current = null - } - setVerseSelection(next.verses.length > 0 ? next : null) - await onVerseSelect?.(next) - }, - [onVerseSelect, prompt.kind], - ) + const handleVerseSelect = async (next: BibleReaderVerseSelection) => { + // A new non-empty selection is the user picking other verses. Drop a + // held tap so settle cannot prompt for the old ones. An empty payload is + // our own `closeVerseActions` after the swatch press — keep the hold. + if (next.verses.length > 0 && prompt.kind === 'none' && pendingIntentRef.current !== null) { + pendingIntentRef.current = null + } + setVerseSelection(next.verses.length > 0 ? next : null) + await onVerseSelect?.(next) + } - const closeVerseActions = useCallback(() => { + const closeVerseActions = () => { setVerseSelection(null) setInternalClearCount((count) => count + 1) - }, []) + } // Which circles the tray shows, projected from the same painted array the // WebView renders. A swatch can never disagree with the passage behind it. @@ -369,78 +366,65 @@ export function BibleReader({ ) }, [applyHighlight, onHighlightError, versionId, book, chapter]) - const handleSwatchPress = useCallback( - (swatch: VerseActionSwatch) => { - const verses = verseSelection?.verses ?? [] - // Read the selection first: closing drops the mirror this reads from. - closeVerseActions() - if (verses.length === 0) return - // `remove` goes straight to the unguarded write: a user looking at a - // highlight already has the permissions it needs (ADR 0016). - if (swatch.state === 'remove') { - void removeHighlight(swatch.color, verses).then((outcome) => + const handleSwatchPress = (swatch: VerseActionSwatch) => { + const verses = verseSelection?.verses ?? [] + // Read the selection first: closing drops the mirror this reads from. + closeVerseActions() + if (verses.length === 0) return + // `remove` goes straight to the unguarded write: a user looking at a + // highlight already has the permissions it needs (ADR 0016). + if (swatch.state === 'remove') { + void removeHighlight(swatch.color, verses).then((outcome) => + reportHighlightWriteError(outcome, onHighlightError), + ) + return + } + switch (authGate) { + case 'settling': + case 'signed-out': { + // The flow calls `signIn()` with no UI of its own, so the reader owns + // this pre-step: hold the intent, ask, hand it over on confirm. + // While bootstrap is still settling, hold the intent and wait. Opening + // the sheet now would prompt a stored session; applying now would drop + // a signed-out tap after the write reverts. + pendingIntentRef.current = { + color: swatch.color, + verses, + scope: { versionId, book, chapter }, + } + if (authGate === 'signed-out') { + setPrompt({ kind: 'sign-in', scope: { versionId, book, chapter } }) + } + return + } + case 'unconfigured': + case 'ready': + // Fire-and-forget: the paint is optimistic inside `useHighlights`, so + // the verse changes color on this frame instead of after the round-trip. + void applyHighlight(swatch.color, verses).then((outcome) => reportHighlightWriteError(outcome, onHighlightError), ) return + default: { + const _exhaustive: never = authGate + return _exhaustive } - switch (authGate) { - case 'settling': - case 'signed-out': { - // The flow calls `signIn()` with no UI of its own, so the reader owns - // this pre-step: hold the intent, ask, hand it over on confirm. - // While bootstrap is still settling, hold the intent and wait. Opening - // the sheet now would prompt a stored session; applying now would drop - // a signed-out tap after the write reverts. - pendingIntentRef.current = { - color: swatch.color, - verses, - scope: { versionId, book, chapter }, - } - if (authGate === 'signed-out') { - setPrompt({ kind: 'sign-in', scope: { versionId, book, chapter } }) - } - return - } - case 'unconfigured': - case 'ready': - // Fire-and-forget: the paint is optimistic inside `useHighlights`, so - // the verse changes color on this frame instead of after the round-trip. - void applyHighlight(swatch.color, verses).then((outcome) => - reportHighlightWriteError(outcome, onHighlightError), - ) - return - default: { - const _exhaustive: never = authGate - return _exhaustive - } - } - }, - [ - verseSelection, - closeVerseActions, - removeHighlight, - applyHighlight, - authGate, - onHighlightError, - versionId, - book, - chapter, - ], - ) + } + } - const handleSignInConfirm = useCallback(() => { + const handleSignInConfirm = () => { setPrompt(NO_PROMPT) // Straight back into the flow, which signs in, asks for consent if the grant // is still missing, and writes. The user never reselects the verse. replayPendingIntent() - }, [replayPendingIntent]) + } // "No Thanks", a swipe-down, a backdrop tap, and displacement all land here. // Every one discards the intent, and nothing is written. - const handleSignInDismiss = useCallback(() => { + const handleSignInDismiss = () => { pendingIntentRef.current = null setPrompt(NO_PROMPT) - }, []) + } useEffect(() => { if (authGate === 'unconfigured' || authGate === 'settling') return @@ -465,30 +449,21 @@ export function BibleReader({ } }, [authGate, prompt.kind, versionId, book, chapter, replayPendingIntent]) - const handleOpenBibleThemeSettings = useCallback(() => { + const handleOpenBibleThemeSettings = () => { setIsSettingsSheetOpen(true) - }, []) + } - const handleBookChange = useCallback( - async (b: string) => { - setBook(b) - }, - [setBook], - ) + const handleBookChange = async (b: string) => { + setBook(b) + } - const handleChapterChange = useCallback( - async (c: string) => { - setChapter(c) - }, - [setChapter], - ) + const handleChapterChange = async (c: string) => { + setChapter(c) + } - const handleVersionChange = useCallback( - async (id: number) => { - setVersionId(id) - }, - [setVersionId], - ) + const handleVersionChange = async (id: number) => { + setVersionId(id) + } const onFootnotePress = Platform.OS !== 'web' @@ -499,80 +474,68 @@ export function BibleReader({ })) : undefined - const handleChapterPickerPress = useCallback( - async (data: BibleChapterPickerPressData) => { - if (Platform.OS === 'web' || !showToolbar) return - if (consumerOnChapterPickerPress) { - await consumerOnChapterPickerPress(data) - } else { - setIsPickerOpen(true) - } - }, - [consumerOnChapterPickerPress, showToolbar], - ) + const handleChapterPickerPress = async (data: BibleChapterPickerPressData) => { + if (Platform.OS === 'web' || !showToolbar) return + if (consumerOnChapterPickerPress) { + await consumerOnChapterPickerPress(data) + } else { + setIsPickerOpen(true) + } + } - const handleVersionPickerPress = useCallback( - async (data: BibleVersionPickerPressData) => { - if (Platform.OS === 'web' || !showToolbar) return - if (consumerOnVersionPickerPress) { - await consumerOnVersionPickerPress(data) - } else { - setIsVersionPickerOpen(true) - } - }, - [consumerOnVersionPickerPress, showToolbar], - ) + const handleVersionPickerPress = async (data: BibleVersionPickerPressData) => { + if (Platform.OS === 'web' || !showToolbar) return + if (consumerOnVersionPickerPress) { + await consumerOnVersionPickerPress(data) + } else { + setIsVersionPickerOpen(true) + } + } // The consumer override wins. Otherwise the native fallback runs, because // browser defaults do not work inside an Expo DOM WebView. - const handleCopy = useCallback( - async (data: BibleReaderShareData) => { - try { - if (consumerOnCopy) { - await consumerOnCopy(data) - return - } - await Clipboard.setStringAsync(data.text) - } catch (error) { - // Swallowed. A failed copy reads to the user like a dismissed sheet, - // and there is nothing useful to say about it. - console.error('BibleReader copy failed:', error) + const handleCopy = async (data: BibleReaderShareData) => { + try { + if (consumerOnCopy) { + await consumerOnCopy(data) + return } - }, - [consumerOnCopy], - ) + await Clipboard.setStringAsync(data.text) + } catch (error) { + // Swallowed. A failed copy reads to the user like a dismissed sheet, + // and there is nothing useful to say about it. + console.error('BibleReader copy failed:', error) + } + } - const handleShare = useCallback( - async (data: BibleReaderShareData) => { - try { - if (consumerOnShare) { - await consumerOnShare(data) - return - } - await Share.share({ message: data.text }) - } catch (error) { - console.error('BibleReader share failed:', error) + const handleShare = async (data: BibleReaderShareData) => { + try { + if (consumerOnShare) { + await consumerOnShare(data) + return } - }, - [consumerOnShare], - ) + await Share.share({ message: data.text }) + } catch (error) { + console.error('BibleReader share failed:', error) + } + } // `shareData` rides in on `onVerseSelect`, so these handlers need no round-trip // back into the WebView. Read the data before `closeVerseActions` drops the // selection. - const handleCopyPress = useCallback(() => { + const handleCopyPress = () => { const data = verseSelection?.shareData closeVerseActions() if (data) void handleCopy(data) - }, [verseSelection, handleCopy, closeVerseActions]) + } - const handleSharePress = useCallback(() => { + const handleSharePress = () => { const data = verseSelection?.shareData closeVerseActions() if (data) void handleShare(data) - }, [verseSelection, handleShare, closeVerseActions]) + } - const onExternalLinkPress = useCallback(async (url: string) => { + const onExternalLinkPress = async (url: string) => { try { await WebBrowser.openBrowserAsync(url, { dismissButtonStyle: 'close', @@ -580,7 +543,7 @@ export function BibleReader({ } catch (error) { console.error(error) } - }, []) + } const showFootnoteSheet = Platform.OS !== 'web' && !consumerOnFootnotePress const showPickerSheet = Platform.OS !== 'web' && showToolbar && !consumerOnChapterPickerPress diff --git a/packages/ui/src/native/bible-version-picker-sheet.tsx b/packages/ui/src/native/bible-version-picker-sheet.tsx index 33cb9739..5811e78b 100644 --- a/packages/ui/src/native/bible-version-picker-sheet.tsx +++ b/packages/ui/src/native/bible-version-picker-sheet.tsx @@ -1,5 +1,5 @@ import { useYouVersion } from '@youversion/platform-react-native-expo-core' -import { useCallback, useState } from 'react' +import { useState } from 'react' import { useSdkTranslation } from '../i18n/use-sdk-translation' import { Platform, StyleSheet, View, useWindowDimensions } from 'react-native' import VersionPickerContentDOM from '../dom/bible-version-picker-content' @@ -39,9 +39,9 @@ export function BibleVersionPickerSheet({ const [resetKey, setResetKey] = useState(0) const [wasOpen, setWasOpen] = useState(false) const [dismissKeyboardNonce, setDismissKeyboardNonce] = useState(0) - const handleDismissKeyboardStart = useCallback(() => { + const handleDismissKeyboardStart = () => { setDismissKeyboardNonce((n) => n + 1) - }, []) + } if (isOpen !== wasOpen) { setWasOpen(isOpen) if (isOpen) setResetKey((k) => k + 1) diff --git a/packages/ui/src/native/sign-in-with-youversion-sheet.tsx b/packages/ui/src/native/sign-in-with-youversion-sheet.tsx index 02169ce7..11101fa2 100644 --- a/packages/ui/src/native/sign-in-with-youversion-sheet.tsx +++ b/packages/ui/src/native/sign-in-with-youversion-sheet.tsx @@ -1,4 +1,3 @@ -import { useMemo } from 'react' import { StyleSheet, Text, View } from 'react-native' import { useSdkTranslation } from '../i18n/use-sdk-translation' @@ -11,6 +10,7 @@ import { YouVersionPlatformLogo, youVersionPlatformLogoSize } from './youversion /** Wide enough to read at the sheet's width, narrow enough to leave margin. */ const WORDMARK_WIDTH = 190 +const WORDMARK_SIZE = youVersionPlatformLogoSize(WORDMARK_WIDTH) export type SignInWithYouVersionSheetProps = { isOpen: boolean @@ -38,7 +38,6 @@ export function SignInWithYouVersionSheet({ }: SignInWithYouVersionSheetProps) { const { t } = useSdkTranslation() const appName = resolveAppName() - const logoSize = useMemo(() => youVersionPlatformLogoSize(WORDMARK_WIDTH), []) return ( @@ -50,8 +49,8 @@ export function SignInWithYouVersionSheet({ diff --git a/packages/ui/src/native/verse-of-the-day.tsx b/packages/ui/src/native/verse-of-the-day.tsx index 66355010..b851c7af 100644 --- a/packages/ui/src/native/verse-of-the-day.tsx +++ b/packages/ui/src/native/verse-of-the-day.tsx @@ -1,6 +1,6 @@ import type { VerseOfTheDayShareData } from '@youversion/platform-react-ui' import { useYouVersion } from '@youversion/platform-react-native-expo-core' -import { useCallback, useState } from 'react' +import { useState } from 'react' import { Platform, Share } from 'react-native' import type { VerseOfTheDayProps as VerseOfTheDayDOMProps } from '../dom/verse-of-the-day' import VerseOfTheDayDOM from '../dom/verse-of-the-day' @@ -34,20 +34,17 @@ export function VerseOfTheDay({ const passageId = useVerseOfTheDayPassageId(dayOfYear) const scope = highlightScopeFor(passageId, versionId) - const handleShare = useCallback( - async (data: VerseOfTheDayShareData) => { - try { - if (consumerOnShare) { - await consumerOnShare(data) - return - } - await Share.share({ message: data.text }) - } catch (error) { - console.error('VerseOfTheDay share failed:', error) + const handleShare = async (data: VerseOfTheDayShareData) => { + try { + if (consumerOnShare) { + await consumerOnShare(data) + return } - }, - [consumerOnShare], - ) + await Share.share({ message: data.text }) + } catch (error) { + console.error('VerseOfTheDay share failed:', error) + } + } const onShare = Platform.OS !== 'web' ? handleShare : undefined