Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
fe2d93d
feat(core): wrap platform-core HighlightsClient with RN token auth (Y…
Dustin-Kelley Jul 27, 2026
98795f7
feat(core): highlights API wrapper + MMKV highlights cache (YPE-4170)…
Dustin-Kelley Jul 27, 2026
ae963bd
fix(core): shim crypto.randomUUID so RN highlight creates send a vali…
Dustin-Kelley Jul 27, 2026
7621945
feat(core): useHighlights — optimistic highlight writes over an insta…
Dustin-Kelley Jul 28, 2026
9f5ef2c
chore(deps): update Web SDK packages to 2.4.0 (#103)
Dustin-Kelley Jul 28, 2026
c548b21
feat(core): wrap platform-core HighlightsClient with RN token auth (Y…
Dustin-Kelley Jul 27, 2026
4de195b
chore(deps): update Web SDK packages to 2.4.0 (#103)
Dustin-Kelley Jul 28, 2026
0a7ba26
chore(deps): update Web SDK packages to 2.5.0 (#116)
Dustin-Kelley Aug 4, 2026
3f47138
feat(core): granted_permissions read-back + per-user permission cache…
cameronapak Aug 4, 2026
cc3c1b0
feat(core): just-in-time data-exchange permission grant (YPE-3709) (2…
cameronapak Aug 4, 2026
96b0846
feat(core): guarded highlight permission flow (YPE-3709) (3/3) (#114)
cameronapak Aug 5, 2026
17f4ae7
feat(ui): the reader renders native-owned highlights (YPE-3710) (U1) …
cameronapak Aug 5, 2026
b5c094d
feat(ui): verse actions are a native bottom sheet (YPE-3712) (U2) (#120)
cameronapak Aug 6, 2026
b48250b
feat(example): restore the example app to a plain SDK showcase (YPE-3…
Dustin-Kelley Aug 7, 2026
a523cb5
fix(core): report whether the token refresh actually worked(YPE-4297)…
Dustin-Kelley Aug 11, 2026
4cc1d51
feat(core): highlight writes park offline and reconcile on reconnect …
bmanquen Aug 11, 2026
677de3f
feat(core): refresh highlights when the app becomes active (YPE-4491)
cameronapak Aug 11, 2026
b1d647f
chore: drop advisor plan files from the YPE-4491 PR
cameronapak Aug 11, 2026
057c2ff
chore: drop AGENTS.md learned sections from YPE-4491
cameronapak Aug 11, 2026
0c1303a
test(core): pin AppState listener cleanup on unmount (YPE-4491)
cursoragent Aug 12, 2026
fe154ff
feat(highlights): paint and clear valid non-palette API hex (YPE-4494)
cursoragent Aug 12, 2026
dc856d1
docs(highlights): align YPE-4494 rules and drop unused projectPaintCo…
cursoragent Aug 12, 2026
c08f2cc
fix(ui): count all active colors for showAllApplyColors (YPE-4494)
cursoragent Aug 12, 2026
734d41b
refactor(ui): name activeHighlights set to match web showAllApplyColo…
cursoragent Aug 12, 2026
aa8ab5a
docs(agents): list isValidHighlightHex in Core Exports (YPE-4494)
cursoragent Aug 12, 2026
406fff3
docs(highlights): align AGENTS and ADR 0017 with YPE-4494 review
cursoragent Aug 12, 2026
ecb080d
fix(ui): pin platform-react-ui@2.6.2 for non-palette paint (YPE-4494)
cursoragent Aug 12, 2026
44cd729
test(core): align drain-host test with sync getOrSetInstallationId
cursoragent Aug 12, 2026
866e713
feat(ui): refreshHighlights, onHighlightError, sign-out guard on high…
cursoragent Aug 12, 2026
cd3aa25
fix(ui): bypass sign-out Alert on web for auth button
cursoragent Aug 12, 2026
23b3847
chore(ui): drop unused ReactNode import in auth button test
cursoragent Aug 12, 2026
74c1d1a
fix(ui): web sign-out in hook and isolate onHighlightError throws
cursoragent Aug 13, 2026
916b636
fix(ui): swallow rejected async onHighlightError
cursoragent Aug 13, 2026
05aa013
fix(ui): contain signOut rejections and gate signed-out guard
cursoragent Aug 13, 2026
95aa618
fix(ui): contain rejecting web signOut in useSignOutGuard
cursoragent Aug 13, 2026
05c0d55
fix(ui): skip per-frame re-renders on the swatch tray
cameronapak Aug 13, 2026
d4ef5fb
fix(ui): make HighlightWriteError a two-member union
cursoragent Aug 13, 2026
75a3811
fix(ui): explicit sign-out still clears session when unauthenticated
cursoragent Aug 13, 2026
5dedcb6
Merge pull request #132 from youversion/cursor/cp/ype-104-ui-deltas-o…
cameronapak Aug 13, 2026
4a20d6e
refactor(ui): extract swatch tray fade gates to lib
cursoragent Aug 13, 2026
0e5fb10
Merge pull request #133 from youversion/fix/verse-action-swatch-scrol…
cameronapak Aug 13, 2026
99a30e4
refactor(api): narrow the published surface
bmanquen Aug 14, 2026
38cb6c4
Merge pull request #135 from youversion/bm/update-docs
cameronapak Aug 14, 2026
0961227
fix(core): keep parked highlights when a forced refresh fails (#136)
cameronapak Aug 14, 2026
524e2ab
Release hardening for highlights: token out of the WebView, sign-out …
cameronapak Aug 14, 2026
a7d4940
docs: shorten highlights changelog and ADRs (#138)
cameronapak Aug 14, 2026
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
96 changes: 96 additions & 0 deletions .changeset/native-highlights-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
---
'@youversion/platform-react-native-expo-core': minor
'@youversion/platform-react-native-expo-ui': minor
---

Bible highlights on native. The reader paints the highlights of the signed-in user. Verse actions are a native bottom sheet. Highlights made offline survive a relaunch and land on their own. A user who taps a color before sign-in or grant still gets that highlight.

## Action required

Install three new peer modules and rebuild the dev client. A JS-only reload shows `Cannot find native module`.

```bash
npx expo install expo-network expo-clipboard expo-application
```

- `expo-network` is a core peer. It wakes parked writes when connectivity returns.
- `expo-clipboard` is a UI peer. It is the Copy fallback in the verse action sheet.
- `expo-application` is a UI peer. It supplies the app name on the sign-in sheet. Core no longer depends on it.

CAUTION: The default serif font of the reader changes from Source Serif 4 to Untitled Serif. The WebView fetches a stylesheet from `api.youversion.com` and font files from `cdn.youversion.com`. There is no opt-out. If those hosts are blocked, serif text falls back to Source Serif 4. Readers who chose Source Serif are migrated. Any other `fontFamily` you pass is left untouched.

## BibleReader

`BibleReader` owns highlights on native. It reads `useHighlights` for the current passage and passes the result as a controlled prop. The WebView does not fetch highlights, store them, or hold a token.

A verse selection opens a native sheet with the reference, color swatches, Copy, and Share. No new prop turns this on.

- Swatches: a remove circle for each color on the selection, then an apply circle for the palette colors not already covering the selection.
- Sign-in and consent: the sheet asks for whatever is missing, then applies the chosen color. This needs `auth.permissions` to include `highlights`.
- Copy and Share fall back to `expo-clipboard` and React Native `Share`. Optional `onCopy` and `onShare` take either over. Both receive `BibleReaderShareData`.

The sheet has no backdrop. A backdrop blocks the next verse tap. The user dismisses the sheet with a swipe down, a deselect, or an action on the sheet. Themed sheets draw an upward drop shadow so the sheet still separates from the page.

Selection across the bridge:

- `onVerseSelect(selection)` fires on every change, including a clear (`verses: []`).
- `clearSelectionSignal` dismisses the selection from native. The host increments the value. The value at mount is the baseline.

`BibleReaderVerseSelection` and `BibleReaderShareData` are re-exported from this package.

`ref.refreshHighlights()` re-fetches the current passage. A screen that regains focus can call it.

`onHighlightError` reports `{ status: 'queued' }` and `{ status: 'error', reason: 'transient' }` only. Other outcomes stay silent. The `HighlightWriteError` type is exported.

Sign-out from the reader menu and from `YouVersionAuthButton` asks first. If parked writes are still waiting, the alert is "Save your highlights?". The Confirm action calls `signOut()`. `useYVAuth().signOut()` still signs out at once. `useSignOutGuard` is exported for a host sign-out UI. `hasQueuedHighlightWrites(userId)` chooses the alert variant and never throws.

On web, the Web SDK popover is the verse-action UI. Sign-out is unprompted because `Alert.alert` is a no-op on React Native Web.

## useHighlights

`useHighlights({ versionId, book, chapter })` is the public surface for highlight data. It paints from an MMKV cache on the first render. `apply` and `remove` are optimistic. If the server refuses a write, the paint reverts.

`apply` and `remove` resolve a `HighlightWriteOutcome`: `ok`, `noop`, `queued`, or `error`. `queued` is new. It is a `minor` because no existing status changed meaning. An exhaustive `switch` with no `default` is the only consumer branch that breaks. An `error` carries `reason` (`not-signed-in` / `auth` / `invalid` / `transient`) plus `failedVerses` and `succeededVerses`. The hook `error` is fetch-only.

Also exported: `deriveServerColors`, `HIGHLIGHT_COLORS`, `isHighlightColor`, `refresh()`, and the `Highlight` / `HighlightColor` / `HighlightScope` / `ServerColors` types.

`apply` accepts only the five palette colors. A valid non-palette hex already on the account paints and clears by exact value. An unparseable hex is dropped.

`isRefreshing` means a GET is in flight. `highlights` is always safe to render. Mounted subscriptions also refresh when the app becomes active.

The GET runs only for an app that requested the `highlights` permission on `YouVersionProvider`. The gate reads the requested list, not a grant.

## Offline writes

A tap with no service keeps its paint and parks the write. The write is stored per user and chapter. It survives a relaunch. When service returns, the write lands.

- Unreachable or 5xx: paint stands. Outcome is `{ status: 'queued', verses }`.
- 401, 403, or any other 4xx: paint reverts. Outcome reports the refusal.

`queued` repeats on every tap of a verse that is still parked. If you show "saved offline" once, hold that copy in your own state.

Sign-out drops every parked write with the highlights cache and the grant cache. A write parked on one account cannot land on the next account.

A 401 or 403 on the drain earns one forced refresh and one retry. Only a second auth refusal under a minted token drops the entry. A failed force drops nothing.

## Highlighting before sign-in

`useHighlightPermissionFlow` wraps `useHighlights` and guards `apply`. It holds the pending highlight, runs sign-in and/or consent, then applies. `remove` passes through.

It returns the `useHighlights` result plus `isConfirming`, `confirm()`, `decline()`, and `flowError`. A cancel or decline resolves `noop`. `BibleReader` already wires the prompts. This hook is for a custom highlight UI.

This needs `auth` on `YouVersionProvider` and the `highlights` permission. With no `auth`, the flow behaves as signed out.

## Permissions and tokens

`useYVAuth()` now reports `grantedPermissions`, `hasPermission()`, `invalidatePermissions()`, and `requestedPermissions`. `grantedPermissions` is `null` (unknown), `[]` (denied), or a list (granted). The grant is read from the OAuth app redirect and cached per user.

`requestPermissions(permissions)` asks a signed-in user for a grant without sign-out. It resolves a `DataExchangeOutcome` and never throws: `granted`, `cancel`, or `failure` (`not-signed-in` / `not-permitted` / `user-changed` / `in-progress` / `transient`). The grant merges. The consent page returns to your `redirectUri`. If that URI does not match the registered callback, the outcome is `cancel`.

The cached grant is a hint. A privileged action gates on the pre-flight, not on a cached `true`.

`getAccessToken(options?)` resolves `{ status: 'ok', token, userId }` or `{ status: 'unavailable', reason: 'signed-out' | 'refresh-failed' }`. It refreshes only near expiry unless you pass `{ force: true }`. It never rejects. `refresh-failed` keeps the session. Highlights writes and `requestPermissions` treat `refresh-failed` as `transient` and do not send the request.

## Dependencies

`@youversion/platform-core` and `@youversion/platform-react-ui` move to 2.6.2. That release supplies controlled highlights, data-exchange primitives, and a fix that reads an empty-body 2xx DELETE as success.
5 changes: 0 additions & 5 deletions .changeset/sync-localization-reactnative-ace9bbd.md

This file was deleted.

9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ apps/example/web-build/
# Cursor
.cursor/

# Claude
.claude/

# Firecrawl
.firecrawl/

Expand All @@ -40,3 +43,9 @@ apps/example/web-build/

#omc
.omc

# Riptide artifacts (cloud-synced) and workspace config
.humanlayer/

# Agent scratch (specs and tickets)
.scratch/
Loading