Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
10 changes: 10 additions & 0 deletions .changeset/bible-reader-controlled-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@youversion/platform-react-ui': minor
---

BibleReader gains a controlled highlights mode (YPE-3705) for hosts that own highlight data themselves (e.g. React Native / Expo DOM hosts keeping the user token out of the WebView).

- New `BibleReader.Root` props: `highlights?: Highlight[]` (core API shape; presence puts the highlight slice in controlled mode, latched at first mount), `onVerseSelect` (both modes, fires on every selection change — `verses: []` whenever a non-empty selection clears, including after highlight/copy/share actions, popover dismiss, and navigation), and `onHighlightApply` / `onHighlightRemove` (controlled mode only; ignored in self-contained mode). Payloads are serializable, bridge-safe objects (`BibleReaderVerseSelection`, `BibleReaderHighlightIntent`) with always-per-verse `passageIds`.
- In controlled mode the reader is a pure projection: highlights render solely from the prop (filtered by displayed version + chapter, range USFMs like `JHN.3.16-18` expanded per verse), color taps emit intents and paint nothing until the host round-trips an updated prop, and neither the highlights API nor any local store is touched. No auth surface can originate from the highlight path.
- Self-contained behavior (no `highlights` prop) uses the server-backed `useBibleReaderHighlights` path (YPE-1034), dark-launched behind `HIGHLIGHTS_LIVE`.
- `@youversion/platform-react-ui` now re-exports the `Highlight` type from `@youversion/platform-core`.
20 changes: 20 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,26 @@ gated on the internal `HIGHLIGHTS_LIVE` dark-launch flag and an
authenticated user — while the flag is off or the user has no session, the
reader is inert: no fetches, no writes, nothing rendered from the API.

## Controlled mode

The alternate `BibleReader` posture (YPE-3705): the host owns highlight
data, auth, and persistence (e.g. a React Native / Expo DOM host keeping
the user token out of the WebView). The host passes `highlights:
Highlight[]` into `BibleReader.Root` and receives highlight intents; the
reader is a pure projection — no API calls, no local persistence, no
sign-in surface. Presence of the prop selects the posture (latched at
first mount). Controlled mode bypasses `HIGHLIGHTS_LIVE`: the color row
is always interactive so the public prop surface can ship while
self-contained stays dark.

## Highlight intent

The reader's request that a highlight be applied or removed
(`onHighlightApply` / `onHighlightRemove`). Always an intent, never a
completed fact — intent and fact are deliberately never given the same
name. In controlled mode the highlight appears only when the host
round-trips an updated `highlights` prop.

## Verse action popover

The floating action bar (YPE-642) that appears over a verse selection,
Expand Down
87 changes: 87 additions & 0 deletions docs/adr/YPE-3705-controlled-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# YPE-3705 — Controlled (headless) BibleReader highlights

Status: **Accepted** (grilling session 2026-07-20, Dustin + Claude)
Component: `packages/ui/src/components/bible-reader.tsx` (+ `packages/ui/src/lib/highlight-projection.ts`)
Epic: YPE-2894 (RN Expo Highlights) · Blocks: YPE-3710, YPE-3711, YPE-3712
Related: PR #288 (highlights UI + state machine + API, open) · PR #269 (verse action popover) · YPE-1034 ADR (on PR #288)

## Context

Native hosts (the RN Expo SDK embedding the web reader as a DOM component) own
highlight data, auth, and persistence natively; the epic's goal is keeping the
**user token** out of the WebView. Bible *content* fetching is app-key catalog
traffic and is unchanged.

The reader therefore has two postures for its highlight slice:
**self-contained** (YPE-1034 — the reader fetches and writes
highlights through the SDK's own auth session) and **controlled** (this ADR —
the host passes `highlights: Highlight[]` into `BibleReader.Root` and receives
intent events; presence of the prop selects the posture, latched at first
mount). The prop/event surface and its edge semantics are documented by the
public types and the tests in `bible-reader-controlled.test.tsx`; this ADR
records only what the code cannot show.

## Coexistence model

Controlled mode performs **no highlight persistence of any kind** — no API
calls, no localStorage, no sign-in/permission dialogs or redirects. The two
postures never mix: a controlled reader's highlight slice is a pure projection
of the prop, and a self-contained reader never calls the intent-event props.
`onVerseSelect` is a mode-independent observation; `onHighlightApply`/
`onHighlightRemove` are controlled-only requests (a self-contained "applied"
notification would be a different event under a different name).

## Non-obvious decisions

- **Pure projection, no optimistic echo.** A color tap paints nothing; the
highlight appears when the host round-trips an updated prop. The native data
layer (YPE-3708) is the only optimistic layer — two optimistic layers cannot
agree on failure semantics, and only the host knows whether a write succeeded.
- **Controlled mode bypasses the `HIGHLIGHTS_LIVE` dark-launch flag** (a
PR #288 concept). The flag strictly means "is the self-contained server path
live"; the color row is always interactive in controlled mode. Consequence:
the controlled prop surface is public released API while self-contained
stays dark.

## Rejected alternatives

- **Fully headless reader (pure projection for everything)** — would require
the host to supply passage HTML, books, and version metadata as props,
tripling the bridge surface for app-key catalog data that isn't user data.
The pure-presentational layer already exists below (`BibleTextView`); the
epic's goal is native ownership of *user data and tokens*, so only the
highlight slice becomes a pure projection while the reader keeps fetching
content.
- **Explicit mode prop** — a second axis that can contradict the data;
presence-of-prop is the established controlled/uncontrolled idiom.
- **Chapter-scoped `Record<number, string>` prop** — loses the identity fields
that make stale bridge data harmless, and overloads the internal
`highlightedVerses` name.
- **Reader-side optimism** — see above.
- **Machine-level controlled state** — controlled mode wants the machine
provably inert; that is an enable guard, not a statechart region.
- **Stacking on PR #288** — would gate the RN epic on #288's merge date; built
in parallel on `main` instead. The re-seat onto the shared adapter has since
landed (see below).

## Integration with the self-contained path (landed)

Controlled mode is seated in the `useBibleReaderHighlights` adapter (the
reader's single seam onto highlights, shared with YPE-1034's self-contained
path) via a `controlled` input, latched by `Root` at first mount:

- When `controlled` is set, the `useHighlights` fetch stays `enabled: false`
and the adapter performs no writes and keeps no optimistic overlay —
controlled mode is a pure projection of the host's highlights
(`deriveHighlightedVerses`) plus intent forwarding from `apply`/`remove`.
- Controlled mode bypasses `isHighlightsLive()` — the dark-launch flag gates
only the self-contained server path, so the color row stays interactive and
the controlled prop surface ships while self-contained remains dark.
- The glossary (`CONTEXT.md`) defines "Controlled mode" and "Highlight
intent"; intent and fact are deliberately never given the same name.

## Out of scope

Notes, custom colors, >5 colors (epic fast-follow); self-contained notification
events; controlled *content* (the reader keeps fetching passages/books/versions);
offline/write-queue concerns (native-side, YPE-3717).
2 changes: 1 addition & 1 deletion packages/ui/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ src/index.ts # Entry point (re-exports components, types, hooks)
- Components exported from `src/components/`
- Re-exports from `@youversion/platform-core`:
- `SignInWithYouVersionPermission`, `SignInWithYouVersionResult`, `YouVersionAPIUsers`
- `ApiConfig`, `AuthenticationState` types
- `ApiConfig`, `AuthenticationState`, `Highlight` types
- Re-exports from `@youversion/platform-react-hooks`:
- `YouVersionProvider`, `useYVAuth`, `UseYVAuthReturn` type

Expand Down
Loading
Loading