From ffe6dc1dc24d52b8953ad6a65f91a5401289bebc Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Tue, 14 Jul 2026 10:46:36 -0500 Subject: [PATCH 01/12] chore: consolidate pending changesets into the 1.0.0 initial release Fold API-relevant changeset content into the curated 1.0.0 CHANGELOG entries and remove the nine pending changesets. Both packages stay at 1.0.0 (unpublished), so everything ships as the initial release. Co-Authored-By: Claude Fable 5 --- .changeset/android-picker-viewport-jump.md | 5 ----- .changeset/auth-avatar-and-cancel.md | 5 ----- .changeset/bible-card-version-picker-default.md | 5 ----- .changeset/default-bible-version-id-export.md | 5 ----- .changeset/export-component-prop-types.md | 5 ----- .changeset/react-doctor-top-fixes.md | 6 ------ .changeset/refactor-useeffect-anti-patterns.md | 5 ----- .changeset/sheet-header-surface-parity.md | 5 ----- .changeset/sheet-max-width-wide-screens.md | 5 ----- packages/core/CHANGELOG.md | 6 ++++++ packages/ui/CHANGELOG.md | 8 +++++++- 11 files changed, 13 insertions(+), 47 deletions(-) delete mode 100644 .changeset/android-picker-viewport-jump.md delete mode 100644 .changeset/auth-avatar-and-cancel.md delete mode 100644 .changeset/bible-card-version-picker-default.md delete mode 100644 .changeset/default-bible-version-id-export.md delete mode 100644 .changeset/export-component-prop-types.md delete mode 100644 .changeset/react-doctor-top-fixes.md delete mode 100644 .changeset/refactor-useeffect-anti-patterns.md delete mode 100644 .changeset/sheet-header-surface-parity.md delete mode 100644 .changeset/sheet-max-width-wide-screens.md diff --git a/.changeset/android-picker-viewport-jump.md b/.changeset/android-picker-viewport-jump.md deleted file mode 100644 index e2fc60b..0000000 --- a/.changeset/android-picker-viewport-jump.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@youversion/platform-react-native-expo-ui': patch ---- - -Fix jarring layout jump when the Bible Version and Chapter picker sheets open on Android. The pre-warmed picker WebView sits in an offscreen inert sheet host where Android reports `visualViewport.height` as 0; the keyboard-viewport listener wrote that as `--yv-visible-height: 0px`, collapsing the picker shell (search bar at the top of an empty sheet) until a late viewport resize snapped everything into place mid-animation. Viewport heights below a plausibility floor are now treated as "hidden WebView" and fall back to the stylesheet default (100vh), so the pre-warmed layout is already correct when the sheet opens. diff --git a/.changeset/auth-avatar-and-cancel.md b/.changeset/auth-avatar-and-cancel.md deleted file mode 100644 index 5fed76f..0000000 --- a/.changeset/auth-avatar-and-cancel.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@youversion/platform-react-native-expo-core': patch ---- - -Harden the auth flow against two upstream auth-page issues. `deriveUserInfo` now drops placeholder `profile_picture` values (e.g. `https://none/`) and any non-`https` avatar URL (iOS ATS and Android cleartext defaults block `http` image loads anyway) so consumers no longer receive a broken avatar URL. Sign-in now treats an `access_denied` callback (the auth page's Cancel button) as a clean cancel instead of throwing; `state` is still validated before any token exchange on the success path. Cached user info is now validated with a `zod` schema on read instead of being blindly cast, so a corrupt or legacy cache entry can no longer surface wrong-typed fields to consumers. diff --git a/.changeset/bible-card-version-picker-default.md b/.changeset/bible-card-version-picker-default.md deleted file mode 100644 index 3ed692d..0000000 --- a/.changeset/bible-card-version-picker-default.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@youversion/platform-react-native-expo-ui': minor ---- - -`BibleCard` now defaults `showVersionPicker` to `false`, matching the React Web SDK. The version picker trigger no longer renders and the built-in `BibleVersionPickerSheet` no longer mounts unless consumers pass `showVersionPicker` explicitly. diff --git a/.changeset/default-bible-version-id-export.md b/.changeset/default-bible-version-id-export.md deleted file mode 100644 index 0c082ab..0000000 --- a/.changeset/default-bible-version-id-export.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@youversion/platform-react-native-expo-ui': minor ---- - -Export `DEFAULT_BIBLE_VERSION_ID` (3034, Berean Standard Bible) from the UI entry so consumers can reference the SDK's default Bible version instead of hardcoding the numeric ID. The constant now backs the internal `defaultVersionId` fallbacks for `BibleCard`, `BibleReader`, and the picker sheets. diff --git a/.changeset/export-component-prop-types.md b/.changeset/export-component-prop-types.md deleted file mode 100644 index ddca124..0000000 --- a/.changeset/export-component-prop-types.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@youversion/platform-react-native-expo-ui': minor ---- - -Re-export component prop types from the UI entry so consumers can import them directly instead of re-deriving via `React.ComponentProps`. Adds `BibleCardProps`, `BibleReaderProps`, `BibleReaderSettingsSheetProps`, `BibleTextViewProps`, `VerseOfTheDayProps`, and `YouVersionAuthButtonProps`. diff --git a/.changeset/react-doctor-top-fixes.md b/.changeset/react-doctor-top-fixes.md deleted file mode 100644 index fb619ff..0000000 --- a/.changeset/react-doctor-top-fixes.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@youversion/platform-react-native-expo-core': patch -'@youversion/platform-react-native-expo-ui': patch ---- - -Fix the top issues flagged by React Doctor. Replace relative barrel imports (`../storage`, `../lib`, `../hooks`, `../i18n`) with direct module paths to trim the app bundle and speed startup. Fix a stale-closure risk in the auth bootstrap effect by holding the latest `setAuthState`/`refreshToken`/`clearAuthState` in a ref instead of silencing the exhaustive-deps warning, preserving mount-only behavior. Reset version-picker state during render (previous-prop comparison) instead of in an effect, removing a brief stale-UI frame on sheet reopen in both the DOM picker and the native picker sheet. Drop a dead duplicate `READER_SETTINGS_PERSIST_KEY` export from core storage. diff --git a/.changeset/refactor-useeffect-anti-patterns.md b/.changeset/refactor-useeffect-anti-patterns.md deleted file mode 100644 index 0dfdeed..0000000 --- a/.changeset/refactor-useeffect-anti-patterns.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@youversion/platform-react-native-expo-ui': patch ---- - -Refactor `useEffect` anti-patterns per React's "You Might Not Need an Effect" guidance. `BibleCard` and `BibleReader` now persist uncontrolled version/location to their MMKV-backed stores from the `useControllableState` `onChange` handler instead of a separate state-mirroring effect. `onChange` fires only when the value actually changes, so defaults are no longer written to storage on mount/hydration and there is a single source of persistence rather than a duplicated effect. `NativeSheet` resets its Android loader during render via previous-`openKey` comparison instead of in the imperative snap effect, eliminating a one-frame flash of the previous sheet content marked ready on repeated opens. No API changes; the only behavioral nuance is that storage stays empty until the user changes something (identical fallback UX). diff --git a/.changeset/sheet-header-surface-parity.md b/.changeset/sheet-header-surface-parity.md deleted file mode 100644 index b720b80..0000000 --- a/.changeset/sheet-header-surface-parity.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@youversion/platform-react-native-expo-ui': patch ---- - -Match the version/chapter picker sheet header to the WebView surface (white in light mode, dark in dark mode) and keep the muted strip behind the search bar. Also corrects the dark sheet surface token to match the Web SDK background. diff --git a/.changeset/sheet-max-width-wide-screens.md b/.changeset/sheet-max-width-wide-screens.md deleted file mode 100644 index 2eefe02..0000000 --- a/.changeset/sheet-max-width-wide-screens.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@youversion/platform-react-native-expo-ui': patch ---- - -Cap all Native Sheets (version picker, chapter picker, reader settings, footnotes) at a maximum width of 640, horizontally centered, on wide screens like iPad. Below that breakpoint sheets remain full-width. The cap is applied once in `NativeSheet` via a computed horizontal margin on `@gorhom/bottom-sheet`'s `style` prop, so the whole sheet surface (handle, header, content, footer) is capped while the backdrop still covers the full screen. diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 8474214..6731fef 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -9,3 +9,9 @@ Initial release. Installation id, optional PKCE authentication, and storage adap - `YouVersionProvider` — installation id plus optional `auth` config (forwarded by the UI provider), and the `useYouVersion` hook - PKCE OAuth via `useYVAuth`, with auth types `AuthConfig`, `AuthScope`, and `YVUserInfo` - Token storage in `expo-secure-store`; token expiry and cached user info in MMKV via `mmkvStorage` + +**Auth hardening** + +- User info drops placeholder and non-`https` avatar URLs (blocked by iOS ATS and Android cleartext defaults anyway), so consumers never receive a broken picture URL +- Canceling sign-in (`access_denied` callback) is treated as a clean cancel rather than an error +- Cached user info is validated with a zod schema on read, so a corrupt or legacy cache entry can't surface wrong-typed fields diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 3dc211c..c7d84f4 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -9,7 +9,7 @@ Initial release. Drop YouVersion Bible content into an Expo app on iOS and Andro **Scripture display** - `BibleTextView` — render a verse or verse range from a USFM reference -- `BibleCard` — a verse with built-in reader controls +- `BibleCard` — a verse with built-in reader controls; opt into the version picker with `showVersionPicker` (off by default, matching the Web SDK) - `VerseOfTheDay` — the daily verse, ready to drop in **Bible reader** @@ -30,3 +30,9 @@ Initial release. Drop YouVersion Bible content into an Expo app on iOS and Andro - Footnotes, chapter, and version pickers open in native bottom sheets via `@gorhom/bottom-sheet` - WebView pre-warming so sheets open without a cold-start flash +- Sheets cap at 640 wide and center on large screens like iPad; full-width below that breakpoint + +**Types & constants** + +- `DEFAULT_BIBLE_VERSION_ID` (3034, Berean Standard Bible) — the SDK's default Bible version, exported so you don't hardcode the numeric id +- Component prop types exported for direct import: `BibleCardProps`, `BibleReaderProps`, `BibleReaderSettingsSheetProps`, `BibleTextViewProps`, `VerseOfTheDayProps`, `YouVersionAuthButtonProps` From 3ed230e242639aedfe81cacfdf17e393cdabae19 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Tue, 14 Jul 2026 10:56:16 -0500 Subject: [PATCH 02/12] chore: release as 0.9.1 instead of 1.0.0 Co-Authored-By: Claude Fable 5 --- packages/core/CHANGELOG.md | 2 +- packages/core/package.json | 2 +- packages/ui/CHANGELOG.md | 2 +- packages/ui/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 6731fef..3c19a89 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,6 +1,6 @@ # @youversion/platform-react-native-expo-core -## 1.0.0 +## 0.9.1 Initial release. Installation id, optional PKCE authentication, and storage adapters for the YouVersion Platform React Native (Expo) SDK. diff --git a/packages/core/package.json b/packages/core/package.json index 05db035..0b58deb 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@youversion/platform-react-native-expo-core", - "version": "0.9.0", + "version": "0.9.1", "description": "YouVersion Platform core for React Native (Expo) — installation id, optional PKCE auth, and storage.", "keywords": [ "youversion", diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index c7d84f4..a8e595c 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -1,6 +1,6 @@ # @youversion/platform-react-native-expo-ui -## 1.0.0 +## 0.9.1 Initial release. Drop YouVersion Bible content into an Expo app on iOS and Android, with native bottom sheets, theming, and optional sign-in. Built on the [React Web SDK](https://github.com/youversion/platform-sdk-react) wrapped as [Expo DOM Components](https://docs.expo.dev/guides/dom-components/), with native affordances layered on top. diff --git a/packages/ui/package.json b/packages/ui/package.json index a12964c..373a3ae 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@youversion/platform-react-native-expo-ui", - "version": "0.9.0", + "version": "0.9.1", "description": "YouVersion Platform Bible components for React Native (Expo) — reader, verse cards, Verse of the Day, theming, and native sheets.", "keywords": [ "youversion", From 64decdefabbdc735917ca47eaf6639c9767b67b8 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Tue, 14 Jul 2026 13:23:54 -0500 Subject: [PATCH 03/12] Fix: This makes it where running the build script locally doesn't trigger HMR --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f4b08f2..9c4e6e6 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "url": "https://github.com/youversion/platform-sdk-reactnative-expo.git" }, "scripts": { - "build": "turbo build", + "build": "CI=true turbo build", "dev": "turbo dev", "typecheck": "turbo typecheck", "test": "turbo test", From c429155198d62519a7175013b5485677f2feb5bd Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Tue, 14 Jul 2026 13:57:44 -0500 Subject: [PATCH 04/12] feat: seal package import surface to the root entry point Add a root-only `exports` map to both packages so consumers can only import from the package root (`@youversion/platform-react-native-expo-ui` and `-core`). Deep imports into internal build files (e.g. `.../build/dom/bible-card`) are no longer supported: Node resolution and TypeScript reject them, keeping the internal file layout non-API before the first real release (0.9.1). - exports["."] -> src/index.ts for dev; publishConfig.exports swaps to build/index.js + build/index.d.ts at publish (verified applied in the pnpm pack tarball). `./package.json` stays exposed. - ADR 0011 gains a "Sealed import surface" section, including the enforcement boundary: Node and TypeScript enforce the seal strictly, Metro does not (it falls back to filesystem resolution for unmatched subpaths even with unstable_enablePackageExports on). - CONTEXT.md and both CHANGELOGs note the sealed root-only surface. Verified: typecheck/test/build green; tarball root import resolves to build/index.js, types resolve under node16 + bundler, deep imports throw ERR_PACKAGE_PATH_NOT_EXPORTED; example app and a fresh SDK 56 app built from the tarballs both bundle; the fresh app renders real Bible content (VerseOfTheDay + BibleTextView) on an iOS simulator. Co-Authored-By: Claude Fable 5 --- CONTEXT.md | 2 +- docs/adr/0011-compiled-distribution.md | 26 ++++++++++++++++++++++++++ packages/core/CHANGELOG.md | 4 ++++ packages/core/package.json | 16 +++++++++++++++- packages/ui/CHANGELOG.md | 4 ++++ packages/ui/package.json | 16 +++++++++++++++- 6 files changed, 65 insertions(+), 3 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 8a7cbc0..dc6f12c 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -87,7 +87,7 @@ The visible controls around reader content, including chapter navigation, versio _Avoid_: Toolbar when referring to product behavior rather than the Web SDK component name **Compiled Distribution**: -The published package ships a compiled `build/` output (`expo-module build`, plain `tsc` → JS + `.d.ts`), not raw source. `tsc` preserves the `'use dom'` directive and the Expo Metro plugin processes it from compiled files in `node_modules`, so Expo DOM Components work when installed from npm. In-repo dev still resolves TypeScript source directly (`main` → `src/`); `publishConfig` swaps to `build/` at publish time, applied by pnpm (`pnpm publish` and `pnpm pack`) but not by raw `npm publish`. See ADR 0011 (supersedes the earlier source-only model). +The published package ships a compiled `build/` output (`expo-module build`, plain `tsc` → JS + `.d.ts`), not raw source. `tsc` preserves the `'use dom'` directive and the Expo Metro plugin processes it from compiled files in `node_modules`, so Expo DOM Components work when installed from npm. In-repo dev still resolves TypeScript source directly (`main` → `src/`); `publishConfig` swaps to `build/` at publish time, applied by pnpm (`pnpm publish` and `pnpm pack`) but not by raw `npm publish`. The import surface is sealed to the root entry point via a root-only `exports` map (dev → `src/index.ts`, publish → `build/index.js`), so deep imports into internal files are not part of the API. See ADR 0011 (supersedes the earlier source-only model). _Avoid_: Source-only, "a compiled build strips the directive" **SDK Attribution Header**: diff --git a/docs/adr/0011-compiled-distribution.md b/docs/adr/0011-compiled-distribution.md index 37fbb72..e30dcb2 100644 --- a/docs/adr/0011-compiled-distribution.md +++ b/docs/adr/0011-compiled-distribution.md @@ -28,6 +28,32 @@ The build tool is [`expo-module-scripts`](https://www.npmjs.com/package/expo-mod - `publishConfig` swaps `main` / `types` to `build/`, and `files: ["build"]` keeps source out of the tarball, so the published package ships compiled JS plus real `.d.ts`. - Releases MUST go through `pnpm changeset publish` (the configured pipeline). `pnpm publish` applies the `publishConfig` field overrides; raw `npm publish` does not — `main` would stay `src/index.ts` while `files: ["build"]` excludes `src/`, so the tarball would be broken (entry point missing or unable to resolve its imports), not a clean source ship. See [PUBLISHING.md](../../PUBLISHING.md). +## Sealed import surface + +Both packages declare a root-only `exports` map. The only entry points are `"."` (the package root) and `"./package.json"`; there are no subpath entries, so the supported way to consume either package is `import … from '@youversion/platform-react-native-expo-ui'` / `-core`, and a deep import like `@youversion/platform-react-native-expo-ui/build/dom/bible-card` is not part of the public API. + +Why seal it: + +- **Raw DOM components are not package API.** Only the barrel (`src/index.ts` → `build/index.js`) is public. DOM component files exist purely as the WebView-backed implementation of the native wrappers. +- **Deep imports would freeze the internal file layout.** Without `exports`, `build/dom/*`, `build/native/*`, and `build/lib/*` are all reachable, so any consumer importing them turns our directory structure into a compatibility contract. The map keeps refactors of internal layout non-breaking. + +### Enforcement boundary (important) + +The map is enforced strictly by Node's own resolver and by TypeScript, and loosely by Metro: + +- **Node resolution** hard-errors on an unmatched subpath: `require.resolve('@youversion/platform-react-native-expo-ui/build/dom/bible-card')` throws `ERR_PACKAGE_PATH_NOT_EXPORTED`. Same for any strict `exports`-aware bundler (webpack, Vite, esbuild, Rollup with default settings). +- **TypeScript** (`moduleResolution` `node16`/`nodenext`/`bundler`) refuses to resolve types for a deep path, so a consumer writing a deep import gets a compile error in-editor and in CI typecheck. This is the primary developer-facing guardrail — the import is rejected before it can ship. +- **Metro does _not_ enforce it.** Even with `resolver.unstable_enablePackageExports: true` (the Expo SDK 56 default), Metro falls back to plain filesystem resolution for a subpath that the `exports` map does not match, instead of hard-erroring the way Node does. Verified: a fresh SDK 56 app installing the packed tarballs bundles a deep import into `build/dom/*` without error. So at the RN bundler level a determined consumer can still reach internal files at runtime; the `exports` map does not make that physically impossible, it makes it unsupported and type-rejected. If we ever need Metro-level enforcement we would have to remove the files from the tarball or split internals into a separate unpublished package — out of scope here. + +Net: the seal reliably stops deep imports from being _written_ (TypeScript) and from resolving under Node and standard web bundlers; it documents internal files as non-API; it does not physically block Metro's loose runtime fallback. + +Dev vs. publish, same swap as `main`/`types`: + +- Top-level `exports["."]` points at `./src/index.ts` (both `types` and `default`), so in-repo Metro and `tsc` resolve TypeScript source directly — no build step for local dev or the example app. +- `publishConfig.exports` overrides `"."` to `types: ./build/index.d.ts`, `default: ./build/index.js`, applied by `pnpm publish` exactly like the `main`/`types` overrides. `"./package.json"` stays exposed in both. + +Timing: loosening an `exports` map later (adding subpaths) is additive and non-breaking, while tightening one later (removing subpaths consumers already import) is breaking. Sealing before the first real release (0.9.1) means the surface starts minimal and can only widen, never narrow, so no consumer ever depends on an internal path we later want to remove. + ## Consequences - Gains real `.d.ts` and removes 0002's `moduleResolution: bundler`-only constraint for consumers. diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 3c19a89..d07e1e0 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -15,3 +15,7 @@ Initial release. Installation id, optional PKCE authentication, and storage adap - User info drops placeholder and non-`https` avatar URLs (blocked by iOS ATS and Android cleartext defaults anyway), so consumers never receive a broken picture URL - Canceling sign-in (`access_denied` callback) is treated as a clean cancel rather than an error - Cached user info is validated with a zod schema on read, so a corrupt or legacy cache entry can't surface wrong-typed fields + +### Package surface + +- Imports are restricted to the package root via an `exports` map — import everything from `@youversion/platform-react-native-expo-core`. Deep imports (e.g. into `build/`) are not part of the public API. diff --git a/packages/core/package.json b/packages/core/package.json index 0b58deb..369a148 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -20,6 +20,13 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./package.json": "./package.json" + }, "files": [ "build", "README.md", @@ -27,7 +34,14 @@ ], "publishConfig": { "main": "build/index.js", - "types": "build/index.d.ts" + "types": "build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "default": "./build/index.js" + }, + "./package.json": "./package.json" + } }, "scripts": { "build": "expo-module build", diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index a8e595c..cdb1bcd 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -36,3 +36,7 @@ Initial release. Drop YouVersion Bible content into an Expo app on iOS and Andro - `DEFAULT_BIBLE_VERSION_ID` (3034, Berean Standard Bible) — the SDK's default Bible version, exported so you don't hardcode the numeric id - Component prop types exported for direct import: `BibleCardProps`, `BibleReaderProps`, `BibleReaderSettingsSheetProps`, `BibleTextViewProps`, `VerseOfTheDayProps`, `YouVersionAuthButtonProps` + +### Package surface + +- Imports are restricted to the package root via an `exports` map — import everything from `@youversion/platform-react-native-expo-ui`. Deep imports (e.g. into `build/`) are not part of the public API. diff --git a/packages/ui/package.json b/packages/ui/package.json index 373a3ae..f533df5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -20,6 +20,13 @@ "license": "Apache-2.0", "main": "src/index.ts", "types": "src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./package.json": "./package.json" + }, "files": [ "build", "README.md", @@ -27,7 +34,14 @@ ], "publishConfig": { "main": "build/index.js", - "types": "build/index.d.ts" + "types": "build/index.d.ts", + "exports": { + ".": { + "types": "./build/index.d.ts", + "default": "./build/index.js" + }, + "./package.json": "./package.json" + } }, "scripts": { "build": "expo-module build", From 6bdf39c3d986e3a588726cc409a89b67803ec199 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Tue, 14 Jul 2026 15:02:31 -0500 Subject: [PATCH 05/12] docs: package READMEs cross-link and point to dev docs Co-Authored-By: Claude Fable 5 --- packages/core/README.md | 51 +++++++++++++++++++++++++++++++++++++---- packages/ui/README.md | 48 +++++++++++++++++++++++++++++++++----- 2 files changed, 88 insertions(+), 11 deletions(-) diff --git a/packages/core/README.md b/packages/core/README.md index e73801c..6a633c1 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,6 +1,22 @@ +![License](https://img.shields.io/badge/license-Apache%202.0-blue) + # @youversion/platform-react-native-expo-core -Core for the YouVersion Platform React Native (Expo) SDK: installation id, optional PKCE OAuth (`useYVAuth`), and storage (`expo-secure-store` + MMKV). Bible UI components live in [`@youversion/platform-react-native-expo-ui`](https://github.com/youversion/platform-sdk-reactnative-expo). +Auth and storage primitives for the YouVersion Platform React Native (Expo) SDK: installation id, optional PKCE OAuth, and token storage. + +## When to use this package + +Use `@youversion/platform-react-native-expo-core` when you need: + +- ✅ Sign-in via optional PKCE OAuth (`useYVAuth`, `auth` config on the provider) +- ✅ A YouVersion installation id for your app +- ✅ Token storage handled for you (`expo-secure-store` + MMKV) + +**Use other packages instead if you:** + +- ❌ Want ready-made Bible UI → use [@youversion/platform-react-native-expo-ui](https://www.npmjs.com/package/@youversion/platform-react-native-expo-ui) + +Most apps get this package as a dependency of the UI package, but install it directly so TypeScript resolves the auth APIs. Import Bible components from the UI package; import `useYVAuth` from core. ## Install @@ -8,12 +24,37 @@ Core for the YouVersion Platform React Native (Expo) SDK: installation id, optio npx expo install @youversion/platform-react-native-expo-core ``` -Most apps install this automatically as a dependency of `@youversion/platform-react-native-expo-ui`. Requires Expo SDK 56+, React 19, and a dev build (not Expo Go). Peer dependencies are listed in `package.json`. +Requires Expo SDK 56+, React 19, and a [development build](https://docs.expo.dev/develop/development-builds/introduction/) (not Expo Go — the SDK relies on native modules). Peer dependencies are listed in [`package.json`](./package.json). + +Get your App Key at [platform.youversion.com](https://platform.youversion.com/). -## Documentation +## Usage -Full setup, usage, and examples: **[github.com/youversion/platform-sdk-reactnative-expo](https://github.com/youversion/platform-sdk-reactnative-expo#readme)** +Configure `auth` on the provider, then read auth state with `useYVAuth`: + +```tsx +import { YouVersionProvider, useYVAuth } from '@youversion/platform-react-native-expo-core' + +function Profile() { + const { isAuthenticated, userInfo, signIn, signOut } = useYVAuth() + return {isAuthenticated ? userInfo?.name : 'Signed out'} +} + +export default function App() { + return ( + + + + ) +} +``` + +## Documentation and API Reference + +- [React Native (Expo) SDK Quick Start](https://developers.youversion.com/sdks/react-native) ## License -[Apache-2.0](./LICENSE) +This SDK is licensed under [Apache 2.0](./LICENSE). + +Licensing information for the Bible versions is available at the [YouVersion Platform](https://platform.youversion.com/) site. diff --git a/packages/ui/README.md b/packages/ui/README.md index 0d278a0..4e1f46f 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -1,8 +1,21 @@ +![License](https://img.shields.io/badge/license-Apache%202.0-blue) + # @youversion/platform-react-native-expo-ui -YouVersion Platform Bible components for React Native (Expo): `BibleReader`, `BibleCard`, `BibleTextView`, `VerseOfTheDay`, theming, and native bottom sheets. Built on the [React Web SDK](https://github.com/youversion/platform-sdk-react) via [Expo DOM Components](https://docs.expo.dev/guides/dom-components/). +Drop-in YouVersion Bible components for React Native (Expo) apps. Built on the React Web SDK via [Expo DOM Components](https://docs.expo.dev/guides/dom-components/). + +## When to use this package + +Use `@youversion/platform-react-native-expo-ui` when you need: -Pairs with `@youversion/platform-react-native-expo-core` (installed alongside). +- ✅ Pre-built Bible components for Expo: `BibleCard`, `BibleReader`, `BibleTextView`, `VerseOfTheDay` +- ✅ Version/chapter picker and reader-settings bottom sheets, plus `YouVersionAuthButton` +- ✅ Light/dark theming and a single `YouVersionProvider` to wrap your app +- ✅ Minimal setup: install, wrap, render + +**Use other packages instead if you:** + +- ❌ Only need auth or storage APIs (no UI) → use [@youversion/platform-react-native-expo-core](https://www.npmjs.com/package/@youversion/platform-react-native-expo-core) directly ## Install @@ -10,12 +23,35 @@ Pairs with `@youversion/platform-react-native-expo-core` (installed alongside). npx expo install @youversion/platform-react-native-expo-ui @youversion/platform-react-native-expo-core ``` -Requires Expo SDK 56+, React 19, and a [dev build](https://docs.expo.dev/develop/development-builds/introduction/) (not Expo Go). Peer dependencies are listed in `package.json`. +Requires Expo SDK 56+, React 19, and a [development build](https://docs.expo.dev/develop/development-builds/introduction/) (not Expo Go — the SDK relies on native modules). Peer dependencies are listed in [`package.json`](./package.json). + +Get your App Key at [platform.youversion.com](https://platform.youversion.com/). -## Documentation +## Usage -Full setup, usage, and examples: **[github.com/youversion/platform-sdk-reactnative-expo](https://github.com/youversion/platform-sdk-reactnative-expo#readme)** +Keep `GestureHandlerRootView` outside `YouVersionProvider`, then render components inside: + +```tsx +import { GestureHandlerRootView } from 'react-native-gesture-handler' +import { YouVersionProvider, BibleCard } from '@youversion/platform-react-native-expo-ui' + +export default function App() { + return ( + + + + + + ) +} +``` + +## Documentation and API Reference + +- [React Native (Expo) SDK Quick Start](https://developers.youversion.com/sdks/react-native) ## License -[Apache-2.0](./LICENSE) +This SDK is licensed under [Apache 2.0](./LICENSE). + +Licensing information for the Bible versions is available at the [YouVersion Platform](https://platform.youversion.com/) site. From 4aaa8b9b3b83ec22abbbc58d4d74a03204904fae Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Tue, 14 Jul 2026 15:03:59 -0500 Subject: [PATCH 06/12] docs: import Text in core README snippet Co-Authored-By: Claude Fable 5 --- packages/core/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/README.md b/packages/core/README.md index 6a633c1..2ff5968 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -33,6 +33,7 @@ Get your App Key at [platform.youversion.com](https://platform.youversion.com/). Configure `auth` on the provider, then read auth state with `useYVAuth`: ```tsx +import { Text } from 'react-native' import { YouVersionProvider, useYVAuth } from '@youversion/platform-react-native-expo-core' function Profile() { From 261cd341f21612702258e16b534f46cb418367a1 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Thu, 16 Jul 2026 10:57:55 -0500 Subject: [PATCH 07/12] docs: address review feedback on 0.9.1 release notes and READMEs Un-export DEFAULT_BIBLE_VERSION_ID per review: the constant stays internal as the default for BibleCard/BibleReader/picker sheets, but the SDK no longer publishes an official default version id. README examples pass 3034 literally instead. Also applies the reviewed copy edits to the package READMEs and the UI changelog. Co-Authored-By: Claude Opus 4.8 --- AGENTS.md | 2 +- README.md | 24 ++++++++++++------------ packages/core/README.md | 14 ++++++-------- packages/ui/CHANGELOG.md | 11 ++++++----- packages/ui/README.md | 6 ++---- packages/ui/src/index.ts | 1 - 6 files changed, 27 insertions(+), 31 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 0609efe..7495780 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -142,7 +142,7 @@ Keep `apps/example/metro.config.js` minimal — just `getDefaultConfig(__dirname ## Exports -**UI** (`@youversion/platform-react-native-expo-ui`): `YouVersionProvider`, `BibleCard`, `BibleChapterPickerSheet`, `BibleReader`, `BibleReaderSettingsSheet`, `BibleTextView`, `BibleVersionPickerSheet`, `VerseOfTheDay`, `YouVersionAuthButton`, and the `DEFAULT_BIBLE_VERSION_ID` constant (3034, Berean Standard Bible) +**UI** (`@youversion/platform-react-native-expo-ui`): `YouVersionProvider`, `BibleCard`, `BibleChapterPickerSheet`, `BibleReader`, `BibleReaderSettingsSheet`, `BibleTextView`, `BibleVersionPickerSheet`, `VerseOfTheDay`, and `YouVersionAuthButton` **Core** (`@youversion/platform-react-native-expo-core`): `YouVersionProvider` (installation id + optional auth), `useYouVersion`, `useYVAuth`, `mmkvStorage`, and auth types (`AuthConfig`, `AuthScope`, `YVUserInfo`) diff --git a/README.md b/README.md index 8cfb8a9..2d9d962 100644 --- a/README.md +++ b/README.md @@ -100,13 +100,13 @@ Native SDK strings follow the device locale by default; see the [localization gu Display a verse range or single verse with `BibleTextView`: ```tsx -import { BibleTextView, DEFAULT_BIBLE_VERSION_ID } from '@youversion/platform-react-native-expo-ui' +import { BibleTextView } from '@youversion/platform-react-native-expo-ui' function VerseScreen() { return ( ) } @@ -117,11 +117,11 @@ function VerseScreen() { Display a Bible card with a verse and reader controls: ```tsx -import { BibleCard, DEFAULT_BIBLE_VERSION_ID } from '@youversion/platform-react-native-expo-ui' +import { BibleCard } from '@youversion/platform-react-native-expo-ui' -// DEFAULT_BIBLE_VERSION_ID is 3034 (Berean Standard Bible); find other IDs at platform.youversion.com +// 3034 = Berean Standard Bible (BSB); find other IDs at platform.youversion.com function CardScreen() { - return + return } ``` @@ -134,11 +134,11 @@ function CardScreen() { `BibleReader` gives you a full Bible reading experience, ready to drop in as a tab or full screen: ```tsx -import { BibleReader, DEFAULT_BIBLE_VERSION_ID } from '@youversion/platform-react-native-expo-ui' +import { BibleReader } from '@youversion/platform-react-native-expo-ui' -// DEFAULT_BIBLE_VERSION_ID is 3034 (Berean Standard Bible); find other IDs at platform.youversion.com +// 3034 = Berean Standard Bible (BSB); find other IDs at platform.youversion.com function ReaderScreen() { - return + return } ``` @@ -150,7 +150,7 @@ To present your own picker UI instead of the built-in sheets, pass `onChapterPic ```tsx { // present your own version picker }} @@ -162,11 +162,11 @@ The standalone sheets are also exported (`BibleChapterPickerSheet`, `BibleVersio ### Verse of the Day ```tsx -import { VerseOfTheDay, DEFAULT_BIBLE_VERSION_ID } from '@youversion/platform-react-native-expo-ui' +import { VerseOfTheDay } from '@youversion/platform-react-native-expo-ui' -// DEFAULT_BIBLE_VERSION_ID is 3034 (Berean Standard Bible); find other IDs at platform.youversion.com +// 3034 = Berean Standard Bible (BSB); find other IDs at platform.youversion.com function VotdScreen() { - return + return } ``` diff --git a/packages/core/README.md b/packages/core/README.md index 2ff5968..bdc6978 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -12,11 +12,9 @@ Use `@youversion/platform-react-native-expo-core` when you need: - ✅ A YouVersion installation id for your app - ✅ Token storage handled for you (`expo-secure-store` + MMKV) -**Use other packages instead if you:** +❌ Want ready-made Bible UI instead? Use [@youversion/platform-react-native-expo-ui](https://www.npmjs.com/package/@youversion/platform-react-native-expo-ui). -- ❌ Want ready-made Bible UI → use [@youversion/platform-react-native-expo-ui](https://www.npmjs.com/package/@youversion/platform-react-native-expo-ui) - -Most apps get this package as a dependency of the UI package, but install it directly so TypeScript resolves the auth APIs. Import Bible components from the UI package; import `useYVAuth` from core. +Install this package directly alongside the UI package. The UI package already depends on core, but a direct dependency is what lets TypeScript resolve the auth APIs, so add it any time your app touches auth. Import Bible components from the UI package; import `useYVAuth` from core. ## Install @@ -28,6 +26,10 @@ Requires Expo SDK 56+, React 19, and a [development build](https://docs.expo.dev Get your App Key at [platform.youversion.com](https://platform.youversion.com/). +## Documentation and API Reference + +- [React Native (Expo) SDK Quick Start](https://developers.youversion.com/sdks/react-native) + ## Usage Configure `auth` on the provider, then read auth state with `useYVAuth`: @@ -50,10 +52,6 @@ export default function App() { } ``` -## Documentation and API Reference - -- [React Native (Expo) SDK Quick Start](https://developers.youversion.com/sdks/react-native) - ## License This SDK is licensed under [Apache 2.0](./LICENSE). diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index cdb1bcd..adc16db 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -9,7 +9,7 @@ Initial release. Drop YouVersion Bible content into an Expo app on iOS and Andro **Scripture display** - `BibleTextView` — render a verse or verse range from a USFM reference -- `BibleCard` — a verse with built-in reader controls; opt into the version picker with `showVersionPicker` (off by default, matching the Web SDK) +- `BibleCard` — a verse with built-in reader controls - `VerseOfTheDay` — the daily verse, ready to drop in **Bible reader** @@ -17,6 +17,8 @@ Initial release. Drop YouVersion Bible content into an Expo app on iOS and Andro - `BibleReader` — a full reading experience with built-in chapter and version pickers; bring your own picker UI via `onChapterPickerPress` / `onVersionPickerPress` - Standalone sheets for advanced flows: `BibleChapterPickerSheet`, `BibleVersionPickerSheet`, `BibleReaderSettingsSheet` +Every prop and option for these components is documented at [developers.youversion.com/sdks/react-native](https://developers.youversion.com/sdks/react-native). + **Provider & theming** - `YouVersionProvider` — single root provider supplying your `appKey`, resolved theme, and native sheet support @@ -32,11 +34,10 @@ Initial release. Drop YouVersion Bible content into an Expo app on iOS and Andro - WebView pre-warming so sheets open without a cold-start flash - Sheets cap at 640 wide and center on large screens like iPad; full-width below that breakpoint -**Types & constants** +**Types** -- `DEFAULT_BIBLE_VERSION_ID` (3034, Berean Standard Bible) — the SDK's default Bible version, exported so you don't hardcode the numeric id -- Component prop types exported for direct import: `BibleCardProps`, `BibleReaderProps`, `BibleReaderSettingsSheetProps`, `BibleTextViewProps`, `VerseOfTheDayProps`, `YouVersionAuthButtonProps` +- Prop types are exported for each component, e.g. `BibleCardProps`, `BibleReaderProps`, `BibleTextViewProps` ### Package surface -- Imports are restricted to the package root via an `exports` map — import everything from `@youversion/platform-react-native-expo-ui`. Deep imports (e.g. into `build/`) are not part of the public API. +- Only the package root is importable — import everything from `@youversion/platform-react-native-expo-ui`. If you want to see how it all works, read the source on [GitHub](https://github.com/youversion/platform-sdk-reactnative-expo). diff --git a/packages/ui/README.md b/packages/ui/README.md index 4e1f46f..5937d88 100644 --- a/packages/ui/README.md +++ b/packages/ui/README.md @@ -10,12 +10,10 @@ Use `@youversion/platform-react-native-expo-ui` when you need: - ✅ Pre-built Bible components for Expo: `BibleCard`, `BibleReader`, `BibleTextView`, `VerseOfTheDay` - ✅ Version/chapter picker and reader-settings bottom sheets, plus `YouVersionAuthButton` -- ✅ Light/dark theming and a single `YouVersionProvider` to wrap your app +- ✅ Light/dark theming across every component, from one `YouVersionProvider` at your app root - ✅ Minimal setup: install, wrap, render -**Use other packages instead if you:** - -- ❌ Only need auth or storage APIs (no UI) → use [@youversion/platform-react-native-expo-core](https://www.npmjs.com/package/@youversion/platform-react-native-expo-core) directly +❌ Only need auth or storage APIs, with no UI? Use [@youversion/platform-react-native-expo-core](https://www.npmjs.com/package/@youversion/platform-react-native-expo-core) directly. ## Install diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 480cd88..17f0ace 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -9,7 +9,6 @@ export { YouVersionAuthButton, YouVersionProvider, } from './native' -export { DEFAULT_BIBLE_VERSION_ID } from './lib/constants' export type { BibleCardProps, BibleChapterPickerSheetProps, From 56370d54cf4170d36046eef9360665587f839783 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Thu, 16 Jul 2026 11:07:10 -0500 Subject: [PATCH 08/12] docs: drop installation id bullet from core README Per review: the installation id is automatic and doesn't warrant a "when you need this package" bullet. It stays mentioned in the intro line. Co-Authored-By: Claude Opus 4.8 --- packages/core/README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/core/README.md b/packages/core/README.md index bdc6978..8c09b11 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -9,7 +9,6 @@ Auth and storage primitives for the YouVersion Platform React Native (Expo) SDK: Use `@youversion/platform-react-native-expo-core` when you need: - ✅ Sign-in via optional PKCE OAuth (`useYVAuth`, `auth` config on the provider) -- ✅ A YouVersion installation id for your app - ✅ Token storage handled for you (`expo-secure-store` + MMKV) ❌ Want ready-made Bible UI instead? Use [@youversion/platform-react-native-expo-ui](https://www.npmjs.com/package/@youversion/platform-react-native-expo-ui). From ba07f4c52e5a2067006e80c24d5abf6b78222789 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Thu, 16 Jul 2026 12:10:37 -0500 Subject: [PATCH 09/12] chore: fold the x-yvp-sdk changeset into the 0.9.1 changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #85 landed on main with a pending changeset. Consuming it here keeps .changeset/ empty, which is what makes the release workflow publish on merge instead of opening a "Version Packages" PR. The attribution behavior is folded into the 0.9.1 UI changelog as part of the initial release rather than as a fix, since there is no prior release to have regressed from. Core is untouched — the changeset marked UI only. Co-Authored-By: Claude Opus 4.8 --- .changeset/sdk-version-header-stamp.md | 5 ----- packages/ui/CHANGELOG.md | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 .changeset/sdk-version-header-stamp.md diff --git a/.changeset/sdk-version-header-stamp.md b/.changeset/sdk-version-header-stamp.md deleted file mode 100644 index 808d486..0000000 --- a/.changeset/sdk-version-header-stamp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@youversion/platform-react-native-expo-ui': patch ---- - -Split dev and partner traffic in the `x-yvp-sdk` telemetry header. Every published build previously reported `ReactNativeSDK=Dev`, because the documented "release workflow rewrites the value" step never existed — so partner and internal traffic were indistinguishable. Published builds now report `ReactNativeSDK={version}` and source builds report `ReactNativeSDK={version}-dev`, matching the Web SDK's format so one telemetry rule (`endsWith('-dev')`) covers both SDKs. A `prepublishOnly` stamp (`scripts/stamp-sdk-version.cjs`) flips the build-channel flag in the compiled build and aborts the publish if it cannot confirm the channel. See ADR 0012. diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index adc16db..64266ef 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -38,6 +38,10 @@ Every prop and option for these components is documented at [developers.youversi - Prop types are exported for each component, e.g. `BibleCardProps`, `BibleReaderProps`, `BibleTextViewProps` +**Attribution** + +- SDK traffic is identified to YouVersion in the `x-yvp-sdk` header as `ReactNativeSDK={version}`; builds running from source report `{version}-dev`, matching the Web SDK's format + ### Package surface - Only the package root is importable — import everything from `@youversion/platform-react-native-expo-ui`. If you want to see how it all works, read the source on [GitHub](https://github.com/youversion/platform-sdk-reactnative-expo). From 2c7758843b740f2f32d76260348d5e94be2f76e8 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Thu, 16 Jul 2026 12:27:31 -0500 Subject: [PATCH 10/12] docs(adr-0012): clarify that publish retries are safe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ADR documented the stamp's fail-closed guard but was silent on what happens when a release is retried, leaving the guard's "refuses to re-stamp" behavior to read as a retry hazard. It isn't one. `expo-module prepublishOnly` is `expo-module clean && expo-module build`, so `build/` is regenerated from source — anchor back at `= false` — before the stamp runs. A retried release meets exactly one anchor again rather than the `= true` file from the previous attempt. The transform stays deliberately non-idempotent: it throws `found 0` on already-stamped input, and nothing on the publish path reaches the stamp without a rebuild in front of it. That distinction is what the ADR was missing. Co-Authored-By: Claude Opus 4.8 --- docs/adr/0012-sdk-version-stamp-on-publish.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/adr/0012-sdk-version-stamp-on-publish.md b/docs/adr/0012-sdk-version-stamp-on-publish.md index 36c3c64..bcf15b2 100644 --- a/docs/adr/0012-sdk-version-stamp-on-publish.md +++ b/docs/adr/0012-sdk-version-stamp-on-publish.md @@ -49,6 +49,7 @@ We land on the same **outcome** as the Web SDK via a different mechanism, becaus - **One `.cjs` file** (`scripts/stamp-sdk-version.cjs`) holds both the pure transform and the file IO. The IO runs only under `require.main === module`. `packages/ui` has no `"type": "module"` yet emits ESM-syntax `build/` output, so a Node script cannot cleanly `import` the compiled file; a self-contained CJS script side-steps the module-system mismatch and runs natively at publish with no build step of its own. - **Wired into `prepublishOnly`, not `release.yml`.** Folding it into the build script gives deterministic build-then-stamp ordering (immune to npm/pnpm lifecycle-order quirks) and keeps the mechanism visible from `package.json` rather than buried in a workflow. A CI-only step would recreate the original failure mode: behavior hidden in the workflow, invisible from the source file. - **The guard asserts the positive stamp, and fails closed.** The transform throws unless it finds exactly one `IS_PUBLISH_BUILD = false` anchor, then asserts post-stamp that `= true` is present and `= false` is gone. It deliberately does **not** check that `-dev` is absent: the compiled ternary keeps the suffix in its dead else branch in _every_ build, published or not, so a `-dev` match would false-positive on every artifact. Checking only that `= false` disappeared would fail **open** — if tooling ever folded or minified the constant away, neither literal would survive and a dev build would sail through. Requiring the stamp means a build whose channel cannot be confirmed aborts the publish. +- **Re-running the publish is safe, because the rebuild precedes the stamp.** `expo-module prepublishOnly` is `expo-module clean && expo-module build`, so `build/` is deleted and regenerated from source — anchor back at `= false` — before the stamp runs. A retried release (re-run from the Actions UI, or a second `pnpm publish`) therefore meets exactly one anchor again, not the `= true` file left behind by the previous attempt. The transform itself is deliberately **not** idempotent: `stampPublishBuild` on already-stamped input throws `found 0`, and `node scripts/stamp-sdk-version.cjs` run twice with no rebuild between fails the same way. That is the guard above doing its job, not a retry hazard — nothing on the publish path reaches the stamp without a rebuild in front of it. - **The anchor is checked against real `tsc` output in CI.** `SENTINEL` matches _compiled_ text, so it can drift away from source with every fixture-based unit test still green — and the first sign would be `changeset publish` throwing mid-release, after `packages/core` may already be on npm (a partial release). `src/lib/__tests__/sdk-version-stamp.test.ts` therefore transpiles the real `src/lib/sdk-version.ts` and asserts the anchor survives, moving that failure to the PR that causes it. - **`pnpm pack` does not stamp.** `prepublishOnly` runs on `pnpm publish` only — `pnpm pack` runs neither the build nor the stamp, so a packed tarball reports `-dev` (or ships empty, if `build/` is absent). To reproduce a publish-identical tarball off-CI, run the publish steps by hand: From fbaf00298b570e1a5c2074c9b2179adfb089519d Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Thu, 16 Jul 2026 12:37:02 -0500 Subject: [PATCH 11/12] chore: fold the dependency-pinning changeset into the 0.9.1 changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #86 landed on main with a pending changeset. Consuming it here keeps .changeset/ empty, which is what makes the release workflow publish on merge instead of opening a "Version Packages" PR. The pinning is folded into the 0.9.1 UI changelog as a durable property of the package surface rather than as a change, since there is no prior release to have pinned differently. Core is untouched — the changeset marked UI only. Co-Authored-By: Claude Opus 4.8 --- .changeset/pin-deps-supply-chain.md | 5 ----- packages/ui/CHANGELOG.md | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) delete mode 100644 .changeset/pin-deps-supply-chain.md diff --git a/.changeset/pin-deps-supply-chain.md b/.changeset/pin-deps-supply-chain.md deleted file mode 100644 index c67340f..0000000 --- a/.changeset/pin-deps-supply-chain.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@youversion/platform-react-native-expo-ui': patch ---- - -Pin runtime dependencies to exact versions (`@youversion/platform-react-ui`, `expo-localization`, `i18next`, `react-i18next`, `zustand`) as supply-chain protection, so consumer installs can never silently resolve a newer, potentially compromised release. Peer dependency ranges are unchanged. diff --git a/packages/ui/CHANGELOG.md b/packages/ui/CHANGELOG.md index 64266ef..3d7ee55 100644 --- a/packages/ui/CHANGELOG.md +++ b/packages/ui/CHANGELOG.md @@ -45,3 +45,4 @@ Every prop and option for these components is documented at [developers.youversi ### Package surface - Only the package root is importable — import everything from `@youversion/platform-react-native-expo-ui`. If you want to see how it all works, read the source on [GitHub](https://github.com/youversion/platform-sdk-reactnative-expo). +- Runtime dependencies ship pinned to exact versions, so an install resolves exactly what was published and tested rather than silently picking up a newer release. Peer dependency ranges are unchanged. From 0d5be0d824bfbfec01031e5f3b276f1ebc809074 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Thu, 16 Jul 2026 14:24:43 -0500 Subject: [PATCH 12/12] ci: drop the inert npm upgrade from the release workflow The "Update npm to latest version" step claimed trusted publishing needed npm >= 11.5, or the OIDC handshake would silently fall back to NPM_TOKEN. That mechanism does not exist on this path. `changeset publish` shells out to `pnpm publish`, and pnpm 11 publishes over HTTP itself and runs the OIDC handshake in its own code. There is no `runNpm` on pnpm 11's publish path at all, so the npm on the runner never sees the token and its version cannot affect auth. npm is still invoked once per package for `npm info`, a read-only metadata query that any npm handles, so removing the global upgrade is safe. Correct the surrounding comments to match, since a misleading comment on the first CI publish sends whoever debugs it chasing a dead mechanism: - Describe the OIDC handshake as pnpm's rather than npm's. - Keep NPM_CONFIG_PROVENANCE as an explicit no-op. pnpm reads PNPM_CONFIG_* and ignores it; provenance is auto-enabled by pnpm's OIDC path for public repos on GitHub Actions. That last part is read from pnpm's source, not observed, so the var stays until a real run proves it redundant. - Fix the NODE_AUTH_TOKEN fallback, which as written would not have worked. pnpm never reads NODE_AUTH_TOKEN; it only arrives via an .npmrc, which actions/setup-node writes only when given a `registry-url`, and our setup action does not set one. Document both halves the fallback needs. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d43a86..3e8d523 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,8 +8,11 @@ on: concurrency: ${{ github.workflow }}-${{ github.ref }} # `id-token: write` is required so NPM Trusted Publishing can mint a short-lived -# OIDC token at publish time. With trusted publishing configured on npm there's -# no long-lived NPM_TOKEN to rotate, and every published tarball carries a +# OIDC token at publish time. The handshake is pnpm's own: `changeset publish` +# shells out to `pnpm publish`, and pnpm 11 publishes to the registry over HTTP +# itself rather than delegating to the npm CLI, so the npm version on the runner +# has no bearing on auth. With trusted publishing configured on npm there's no +# long-lived NPM_TOKEN to rotate, and every published tarball carries a # provenance attestation linking it to this exact workflow run. permissions: contents: write @@ -30,11 +33,6 @@ jobs: - uses: ./.github/actions/setup - # Trusted publishing requires npm >= 11.5. Pin to latest to avoid the - # OIDC handshake silently falling back to NPM_TOKEN on older npm. - - name: Update npm to latest version - run: npm install -g npm@latest - # No separate build step: `pnpm release` runs `turbo build` before # `changeset publish`, so the build only happens on the publish path. - name: Create Release Pull Request or Publish to NPM @@ -50,12 +48,29 @@ jobs: title: 'chore: version packages' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # A no-op under pnpm 11, kept as belt and braces. pnpm resolves its own + # settings from PNPM_CONFIG_* and ignores NPM_CONFIG_PROVENANCE, so + # what actually turns provenance on is pnpm's OIDC path auto-enabling + # it for public repos on GitHub Actions, which this repo is. That + # auto-detection is read from pnpm's source, not observed on a real + # run; no OIDC publish has ever executed for these package names. + # Until one has, keep this: it costs nothing if the inference is + # right, and silently loses attestations if it's wrong. NPM_CONFIG_PROVENANCE: true # Publishing auth comes solely from OIDC trusted publishing. We do not # pass a long-lived NPM_TOKEN. An automation token bypasses 2FA, so a - # standing secret widens the attack surface for little gain. If a - # bootstrap publish needs it before trusted publishing is live, add - # NODE_AUTH_TOKEN here as a temporary fallback and remove it after. + # standing secret widens the attack surface for little gain. + # + # If a bootstrap publish needs one before trusted publishing is live, + # NODE_AUTH_TOKEN on its own is not enough: pnpm never reads that + # variable. It only reaches pnpm through an .npmrc, which + # actions/setup-node writes (and points NPM_CONFIG_USERCONFIG at) only + # when it is given a `registry-url`, and .github/actions/setup does not + # set one. A working fallback therefore needs both halves: + # 1. `registry-url: 'https://registry.npmjs.org'` on the setup-node + # step in .github/actions/setup + # 2. `NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}` here + # Remove both once trusted publishing is live. - name: Publish summary if: steps.changesets.outputs.published == 'true'