Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .changeset/paint-highlights-scripture-surfaces.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

feat: paint host highlights on BibleTextView, BibleCard, and VerseOfTheDay

Subscribe those surfaces at chapter scope and always pass Highlight[] into the DOM so paint uses the native cache. Pins @youversion/platform-react-ui to 2.7.0.
Subscribe those surfaces at chapter scope and always pass Highlight[] into the DOM so paint uses the native cache.
2 changes: 1 addition & 1 deletion .changeset/rn-expo-version-filter.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
'@youversion/platform-react-native-expo-ui': minor
---

Add optional version filter lists to `YouVersionProvider`: `permittedVersionIds`, `excludedVersionIds`, and `permittedLanguageTags`. The UI provider forwards them through native wrappers into each DOM web `YouVersionProvider`. Filtering runs in the web SDK once a 4657-shaped `@youversion/platform-react-ui` is published and pinned.
Add optional version filter lists to `YouVersionProvider`: `permittedVersionIds`, `excludedVersionIds`, and `permittedLanguageTags`. The UI provider forwards them through native wrappers into each DOM web `YouVersionProvider`. Pin `@youversion/platform-react-ui` and `@youversion/platform-core` to 2.8.0 so the web SDK enforces those lists in Expo DOM WebViews (YPE-4657/YPE-4658).
2 changes: 1 addition & 1 deletion apps/example/app/_layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function RootLayout() {
scopes: ['profile', 'email'],
permissions: ['highlights'],
}}
// Optional version filter (forwarded to the web SDK once published):
// Optional version filter (enforced in each Expo DOM WebView):
// permittedVersionIds={[111, 206]}
// excludedVersionIds={[3034]}
// permittedLanguageTags={['en', 'zh-Hans']}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
"jest-expo": "56.0.5"
},
"dependencies": {
"@youversion/platform-core": "2.7.0",
"@youversion/platform-core": "2.8.0",
"zod": "4.4.3"
}
}
11 changes: 7 additions & 4 deletions packages/core/src/auth/__tests__/data-exchange-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ beforeEach(() => {
global.fetch = mockFetch as unknown as typeof fetch
})

function header(init: RequestInit, name: string): string | null {
return new Headers(init.headers).get(name)
}

