Skip to content
Open
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
15 changes: 15 additions & 0 deletions .changeset/bible-reader-highlights-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@youversion/platform-react-hooks': patch
'@youversion/platform-react-ui': patch
---

BibleReader highlights now wire to the highlights API behind an internal dark-launch flag; the localStorage highlight store is removed.

- Highlights are server-only account data (YPE-1034): the temporary `youversion-platform:highlights:<versionId>` localStorage store is deleted outright, with no migration.
- A new internal `useBibleReaderHighlights` seam fetches the current chapter's highlights via `useHighlights`, renders them through an in-memory optimistic overlay (reverted on write failure), and collapses contiguous verse selections into range USFMs (e.g. `JHN.3.16-18`) on the wire.
- Everything is gated on an internal `HIGHLIGHTS_LIVE` flag (currently off) plus an authenticated session: while off or signed out, the color row is inert — no fetches, no writes, no rendered highlights. Signing out un-renders highlights immediately.
- `@youversion/platform-react-hooks` now exports `YouVersionAuthContext`, the no-throw alternative to `useYVAuth` for components that must tolerate a missing auth provider (its error message already advertised the export).
- `useApiData` (the fetch engine behind all data hooks, e.g. `useHighlights`, `usePassage`) fixes and behavior changes:
- An `enabled` false→true flip now triggers the fetch even when the caller's deps are unchanged — previously a hook that mounted disabled (e.g. waiting on auth) never fetched after being enabled.
- Disabling now clears `data` (and `error`) instead of keeping the last response, so stale account data cannot linger after sign-out or an auth user switch.
- Responses are now latest-wins across `refetch` too: a stale in-flight request (e.g. a refetch for a previous chapter) resolving after a newer fetch no longer overwrites its data.
15 changes: 15 additions & 0 deletions .changeset/fix-highlights-jam-issues.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@youversion/platform-core': patch
'@youversion/platform-react-hooks': patch
'@youversion/platform-react-ui': patch
---

Fix four highlights-stack bugs surfaced by a staging session, plus a fill fade-in.

