YPE-1023 - feat: wire Highlights API into the Bible reader#281
YPE-1023 - feat: wire Highlights API into the Bible reader#281Dustin-Kelley wants to merge 15 commits into
Conversation
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 detectedLatest commit: 131f1cf The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
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 |
| * 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). |
There was a problem hiding this comment.
Will update this comment in follow up ticket
There was a problem hiding this comment.
💡 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".
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>
| const { highlights, createHighlight, deleteHighlight, refetch } = useHighlights( | ||
| { version_id: versionId, passage_id: chapterPassageId }, | ||
| { enabled: isSignedIn }, | ||
| ); |
There was a problem hiding this 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.
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.
Summary
BibleReader, replacing the localStorage-only highlight store. For signed-in readers, highlights load (chapter-scopedGET) and persist via optimisticcreate/delete, with the server as the source of truth.lib/use-reader-highlights.tshook. It rebuilds the store from the fetched collection filtered to the currentversion_id+ chapter prefix so a stale mid-refetch collection can't leak another scope's colors.highlightsdata-exchange permission at sign-in (readerhandleSignIn+ example navbar button) so the token is actually granted highlights access. This is arequested_permissions[]permission, not an OIDC scope.YouVersionAuthContextfrom@youversion/platform-react-hooksfor null-safe reads of signed-in state withoutuseYVAuth()throwing when no auth provider is mounted.HighlightsClientdocs to describe thehighlightspermission model instead ofread_highlights/write_highlightsscopes.HighlightsClientAPI-contract fixes on this branch (Bearer auth,bible_idwire 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 testpasses across all packages (newuse-reader-highlights.test.tsxcovers fetch scope, map filtering, optimistic apply, and color-matched remove)pnpm typecheckandpnpm lintpasshighlightspermissionMade with Cursor
Greptile Summary
This PR wires the Highlights API into
BibleReader, replacing thelocalStorage-only highlight store with server-backed persistence for signed-in users, and introduces a newuseReaderHighlightshook to encapsulate that state.useReaderHighlightsfetches highlights chapter-scoped viauseHighlights, applies/removes optimistically, and guards against cross-user color bleed using ascopeKey+staleHighlightsref pattern.YouVersionAuthContextis now exported from@youversion/platform-react-hooksso the reader can read sign-in state without throwing when no auth provider is mounted.handleSignInand the example navbar button both request thehighlightsdata-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 hasenabled=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 thehighlightspermission tohandleSignIn), 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
useEffectthat callsrefetch()whenisSignedIntransitions from false to true.Important Files Changed
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%%{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 synchronouslyPrompt To Fix All With AI
Reviews (5): Last reviewed commit: "fix(ui): user1 highlights from rendering..." | Re-trigger Greptile