From b950e95a5287f3356945bb3b3179f5a4b7dd430c Mon Sep 17 00:00:00 2001 From: Kyleasmth <45497561+Kyleasmth@users.noreply.github.com> Date: Thu, 14 May 2026 11:25:00 -0700 Subject: [PATCH 1/6] Add X-YVP-Sdk header and additionalHeaders override --- .changeset/x-yvp-sdk-header.md | 12 +++ packages/core/src/YouVersionAPI.ts | 2 + packages/core/src/__tests__/client.test.ts | 77 +++++++++++++++++++ packages/core/src/client.ts | 3 + packages/core/src/index.ts | 1 + packages/core/src/types/index.ts | 7 ++ packages/core/src/version.ts | 12 +++ .../hooks/src/context/YouVersionContext.tsx | 1 + .../hooks/src/context/YouVersionProvider.tsx | 27 ++++++- packages/hooks/src/useBibleClient.test.tsx | 24 ++++++ packages/hooks/src/useBibleClient.ts | 3 +- packages/hooks/src/useHighlights.ts | 3 +- packages/hooks/src/useLanguageClient.ts | 3 +- scripts/stamp-sdk-version.mjs | 40 ++++++++++ 14 files changed, 210 insertions(+), 5 deletions(-) create mode 100644 .changeset/x-yvp-sdk-header.md create mode 100644 packages/core/src/version.ts create mode 100644 scripts/stamp-sdk-version.mjs diff --git a/.changeset/x-yvp-sdk-header.md b/.changeset/x-yvp-sdk-header.md new file mode 100644 index 00000000..0ac3363d --- /dev/null +++ b/.changeset/x-yvp-sdk-header.md @@ -0,0 +1,12 @@ +--- +"@youversion/platform-core": minor +"@youversion/platform-react-hooks": minor +"@youversion/platform-react-ui": minor +--- + +Add `X-YVP-Sdk` header to every API call and let consumers override headers + +- New `X-YVP-Sdk: ReactSDK={version}` header sent on every request alongside `X-YVP-App-Key`. The version is stamped from `packages/core/package.json` by the release pipeline; local and non-release builds use `Dev` so they can be filtered out in the data lake. +- `SDK_VERSION`, `SDK_NAME`, and `SDK_VERSION_HEADER_NAME` exported from `@youversion/platform-core`. +- `ApiConfig` gains an optional `additionalHeaders` map that is merged into every request. Keys here override the SDK's built-in headers, so wrappers (e.g. the React Native Expo SDK) can replace `X-YVP-Sdk` with their own identifier. +- `YouVersionProvider` gains an `additionalHeaders` prop that flows through context to every hook-built `ApiClient`. diff --git a/packages/core/src/YouVersionAPI.ts b/packages/core/src/YouVersionAPI.ts index 8093221b..2e271453 100644 --- a/packages/core/src/YouVersionAPI.ts +++ b/packages/core/src/YouVersionAPI.ts @@ -1,10 +1,12 @@ import { YouVersionPlatformConfiguration } from './YouVersionPlatformConfiguration'; +import { SDK_VERSION_HEADER_NAME, buildSdkVersionHeaderValue } from './version'; export class YouVersionAPI { static addStandardHeaders(url: URL): Request { const headers: Record = { Accept: 'application/json', 'Content-Type': 'application/json', + [SDK_VERSION_HEADER_NAME]: buildSdkVersionHeaderValue(), }; const appKey = YouVersionPlatformConfiguration.appKey; diff --git a/packages/core/src/__tests__/client.test.ts b/packages/core/src/__tests__/client.test.ts index 2df047c9..3d6090f6 100644 --- a/packages/core/src/__tests__/client.test.ts +++ b/packages/core/src/__tests__/client.test.ts @@ -128,6 +128,83 @@ describe('ApiClient', () => { }); }); + describe('default headers', () => { + it('should send X-YVP-Sdk header with ReactSDK identifier on every request', async () => { + let receivedHeader: string | null = null; + server.use( + http.get('https://test_placeholder.youversion.com/test', ({ request }) => { + receivedHeader = request.headers.get('x-yvp-sdk'); + return HttpResponse.json({}); + }), + ); + + await apiClient.get('/test'); + + expect(receivedHeader).toMatch(/^ReactSDK=.+$/); + }); + + it('should send X-YVP-App-Key header on every request', async () => { + let receivedAppKey: string | null = null; + server.use( + http.get('https://test_placeholder.youversion.com/test', ({ request }) => { + receivedAppKey = request.headers.get('x-yvp-app-key'); + return HttpResponse.json({}); + }), + ); + + await apiClient.get('/test'); + + expect(receivedAppKey).toBe('test-app'); + }); + }); + + describe('additionalHeaders', () => { + it('should send caller-supplied headers in addition to the built-in ones', async () => { + const client = new ApiClient({ + apiHost: 'test_placeholder.youversion.com', + appKey: 'test-app', + additionalHeaders: { 'X-Custom': 'hello' }, + }); + + let receivedCustom: string | null = null; + let receivedAppKey: string | null = null; + server.use( + http.get('https://test_placeholder.youversion.com/test', ({ request }) => { + receivedCustom = request.headers.get('x-custom'); + receivedAppKey = request.headers.get('x-yvp-app-key'); + return HttpResponse.json({}); + }), + ); + + await client.get('/test'); + + expect(receivedCustom).toBe('hello'); + expect(receivedAppKey).toBe('test-app'); + }); + + it('should let additionalHeaders override the built-in X-YVP-Sdk header', async () => { + // Mirrors how a React Native Expo wrapper would replace the web SDK's + // identifier with its own. + const client = new ApiClient({ + apiHost: 'test_placeholder.youversion.com', + appKey: 'test-app', + additionalHeaders: { 'X-YVP-Sdk': 'ReactNativeSDK=1.2.3' }, + }); + + let receivedSdk: string | null = null; + server.use( + http.get('https://test_placeholder.youversion.com/test', ({ request }) => { + receivedSdk = request.headers.get('x-yvp-sdk'); + return HttpResponse.json({}); + }), + ); + + await client.get('/test'); + + expect(receivedSdk).toBe('ReactNativeSDK=1.2.3'); + }); + }); + describe('post', () => { it('should make POST request and return data', async () => { server.use( diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index c33b3249..9f0091bd 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -1,4 +1,5 @@ import type { ApiConfig } from './types'; +import { SDK_VERSION_HEADER_NAME, buildSdkVersionHeaderValue } from './version'; type PrimitiveQueryParam = string | number | boolean; type QueryParams = Record; @@ -34,6 +35,8 @@ export class ApiClient { 'Content-Type': 'application/json', 'X-YVP-App-Key': this.config.appKey, 'X-YVP-Installation-Id': this.config.installationId || 'web-sdk-default', + [SDK_VERSION_HEADER_NAME]: buildSdkVersionHeaderValue(), + ...config.additionalHeaders, }; } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9a04f008..05374090 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -20,3 +20,4 @@ export { type TransformBibleHtmlOptions, type TransformedBibleHtml, } from './bible-html-transformer'; +export * from './version'; diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 6b523599..792bafb4 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -27,6 +27,13 @@ export interface ApiConfig { timeout?: number; installationId?: string; redirectUri?: string; + /** + * Extra HTTP headers merged into every request. Values here override the + * SDK's built-in headers when keys collide — useful for wrappers (e.g. the + * React Native Expo SDK) that need to replace `X-YVP-Sdk` with their own + * identifier. + */ + additionalHeaders?: Record; } export type SignInWithYouVersionPermissionValues = diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts new file mode 100644 index 00000000..10b6eeca --- /dev/null +++ b/packages/core/src/version.ts @@ -0,0 +1,12 @@ +// SDK_VERSION is replaced by the release workflow with the precise published +// version before publishing to npm. Local/non-release builds keep "Dev" so the +// data lake can distinguish them from real release traffic. +export const SDK_VERSION = 'Dev'; + +export const SDK_NAME = 'ReactSDK'; + +export const SDK_VERSION_HEADER_NAME = 'X-YVP-Sdk'; + +export function buildSdkVersionHeaderValue(): string { + return `${SDK_NAME}=${SDK_VERSION}`; +} diff --git a/packages/hooks/src/context/YouVersionContext.tsx b/packages/hooks/src/context/YouVersionContext.tsx index 9bdae246..ba8f418a 100644 --- a/packages/hooks/src/context/YouVersionContext.tsx +++ b/packages/hooks/src/context/YouVersionContext.tsx @@ -8,6 +8,7 @@ type YouVersionContextData = { installationId?: string; theme?: 'light' | 'dark'; authEnabled?: boolean; + additionalHeaders?: Record; }; export const YouVersionContext = createContext(null); diff --git a/packages/hooks/src/context/YouVersionProvider.tsx b/packages/hooks/src/context/YouVersionProvider.tsx index 3e15baa5..05ebf309 100644 --- a/packages/hooks/src/context/YouVersionProvider.tsx +++ b/packages/hooks/src/context/YouVersionProvider.tsx @@ -1,7 +1,7 @@ 'use client'; import type { PropsWithChildren, ReactNode } from 'react'; -import { lazy, Suspense, useEffect, useState } from 'react'; +import { lazy, Suspense, useEffect, useMemo, useState } from 'react'; import { YouVersionContext } from './YouVersionContext'; import { YouVersionPlatformConfiguration } from '@youversion/platform-core'; @@ -10,6 +10,13 @@ interface YouVersionProviderPropsBase { appKey: string; apiHost?: string; theme?: 'light' | 'dark' | 'system'; + /** + * Extra HTTP headers to add to every API call made through hooks created by + * this provider. Values here override the SDK's built-in headers when keys + * collide — useful for wrappers (e.g. the React Native Expo SDK) that need + * to replace `X-YVP-Sdk` with their own identifier. + */ + additionalHeaders?: Record; } interface YouVersionProviderPropsWithAuth extends YouVersionProviderPropsBase { @@ -55,9 +62,24 @@ function useResolvedTheme(theme: 'light' | 'dark' | 'system'): 'light' | 'dark' export function YouVersionProvider( props: PropsWithChildren, ): React.ReactElement { - const { appKey, apiHost = 'api.youversion.com', includeAuth, theme = 'light', children } = props; + const { + appKey, + apiHost = 'api.youversion.com', + includeAuth, + theme = 'light', + additionalHeaders, + children, + } = props; const resolvedTheme = useResolvedTheme(theme); + // Stable identity so memoized consumers (hooks that build ApiClient) don't + // rebuild when the parent re-renders with an inline object literal. + const stableAdditionalHeaders = useMemo( + () => additionalHeaders, + // eslint-disable-next-line react-hooks/exhaustive-deps + [JSON.stringify(additionalHeaders ?? null)], + ); + // Sync props to YouVersionPlatformConfiguration so any code that reads the // static config (e.g. core's auth/PKCE flows, called from user actions) sees // the same values. Children that read via context get the prop directly @@ -73,6 +95,7 @@ export function YouVersionProvider( installationId: YouVersionPlatformConfiguration.installationId, theme: resolvedTheme, authEnabled: !!includeAuth, + additionalHeaders: stableAdditionalHeaders, }; if (includeAuth) { diff --git a/packages/hooks/src/useBibleClient.test.tsx b/packages/hooks/src/useBibleClient.test.tsx index 764b5738..498bbb34 100644 --- a/packages/hooks/src/useBibleClient.test.tsx +++ b/packages/hooks/src/useBibleClient.test.tsx @@ -96,6 +96,30 @@ describe('useBibleClient', () => { }); }); + it('should pass additionalHeaders from context into ApiClient config', () => { + const additionalHeaders = { 'X-YVP-Sdk': 'ReactNativeSDK=1.2.3' }; + + const wrapper = ({ children }: { children: ReactNode }) => ( + + {children} + + ); + + renderHook(() => useBibleClient(), { wrapper }); + + expect(ApiClient).toHaveBeenCalledWith( + expect.objectContaining({ + appKey: mockAppKey, + additionalHeaders, + }), + ); + }); + it('should throw error when appKey is null', () => { const wrapper = ({ children }: { children: ReactNode }) => ( >( () => highlightsClient.getHighlights(options), diff --git a/packages/hooks/src/useLanguageClient.ts b/packages/hooks/src/useLanguageClient.ts index fa740769..4fa6bd25 100644 --- a/packages/hooks/src/useLanguageClient.ts +++ b/packages/hooks/src/useLanguageClient.ts @@ -19,7 +19,8 @@ export function useLanguagesClient(): LanguagesClient { appKey: context.appKey, apiHost: context.apiHost, installationId: context.installationId, + additionalHeaders: context.additionalHeaders, }), ); - }, [context?.apiHost, context?.appKey, context?.installationId]); + }, [context?.apiHost, context?.appKey, context?.installationId, context?.additionalHeaders]); } diff --git a/scripts/stamp-sdk-version.mjs b/scripts/stamp-sdk-version.mjs new file mode 100644 index 00000000..8bb95dad --- /dev/null +++ b/scripts/stamp-sdk-version.mjs @@ -0,0 +1,40 @@ +#!/usr/bin/env node +// Writes the published version from packages/core/package.json into +// packages/core/src/version.ts so the X-YVP-Sdk header carries an exact +// version on every API call. Invoked by the `release` script in CI, just +// before the build that gets published to npm — local builds never run this, +// so SDK_VERSION stays at "Dev" for non-release artifacts. + +import { readFileSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(__dirname, '..'); +const corePackageJsonPath = resolve(repoRoot, 'packages/core/package.json'); +const versionFilePath = resolve(repoRoot, 'packages/core/src/version.ts'); + +const corePkg = JSON.parse(readFileSync(corePackageJsonPath, 'utf8')); +const version = corePkg.version; + +if (!version || typeof version !== 'string') { + console.error('stamp-sdk-version: could not read version from packages/core/package.json'); + process.exit(1); +} + +const source = readFileSync(versionFilePath, 'utf8'); +const stamped = source.replace( + /export const SDK_VERSION = '[^']*';/, + `export const SDK_VERSION = '${version}';`, +); + +if (stamped === source) { + console.error( + `stamp-sdk-version: failed to update SDK_VERSION in ${versionFilePath}. ` + + 'Has the export signature changed?', + ); + process.exit(1); +} + +writeFileSync(versionFilePath, stamped); +console.log(`stamp-sdk-version: SDK_VERSION = '${version}'`); From 8e301278f72c9d10e09361562923c6fd1d5a8410 Mon Sep 17 00:00:00 2001 From: Kyleasmth <45497561+Kyleasmth@users.noreply.github.com> Date: Thu, 14 May 2026 13:17:21 -0700 Subject: [PATCH 2/6] YPE-2295: Wire release stamp + address Greptile review --- package.json | 2 +- packages/hooks/src/context/YouVersionProvider.tsx | 9 +++++++-- scripts/stamp-sdk-version.mjs | 4 +++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index de1ecc21..06de5cf9 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", "changeset": "changeset", "version-packages": "changeset version", - "release": "turbo build && npm exec --yes -- changeset publish", + "release": "node scripts/stamp-sdk-version.mjs && turbo build --force && npm exec --yes -- changeset publish", "prepare": "husky", "analyze": "node scripts/analyze.mjs", "analyze:select": "node scripts/analyze-select.mjs" diff --git a/packages/hooks/src/context/YouVersionProvider.tsx b/packages/hooks/src/context/YouVersionProvider.tsx index 05ebf309..56bc243c 100644 --- a/packages/hooks/src/context/YouVersionProvider.tsx +++ b/packages/hooks/src/context/YouVersionProvider.tsx @@ -73,11 +73,16 @@ export function YouVersionProvider( const resolvedTheme = useResolvedTheme(theme); // Stable identity so memoized consumers (hooks that build ApiClient) don't - // rebuild when the parent re-renders with an inline object literal. + // rebuild when the parent re-renders with an inline object literal. Sort + // entries before serialising so key-insertion-order differences don't + // invalidate the memo for headers that are semantically identical. + const additionalHeadersKey = additionalHeaders + ? JSON.stringify(Object.entries(additionalHeaders).sort(([a], [b]) => a.localeCompare(b))) + : null; const stableAdditionalHeaders = useMemo( () => additionalHeaders, // eslint-disable-next-line react-hooks/exhaustive-deps - [JSON.stringify(additionalHeaders ?? null)], + [additionalHeadersKey], ); // Sync props to YouVersionPlatformConfiguration so any code that reads the diff --git a/scripts/stamp-sdk-version.mjs b/scripts/stamp-sdk-version.mjs index 8bb95dad..f416db10 100644 --- a/scripts/stamp-sdk-version.mjs +++ b/scripts/stamp-sdk-version.mjs @@ -23,9 +23,11 @@ if (!version || typeof version !== 'string') { } const source = readFileSync(versionFilePath, 'utf8'); +// Pass a replacer function so `$&`, `$'`, `` $` ``, and `$n` sequences in +// `version` (unlikely in semver, but possible) aren't interpreted by replace(). const stamped = source.replace( /export const SDK_VERSION = '[^']*';/, - `export const SDK_VERSION = '${version}';`, + () => `export const SDK_VERSION = '${version}';`, ); if (stamped === source) { From 73a10a541e0e2e18577de31eebd43fce5b5d9715 Mon Sep 17 00:00:00 2001 From: Kyleasmth <45497561+Kyleasmth@users.noreply.github.com> Date: Fri, 15 May 2026 09:39:35 -0700 Subject: [PATCH 3/6] YPE-2295: Switch to tsup define for SDK version injection Replace the fragile stamp-sdk-version.mjs script with tsup's define plugin, which injects the version from package.json at build time. This approach: - Eliminates regex fragility - Removes the need for --force rebuilds - Matches the pattern used by Stripe, Sentry, AWS SDKs - Simplifies the release pipeline The version is injected at build time and falls back to 'Dev' at runtime for test environments. --- package.json | 2 +- packages/core/src/version.ts | 12 +++++----- packages/core/tsup.config.ts | 14 ++++++++++++ scripts/stamp-sdk-version.mjs | 42 ----------------------------------- 4 files changed, 22 insertions(+), 48 deletions(-) create mode 100644 packages/core/tsup.config.ts delete mode 100644 scripts/stamp-sdk-version.mjs diff --git a/package.json b/package.json index 06de5cf9..de1ecc21 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "format": "prettier --write \"**/*.{ts,tsx,js,jsx,json,md}\"", "changeset": "changeset", "version-packages": "changeset version", - "release": "node scripts/stamp-sdk-version.mjs && turbo build --force && npm exec --yes -- changeset publish", + "release": "turbo build && npm exec --yes -- changeset publish", "prepare": "husky", "analyze": "node scripts/analyze.mjs", "analyze:select": "node scripts/analyze-select.mjs" diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index 10b6eeca..2f6efddb 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1,12 +1,14 @@ -// SDK_VERSION is replaced by the release workflow with the precise published -// version before publishing to npm. Local/non-release builds keep "Dev" so the -// data lake can distinguish them from real release traffic. -export const SDK_VERSION = 'Dev'; +// SDK_VERSION is injected by tsup during build using the version from package.json. +// It contains the exact published version on release builds. In development/test, +// it falls back to "Dev" if not defined by the bundler. +declare const SDK_VERSION: string; + +const resolvedSdkVersion = typeof SDK_VERSION !== 'undefined' ? SDK_VERSION : 'Dev'; export const SDK_NAME = 'ReactSDK'; export const SDK_VERSION_HEADER_NAME = 'X-YVP-Sdk'; export function buildSdkVersionHeaderValue(): string { - return `${SDK_NAME}=${SDK_VERSION}`; + return `${SDK_NAME}=${resolvedSdkVersion}`; } diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts new file mode 100644 index 00000000..ddc4ab79 --- /dev/null +++ b/packages/core/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; + +const pkg = JSON.parse(readFileSync(resolve(__dirname, './package.json'), 'utf8')); + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + define: { + SDK_VERSION: JSON.stringify(pkg.version), + }, +}); diff --git a/scripts/stamp-sdk-version.mjs b/scripts/stamp-sdk-version.mjs deleted file mode 100644 index f416db10..00000000 --- a/scripts/stamp-sdk-version.mjs +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env node -// Writes the published version from packages/core/package.json into -// packages/core/src/version.ts so the X-YVP-Sdk header carries an exact -// version on every API call. Invoked by the `release` script in CI, just -// before the build that gets published to npm — local builds never run this, -// so SDK_VERSION stays at "Dev" for non-release artifacts. - -import { readFileSync, writeFileSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const repoRoot = resolve(__dirname, '..'); -const corePackageJsonPath = resolve(repoRoot, 'packages/core/package.json'); -const versionFilePath = resolve(repoRoot, 'packages/core/src/version.ts'); - -const corePkg = JSON.parse(readFileSync(corePackageJsonPath, 'utf8')); -const version = corePkg.version; - -if (!version || typeof version !== 'string') { - console.error('stamp-sdk-version: could not read version from packages/core/package.json'); - process.exit(1); -} - -const source = readFileSync(versionFilePath, 'utf8'); -// Pass a replacer function so `$&`, `$'`, `` $` ``, and `$n` sequences in -// `version` (unlikely in semver, but possible) aren't interpreted by replace(). -const stamped = source.replace( - /export const SDK_VERSION = '[^']*';/, - () => `export const SDK_VERSION = '${version}';`, -); - -if (stamped === source) { - console.error( - `stamp-sdk-version: failed to update SDK_VERSION in ${versionFilePath}. ` + - 'Has the export signature changed?', - ); - process.exit(1); -} - -writeFileSync(versionFilePath, stamped); -console.log(`stamp-sdk-version: SDK_VERSION = '${version}'`); From 41c33ecdf7ffa693146589cd7d3aa08b10e68543 Mon Sep 17 00:00:00 2001 From: Kyleasmth <45497561+Kyleasmth@users.noreply.github.com> Date: Fri, 15 May 2026 11:24:09 -0700 Subject: [PATCH 4/6] YPE-2295: Import SDK version from package.json directly --- .changeset/x-yvp-sdk-header.md | 2 +- packages/core/src/version.ts | 9 +++------ packages/core/tsup.config.ts | 14 -------------- 3 files changed, 4 insertions(+), 21 deletions(-) delete mode 100644 packages/core/tsup.config.ts diff --git a/.changeset/x-yvp-sdk-header.md b/.changeset/x-yvp-sdk-header.md index 0ac3363d..d8634597 100644 --- a/.changeset/x-yvp-sdk-header.md +++ b/.changeset/x-yvp-sdk-header.md @@ -6,7 +6,7 @@ Add `X-YVP-Sdk` header to every API call and let consumers override headers -- New `X-YVP-Sdk: ReactSDK={version}` header sent on every request alongside `X-YVP-App-Key`. The version is stamped from `packages/core/package.json` by the release pipeline; local and non-release builds use `Dev` so they can be filtered out in the data lake. +- New `X-YVP-Sdk: ReactSDK={version}` header sent on every request alongside `X-YVP-App-Key`. The version is imported directly from `packages/core/package.json` and inlined by the bundler at build time. - `SDK_VERSION`, `SDK_NAME`, and `SDK_VERSION_HEADER_NAME` exported from `@youversion/platform-core`. - `ApiConfig` gains an optional `additionalHeaders` map that is merged into every request. Keys here override the SDK's built-in headers, so wrappers (e.g. the React Native Expo SDK) can replace `X-YVP-Sdk` with their own identifier. - `YouVersionProvider` gains an `additionalHeaders` prop that flows through context to every hook-built `ApiClient`. diff --git a/packages/core/src/version.ts b/packages/core/src/version.ts index 2f6efddb..a731e36c 100644 --- a/packages/core/src/version.ts +++ b/packages/core/src/version.ts @@ -1,14 +1,11 @@ -// SDK_VERSION is injected by tsup during build using the version from package.json. -// It contains the exact published version on release builds. In development/test, -// it falls back to "Dev" if not defined by the bundler. -declare const SDK_VERSION: string; +import pkg from '../package.json' with { type: 'json' }; -const resolvedSdkVersion = typeof SDK_VERSION !== 'undefined' ? SDK_VERSION : 'Dev'; +export const SDK_VERSION = pkg.version; export const SDK_NAME = 'ReactSDK'; export const SDK_VERSION_HEADER_NAME = 'X-YVP-Sdk'; export function buildSdkVersionHeaderValue(): string { - return `${SDK_NAME}=${resolvedSdkVersion}`; + return `${SDK_NAME}=${SDK_VERSION}`; } diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts deleted file mode 100644 index ddc4ab79..00000000 --- a/packages/core/tsup.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'tsup'; -import { readFileSync } from 'fs'; -import { resolve } from 'path'; - -const pkg = JSON.parse(readFileSync(resolve(__dirname, './package.json'), 'utf8')); - -export default defineConfig({ - entry: ['src/index.ts'], - format: ['cjs', 'esm'], - dts: true, - define: { - SDK_VERSION: JSON.stringify(pkg.version), - }, -}); From e1950d89485db5831c4ec3de754edddb69400e01 Mon Sep 17 00:00:00 2001 From: Kyleasmth <45497561+Kyleasmth@users.noreply.github.com> Date: Mon, 18 May 2026 13:52:40 -0700 Subject: [PATCH 5/6] YPE-2295: Fix useBibleClient test after main refactor --- packages/hooks/src/useBibleClient.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/hooks/src/useBibleClient.test.tsx b/packages/hooks/src/useBibleClient.test.tsx index 498bbb34..9489ee34 100644 --- a/packages/hooks/src/useBibleClient.test.tsx +++ b/packages/hooks/src/useBibleClient.test.tsx @@ -102,7 +102,7 @@ describe('useBibleClient', () => { const wrapper = ({ children }: { children: ReactNode }) => ( @@ -114,7 +114,7 @@ describe('useBibleClient', () => { expect(ApiClient).toHaveBeenCalledWith( expect.objectContaining({ - appKey: mockAppKey, + appKey: 'test-app-key', additionalHeaders, }), ); From a9f19de8a30b4993e520d11353f7772bbafa3920 Mon Sep 17 00:00:00 2001 From: Kyleasmth <45497561+Kyleasmth@users.noreply.github.com> Date: Tue, 19 May 2026 12:31:56 -0700 Subject: [PATCH 6/6] YPE-2295: Test additionalHeaders pass-through in UI YouVersionProvider --- .../components/YouVersionProvider.test.tsx | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 packages/ui/src/components/YouVersionProvider.test.tsx diff --git a/packages/ui/src/components/YouVersionProvider.test.tsx b/packages/ui/src/components/YouVersionProvider.test.tsx new file mode 100644 index 00000000..69ecc7ff --- /dev/null +++ b/packages/ui/src/components/YouVersionProvider.test.tsx @@ -0,0 +1,48 @@ +/** + * @vitest-environment jsdom + */ +import { describe, it, expect, vi } from 'vitest'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { YouVersionProvider } from '@/components/YouVersionProvider'; + +const baseProviderMock = + vi.fn<(props: Record & { children?: React.ReactNode }) => React.ReactElement>(); +baseProviderMock.mockImplementation(({ children }) => <>{children}); + +vi.mock('@youversion/platform-react-hooks', () => ({ + YouVersionProvider: (props: Record & { children?: React.ReactNode }) => + baseProviderMock(props), + useYVAuth: vi.fn(), +})); + +describe('UI YouVersionProvider', () => { + it('forwards additionalHeaders to the underlying hooks provider', () => { + const additionalHeaders = { 'X-YVP-Sdk': 'ReactNativeSDK=1.2.3' }; + + render( + +
hello
+
, + ); + + expect(baseProviderMock).toHaveBeenCalledWith( + expect.objectContaining({ + appKey: 'test-key', + additionalHeaders, + }), + ); + }); + + it('omits additionalHeaders when not provided', () => { + render( + +
hello
+
, + ); + + const lastCall = baseProviderMock.mock.calls.at(-1)?.[0] as Record; + expect(lastCall?.appKey).toBe('test-key'); + expect(lastCall?.additionalHeaders).toBeUndefined(); + }); +});