- **Invisible primary button (ui):** the `default` button variant paired `bg-background` with `text-primary-foreground`, resolving to white-on-white in the light theme (the highlight permission dialog's Continue button was invisible). It now uses the standard `bg-primary` / `text-primary-foreground` pairing. `YouVersionAuthButton` pins `bg-background` explicitly so its neutral brand surface is unchanged.
- **Optimistic overlay dropped before the write is visible (ui):** after a successful highlight write the seam hook dropped the optimistic overlay as soon as ANY refetch landed, trusting it as server truth. Under read-after-write lag (staging slowness, prod read replicas) that GET often did not yet reflect the write, so a highlight flickered out and back on apply, or a removed highlight reappeared. The overlay is now retired only once a fetch actually reflects the write (apply → the verse shows the written color; remove → the verse no longer shows the removed color); until then the overlay wins. Reset paths (chapter/version change, sign-out) release any write the server never converges on.
- **Refetch coalescing (hooks behavior change):** `useHighlights.createHighlight` / `deleteHighlight` no longer refetch on each call. The sole consumer (`useBibleReaderHighlights`) now issues a single refetch after each batch settles — success or failure — so highlighting verses `[2,3,5]` fires two POSTs but only one GET (previously two). Batches with partial failures settle per sub-write: succeeded writes reconcile to server truth, only the failed writes' verses revert.
- **DELETE by range is unsupported (core + ui):** range passage-ids (e.g. `JHN.1.2-3`) returned a non-2xx from staging and threw. The remove path now sends one DELETE per verse (`JHN.1.2`, `JHN.1.3`); POST/apply still collapses to ranges, which works. The `HighlightsClient.deleteHighlight` docstring no longer claims range delete is supported (marked unverified pending API-team confirmation). Large removals issue more DELETEs but still coalesce to a single refetch.
- **Highlight fade-in (core styles):** verse highlight fills now transition their background color (~250ms, disabled under `prefers-reduced-motion`) instead of popping in, matching the Bible app. The selection underline and layout are untouched.

Known/accepted trade-off (pending product sign-off): the optimistic overlay holds the locally-written value until a fetch reflects that write. A concurrent change from another device to the same verse therefore renders stale until navigation or the next write on this client. This is the deliberate cost of eliminating the flicker/ghost bugs above; documented in code comments in `useBibleReaderHighlights`.
19 changes: 19 additions & 0 deletions .changeset/highlight-auth-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@youversion/platform-core': patch
'@youversion/platform-react-hooks': patch
'@youversion/platform-react-ui': patch
---

Add the highlight auth flow: a color tap in BibleReader without a session or the `highlights` permission now stashes the intent and runs the two-path grant flow (YPE-1034, still behind the internal `HIGHLIGHTS_LIVE` flag).

- **Core**: new `DataExchangeClient.updateToken` (`POST /data-exchange/token`, 201 → `{ token }`, Zod-validated) plus `buildDataExchangeUrl` / `parseDataExchangeCallback` / `handleDataExchangeCallback` for the hosted just-in-time grant. Sign-in and data-exchange callbacks now parse `granted_permissions` and seed an optimistic permission cache on `YouVersionPlatformConfiguration` (`grantedPermissions`, `hasPermission`, `saveGrantedPermissions`, `removeGrantedPermission`); the cache is cleared on sign-out and a 401/403 invalidates it (server truth wins). `SignInWithYouVersionResult` gains a `permissions` field.
- **Hooks**: new `useHighlightAuthActions` exposing the one-fell-swoop sign-in (requesting `highlights`), the just-in-time data-exchange redirect, the permission-cache reads/invalidation, and the data-exchange return handler.
- **UI**: `useBibleReaderHighlights` now runs the state machine — pending highlights persist to `sessionStorage` (~10-minute expiry) to survive the redirect round-trip and apply automatically on a granted return; a just-in-time permission confirm dialog (`HighlightPermissionDialog`, copy matched to the native SDK) gates the data-exchange grant. Write failures route by status: 401/403 invalidates the cache, keeps the pending highlight, and re-prompts; 5xx/network reverts the optimistic overlay and discards. Apply/remove writes are serialized through a FIFO queue with per-verse ownership so overlapping operations settle to the last-issued state. Copy/share-only behavior when no auth provider is configured is unchanged.

Deferred follow-ups (documented at each code site in `bible-reader-highlights-machine.ts`, accepted for dark launch):

1. A 401 from an expired token (not a missing permission) misroutes to the permission re-prompt; follow-up is distinguishing auth-expiry from permission-denied at the `isPermissionError` boundary.
2. A failure while applying a resumed pending highlight only logs + reverts (the pending intent was cleared before the write), so a 401 there loses the highlight instead of keep-pending + re-prompt; follow-up is routing resume-write failures through the standard apply failure handling.
3. ~~A 401/403 on `remove` opens the permission dialog with no pending highlight.~~ Resolved in the PR-288 xstate rewrite: a remove failure now invalidates the cache without re-prompting.

Note: the signed-out one-fell-swoop redirect described above is superseded by the sign-in dialog introduced in the PR-288 xstate rewrite (see the separate changeset) — a signed-out color tap now opens a dialog before OAuth launches.
9 changes: 9 additions & 0 deletions .changeset/recent-highlight-colors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@youversion/platform-react-ui': patch
---

BibleReader's verse action popover now shows the user's recent highlight colors and marks active swatches with a checkmark (YPE-1034 PR3, still behind the internal `HIGHLIGHTS_LIVE` flag).

- **Recent colors**: when highlights are live (flag on + auth provider + signed in), the color row renders the server's recent-colors list (recently used first, then defaults) via `useBibleReaderHighlights` → `useHighlights.getRecentColors`. The list is deduped first-occurrence-wins, normalized (lowercase, leading `#` stripped, non-`/^[0-9a-f]{6}$/` entries dropped), never reordered (server order is the truth), and the row scrolls horizontally if it overflows. It falls back to the hardcoded `HIGHLIGHT_COLORS` palette whenever recents are unavailable — flag off, no auth provider, signed out, fetch pending, or fetch failed. The fetch runs once per live edge (not per tap or per navigation).
- **Checkmark swap**: active/remove swatches now render a 24px checkmark (`icons/check`) instead of the X, matching iOS (platform-sdk-swift #179). Same theme-invariant `#121212` fill and identical behavior — tapping still removes the highlight.
- **Type**: `HighlightColor` (exported from `verse-action-popover`) is widened from the five-literal palette union to `string`, since recents can be any hex. This is a loosening (every prior value still assignable), so it is non-breaking for consumers. `HIGHLIGHT_COLORS` remains exported as the default palette.
13 changes: 13 additions & 0 deletions .changeset/xstate-highlights-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@youversion/platform-react-ui': patch
---

Rewrite the BibleReader highlights flow as an explicit xstate v5 statechart (YPE-1034 / PR-288, still behind the internal `HIGHLIGHTS_LIVE` flag). `useBibleReaderHighlights` is now a thin adapter over `bibleReaderHighlightsMachine`; the machine (authored with `setup()` and named guards/actions/actors, so it is Stately-visualizable) owns the whole flow — optimistic overlay, serialized writes with per-verse ownership, reconcile, the auth flow, and the resume-on-return path. A mermaid statechart lives in `docs/highlight-flow-statechart.md`. All previously-shipped invariants are preserved (writes serialized, per-verse ownership tokens, per-sub-write settlement, apply collapses to ranges / remove DELETEs per verse, one refetch per settled batch, scope-change drops the overlay, `live` gates rendering/fetching).

Behavior changes:

- **Sign-in dialog first (signed out):** a signed-out color tap now opens a `SignInDialog` (introducing the app, with an optional integrator prompt) instead of redirecting to OAuth immediately. Confirm launches the sign-in redirect requesting `highlights`; decline/dismiss discards the pending highlight and keeps the verse selection. The dialog's `appName` comes from `YouVersionPlatformConfiguration.appName` (falling back to "This app") and its prompt from `signInPromptMessage`. The hook return gains `signInDialogOpen`, `confirmSignInDialog`, and `cancelSignInDialog`.
- **Flag-off hides the highlights UI:** when `HIGHLIGHTS_LIVE` is off, `VerseActionPopover` hides the color row and the remove (checkmark) circles entirely via a new `highlightsEnabled` prop — only Copy / Share remain. Previously the row still rendered while taps were inert.
- **"Vapor" fix:** a deleted highlight no longer briefly reappears when a stale read-replica fetch lands after the delete settled. The reconcile step no longer retires remove-overlay entries; a removed verse is held until a reset path (scope change, sign-out, or a newer write). Apply-side convergence is unchanged. This matches the already-accepted trade-off (a concurrent same-verse edit from another device renders stale until navigation or the next write).
- **Remove-failure no longer re-prompts:** a 401/403 on a remove invalidates the permission cache but no longer opens the permission dialog (there is no pending highlight to resume), resolving a documented deferred wart.
- **Resume-failure now re-prompts like a user apply:** a 401/403 on a pending highlight resumed after sign-in / data-exchange re-stashes that highlight (using its own scope, so a cross-chapter return re-prompts on the right passage) and re-opens the permission dialog, instead of silently dropping the user's original color tap. Previously this path only logged + reverted (a documented deferred wart from the pre-rewrite hook).
85 changes: 85 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Ubiquitous Language

Glossary of domain terms for the YouVersion Platform SDK. Terms here are
canonical: code, docs, and conversation should use them exactly.

## Highlight

A user-owned color marking on a Bible passage, stored on the user's
YouVersion account. Highlights are **account data, not device data**: they
require an authenticated user and are never persisted locally by the SDK
(decided in YPE-1034, superseding the temporary localStorage store from
YPE-642 / ADR-001).

Identified by the pair (**Bible version**, **passage**). A highlight has
exactly one **color**.

## Passage

A verse or contiguous verse range in one chapter of one Bible version,
identified by a USFM string (`JHN.3.16`, `JHN.3.16-18`). A chapter USFM
(`JHN.3`) is a passage *scope* used for querying, not a highlightable unit.

## Bible version

A translation/edition of the Bible, identified by a numeric id. The SDK
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.

## Color

A highlight's fill, a 6-character lowercase hex string without `#`
(`fff9b1`). Uppercase input is accepted and normalized at the API boundary.

## Verse selection

The ephemeral set of verses the reader has tapped in `BibleReader`. Drives
the verse action popover. Never persisted; cleared on navigation.

## Self-contained mode

The default `BibleReader` posture (YPE-1034): the reader fetches and writes
highlights itself through the SDK's own auth session. The color row is
always offered; tapping a color when the user lacks a session or the
highlights permission enters the highlight auth flow (July 9 2026 sync,
superseding the earlier hide-when-signed-out idea).

## Highlights permission

The per-app grant that authorizes highlight reads/writes for a user. It is
**not an OIDC scope**: it travels as `requested_permissions[]=highlights`
alongside `scope` at authorize time (PR #280) and is granted via a data
exchange consent. Permissions are open-ended strings, never enums (more
arrive later, e.g. verse notes). The **server is the source of truth** for
whether it is granted; any client-side permission cache is optimistic only.

## Pending highlight

The user's stashed highlight intent (verses + color + timestamp) while the
highlight auth flow is in flight. Survives a redirect round-trip via
sessionStorage; expires stale (~10 min) so an abandoned round-trip can
never apply a highlight during a later sign-in. Discarded on decline,
cancel, or failure.

## Highlight auth flow

The state machine (see the React Web auth state machine doc) that turns a
color tap into an applied highlight across two user paths: one-fell-swoop
(sign-in requesting the highlights permission together) and just-in-time
(already signed in, permission confirm dialog → data exchange grant).
Cancellation at any point keeps the verse selection intact and discards
only the pending highlight.

## Controlled mode

A planned `BibleReader` posture (YPE-3705): the host application supplies
rendered highlights as data and receives highlight-intent events, and the
reader makes no highlight network calls. The host owns auth, persistence,
and conflict rules. Used by native hosts (RN Expo SDK) embedding the web
reader.

## Verse action popover

The floating action bar (YPE-642) that appears over a verse selection,
offering highlight colors, copy, and share.
52 changes: 52 additions & 0 deletions docs/adr/YPE-1034-highlights-server-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# YPE-1034 — Wire the highlights API into BibleReader

Status: **Decided** (grilling session 2026-07-10, Cam + Dustin)
Component: `packages/ui/src/components/bible-reader.tsx` (+ hooks `useHighlights`)
Inputs: July 9 2026 highlights sync (Notion: "React Web SDK Highlights:
Implementation Brief"), auth state machine doc, YPE-642 gotchas doc,
Swift reference PR platform-sdk-swift#179.

## ADR-001 — Highlights are server-only; the localStorage store is removed

**Supersedes ADR-001 in [YPE-642](./YPE-642-verse-action-popover.md).**

### Decision

The localStorage highlight store (`youversion-platform:highlights:<versionId>`)
is deleted outright — no migration, no signed-out fallback. Highlights are
fetched from and written to `/v1/highlights` exclusively, through the SDK's
authenticated session. A user with no session (or whose app lacks the
`highlights` permission) enters the highlight auth flow when they tap a color;
their intent is stashed as a **pending highlight** (sessionStorage, ~10-minute
expiry), never as a persisted local highlight.

The only local traces of highlight state are:
- the in-memory optimistic overlay while a write is in flight,
- the pending highlight during an auth round-trip,
- an optimistic localStorage cache of the *permission* grant (not highlight
data), which the server can invalidate at any time via 401/403.

### Why

- Highlights are account data. A browser-profile copy silently diverges from
the user's YouVersion account and dies at the browser boundary.
- YPE-642's store was explicitly a stand-in for this ticket (its ADR-001 said
"server sync is a separate ticket" — this is that ticket).
- The SDK is pre-1.0 with few consumers; the migration code for weeks-old
throwaway data would outlive its usefulness.
- Two persistence paths (API + local) double the state machine and create an
unanswerable merge question on sign-in.

### Consequences

- Sign-out immediately un-renders all highlights.
- Signed-out readers see no highlights; that is correct, not a regression.
- The RN Expo / native-host story (YPE-3705) supplies highlights via
controlled props instead — it does not resurrect local persistence.

### Alternatives rejected

- **localStorage for signed-out + API for signed-in:** merge-on-sign-in
conflicts, doubled state machine, data that still dies per-device.
- **One-time migration of existing local highlights:** permanent code for
transient data nobody has accumulated meaningfully.
15 changes: 15 additions & 0 deletions docs/adr/YPE-642-verse-action-popover.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ gone.** #131's `VerseActionPopover` (correct AC logic, tested, already uses Radi
## Decisions (ADRs)

### ADR-001 — localStorage only this PR
> **Superseded** by [YPE-1034 ADR-001](./YPE-1034-highlights-server-only.md):
> highlights are server-only; the localStorage store is removed.

Highlights persist client-side only. Server sync is a **separate ticket**.
No network, no API client this PR.

Expand Down Expand Up @@ -143,6 +146,18 @@ Selection is always enabled in BibleReader (no opt-out prop for now; YAGNI).
- Tunable; swap to a ring/bg later if Figma says otherwise.

## As-built notes (deviations from the design above)
- **ADR-005 active-swatch icon (YPE-1034 PR3):** the 24px **X** on active/remove
swatches was replaced with a 24px **checkmark** (`icons/check`), matching iOS
(platform-sdk-swift #179). Same Text/Everdark (`--yv-gray-50` = `#121212`,
theme-invariant) fill and identical behavior — tapping still removes the
highlight; the swatch's `Clear highlight` aria-label is unchanged.
- **Recent colors (YPE-1034 PR3):** when highlights are live (flag on + auth
provider + signed in) the color row renders the server's recent-colors list
(recently used first, then defaults) in server order, deduped, and
horizontally scrollable if it overflows. It falls back to the hardcoded
`HIGHLIGHT_COLORS` palette whenever recents are unavailable (flag off, no auth
provider, signed out, fetch pending or failed). `HighlightColor` was widened
from the five-literal union to `string` since recents can be any hex.
- **ADR-004 revised:** selection + highlights live in `Content`, **not** Root
context. Copy/Share/anchor all need the rendered verse DOM (which lives in
Content), so Root ownership would fragment the feature. BibleTextView stays
Expand Down
Loading
Loading