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
7 changes: 7 additions & 0 deletions .changeset/usable-bible-version-filter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@youversion/platform-core': minor
'@youversion/platform-react-hooks': minor
'@youversion/platform-react-ui': minor
---

Add `permittedVersionIds`, `excludedVersionIds`, and `permittedLanguageTags` so apps can limit which Bible versions the SDK uses (YPE-4657).
60 changes: 60 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,66 @@ calls this `version_id`; the highlights wire API calls the same value
`bible_id`. The SDK name is canonical in public types; mapping happens at
the API boundary only.

A Bible version is **usable** for an app only when it satisfies the
integrator **version filter**. An unusable id is not a Bible version this
app may use: lists omit it, pickers omit it, restored recents omit it, and
content fetches refuse it. The host passing `versionId` does not override
the filter.

## Version filter

Integrator policy (YPE-4657) for which Bible versions this app may use,
set on `YouVersionProvider` and stored on
`YouVersionPlatformConfiguration` (same path as `appName` /
`signInPromptMessage`). Three optional lists, Swift names, React `Id`
spelling:

- `permittedVersionIds` — allowlist of Bible version ids
- `excludedVersionIds` — denylist of Bible version ids
- `permittedLanguageTags` — allowlist of BCP 47 tags (`en`, `zh-Hans`).
This is `BibleVersion.language_tag` / `Language.id`, not a numeric id.
_Avoid_: `permittedLanguageIDs`, `permittedLanguageIds` (ticket draft;
Swift and this SDK both say tags)

Unset (`undefined`) on a permit list means no restriction on that
dimension. An empty permit list (`[]`) permits nothing. Unset or empty
`excludedVersionIds` excludes nothing.

A version is usable when: it is not in `excludedVersionIds`, and (if
`permittedVersionIds` is set) its id is in that list, and (if
`permittedLanguageTags` is set) its `language_tag` is in that list. The
two allowlists combine with AND. Exclusion wins if an id is in both
permit and exclude lists.

`versionId: number` stays a number. Types cannot enforce the filter
(runtime lists; `Exclude<number, 4212>` is still `number`).

## Version refuse

What the SDK does with an unusable Bible version (YPE-4657). One rule
for every surface (reader, card, text, VOTD) and for core/hooks.

Core throws before returning scripture (and before the network when the
id alone decides). The error carries `status: 403` so existing UI maps
it to `forbiddenError` ("This app isn't allowed to access this Bible
content."). Hooks surface that `error`. UI does not throw, does not
render an empty tree, does not swap in another version. Dev: one
`console.warn` for a host-passed unusable `versionId`.

Language-only allowlists need `language_tag`. Content methods may fetch
version metadata once; if metadata is missing, refuse (fail closed).
`getVersion` itself is refused when the id is already unusable without
the tag.

Filtered `getVersions` / `getLanguages` pages must not hide a usable
row that exists on a later server page of the same query. Picker recents
are hidden at read time; localStorage is not rewritten, so lifting the
filter restores them.

_Avoid_: picker-only policy (hide in the chooser, still render
`versionId`); silent fallback to `DEFAULT_LICENSE_FREE_BIBLE_VERSION`;
opening the picker as the refuse (text/VOTD have none).

## Color

A highlight's fill, a 6-character lowercase hex string without `#`
Expand Down
67 changes: 67 additions & 0 deletions docs/adr/YPE-4657-usable-bible-version.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# YPE-4657 — Usable Bible version (version filter)

Status: **Accepted** (grilling session 2026-08-18)
Component: `@youversion/platform-core` (predicate + client refuse) → hooks inherit → UI error path
Epic: YPE-1315 (React-SDK v2)
Sister: Swift `permittedVersionIds` / `excludedVersionIds` / `permittedLanguageTags`
Related: [Swift version picker docs](https://developers.youversion.com/sdks/swift/components)

This ADR records the ticket shape. Public types and tests own the API
once implemented. Glossary: `CONTEXT.md` (**version filter**, **version
refuse**, usable **Bible version**).

## Context

Swift lets an integrator limit which Bible versions the SDK may use.
YPE-4657 adds the same capability to React. David’s bar: as close to
Swift as possible, without making React worse to comply.

Swift’s public docs talk about limiting the **version picker**. Swift’s
reader also drops unusable ids from restore and fallback, then opens the
picker if nothing remains. `BibleTextView` does **not** apply the client
filter; it fetches the host `versionId` and only shows “unavailable” on
server 401.

React has reader, card, text, and VOTD. Text and VOTD have no picker.
Copying Swift’s split would mean two refuse rules. Types cannot close
the hole: filters are runtime lists, every prop is `versionId: number`,
and `Exclude<number, 4212>` is still `number`.

## Decisions

- **Usable-version policy, one refuse.** An unusable id is not a Bible
version this app may use. Lists, pickers, recents, and content fetches
all refuse. The host passing `versionId` does not override the filter.
- **Names.** `permittedVersionIds`, `excludedVersionIds`,
`permittedLanguageTags`. Swift’s words, React’s `Id` (not `IDs`).
Ticket draft `permittedLanguageIDs` is rejected: `Language.id` is
already a BCP 47 tag; `BibleVersion.language_tag` is the field.
- **Predicate.** Unset permit list = no restriction. Empty permit list =
permit nothing. Unset/empty exclude = exclude nothing. Allowlists AND.
Exclusion wins. Same as Swift.
- **Config path.** Optional `YouVersionProvider` props, written onto
`YouVersionPlatformConfiguration` (same as `appName` /
`signInPromptMessage`). No new `configure()`. Core-only hosts set the
statics. UI provider must mirror onto its bundled core copy. Filters
must be visible to the first `BibleClient` call after mount.
- **Refuse presentation.** Core throws `Error` with `status: 403` before
returning scripture (no network when the id alone decides). Hooks pass
`error` through. UI reuses `VerseUnavailableMessage` /
`forbiddenError`. No throw from UI, no blank tree, no silent fallback
to `DEFAULT_LICENSE_FREE_BIBLE_VERSION`. One dev `console.warn` for a
host-passed unusable id.
- **Language allowlist.** Content methods may `getVersion` once for
`language_tag`, then refuse or continue. Missing metadata = refuse.
`getVersion` is itself refused when the id is unusable without the tag.
- **Lists.** Filtered pages must not drop a usable row that lives on a
later server page of the same query. Recents: hide at read time; do
not rewrite localStorage.
- **Out of scope for v1.** Branded/`as const` `versionId` generics.
Swift’s “open the picker when fallbacks fail.”

## Why not Swift’s split

React’s BibleText / VOTD cannot open a picker. A picker-only rule would
also leak scripture through `usePassage` / `BibleClient`. One core
refuse is the smaller surface and matches “this app may not use that
version.”
11 changes: 10 additions & 1 deletion packages/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,22 @@ Get your App Key at [platform.youversion.com](https://platform.youversion.com/)
## Usage

```ts
import { ApiClient, BibleClient } from '@youversion/platform-core';
import {
ApiClient,
BibleClient,
YouVersionPlatformConfiguration,
} from '@youversion/platform-core';

const apiClient = new ApiClient({
appKey: 'YOUR_APP_KEY',
});
const bibleClient = new BibleClient(apiClient);

// Optional: limit which Bible versions this app may use (YPE-4657)
YouVersionPlatformConfiguration.permittedLanguageTags = ['en'];
YouVersionPlatformConfiguration.permittedVersionIds = [111, 3034];
YouVersionPlatformConfiguration.excludedVersionIds = [4212];

// Find available Bible versions in English
const versions = await bibleClient.getVersions('en*');
console.log(versions.data[0].title);
Expand Down
39 changes: 39 additions & 0 deletions packages/core/src/YouVersionPlatformConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export class YouVersionPlatformConfiguration {
private static _expiryDateKey: string | null = null;
private static _signInPromptMessage: string | undefined = undefined;
private static _appName: string | undefined = undefined;
private static _permittedVersionIds: number[] | undefined = undefined;
private static _excludedVersionIds: number[] | undefined = undefined;
private static _permittedLanguageTags: string[] | undefined = undefined;

private static getOrSetInstallationId(): string {
const storage = getLocalStorage();
Expand Down Expand Up @@ -317,4 +320,40 @@ export class YouVersionPlatformConfiguration {
static set appName(value: string | undefined) {
this._appName = value;
}

/**
* Allowlist of Bible version ids this app may use. Unset = no restriction.
* An empty array permits nothing. See YPE-4657.
*/
static get permittedVersionIds(): number[] | undefined {
return this._permittedVersionIds;
}

static set permittedVersionIds(value: number[] | undefined) {
this._permittedVersionIds = value;
}

/**
* Denylist of Bible version ids this app may not use. Unset or empty =
* exclude nothing. Exclusion wins over `permittedVersionIds`.
*/
static get excludedVersionIds(): number[] | undefined {
return this._excludedVersionIds;
}

static set excludedVersionIds(value: number[] | undefined) {
this._excludedVersionIds = value;
}

/**
* Allowlist of BCP 47 language tags (`en`, `zh-Hans`). Unset = no
* restriction. An empty array permits nothing.
*/
static get permittedLanguageTags(): string[] | undefined {
return this._permittedLanguageTags;
}

static set permittedLanguageTags(value: string[] | undefined) {
this._permittedLanguageTags = value;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ describe('YouVersionPlatformConfiguration', () => {
YouVersionPlatformConfiguration.installationId = null;
YouVersionPlatformConfiguration.clearAuthTokens();
YouVersionPlatformConfiguration.apiHost = envApiHost;
YouVersionPlatformConfiguration.permittedVersionIds = undefined;
YouVersionPlatformConfiguration.excludedVersionIds = undefined;
YouVersionPlatformConfiguration.permittedLanguageTags = undefined;
});

afterEach(() => {
Expand Down
Loading
Loading