diff --git a/.changeset/rn-expo-version-filter.md b/.changeset/rn-expo-version-filter.md new file mode 100644 index 00000000..bba1738c --- /dev/null +++ b/.changeset/rn-expo-version-filter.md @@ -0,0 +1,6 @@ +--- +'@youversion/platform-react-native-expo-core': minor +'@youversion/platform-react-native-expo-ui': minor +--- + +Add optional version filter lists to `YouVersionProvider`: `permittedVersionIds`, `excludedVersionIds`, and `permittedLanguageTags`. The UI provider forwards them through native wrappers into each DOM web `YouVersionProvider`. Filtering runs in the web SDK once a 4657-shaped `@youversion/platform-react-ui` is published and pinned. diff --git a/CONTEXT.md b/CONTEXT.md index fda0b089..3b2c350c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -66,6 +66,14 @@ _Avoid_: Picker modal, chapter popover A **Native Wrapper** that hosts Bible version picker content inside one **Native Sheet**. The native side passes the current `versionId` in and receives a new `versionId` via `onSelect`. In-sheet navigation (version list ↔ language list) is owned by the **Version Picker Shell Layout** — not native. _Avoid_: Version modal, stacked picker sheets, native language-panel flags +**Version Filter**: +Optional allowlists on core `YouVersionProvider` — `permittedVersionIds`, `excludedVersionIds`, `permittedLanguageTags` — that restrict which Bible versions and languages the web SDK may use. Unset permit list = no restriction; `[]` = permit nothing; exclusion wins; language tags are BCP 47. Native stores and forwards the lists into each Expo DOM web `YouVersionProvider`; it does not re-implement the web usability predicate. +_Avoid_: Per-component filter props; a native `isUsableVersion` helper; renaming to platform-configuration types + +**Version Refuse**: +When a persisted or host `versionId` is not permitted, native chrome still passes that id into the WebView and lets the web SDK refuse. Native does not auto-pick another version, silently fall back to the default version id, or rewrite **Reader Location** / Bible Card version MMKV on refuse. First-open defaults when there is no stored or host id are unchanged. +_Avoid_: Silent 3034 swap; rewriting recents or persisted location on refuse; picker-only refuse while text still renders + **Version Picker Shell Layout**: The Expo DOM wrapper (`bible-version-picker-content.tsx`) for version picker sheet content. It owns the version ↔ language cross-fade, shell height, and keyboard overlap via `visualViewport` (same role as **Chapter Picker Shell Layout** for chapter picker). Web uses Radix popover + `isLanguagesOpen`; mobile duplicates layout outside that **Presentation Shell**. On the language trigger, call `event.preventDefault()` so the Web SDK does not also run `setIsLanguagesOpen`. _Avoid_: Assuming `BibleVersionPicker.Content` popover layout applies inside **Native Sheet** diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f409dc69..08057b7f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -81,11 +81,11 @@ commit — Android **and** iOS. New commits do not rebuild automatically. To upload the current PR head again, an approved collaborator comments one of the following on the open PR: -| Comment | Platforms rebuilt | -|---|---| -| `/app-live ` | Android **and** iOS | -| `/app-live-ios ` | iOS only | -| `/app-live-android ` | Android only | +| Comment | Platforms rebuilt | +| ------------------------- | ------------------- | +| `/app-live ` | Android **and** iOS | +| `/app-live-ios ` | iOS only | +| `/app-live-android ` | Android only | `` is the full 40-character sha of the head commit being approved, and the comment must contain nothing else. The single-platform commands are diff --git a/apps/example/app/(tabs)/profile.tsx b/apps/example/app/(tabs)/profile.tsx index 01024815..9ccb5818 100644 --- a/apps/example/app/(tabs)/profile.tsx +++ b/apps/example/app/(tabs)/profile.tsx @@ -24,7 +24,9 @@ export default function ProfileScreen() { ) : null} You are signed in as {userInfo?.name ?? '(no name)'} - {userInfo?.email ?? '(no email)'} + + {userInfo?.email ?? '(no email)'} + diff --git a/apps/example/app/_layout.tsx b/apps/example/app/_layout.tsx index 2d24ab8e..c066f83d 100644 --- a/apps/example/app/_layout.tsx +++ b/apps/example/app/_layout.tsx @@ -14,7 +14,6 @@ const REDIRECT_URI = 'youversionauth://callback' export default function RootLayout() { const appKey = process.env.EXPO_PUBLIC_YOUVERSION_APP_KEY - return ( {appKey ? ( @@ -26,6 +25,10 @@ export default function RootLayout() { scopes: ['profile', 'email'], permissions: ['highlights'], }} + // Optional version filter (forwarded to the web SDK once published): + // permittedVersionIds={[111, 206]} + // excludedVersionIds={[3034]} + // permittedLanguageTags={['en', 'zh-Hans']} > diff --git a/docs/adr/0013-native-highlights-optimistic-layer.md b/docs/adr/0013-native-highlights-optimistic-layer.md index a1218ae2..05e70157 100644 --- a/docs/adr/0013-native-highlights-optimistic-layer.md +++ b/docs/adr/0013-native-highlights-optimistic-layer.md @@ -12,7 +12,7 @@ The paint math lives in `packages/core/src/highlights/optimistic.ts`. It is a po Web never retires a remove overlay. That stops a stale GET from painting the color that was just deleted ("vapor"). The cost is unbounded: a new color from another device stays invisible until the user leaves the chapter. -We keep the vapor fix and drop most of that cost. If the server reports a *different* color, the remove entry retires. That color cannot be an echo of the deletion. It is newer data. The remaining failure needs the server two steps behind (green → yellow → remove, then a GET that still reports green). +We keep the vapor fix and drop most of that cost. If the server reports a _different_ color, the remove entry retires. That color cannot be an echo of the deletion. It is newer data. The remaining failure needs the server two steps behind (green → yellow → remove, then a GET that still reports green). `shouldRetire` is one named function for that reason. Revert to web behavior with `return false` in the remove branch. Tests pin both directions, because each side reads like a bug. diff --git a/docs/adr/0014-cached-grant-is-a-hint.md b/docs/adr/0014-cached-grant-is-a-hint.md index a46af864..a62f147f 100644 --- a/docs/adr/0014-cached-grant-is-a-hint.md +++ b/docs/adr/0014-cached-grant-is-a-hint.md @@ -13,9 +13,9 @@ The cache is a **hint**. Use it to choose UI and to skip a redundant prompt. The Clearing stays best-effort. No extra write is layered on a store that just refused a write. -| Failure | Behavior | -| --- | --- | -| Normal clear | Entry removed | +| Failure | Behavior | +| ------------- | -------------------------------------------- | +| Normal clear | Entry removed | | Removal fails | Stale grant accepted — bounded by the server | The second row needs an MMKV removal to fail. The worst outcome is a skipped prompt, then a request the server denies. Reviewers will keep rediscovering this. It is a decision, not a missing `try`. diff --git a/docs/adr/0015-data-exchange-return-scheme.md b/docs/adr/0015-data-exchange-return-scheme.md index 97d77304..6fe31839 100644 --- a/docs/adr/0015-data-exchange-return-scheme.md +++ b/docs/adr/0015-data-exchange-return-scheme.md @@ -13,11 +13,11 @@ The first version of this ADR set a hardcoded `youversionauth://callback` and to Measured on a Pixel 6 Pro API 34 with a real app key (2026-08-04): -| Registered callback | Return reached the SDK | -| --- | --- | -| App `redirectUri` only | Yes, when the SDK watched that same URI | -| SDK-owned scheme only | Yes for data exchange. Sign-in then failed with `invalid_request: redirect_uri does not match registered callback URL` | -| Both registered | The page still returned to the app key URL. A watcher on the other scheme saw `cancel` | +| Registered callback | Return reached the SDK | +| ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | +| App `redirectUri` only | Yes, when the SDK watched that same URI | +| SDK-owned scheme only | Yes for data exchange. Sign-in then failed with `invalid_request: redirect_uri does not match registered callback URL` | +| Both registered | The page still returned to the app key URL. A watcher on the other scheme saw `cancel` | Swift and Kotlin already use one URL for both flows (`youversionauth://callback`). The example app matches that. Consumers can pass their own `redirectUri`. The SDK does not care which string it is. It must match the registered callback. diff --git a/packages/core/README.md b/packages/core/README.md index f0170081..459b0832 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -54,6 +54,27 @@ export default function App() { } ``` +### Version filter + +Optional lists on `YouVersionProvider` restrict which Bible versions and languages the SDK may use. They are stored on the provider context and forwarded into each DOM component's web `YouVersionProvider` for the web SDK to enforce. + +- `permittedVersionIds?: number[]` — unset means no restriction; `[]` means permit nothing +- `excludedVersionIds?: number[]` — exclusion wins over permits +- `permittedLanguageTags?: string[]` — BCP 47 tags (e.g. `en`, `zh-Hans`) + +Native chrome does not auto-pick another version or rewrite persisted reader location when a stored or host `versionId` is refused. The id is still passed into the WebView; the web SDK handles version refuse. First-open defaults when there is no stored or host id are unchanged. + +```tsx + + {/* ... */} + +``` + ### Permissions `auth.permissions` asks for YouVersion Platform permissions (e.g. `'highlights'`) at sign-in; the user can decline. Read the grant back with `useYVAuth()`: `hasPermission(permission)`, or `grantedPermissions` for the list (`null` = nothing requested or nothing known yet, `[]` = declined). diff --git a/packages/core/src/__tests__/youversion-provider.test.tsx b/packages/core/src/__tests__/youversion-provider.test.tsx index 9ba50bec..0b2b4b08 100644 --- a/packages/core/src/__tests__/youversion-provider.test.tsx +++ b/packages/core/src/__tests__/youversion-provider.test.tsx @@ -91,4 +91,57 @@ describe('YouVersionProvider', () => { expect(screen.getByTestId('content')).toBeTruthy() expect(MockDrainHost).toHaveBeenCalled() }) + + it('stores version filter lists on context when provided', () => { + render( + + + , + ) + + expect(JSON.parse(screen.getByTestId('ctx').props.children)).toEqual( + expect.objectContaining({ + installationId: 'inst-1', + appKey: 'appkey', + apiHost: 'api.youversion.com', + permittedVersionIds: [111, 206], + excludedVersionIds: [3034], + permittedLanguageTags: ['en', 'zh-Hans'], + }), + ) + }) + + it('keeps unset version filter lists distinct from empty arrays on context', () => { + const { rerender } = render( + + + , + ) + + const unset = JSON.parse(screen.getByTestId('ctx').props.children) as Record + expect(unset.permittedVersionIds).toBeUndefined() + expect(unset.excludedVersionIds).toBeUndefined() + expect(unset.permittedLanguageTags).toBeUndefined() + + rerender( + + + , + ) + + const empty = JSON.parse(screen.getByTestId('ctx').props.children) as Record + expect(empty.permittedVersionIds).toEqual([]) + expect(empty.excludedVersionIds).toEqual([]) + expect(empty.permittedLanguageTags).toEqual([]) + }) }) diff --git a/packages/core/src/highlights/__tests__/cache.test.ts b/packages/core/src/highlights/__tests__/cache.test.ts index ebdec5ac..53054b41 100644 --- a/packages/core/src/highlights/__tests__/cache.test.ts +++ b/packages/core/src/highlights/__tests__/cache.test.ts @@ -227,7 +227,11 @@ describe('deriveServerColors', () => { it('drops invalid hex from paint projection', () => { expect( deriveServerColors( - [highlight('JHN.3.16', 'fffe00'), highlight('JHN.3.17', 'gg0000'), highlight('JHN.3.18', '123456')], + [ + highlight('JHN.3.16', 'fffe00'), + highlight('JHN.3.17', 'gg0000'), + highlight('JHN.3.18', '123456'), + ], scope, ), ).toEqual({ 16: 'fffe00', 18: '123456' }) diff --git a/packages/core/src/youversion-context.tsx b/packages/core/src/youversion-context.tsx index 8d3f7b32..bdae7aa8 100644 --- a/packages/core/src/youversion-context.tsx +++ b/packages/core/src/youversion-context.tsx @@ -13,6 +13,12 @@ export type YouVersionContextValue = { apiHost: string installationId: string authRedirectUrl?: string + /** Version filter: unset = no restriction; `[]` = permit nothing. See web SDK version filter. */ + permittedVersionIds?: number[] + /** Version filter: excluded version ids win over permits. */ + excludedVersionIds?: number[] + /** Version filter: BCP 47 language tags (e.g. `en`, `zh-Hans`). */ + permittedLanguageTags?: string[] } export const YouVersionContext = createContext(null) diff --git a/packages/core/src/youversion-provider.tsx b/packages/core/src/youversion-provider.tsx index cc376634..d0fc768f 100644 --- a/packages/core/src/youversion-provider.tsx +++ b/packages/core/src/youversion-provider.tsx @@ -10,6 +10,12 @@ export type YouVersionProviderProps = { appKey: string apiHost?: string auth?: AuthConfig + /** Version filter: unset = no restriction; `[]` = permit nothing. Forwarded to web SDK. */ + permittedVersionIds?: number[] + /** Version filter: excluded version ids win over permits. Forwarded to web SDK. */ + excludedVersionIds?: number[] + /** Version filter: BCP 47 language tags (e.g. `en`, `zh-Hans`). Forwarded to web SDK. */ + permittedLanguageTags?: string[] /** * Kept for API compatibility. Installation ID resolution is synchronous, so * children render immediately and this prop is unused. @@ -22,14 +28,33 @@ export default function YouVersionProvider({ appKey, apiHost = DEFAULT_API_HOST, auth, + permittedVersionIds, + excludedVersionIds, + permittedLanguageTags, fallback: _fallback = null, children, }: YouVersionProviderProps) { const [installationId] = useState(getOrSetInstallationId) const config = useMemo( - () => ({ installationId, appKey, apiHost, authRedirectUrl: auth?.redirectUri }), - [installationId, appKey, apiHost, auth?.redirectUri], + () => ({ + installationId, + appKey, + apiHost, + authRedirectUrl: auth?.redirectUri, + permittedVersionIds, + excludedVersionIds, + permittedLanguageTags, + }), + [ + installationId, + appKey, + apiHost, + auth?.redirectUri, + permittedVersionIds, + excludedVersionIds, + permittedLanguageTags, + ], ) return ( diff --git a/packages/ui/README.md b/packages/ui/README.md index 9f6c362f..f24fbfb5 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -47,6 +47,8 @@ export default function App() { } ``` +The UI `YouVersionProvider` forwards the same optional version filter lists as core (`permittedVersionIds`, `excludedVersionIds`, `permittedLanguageTags`). See the core package README for unset vs `[]`, exclusion wins, and native refuse behavior. + ## Documentation and API Reference - [React Native (Expo) SDK Quick Start](https://developers.youversion.com/sdks/react-native) diff --git a/packages/ui/jest.setup.js b/packages/ui/jest.setup.js index 588fa715..c54ac70f 100644 --- a/packages/ui/jest.setup.js +++ b/packages/ui/jest.setup.js @@ -108,14 +108,24 @@ jest.mock('@youversion/platform-react-native-expo-core', () => { const TestContext = React.createContext(null) - function YouVersionProvider({ appKey, apiHost, children }) { + function YouVersionProvider({ + appKey, + apiHost, + permittedVersionIds, + excludedVersionIds, + permittedLanguageTags, + children, + }) { const value = React.useMemo( () => ({ appKey, apiHost: apiHost ?? 'https://api.youversion.com', installationId: 'test-installation-id', + permittedVersionIds, + excludedVersionIds, + permittedLanguageTags, }), - [appKey, apiHost], + [appKey, apiHost, permittedVersionIds, excludedVersionIds, permittedLanguageTags], ) return React.createElement(TestContext.Provider, { value }, children) } diff --git a/packages/ui/src/dom/bible-card.tsx b/packages/ui/src/dom/bible-card.tsx index bdf17494..c2141dab 100644 --- a/packages/ui/src/dom/bible-card.tsx +++ b/packages/ui/src/dom/bible-card.tsx @@ -6,6 +6,7 @@ import type { ComponentType } from 'react' import { applySDKConfig } from '../lib/dom-apply' import { ContentSizedBody } from '../lib/content-sized-body' +import type { InternalVersionFilterProps } from '../lib/version-filter-props' import { YouVersionProvider } from '../lib/web-yv-provider' type WebBibleCardProps = import('@youversion/platform-react-ui').BibleCardProps @@ -15,7 +16,7 @@ type NativeActionBibleCardProps = WebBibleCardProps & { onFootnotePress?: (data: FootnoteData) => Promise } -export type BibleCardProps = Omit< +type BibleCardBridgeProps = Omit< WebBibleCardProps, 'onVersionChange' | 'onVersionPickerPress' | 'onFootnotePress' > & { @@ -29,6 +30,10 @@ export type BibleCardProps = Omit< dom?: import('expo/dom').DOMProps } +export type BibleCardProps = BibleCardBridgeProps + +type BibleCardDOMProps = BibleCardBridgeProps & InternalVersionFilterProps + export default function BibleCardDOM({ appKey, apiHost, @@ -37,13 +42,22 @@ export default function BibleCardDOM({ onVersionChange, onVersionPickerPress, onFootnotePress, + permittedVersionIds, + excludedVersionIds, + permittedLanguageTags, ...props -}: BibleCardProps) { +}: BibleCardDOMProps) { applySDKConfig({ appKey, apiHost, installationId }) const NativeActionBibleCard = BibleCard as ComponentType return ( - + value?.replace(/[{};]/g, '').trim() -export default function BibleReaderDOM(props: BibleReaderProps) { +export default function BibleReaderDOM(props: BibleReaderDOMProps) { const { appKey, apiHost, @@ -127,6 +130,9 @@ export default function BibleReaderDOM(props: BibleReaderProps) { backgroundColor, foregroundColor, bottomScrollPadding = 0, + permittedVersionIds, + excludedVersionIds, + permittedLanguageTags, } = props applySDKConfig({ appKey, apiHost, installationId }) @@ -282,11 +288,20 @@ export default function BibleReaderDOM(props: BibleReaderProps) { appKey={appKey} theme={theme} userInfo={providerUserInfo} + permittedVersionIds={permittedVersionIds} + excludedVersionIds={excludedVersionIds} + permittedLanguageTags={permittedLanguageTags} > {providerContent} ) : ( - + {providerContent} ) diff --git a/packages/ui/src/dom/bible-text-view.tsx b/packages/ui/src/dom/bible-text-view.tsx index ca9d05cc..bb7f47f0 100644 --- a/packages/ui/src/dom/bible-text-view.tsx +++ b/packages/ui/src/dom/bible-text-view.tsx @@ -5,6 +5,7 @@ import { BibleTextView } from '@youversion/platform-react-ui' import { applySDKConfig } from '../lib/dom-apply' import { toWebError, type DomError } from '../lib/dom-error' +import type { InternalVersionFilterProps } from '../lib/version-filter-props' import { YouVersionProvider } from '../lib/web-yv-provider' type WebBibleTextViewProps = import('@youversion/platform-react-ui').BibleTextViewProps @@ -30,6 +31,8 @@ export type BibleTextViewProps = Omit< dom?: import('expo/dom').DOMProps } +type BibleTextViewDOMProps = BibleTextViewProps & InternalVersionFilterProps + export default function BibleTextViewDOM({ appKey, apiHost, @@ -38,8 +41,11 @@ export default function BibleTextViewDOM({ onVerseSelect, onFootnotePress, passageState, + permittedVersionIds, + excludedVersionIds, + permittedLanguageTags, ...props -}: BibleTextViewProps) { +}: BibleTextViewDOMProps) { applySDKConfig({ apiHost, appKey, installationId }) const webPassageState: WebBibleTextViewProps['passageState'] = passageState != null @@ -50,7 +56,13 @@ export default function BibleTextViewDOM({ : undefined return ( - + +
{/* key remounts the picker tree on each sheet open to reset scroll and filter state */} diff --git a/packages/ui/src/dom/chapter-picker-content.tsx b/packages/ui/src/dom/chapter-picker-content.tsx index 36e4fa2a..15c92238 100644 --- a/packages/ui/src/dom/chapter-picker-content.tsx +++ b/packages/ui/src/dom/chapter-picker-content.tsx @@ -11,6 +11,7 @@ import { attachPickerKeyboardViewportListeners, resyncPickerKeyboardViewport, } from '../lib/picker-keyboard-viewport' +import type { InternalVersionFilterProps } from '../lib/version-filter-props' import { YouVersionProvider } from '../lib/web-yv-provider' export type ChapterPickerContentDOMProps = { @@ -28,6 +29,8 @@ export type ChapterPickerContentDOMProps = { dom?: import('expo/dom').DOMProps } +type ChapterPickerContentInternalProps = ChapterPickerContentDOMProps & InternalVersionFilterProps + export default function ChapterPickerContentDOM({ appKey, book, @@ -38,7 +41,10 @@ export default function ChapterPickerContentDOM({ dismissKeyboardNonce, resetKey, onSelect, -}: ChapterPickerContentDOMProps) { + permittedVersionIds, + excludedVersionIds, + permittedLanguageTags, +}: ChapterPickerContentInternalProps) { useDismissKeyboardOnClose(isOpen) useDismissKeyboardOnSignal(dismissKeyboardNonce) @@ -57,7 +63,13 @@ export default function ChapterPickerContentDOM({ }, [isOpen]) return ( - +
{/* key remounts the picker tree when resetKey changes (on sheet close) to clear scroll and filter state */} diff --git a/packages/ui/src/dom/verse-of-the-day.tsx b/packages/ui/src/dom/verse-of-the-day.tsx index 825c9639..354f9274 100644 --- a/packages/ui/src/dom/verse-of-the-day.tsx +++ b/packages/ui/src/dom/verse-of-the-day.tsx @@ -4,6 +4,7 @@ import { VerseOfTheDay } from '@youversion/platform-react-ui' import type { VerseOfTheDayProps as WebVerseOfTheDayProps } from '@youversion/platform-react-ui' import { applySDKConfig } from '../lib/dom-apply' import { ContentSizedBody } from '../lib/content-sized-body' +import type { InternalVersionFilterProps } from '../lib/version-filter-props' import { YouVersionProvider } from '../lib/web-yv-provider' export type VerseOfTheDayProps = WebVerseOfTheDayProps & { @@ -14,18 +15,29 @@ export type VerseOfTheDayProps = WebVerseOfTheDayProps & { dom?: import('expo/dom').DOMProps } +type VerseOfTheDayDOMProps = VerseOfTheDayProps & InternalVersionFilterProps + export default function VerseOfTheDayDOM({ appKey, apiHost, installationId, theme = 'light', onShare, + permittedVersionIds, + excludedVersionIds, + permittedLanguageTags, ...props -}: VerseOfTheDayProps) { +}: VerseOfTheDayDOMProps) { applySDKConfig({ appKey, apiHost, installationId }) return ( - + diff --git a/packages/ui/src/lib/__tests__/report-highlight-write-error.test.ts b/packages/ui/src/lib/__tests__/report-highlight-write-error.test.ts index 7f769641..8ed2a57b 100644 --- a/packages/ui/src/lib/__tests__/report-highlight-write-error.test.ts +++ b/packages/ui/src/lib/__tests__/report-highlight-write-error.test.ts @@ -5,12 +5,8 @@ import { type HighlightWriteError, } from '../report-highlight-write-error' -type AssertQueuedHasNoReason = Extract< - HighlightWriteError, - { status: 'queued' } -> extends { reason?: unknown } - ? never - : true +type AssertQueuedHasNoReason = + Extract extends { reason?: unknown } ? never : true const assertQueuedHasNoReason: AssertQueuedHasNoReason = true void assertQueuedHasNoReason @@ -47,9 +43,7 @@ describe('reportHighlightWriteError', () => { }) it('does nothing when no handler is passed', () => { - expect(() => - reportHighlightWriteError({ status: 'queued', verses: [1, 2] }), - ).not.toThrow() + expect(() => reportHighlightWriteError({ status: 'queued', verses: [1, 2] })).not.toThrow() }) it('swallows a throwing onHighlightError callback', () => { diff --git a/packages/ui/src/lib/__tests__/web-yv-provider.test.tsx b/packages/ui/src/lib/__tests__/web-yv-provider.test.tsx index 9ccf7765..a7674710 100644 --- a/packages/ui/src/lib/__tests__/web-yv-provider.test.tsx +++ b/packages/ui/src/lib/__tests__/web-yv-provider.test.tsx @@ -1,4 +1,6 @@ -import type { ReactElement } from 'react' +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { type ReactElement } from 'react' import pkg from '../../../package.json' import { YouVersionProvider } from '../web-yv-provider' @@ -15,7 +17,12 @@ jest.mock('@youversion/platform-react-ui', () => ({ // break these. const SDK_HEADER_VALUE = `ReactNativeSDK=${pkg.version}-dev` -type RenderedProps = { additionalHeaders?: Record } +type RenderedProps = { + additionalHeaders?: Record + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] +} function renderShim(props: Record): RenderedProps { const element = YouVersionProvider({ @@ -26,6 +33,15 @@ function renderShim(props: Record): RenderedProps { return element.props } +const DOM_ENTRIES = [ + 'bible-card.tsx', + 'bible-reader.tsx', + 'bible-text-view.tsx', + 'verse-of-the-day.tsx', + 'bible-version-picker-content.tsx', + 'chapter-picker-content.tsx', +] as const + describe('web YouVersionProvider', () => { it('injects the x-yvp-sdk header when consumer passes no additionalHeaders', () => { expect(renderShim({}).additionalHeaders).toEqual({ @@ -50,4 +66,44 @@ describe('web YouVersionProvider', () => { 'x-custom': 'ok', }) }) + + it('forwards version filter lists to the web YouVersionProvider', () => { + expect( + renderShim({ + permittedVersionIds: [111, 206], + excludedVersionIds: [3034], + permittedLanguageTags: ['en', 'zh-Hans'], + }), + ).toEqual( + expect.objectContaining({ + permittedVersionIds: [111, 206], + excludedVersionIds: [3034], + permittedLanguageTags: ['en', 'zh-Hans'], + }), + ) + }) + + it('forwards empty version filter arrays without coercing to undefined', () => { + const props = renderShim({ + permittedVersionIds: [], + excludedVersionIds: [], + permittedLanguageTags: [], + }) + expect(props.permittedVersionIds).toEqual([]) + expect(props.excludedVersionIds).toEqual([]) + expect(props.permittedLanguageTags).toEqual([]) + }) + + // Layer-3 mocks replace every `'use dom'` entry, so nothing observes whether + // those files pass filter lists into web YouVersionProvider. Source asserts + // (same pattern as bible-reader-highlights-bridge) close that seam. + it.each(DOM_ENTRIES)( + '%s source forwards version filter lists onto web YouVersionProvider', + (filename) => { + const source = readFileSync(join(__dirname, '../../dom', filename), 'utf8') + expect(source).toMatch(/^\s*permittedVersionIds=\{permittedVersionIds\}$/m) + expect(source).toMatch(/^\s*excludedVersionIds=\{excludedVersionIds\}$/m) + expect(source).toMatch(/^\s*permittedLanguageTags=\{permittedLanguageTags\}$/m) + }, + ) }) diff --git a/packages/ui/src/lib/verse-action-swatches.ts b/packages/ui/src/lib/verse-action-swatches.ts index 44abd7e5..cc7bb405 100644 --- a/packages/ui/src/lib/verse-action-swatches.ts +++ b/packages/ui/src/lib/verse-action-swatches.ts @@ -71,8 +71,7 @@ export function buildVerseActionSwatches( // "all active"; activeHighlights.size > 1 counts all valid colors (palette or // non-palette). Matches web buildVerseActionSwatches (platform-sdk-react #330). const showAllApplyColors = - !allPaletteColorsActive && - (unHighlightedCount > 0 || activeHighlights.size > 1) + !allPaletteColorsActive && (unHighlightedCount > 0 || activeHighlights.size > 1) const colorsToApply = showAllApplyColors ? HIGHLIGHT_COLORS : HIGHLIGHT_COLORS.filter((color) => !activePaletteColors.has(color)) @@ -81,9 +80,9 @@ export function buildVerseActionSwatches( ...HIGHLIGHT_COLORS.filter((color) => activePaletteColors.has(color)).map( (color): VerseActionSwatch => ({ color, state: 'remove' }), ), - ...[...activeNonPaletteColors].sort().map( - (color): VerseActionSwatch => ({ color, state: 'remove' }), - ), + ...[...activeNonPaletteColors] + .sort() + .map((color): VerseActionSwatch => ({ color, state: 'remove' })), ] return [ diff --git a/packages/ui/src/lib/version-filter-props.ts b/packages/ui/src/lib/version-filter-props.ts new file mode 100644 index 00000000..fbdc2fc1 --- /dev/null +++ b/packages/ui/src/lib/version-filter-props.ts @@ -0,0 +1,7 @@ +import type { YouVersionContextValue } from '@youversion/platform-react-native-expo-core' + +/** Internal bridge plumbing from core context into web `YouVersionProvider`. Not a public component API. */ +export type InternalVersionFilterProps = Pick< + YouVersionContextValue, + 'permittedVersionIds' | 'excludedVersionIds' | 'permittedLanguageTags' +> diff --git a/packages/ui/src/lib/web-yv-provider.ts b/packages/ui/src/lib/web-yv-provider.ts index 3ed33ab5..4f9ceb91 100644 --- a/packages/ui/src/lib/web-yv-provider.ts +++ b/packages/ui/src/lib/web-yv-provider.ts @@ -10,10 +10,13 @@ import { getSdkHeaders } from './sdk-version' ensureDomLocalStorage() // `additionalHeaders` ships in the next Web SDK release; widen the prop type -// locally until that publishes. +// locally until that publishes. Same for version filter lists (YPE-4657). type BaseProps = ComponentProps type ProviderProps = BaseProps & { additionalHeaders?: Record + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] } const TypedProvider = BaseYouVersionProvider as ComponentType diff --git a/packages/ui/src/native/__tests__/bible-card-version-persistence.test.tsx b/packages/ui/src/native/__tests__/bible-card-version-persistence.test.tsx index cc266c60..0a1f366e 100644 --- a/packages/ui/src/native/__tests__/bible-card-version-persistence.test.tsx +++ b/packages/ui/src/native/__tests__/bible-card-version-persistence.test.tsx @@ -83,6 +83,12 @@ const wrapper = ({ children }: { children: ReactNode }) => ( ) +const refuseFilterWrapper = ({ children }: { children: ReactNode }) => ( + + {children} + +) + async function resetBibleCardVersionStore() { mmkvStorage.remove(BIBLE_CARD_VERSION_PERSIST_KEY) useBibleCardVersionStore.setState(bibleCardVersionStoreInitialState) @@ -169,4 +175,23 @@ describe('BibleCard version persistence', () => { expect(latestDomProps.versionId).toBe(59) }) + + it('passes a stored versionId into the DOM when version filter lists would refuse it', async () => { + await seedBibleCardVersion(59) + + render(, { wrapper: refuseFilterWrapper }) + + expect(latestDomProps.versionId).toBe(59) + + const raw = mmkvStorage.getString(BIBLE_CARD_VERSION_PERSIST_KEY) + expect(raw).toBeTruthy() + const parsed = JSON.parse(raw!) as { state: { versionId?: number } } + expect(parsed.state.versionId).toBe(59) + }) + + it('passes a host versionId into the DOM when version filter lists would refuse it', () => { + render(, { wrapper: refuseFilterWrapper }) + + expect(latestDomProps.versionId).toBe(59) + }) }) diff --git a/packages/ui/src/native/__tests__/bible-card.test.tsx b/packages/ui/src/native/__tests__/bible-card.test.tsx index d552c9d3..2f49398a 100644 --- a/packages/ui/src/native/__tests__/bible-card.test.tsx +++ b/packages/ui/src/native/__tests__/bible-card.test.tsx @@ -6,6 +6,7 @@ import { Platform } from 'react-native' import type { ReactNode } from 'react' import { BibleCard } from '../bible-card' +import { YouVersionProvider } from '../youversion-provider' import { bibleCardVersionStoreInitialState, useBibleCardVersionStore, @@ -76,6 +77,9 @@ jest.mock('../../dom/footnote-content', () => { let latestDomProps: { dom?: { matchContents?: boolean; containerStyle?: unknown } + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] } = {} jest.mock('../../dom/bible-card', () => { @@ -146,6 +150,35 @@ describe('BibleCard', () => { expect(getByTestId('mock-dom-match-contents').children).toContain('1') }) + function versionFilterWrapper(lists: { + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] + }) { + function FilterWrapper({ children }: { children: ReactNode }) { + return ( + + {children} + + ) + } + return FilterWrapper + } + + it('forwards version filter lists from YouVersionProvider to the DOM entry', () => { + render(, { + wrapper: versionFilterWrapper({ + permittedVersionIds: [111], + excludedVersionIds: [3034], + permittedLanguageTags: ['en'], + }), + }) + + expect(latestDomProps.permittedVersionIds).toEqual([111]) + expect(latestDomProps.excludedVersionIds).toEqual([3034]) + expect(latestDomProps.permittedLanguageTags).toEqual(['en']) + }) + it('applies the embed dom defaults when no dom prop is passed', () => { render(, { wrapper: wrapper(), diff --git a/packages/ui/src/native/__tests__/bible-chapter-picker-sheet.test.tsx b/packages/ui/src/native/__tests__/bible-chapter-picker-sheet.test.tsx index 50b6b9ce..ee642e64 100644 --- a/packages/ui/src/native/__tests__/bible-chapter-picker-sheet.test.tsx +++ b/packages/ui/src/native/__tests__/bible-chapter-picker-sheet.test.tsx @@ -8,6 +8,9 @@ import type { BibleChapterPickerSelectData } from '@youversion/platform-react-ui let latestDomProps: { theme?: string resetKey?: number + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] onSelect?: (data: BibleChapterPickerSelectData) => Promise } = {} @@ -20,6 +23,9 @@ jest.mock('../../dom/chapter-picker-content', () => { default: function MockDOM(props: { theme?: string resetKey?: number + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] onSelect?: (data: BibleChapterPickerSelectData) => Promise }) { latestDomProps = props @@ -75,6 +81,20 @@ const wrapper = ({ children }: { children: ReactNode }) => ( ) +function versionFilterWrapper(lists: { + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] +}) { + return function FilterWrapper({ children }: { children: ReactNode }) { + return ( + + {children} + + ) + } +} + const SAMPLE_SELECTION: BibleChapterPickerSelectData = { book: 'GEN', chapter: '3', @@ -174,4 +194,32 @@ describe('BibleChapterPickerSheet', () => { expect(latestDomProps.resetKey).toBeGreaterThan(firstKey!) }) + + it('forwards version filter lists from YouVersionProvider to DOM content', () => { + render( {}} />, { + wrapper: versionFilterWrapper({ + permittedVersionIds: [111], + excludedVersionIds: [3034], + permittedLanguageTags: ['en'], + }), + }) + + expect(latestDomProps.permittedVersionIds).toEqual([111]) + expect(latestDomProps.excludedVersionIds).toEqual([3034]) + expect(latestDomProps.permittedLanguageTags).toEqual(['en']) + }) + + it('forwards empty version filter arrays to DOM content without coercing to undefined', () => { + render( {}} />, { + wrapper: versionFilterWrapper({ + permittedVersionIds: [], + excludedVersionIds: [], + permittedLanguageTags: [], + }), + }) + + expect(latestDomProps.permittedVersionIds).toEqual([]) + expect(latestDomProps.excludedVersionIds).toEqual([]) + expect(latestDomProps.permittedLanguageTags).toEqual([]) + }) }) diff --git a/packages/ui/src/native/__tests__/bible-reader-consumer-api.test.tsx b/packages/ui/src/native/__tests__/bible-reader-consumer-api.test.tsx index 108a95cb..5bc500b2 100644 --- a/packages/ui/src/native/__tests__/bible-reader-consumer-api.test.tsx +++ b/packages/ui/src/native/__tests__/bible-reader-consumer-api.test.tsx @@ -22,13 +22,13 @@ const SELECTION: BibleReaderVerseSelection = { shareData: null, } -const highlightPermissionFlowApply = jest.fn< - Promise, - [string, number[]] ->(async () => ({ status: 'ok', verses: [1, 2] })) -const rawRemove = jest.fn, [string, number[]]>( +const highlightPermissionFlowApply = jest.fn, [string, number[]]>( async () => ({ status: 'ok', verses: [1, 2] }), ) +const rawRemove = jest.fn, [string, number[]]>(async () => ({ + status: 'ok', + verses: [1, 2], +})) const refreshHighlights = jest.fn(async () => undefined) function stubHighlightPermissionFlow() { diff --git a/packages/ui/src/native/__tests__/bible-reader-location.test.tsx b/packages/ui/src/native/__tests__/bible-reader-location.test.tsx index 8b8b6e98..5897e970 100644 --- a/packages/ui/src/native/__tests__/bible-reader-location.test.tsx +++ b/packages/ui/src/native/__tests__/bible-reader-location.test.tsx @@ -10,6 +10,12 @@ import { import { BibleReader } from '../bible-reader' import { YouVersionProvider } from '../youversion-provider' +let latestReaderDomProps: { + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] +} = {} + jest.mock('../../dom/bible-reader', () => { // eslint-disable-next-line @typescript-eslint/no-require-imports const { View, Text, Pressable } = require('react-native') @@ -19,10 +25,14 @@ jest.mock('../../dom/bible-reader', () => { book?: string chapter?: string versionId?: number + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] onBookChange?: (book: string) => Promise onChapterChange?: (chapter: string) => Promise onVersionChange?: (versionId: number) => Promise }) { + latestReaderDomProps = props return ( {props.book ?? 'none'} @@ -99,6 +109,26 @@ const wrapper = ({ children }: { children: ReactNode }) => ( ) +const refuseFilterWrapper = ({ children }: { children: ReactNode }) => ( + + {children} + +) + +function versionFilterWrapper(lists: { + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] +}) { + return function FilterWrapper({ children }: { children: ReactNode }) { + return ( + + {children} + + ) + } +} + async function resetReaderLocationStore() { mmkvStorage.clearAll() useReaderLocationStore.setState(readerLocationStoreInitialState) @@ -118,6 +148,7 @@ async function seedReaderLocation(location: { book: string; chapter: string; ver describe('BibleReader Reader Location persistence', () => { beforeEach(async () => { + latestReaderDomProps = {} await resetReaderLocationStore() }) @@ -176,4 +207,37 @@ describe('BibleReader Reader Location persistence', () => { const parsed = JSON.parse(raw!) as { state: { chapter?: string } } expect(parsed.state.chapter).toBe('5') }) + + it('passes a stored versionId into the DOM when version filter lists would refuse it', async () => { + await seedReaderLocation({ book: 'GEN', chapter: '2', versionId: 59 }) + + const { getByTestId } = render(, { wrapper: refuseFilterWrapper }) + + expect(getByTestId('version-id').props.children).toBe('59') + + const raw = mmkvStorage.getString(READER_LOCATION_PERSIST_KEY) + expect(raw).toBeTruthy() + const parsed = JSON.parse(raw!) as { state: { versionId?: number } } + expect(parsed.state.versionId).toBe(59) + }) + + it('passes a host versionId into the DOM when version filter lists would refuse it', () => { + const { getByTestId } = render(, { wrapper: refuseFilterWrapper }) + + expect(getByTestId('version-id').props.children).toBe('59') + }) + + it('forwards version filter lists from YouVersionProvider to the DOM entry', () => { + render(, { + wrapper: versionFilterWrapper({ + permittedVersionIds: [111], + excludedVersionIds: [3034], + permittedLanguageTags: ['en'], + }), + }) + + expect(latestReaderDomProps.permittedVersionIds).toEqual([111]) + expect(latestReaderDomProps.excludedVersionIds).toEqual([3034]) + expect(latestReaderDomProps.permittedLanguageTags).toEqual(['en']) + }) }) diff --git a/packages/ui/src/native/__tests__/bible-text-view.test.tsx b/packages/ui/src/native/__tests__/bible-text-view.test.tsx index 7ad440f3..f4d43c4e 100644 --- a/packages/ui/src/native/__tests__/bible-text-view.test.tsx +++ b/packages/ui/src/native/__tests__/bible-text-view.test.tsx @@ -4,6 +4,7 @@ import { Platform } from 'react-native' import type { ReactNode } from 'react' import { BibleTextView } from '../bible-text-view' +import { YouVersionProvider } from '../youversion-provider' import { youVersionProviderWrapper as wrapper } from '../../test-utils/youversion-provider-wrapper' const sampleFootnote: FootnoteData = { @@ -12,6 +13,12 @@ const sampleFootnote: FootnoteData = { verseHtml: '

footnote

', } +let latestTextViewDomProps: { + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] +} = {} + jest.mock('../../dom/bible-text-view', () => { // eslint-disable-next-line @typescript-eslint/no-require-imports const { Pressable, Text, View } = require('react-native') @@ -24,8 +31,12 @@ jest.mock('../../dom/bible-text-view', () => { showVerseNumbers?: boolean fontSize?: number theme?: string + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] onFootnotePress?: (data: FootnoteData) => Promise }) { + latestTextViewDomProps = props return ( {props.appKey} @@ -103,6 +114,10 @@ jest.mock('../native-sheet', () => { describe('BibleTextView', () => { const originalOs = Platform.OS + beforeEach(() => { + latestTextViewDomProps = {} + }) + afterEach(() => { Object.defineProperty(Platform, 'OS', { configurable: true, @@ -208,4 +223,24 @@ describe('BibleTextView', () => { expect(getByTestId('mock-footnote-theme').children).toContain('light') }) + + it('forwards version filter lists from YouVersionProvider to the DOM entry', () => { + render(, { + wrapper: ({ children }: { children: ReactNode }) => ( + + {children} + + ), + }) + + expect(latestTextViewDomProps.permittedVersionIds).toEqual([111]) + expect(latestTextViewDomProps.excludedVersionIds).toEqual([3034]) + expect(latestTextViewDomProps.permittedLanguageTags).toEqual(['en']) + }) }) diff --git a/packages/ui/src/native/__tests__/bible-version-picker-sheet.test.tsx b/packages/ui/src/native/__tests__/bible-version-picker-sheet.test.tsx index f4ee51fe..338e3431 100644 --- a/packages/ui/src/native/__tests__/bible-version-picker-sheet.test.tsx +++ b/packages/ui/src/native/__tests__/bible-version-picker-sheet.test.tsx @@ -9,6 +9,9 @@ type MockDomProps = { theme?: string versionId?: number resetKey?: number + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] onVersionChange?: (versionId: number) => Promise } @@ -56,6 +59,20 @@ const wrapper = ({ children }: { children: ReactNode }) => ( ) +function versionFilterWrapper(lists: { + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] +}) { + return function FilterWrapper({ children }: { children: ReactNode }) { + return ( + + {children} + + ) + } +} + describe('BibleVersionPickerSheet', () => { beforeEach(() => { latestDomProps = {} @@ -168,4 +185,32 @@ describe('BibleVersionPickerSheet', () => { expect(latestDomProps).not.toHaveProperty('showLanguagePicker') expect(latestDomProps).not.toHaveProperty('handleShowLanguagePicker') }) + + it('forwards version filter lists from YouVersionProvider to DOM content', () => { + render( {}} />, { + wrapper: versionFilterWrapper({ + permittedVersionIds: [111], + excludedVersionIds: [3034], + permittedLanguageTags: ['en'], + }), + }) + + expect(latestDomProps.permittedVersionIds).toEqual([111]) + expect(latestDomProps.excludedVersionIds).toEqual([3034]) + expect(latestDomProps.permittedLanguageTags).toEqual(['en']) + }) + + it('forwards empty version filter arrays to DOM content without coercing to undefined', () => { + render( {}} />, { + wrapper: versionFilterWrapper({ + permittedVersionIds: [], + excludedVersionIds: [], + permittedLanguageTags: [], + }), + }) + + expect(latestDomProps.permittedVersionIds).toEqual([]) + expect(latestDomProps.excludedVersionIds).toEqual([]) + expect(latestDomProps.permittedLanguageTags).toEqual([]) + }) }) diff --git a/packages/ui/src/native/__tests__/verse-of-the-day.test.tsx b/packages/ui/src/native/__tests__/verse-of-the-day.test.tsx index 9b32371b..50d98d14 100644 --- a/packages/ui/src/native/__tests__/verse-of-the-day.test.tsx +++ b/packages/ui/src/native/__tests__/verse-of-the-day.test.tsx @@ -5,6 +5,7 @@ import { Platform, Share } from 'react-native' import { youVersionProviderWrapper as wrapper } from '../../test-utils/youversion-provider-wrapper' import { VerseOfTheDay } from '../verse-of-the-day' +import { YouVersionProvider } from '../youversion-provider' const sampleShareData: VerseOfTheDayShareData = { text: 'For God so loved the world...\n\nJohn 3:16 NIV', @@ -16,6 +17,9 @@ let latestDomProps: { appKey?: string versionId?: number theme?: string + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] dom?: { matchContents?: boolean; containerStyle?: unknown } onShare?: (data: VerseOfTheDayShareData) => Promise } = {} @@ -29,6 +33,9 @@ jest.mock('../../dom/verse-of-the-day', () => { appKey: string versionId?: number theme?: string + permittedVersionIds?: number[] + excludedVersionIds?: number[] + permittedLanguageTags?: string[] dom?: { matchContents?: boolean } onShare?: (data: VerseOfTheDayShareData) => Promise }) { @@ -215,4 +222,24 @@ describe('VerseOfTheDay', () => { }) expect(Share.share).not.toHaveBeenCalled() }) + + it('forwards version filter lists from YouVersionProvider to the DOM entry', () => { + render(, { + wrapper: ({ children }: { children: React.ReactNode }) => ( + + {children} + + ), + }) + + expect(latestDomProps.permittedVersionIds).toEqual([111]) + expect(latestDomProps.excludedVersionIds).toEqual([3034]) + expect(latestDomProps.permittedLanguageTags).toEqual(['en']) + }) }) diff --git a/packages/ui/src/native/__tests__/youversion-provider.test.tsx b/packages/ui/src/native/__tests__/youversion-provider.test.tsx index 80b3ef46..6aa2b0a7 100644 --- a/packages/ui/src/native/__tests__/youversion-provider.test.tsx +++ b/packages/ui/src/native/__tests__/youversion-provider.test.tsx @@ -4,8 +4,13 @@ import { Text } from 'react-native' import { useLocale } from '../../i18n/locale-context' import { YouVersionProvider } from '../youversion-provider' +let latestCoreProviderProps: Record = {} + jest.mock('@youversion/platform-react-native-expo-core', () => ({ - YouVersionProvider: ({ children }: { children: React.ReactNode }) => children, + YouVersionProvider: (props: Record) => { + latestCoreProviderProps = props + return props.children + }, })) jest.mock('expo-localization', () => ({ @@ -26,6 +31,10 @@ function LocaleProbe() { } describe('YouVersionProvider locale', () => { + beforeEach(() => { + latestCoreProviderProps = {} + }) + // `xx` is not a real language code, so it stays unbundled as locales are synced. it('falls back to en when locale prop is omitted and device locale is unsupported', () => { useLocalesMock.mockReturnValue([{ languageTag: 'xx-XX', languageCode: 'xx' }]) @@ -59,4 +68,43 @@ describe('YouVersionProvider locale', () => { expect(getByTestId('i18n-language').children).toContain('en') }) + + it('forwards version filter lists to core YouVersionProvider', () => { + render( + + + , + ) + + expect(latestCoreProviderProps).toEqual( + expect.objectContaining({ + appKey: 'test-key', + permittedVersionIds: [111], + excludedVersionIds: [3034], + permittedLanguageTags: ['en'], + }), + ) + }) + + it('forwards empty version filter arrays to core without coercing to undefined', () => { + render( + + + , + ) + + expect(latestCoreProviderProps.permittedVersionIds).toEqual([]) + expect(latestCoreProviderProps.excludedVersionIds).toEqual([]) + expect(latestCoreProviderProps.permittedLanguageTags).toEqual([]) + }) }) diff --git a/packages/ui/src/native/bible-card.tsx b/packages/ui/src/native/bible-card.tsx index da4aec80..a62048b2 100644 --- a/packages/ui/src/native/bible-card.tsx +++ b/packages/ui/src/native/bible-card.tsx @@ -120,6 +120,9 @@ export function BibleCard({ appKey={context.appKey} apiHost={context.apiHost} installationId={context.installationId} + permittedVersionIds={context.permittedVersionIds} + excludedVersionIds={context.excludedVersionIds} + permittedLanguageTags={context.permittedLanguageTags} theme={resolvedTheme} versionId={versionId} onVersionChange={handleVersionChange} diff --git a/packages/ui/src/native/bible-chapter-picker-sheet.tsx b/packages/ui/src/native/bible-chapter-picker-sheet.tsx index 8a428908..e15c8c6a 100644 --- a/packages/ui/src/native/bible-chapter-picker-sheet.tsx +++ b/packages/ui/src/native/bible-chapter-picker-sheet.tsx @@ -99,6 +99,9 @@ export function BibleChapterPickerSheet({ dismissKeyboardNonce={dismissKeyboardNonce} resetKey={resetKey} onSelect={handleSelect} + permittedVersionIds={context.permittedVersionIds} + excludedVersionIds={context.excludedVersionIds} + permittedLanguageTags={context.permittedLanguageTags} /> diff --git a/packages/ui/src/native/bible-reader.tsx b/packages/ui/src/native/bible-reader.tsx index 2ff4a56e..b07b692c 100644 --- a/packages/ui/src/native/bible-reader.tsx +++ b/packages/ui/src/native/bible-reader.tsx @@ -616,6 +616,9 @@ export function BibleReader({ appKey={context.appKey} apiHost={context.apiHost} installationId={context.installationId} + permittedVersionIds={context.permittedVersionIds} + excludedVersionIds={context.excludedVersionIds} + permittedLanguageTags={context.permittedLanguageTags} highlights={highlights} verseActions={VERSE_ACTIONS} onVerseSelect={handleVerseSelect} diff --git a/packages/ui/src/native/bible-text-view.tsx b/packages/ui/src/native/bible-text-view.tsx index 0ca36457..0575654d 100644 --- a/packages/ui/src/native/bible-text-view.tsx +++ b/packages/ui/src/native/bible-text-view.tsx @@ -57,6 +57,9 @@ export function BibleTextView({ appKey={context.appKey} apiHost={context.apiHost} installationId={context.installationId} + permittedVersionIds={context.permittedVersionIds} + excludedVersionIds={context.excludedVersionIds} + permittedLanguageTags={context.permittedLanguageTags} theme={theme} onFootnotePress={onFootnotePress} /> diff --git a/packages/ui/src/native/bible-version-picker-sheet.tsx b/packages/ui/src/native/bible-version-picker-sheet.tsx index 8c01947a..33cb9739 100644 --- a/packages/ui/src/native/bible-version-picker-sheet.tsx +++ b/packages/ui/src/native/bible-version-picker-sheet.tsx @@ -89,6 +89,9 @@ export function BibleVersionPickerSheet({ isOpen={isOpen} dismissKeyboardNonce={dismissKeyboardNonce} onVersionChange={handleVersionChange} + permittedVersionIds={context.permittedVersionIds} + excludedVersionIds={context.excludedVersionIds} + permittedLanguageTags={context.permittedLanguageTags} />
diff --git a/packages/ui/src/native/verse-of-the-day.tsx b/packages/ui/src/native/verse-of-the-day.tsx index f1954c35..db2ff8a9 100644 --- a/packages/ui/src/native/verse-of-the-day.tsx +++ b/packages/ui/src/native/verse-of-the-day.tsx @@ -45,6 +45,9 @@ export function VerseOfTheDay({ appKey={context.appKey} apiHost={context.apiHost} installationId={context.installationId} + permittedVersionIds={context.permittedVersionIds} + excludedVersionIds={context.excludedVersionIds} + permittedLanguageTags={context.permittedLanguageTags} theme={theme ?? themeContext} onShare={onShare} /> diff --git a/packages/ui/src/native/youversion-provider.tsx b/packages/ui/src/native/youversion-provider.tsx index 7745d4e1..58baa712 100644 --- a/packages/ui/src/native/youversion-provider.tsx +++ b/packages/ui/src/native/youversion-provider.tsx @@ -18,6 +18,12 @@ export type YouVersionProviderProps = { /** When omitted, native SDK strings follow the device locale (expo-localization). */ locale?: string auth?: AuthConfig + /** Version filter: unset = no restriction; `[]` = permit nothing. Forwarded to core. */ + permittedVersionIds?: number[] + /** Version filter: excluded version ids win over permits. Forwarded to core. */ + excludedVersionIds?: number[] + /** Version filter: BCP 47 language tags (e.g. `en`, `zh-Hans`). Forwarded to core. */ + permittedLanguageTags?: string[] fallback?: ReactNode children: ReactNode } @@ -28,6 +34,9 @@ export function YouVersionProvider({ theme = 'system', locale, auth, + permittedVersionIds, + excludedVersionIds, + permittedLanguageTags, fallback, children, }: YouVersionProviderProps) { @@ -35,7 +44,15 @@ export function YouVersionProvider({ const resolvedTheme = resolveTheme(theme, colorScheme) return ( - + {children}