function jsonResponse(body: unknown, status = 201): Response {
return {
ok: status >= 200 && status < 300,
Expand Down Expand Up @@ -51,10 +55,9 @@ describe('createDataExchangeApi — mintToken', () => {
expect(url).toBe('https://api.example.com/data-exchange/token?app-key=appkey')
expect(init.method).toBe('POST')
expect(JSON.parse(init.body as string)).toEqual({ requested_permissions: ['highlights'] })
const headers = init.headers as Record<string, string>
expect(headers.Authorization).toBe('Bearer tok')
expect(headers['X-YVP-App-Key']).toBe('appkey')
expect(headers['X-YVP-Installation-Id']).toBe('inst-1')
expect(header(init, 'Authorization')).toBe('Bearer tok')
expect(header(init, 'X-YVP-App-Key')).toBe('appkey')
expect(header(init, 'X-YVP-Installation-Id')).toBe('inst-1')
})

it('falls back to the default API host when none is configured', async () => {
Expand Down
27 changes: 14 additions & 13 deletions packages/core/src/highlights/__tests__/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ function jsonResponse(body: unknown, status = 200): Response {
} as unknown as Response
}

function header(init: RequestInit, name: string): string | null {
return new Headers(init.headers).get(name)
}

function errorResponse(status: number, body = ''): Response {
return {
ok: false,
Expand Down Expand Up @@ -70,11 +74,10 @@ describe('createHighlightsApi', () => {
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit]
expect(url).toBe('https://api.example.com/v1/highlights?bible_id=111&passage_id=JHN.3')
expect(init.method).toBe('GET')
const headers = init.headers as Record<string, string>
expect(headers.Authorization).toBe('Bearer tok')
expect(headers['X-YVP-App-Key']).toBe('appkey')
expect(headers['X-YVP-Installation-Id']).toBe('inst-1')
expect(headers['x-yvp-sdk']).toBe('ReactNativeSDK=1.0.0-dev')
expect(header(init, 'Authorization')).toBe('Bearer tok')
expect(header(init, 'X-YVP-App-Key')).toBe('appkey')
expect(header(init, 'X-YVP-Installation-Id')).toBe('inst-1')
expect(header(init, 'x-yvp-sdk')).toBe('ReactSDK=2.8.0, ReactNativeSDK=1.0.0-dev')
})

it('returns auth failure for 401 and 403 without throwing', async () => {
Expand Down Expand Up @@ -155,10 +158,9 @@ describe('createHighlightsApi', () => {
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit]
expect(url).toBe('https://api.example.com/v1/highlights')
expect(init.method).toBe('POST')
const headers = init.headers as Record<string, string>
expect(headers.Authorization).toBe('Bearer tok')
expect(headers['X-YVP-App-Key']).toBe('appkey')
expect(headers['X-YVP-Installation-Id']).toBe('inst-1')
expect(header(init, 'Authorization')).toBe('Bearer tok')
expect(header(init, 'X-YVP-App-Key')).toBe('appkey')
expect(header(init, 'X-YVP-Installation-Id')).toBe('inst-1')
const body = JSON.parse(init.body as string) as {
request_id: string
highlight: { bible_id: number; passage_id: string; color: string }
Expand Down Expand Up @@ -266,10 +268,9 @@ describe('createHighlightsApi', () => {
const [url, init] = mockFetch.mock.calls[0] as [string, RequestInit]
expect(url).toBe('https://api.example.com/v1/highlights/JHN.3.16?bible_id=111')
expect(init.method).toBe('DELETE')
const headers = init.headers as Record<string, string>
expect(headers.Authorization).toBe('Bearer tok')
expect(headers['X-YVP-App-Key']).toBe('appkey')
expect(headers['X-YVP-Installation-Id']).toBe('inst-1')
expect(header(init, 'Authorization')).toBe('Bearer tok')
expect(header(init, 'X-YVP-App-Key')).toBe('appkey')
expect(header(init, 'X-YVP-Installation-Id')).toBe('inst-1')
})

it('returns auth failure for 403', async () => {
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@
"dependencies": {
"@radix-ui/react-use-controllable-state": "1.2.2",
"@rn-primitives/portal": "1.4.0",
"@youversion/platform-core": "2.7.0",
"@youversion/platform-core": "2.8.0",
"@youversion/platform-react-native-expo-core": "workspace:*",
"@youversion/platform-react-ui": "2.7.0",
"@youversion/platform-react-ui": "2.8.0",
"expo-localization": "56.0.6",
"i18next": "26.3.1",
"react-i18next": "17.0.8",
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/lib/__tests__/sdk-version-stamp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const COMPILED = [
"import pkg from '../../package.json';",
'const IS_PUBLISH_BUILD = false;',
'export const SDK_VERSION = IS_PUBLISH_BUILD ? pkg.version : `${pkg.version}-dev`;',
"const SDK_HEADER_NAME = 'x-yvp-sdk';",
"const SDK_HEADER_NAME = 'X-YVP-Sdk';",
'',
].join('\n')

Expand All @@ -30,7 +30,7 @@ describe('stampPublishBuild', () => {

it('leaves the surrounding lines untouched', () => {
const out = stampPublishBuild(COMPILED)
expect(out).toContain("const SDK_HEADER_NAME = 'x-yvp-sdk';")
expect(out).toContain("const SDK_HEADER_NAME = 'X-YVP-Sdk';")
expect(out).toContain("import pkg from '../../package.json';")
})

Expand Down
17 changes: 15 additions & 2 deletions packages/ui/src/lib/__tests__/sdk-version.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import pkg from '../../../package.json'
import { SDK_VERSION, getSdkHeaders } from '../sdk-version'
import { SDK_VERSION, getSdkHeaders, mergeSdkHeaders } from '../sdk-version'

describe('sdk-version', () => {
it('suffixes SDK_VERSION with -dev for non-published builds', () => {
Expand All @@ -12,6 +12,19 @@ describe('sdk-version', () => {
})

it('returns the x-yvp-sdk header in ReactNativeSDK={version}-dev form', () => {
expect(getSdkHeaders()).toEqual({ 'x-yvp-sdk': `ReactNativeSDK=${pkg.version}-dev` })
expect(getSdkHeaders()).toEqual({ 'X-YVP-Sdk': `ReactNativeSDK=${pkg.version}-dev` })
})

it('drops consumer x-yvp-sdk keys so Headers cannot combine them with the SDK stamp', () => {
expect(
mergeSdkHeaders({
'x-yvp-sdk': 'hacked',
'X-YVP-Sdk': 'also-hacked',
'x-custom': 'ok',
}),
).toEqual({
'x-custom': 'ok',
'X-YVP-Sdk': `ReactNativeSDK=${pkg.version}-dev`,
})
})
})
6 changes: 3 additions & 3 deletions packages/ui/src/lib/__tests__/web-yv-provider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ const DOM_ENTRIES = [
describe('web YouVersionProvider', () => {
it('injects the x-yvp-sdk header when consumer passes no additionalHeaders', () => {
expect(renderShim({}).additionalHeaders).toEqual({
'x-yvp-sdk': SDK_HEADER_VALUE,
'X-YVP-Sdk': SDK_HEADER_VALUE,
})
})

it('preserves consumer additionalHeaders on non-colliding keys', () => {
expect(renderShim({ additionalHeaders: { 'x-custom': 'ok' } }).additionalHeaders).toEqual({
'x-yvp-sdk': SDK_HEADER_VALUE,
'X-YVP-Sdk': SDK_HEADER_VALUE,
'x-custom': 'ok',
})
})
Expand All @@ -62,7 +62,7 @@ describe('web YouVersionProvider', () => {
additionalHeaders: { 'x-yvp-sdk': 'hacked', 'x-custom': 'ok' },
}).additionalHeaders,
).toEqual({
'x-yvp-sdk': SDK_HEADER_VALUE,
'X-YVP-Sdk': SDK_HEADER_VALUE,
'x-custom': 'ok',
})
})
Expand Down
15 changes: 14 additions & 1 deletion packages/ui/src/lib/sdk-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,21 @@ const IS_PUBLISH_BUILD = false

export const SDK_VERSION: string = IS_PUBLISH_BUILD ? pkg.version : `${pkg.version}-dev`

const SDK_HEADER_NAME = 'x-yvp-sdk'
// Same name platform-core 2.8.0 writes. A lowercase `x-yvp-sdk` is a
// different object key and `Headers` would combine both values.
const SDK_HEADER_NAME = 'X-YVP-Sdk'

export function getSdkHeaders(): Record<string, string> {
return { [SDK_HEADER_NAME]: `ReactNativeSDK=${SDK_VERSION}` }
}

export function mergeSdkHeaders(
additionalHeaders?: Record<string, string>,
): Record<string, string> {
const rest = Object.fromEntries(
Object.entries(additionalHeaders ?? {}).filter(
([key]) => key.toLowerCase() !== SDK_HEADER_NAME.toLowerCase(),
),
)
return { ...rest, ...getSdkHeaders() }
}
25 changes: 6 additions & 19 deletions packages/ui/src/lib/web-yv-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,21 @@
import { ensureDomLocalStorage } from './dom-local-storage'

import { YouVersionProvider as BaseYouVersionProvider } from '@youversion/platform-react-ui'
import { createElement, type ComponentProps, type ComponentType } from 'react'
import { createElement, type ComponentProps, type ReactElement } from 'react'

import { getSdkHeaders } from './sdk-version'
import { mergeSdkHeaders } from './sdk-version'

ensureDomLocalStorage()

// `additionalHeaders` ships in the next Web SDK release; widen the prop type
// locally until that publishes. Same for version filter lists (YPE-4657).
type BaseProps = ComponentProps<typeof BaseYouVersionProvider>
type ProviderProps = BaseProps & {
additionalHeaders?: Record<string, string>
permittedVersionIds?: number[]
excludedVersionIds?: number[]
permittedLanguageTags?: string[]
}

const TypedProvider = BaseYouVersionProvider as ComponentType<ProviderProps>

// Header set is constant for the life of the bundle, so compute it once.
const SDK_HEADERS = getSdkHeaders()
type ProviderProps = ComponentProps<typeof BaseYouVersionProvider>

// DOM-side wrapper for the Web SDK's `YouVersionProvider`. Stamps the
// `x-yvp-sdk` header onto every API call made from inside a DOM component.
// SDK-attribution headers must always reach the data lake intact, so they
// override any consumer-supplied entry on the same key.
export function YouVersionProvider({ additionalHeaders, ...rest }: ProviderProps) {
return createElement(TypedProvider, {
export function YouVersionProvider({ additionalHeaders, ...rest }: ProviderProps): ReactElement {
return createElement(BaseYouVersionProvider, {
...rest,
additionalHeaders: { ...additionalHeaders, ...SDK_HEADERS },
additionalHeaders: mergeSdkHeaders(additionalHeaders),
})
}
36 changes: 18 additions & 18 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading