Skip to content

YPE-1023 - feat: wire Highlights API into the Bible reader#281

Draft
Dustin-Kelley wants to merge 15 commits into
mainfrom
dk/ype-1034-wire-api-logic-to-highlights
Draft

YPE-1023 - feat: wire Highlights API into the Bible reader#281
Dustin-Kelley wants to merge 15 commits into
mainfrom
dk/ype-1034-wire-api-logic-to-highlights

Conversation

@Dustin-Kelley

@Dustin-Kelley Dustin-Kelley commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Wire the Highlights API into BibleReader, replacing the localStorage-only highlight store. For signed-in readers, highlights load (chapter-scoped GET) and persist via optimistic create/delete, with the server as the source of truth.
  • Extract highlight state into a new lib/use-reader-highlights.ts hook. It rebuilds the store from the fetched collection filtered to the current version_id + chapter prefix so a stale mid-refetch collection can't leak another scope's colors.
  • Request the highlights data-exchange permission at sign-in (reader handleSignIn + example navbar button) so the token is actually granted highlights access. This is a requested_permissions[] permission, not an OIDC scope.
  • Export YouVersionAuthContext from @youversion/platform-react-hooks for null-safe reads of signed-in state without useYVAuth() throwing when no auth provider is mounted.
  • Correct HighlightsClient docs to describe the highlights permission model instead of read_highlights/write_highlights scopes.
  • Also folds in the earlier HighlightsClient API-contract fixes on this branch (Bearer auth, bible_id wire naming, enveloped POST body, 204 handling, color normalization, getRecentColors).

When not signed in, highlighting is currently ephemeral session state (never persisted); the dedicated not-signed-in / permission flow is a follow-up.

Test plan

  • pnpm test passes across all packages (new use-reader-highlights.test.tsx covers fetch scope, map filtering, optimistic apply, and color-matched remove)
  • pnpm typecheck and pnpm lint pass
  • Manual: signed-in reader highlights persist across reloads and survive version/chapter switches without color bleed
  • Manual: signed-out highlighting works in-session (no API calls, not persisted)
  • Manual: sign-in requests the highlights permission

Made with Cursor

Greptile Summary

This PR wires the Highlights API into BibleReader, replacing the localStorage-only highlight store with server-backed persistence for signed-in users, and introduces a new useReaderHighlights hook to encapsulate that state.

  • useReaderHighlights fetches highlights chapter-scoped via useHighlights, applies/removes optimistically, and guards against cross-user color bleed using a scopeKey + staleHighlights ref pattern.
  • YouVersionAuthContext is now exported from @youversion/platform-react-hooks so the reader can read sign-in state without throwing when no auth provider is mounted.
  • The reader's handleSignIn and the example navbar button both request the highlights data-exchange permission so issued tokens actually grant highlights access.

Confidence Score: 4/5

Mostly safe to merge; highlights load correctly after navigation, but there is a gap in the initial sign-in experience when the user is already viewing a chapter.

When a user signs in while already viewing a chapter, useApiData's effect is keyed only on [highlightsClient, version_id, passage_id] — none of which change on sign-in. The stale closure captured by the effect has enabled=false, so no fetch fires. The user's server-side highlights are invisible until they navigate away and back. Since the PR deliberately introduces the sign-in flow (adding the highlights permission to handleSignIn), this empty-store-on-sign-in is a functional gap in the primary happy path.

packages/ui/src/lib/use-reader-highlights.ts — the hook needs a useEffect that calls refetch() when isSignedIn transitions from false to true.

Important Files Changed

Filename Overview
packages/ui/src/lib/use-reader-highlights.ts New hook encapsulating highlight state with API integration; contains a bug where sign-in doesn't trigger a fetch when the user is already viewing a chapter
packages/ui/src/lib/use-reader-highlights.test.tsx Good coverage of signed-in, signed-out, and auth-transition scenarios; the sign-in-while-viewing fetch trigger is not tested (mocked boundary hides the useApiData dep issue)
packages/ui/src/components/bible-reader.tsx localStorage highlight store replaced cleanly with the new hook; handleSignIn updated to request the highlights permission
packages/core/src/highlights.ts JSDoc corrected to accurately describe the highlights permission model (not OAuth scopes)
packages/hooks/src/context/index.ts Exports YouVersionAuthContext for null-safe context reads without requiring useYVAuth()
examples/vite-react/src/components/navbar.tsx Adds highlights permission to the example navbar sign-in button
.changeset/wire-highlights-api-to-reader.md Minor bump changeset covering the three affected packages with accurate description
docs/adr/YPE-642-verse-action-popover.md ADR addendum accurately documents the highlights API wiring, superseding the localStorage-only ADR-001

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant BibleReader
    participant useReaderHighlights
    participant useHighlights
    participant HighlightsAPI

    User->>BibleReader: Open chapter (signed in)
    BibleReader->>useReaderHighlights: "{versionId, book, chapter}"
    useReaderHighlights->>useHighlights: "{version_id, passage_id}, {enabled: true}"
    useHighlights->>HighlightsAPI: "GET /v1/highlights?bible_id=&passage_id="
    HighlightsAPI-->>useHighlights: "Collection<Highlight>"
    useHighlights-->>useReaderHighlights: highlights
    useReaderHighlights-->>BibleReader: highlightsByPassageId (filtered + lowercased)

    User->>BibleReader: Select verses, tap color
    BibleReader->>useReaderHighlights: applyHighlight([16,17], 'ffff00')
    useReaderHighlights->>useReaderHighlights: optimistic setOptimisticStore
    useReaderHighlights->>HighlightsAPI: POST /v1/highlights (per verse)
    alt Success
        HighlightsAPI-->>useReaderHighlights: 200 Highlight
        useHighlights->>HighlightsAPI: refetch GET /v1/highlights
    else Failure
        HighlightsAPI-->>useReaderHighlights: error
        useReaderHighlights->>HighlightsAPI: refetch() to reconcile
    end

    User->>BibleReader: Sign out
    BibleReader->>useReaderHighlights: "isSignedIn = false (scopeKey changes)"
    useReaderHighlights->>useReaderHighlights: optimisticStore cleared synchronously
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant BibleReader
    participant useReaderHighlights
    participant useHighlights
    participant HighlightsAPI

    User->>BibleReader: Open chapter (signed in)
    BibleReader->>useReaderHighlights: "{versionId, book, chapter}"
    useReaderHighlights->>useHighlights: "{version_id, passage_id}, {enabled: true}"
    useHighlights->>HighlightsAPI: "GET /v1/highlights?bible_id=&passage_id="
    HighlightsAPI-->>useHighlights: "Collection<Highlight>"
    useHighlights-->>useReaderHighlights: highlights
    useReaderHighlights-->>BibleReader: highlightsByPassageId (filtered + lowercased)

    User->>BibleReader: Select verses, tap color
    BibleReader->>useReaderHighlights: applyHighlight([16,17], 'ffff00')
    useReaderHighlights->>useReaderHighlights: optimistic setOptimisticStore
    useReaderHighlights->>HighlightsAPI: POST /v1/highlights (per verse)
    alt Success
        HighlightsAPI-->>useReaderHighlights: 200 Highlight
        useHighlights->>HighlightsAPI: refetch GET /v1/highlights
    else Failure
        HighlightsAPI-->>useReaderHighlights: error
        useReaderHighlights->>HighlightsAPI: refetch() to reconcile
    end

    User->>BibleReader: Sign out
    BibleReader->>useReaderHighlights: "isSignedIn = false (scopeKey changes)"
    useReaderHighlights->>useReaderHighlights: optimisticStore cleared synchronously
Loading

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
packages/ui/src/lib/use-reader-highlights.ts:46-49
**Highlights fetch never fires on sign-in without navigation**

When a user signs in while already viewing a chapter, `isSignedIn` flips from `false` to `true` and `enabled` changes accordingly — but `useApiData`'s effect is keyed on `[highlightsClient, version_id, passage_id]` (none of which change on sign-in). The stale `fetchData` closure captured by that effect still has `enabled=false`, so it only calls `setLoading(false)` and returns without fetching. Highlights only appear after the user navigates to a different chapter and back.

The fix belongs in `useReaderHighlights`: add an effect that calls `refetch()` when `isSignedIn` transitions from `false` to `true` (e.g. via a `useRef` tracking the previous value). Calling `refetch()` from the current render's closure will use the new `fetchData` with `enabled=true`, triggering the fetch correctly.

Reviews (5): Last reviewed commit: "fix(ui): user1 highlights from rendering..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Dustin-Kelley and others added 8 commits July 7, 2026 11:52
The client sent requests the API rejects on every call, so highlights
could never be fetched, created, or deleted:

- Send the OAuth token as an Authorization: Bearer header (the API
  rejects the previous lat query parameter with 401)
- Use the API's bible_id naming on the wire for GET/POST/DELETE while
  keeping version_id in the SDK's public types, mapped at the boundary
- Send the required { request_id, highlight: { ... } } envelope on
  create for idempotent retries
- Require version_id and passage_id on getHighlights and version_id on
  deleteHighlight, matching the API's required parameters
- Validate responses with Zod and map from the wire shape
- Un-skip the highlights tests and make the MSW handlers enforce the
  documented contract so future drift fails tests

Co-authored-by: Cursor <cursoragent@cursor.com>
…equest_id

- Wire/SDK color schema is now case-insensitive, matching the client's input
  validator. Previously a valid uppercase color (e.g. FFC66F) passed input
  validation on create but threw "Unexpected highlights API response" when the
  same value came back through HighlightWireSchema. Adds a round-trip test.
- Clarify that request_id is a unique per-request id required by the API and
  does not by itself make caller-level retries idempotent (comment + changeset).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified HighlightsClient against the compiled OpenAPI spec. Follow-ups:

- getHighlights: treat a 204 (no highlights for the passage, the common case)
  as an empty collection instead of throwing "Unexpected highlights API
  response". Adds a regression test.
- createHighlight: normalize color to lowercase before sending; the API's
  color pattern is lowercase-only (^[0-9a-f]{6}$), so an uppercase input would
  otherwise be rejected server-side.
- Add getRecentColors() for GET /v1/highlights/recent-colors (recent + default
  palette colors) and expose it from useHighlights for color pickers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 131f1cf

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@youversion/platform-core Minor
@youversion/platform-react-hooks Minor
@youversion/platform-react-ui Minor
vite-react Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment on lines +20 to +30
* Encapsulates the reader's highlight state. When the user is signed in,
* highlights load from and persist to the Highlights API (chapter-scoped); when
* not signed in they are ephemeral session state (never persisted).
*
* Reads auth via the raw `YouVersionAuthContext` (null when no auth provider is
* mounted) rather than `useYVAuth()`, which throws — the reader must keep
* working for consumers who don't enable auth.
*
* Failure UX is intentionally out of scope: mutations apply optimistically and
* log on error (no revert/toast). The subsequent refetch inside `useHighlights`
* reconciles the store against the server (source of truth).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will update this comment in follow up ticket

@Dustin-Kelley Dustin-Kelley marked this pull request as ready for review July 9, 2026 14:26
@Dustin-Kelley Dustin-Kelley requested a review from cameronapak July 9, 2026 14:26

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fae796217e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/ui/src/lib/use-reader-highlights.ts
Comment thread packages/ui/src/lib/use-reader-highlights.ts Outdated
Comment thread packages/ui/src/lib/use-reader-highlights.ts
Comment thread packages/ui/src/lib/use-reader-highlights.test.tsx
Dustin-Kelley and others added 5 commits July 9, 2026 09:58
Reset optimistic highlight state when auth identity changes so
server-backed highlights are not shown after sign-out or account switch.

Co-authored-by: Cursor <cursoragent@cursor.com>
…tion shape

The test mocked auth `userInfo` with an `id` field, but the hook reads
`userInfo.userId` (the production shape). This made the scope key never
change on sign-out, so the "clears the store when the reader signs out"
test failed. Align the mock with production so the scope key resets.

Co-authored-by: Cursor <cursoragent@cursor.com>
@Dustin-Kelley Dustin-Kelley changed the title feat: wire Highlights API into the Bible reader YPE-1023 - feat: wire Highlights API into the Bible reader Jul 9, 2026
Comment thread packages/ui/src/lib/use-reader-highlights.ts Outdated
Comment on lines +46 to +49
const { highlights, createHighlight, deleteHighlight, refetch } = useHighlights(
{ version_id: versionId, passage_id: chapterPassageId },
{ enabled: isSignedIn },
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Highlights fetch never fires on sign-in without navigation

When a user signs in while already viewing a chapter, isSignedIn flips from false to true and enabled changes accordingly — but useApiData's effect is keyed on [highlightsClient, version_id, passage_id] (none of which change on sign-in). The stale fetchData closure captured by that effect still has enabled=false, so it only calls setLoading(false) and returns without fetching. Highlights only appear after the user navigates to a different chapter and back.

The fix belongs in useReaderHighlights: add an effect that calls refetch() when isSignedIn transitions from false to true (e.g. via a useRef tracking the previous value). Calling refetch() from the current render's closure will use the new fetchData with enabled=true, triggering the fetch correctly.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/ui/src/lib/use-reader-highlights.ts
Line: 46-49

Comment:
**Highlights fetch never fires on sign-in without navigation**

When a user signs in while already viewing a chapter, `isSignedIn` flips from `false` to `true` and `enabled` changes accordingly — but `useApiData`'s effect is keyed on `[highlightsClient, version_id, passage_id]` (none of which change on sign-in). The stale `fetchData` closure captured by that effect still has `enabled=false`, so it only calls `setLoading(false)` and returns without fetching. Highlights only appear after the user navigates to a different chapter and back.

The fix belongs in `useReaderHighlights`: add an effect that calls `refetch()` when `isSignedIn` transitions from `false` to `true` (e.g. via a `useRef` tracking the previous value). Calling `refetch()` from the current render's closure will use the new `fetchData` with `enabled=true`, triggering the fetch correctly.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Cursor Fix in Codex

@Dustin-Kelley Dustin-Kelley marked this pull request as draft July 9, 2026 18:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants