From bcddf2d51a42985efd1d7171f78f45caf0550ab2 Mon Sep 17 00:00:00 2001 From: Maximilian Haupt Date: Fri, 17 Jul 2026 09:21:38 +0200 Subject: [PATCH 1/2] fix(onetrust): harden consent status Avoid stale OneTrust consent status reads and document ATT handling. --- .../README.md | 32 +- .../src/OnetrustCmpAdapter.test.ts | 56 +++ .../src/OnetrustCmpAdapter.ts | 346 +++++++++++++++--- .../src/index.ts | 1 + .../src/react-native-onetrust-cmp.d.ts | 1 + .../src/components/ContentpassConsentGate.tsx | 45 ++- .../src/types/CmpAdapter.ts | 4 +- 7 files changed, 436 insertions(+), 49 deletions(-) diff --git a/packages/react-native-contentpass-cmp-onetrust/README.md b/packages/react-native-contentpass-cmp-onetrust/README.md index 4c9f837..166a2f1 100644 --- a/packages/react-native-contentpass-cmp-onetrust/README.md +++ b/packages/react-native-contentpass-cmp-onetrust/README.md @@ -116,18 +116,48 @@ For a complete working example, see the [`examples/onetrust`](../../examples/one ## API -### `createOnetrustCmpAdapter(sdk)` +### `createOnetrustCmpAdapter(sdk, options)` Factory function that creates a `CmpAdapter` from an initialized OneTrust SDK instance. | Parameter | Type | Description | |-----------|------|-------------| | `sdk` | `OTPublishersNativeSDK` | An initialized OneTrust SDK instance (after `startSDK` has resolved). | +| `options.attGroupIds` | `string[]` | OneTrust group IDs linked to iOS App Tracking Transparency (ATT). These groups are logged but do not keep the Contentpass consent layer open. | Returns `Promise`. The adapter fetches banner and preference center data from the OneTrust SDK during creation, and automatically extracts TCF purpose IDs and the vendor count. +### App Tracking Transparency (ATT) + +ATT is an Apple system permission, not a Contentpass consent decision. The adapter never presents the ATT prompt or attempts to set the ATT status. Configure OneTrust to manage any ATT pre- and post-prompt, and let the application decide when to request ATT. + +If OneTrust has categories linked to ATT, pass their group IDs when creating the adapter. A denied or unresolved ATT permission can leave an ATT-linked OneTrust category disabled even after the user accepts all CMP purposes. Those categories must not keep the Contentpass layer open. + +```tsx +const cmpAdapter = await createOnetrustCmpAdapter(OTPublishersNativeSDK, { + // Use the ATT-linked OptanonGroupIds from the client's OneTrust configuration. + attGroupIds: ['C0004'], +}); +``` + +The adapter logs every group and marks configured ATT groups as `isAttGroup: true`, making the configuration verifiable in device logs. It warns when a configured group ID is absent from the OneTrust preference-center data. + +Clients that want ATT before Contentpass must handle it before constructing the adapter: + +1. Configure the iOS `NSUserTrackingUsageDescription` purpose string. +2. Initialize OneTrust on every app launch. +3. Present OneTrust's ATT flow (`showConsentUI(OTDevicePermission.IDFA)`) at the client's chosen point in the app journey. +4. Synchronize the resulting system status with OneTrust using its native `checkAndLogConsent(for: .idfa)` API. The current React Native OneTrust bridge does not expose that API, so clients need to expose it in their native bridge or obtain an upstream version that does. +5. Create the Contentpass adapter with the ATT-linked `attGroupIds` as shown above. + +Do not run a second OneTrust `saveConsent` call for ATT from `acceptAll()`. The ATT system status and CMP consent are separate operations. + +### Diagnostic logs + +The adapter emits structured `console.debug` logs for CMP initialization, OneTrust events, consent actions, group statuses, ATT status (when the installed bridge exposes it), and stale asynchronous reads. After `acceptAll()` or `denyAll()`, it records immediate snapshots and rechecks consent after 100, 500, and 1000 milliseconds. This makes native persistence delays, re-consent, and ATT-linked group states visible without changing the user's consent. + ### `CmpAdapter` methods provided | Method | Description | diff --git a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.test.ts b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.test.ts index e5b0d51..1bf3b03 100644 --- a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.test.ts +++ b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.test.ts @@ -23,6 +23,18 @@ jest.mock('react-native-onetrust-cmp', () => ({ type EventHandler = (data?: any) => void; +function createDeferred(): { + promise: Promise; + resolve: (value: T) => void; +} { + let resolve!: (value: T) => void; + const promise = new Promise((promiseResolve) => { + resolve = promiseResolve; + }); + + return { promise, resolve }; +} + function createBannerData(description = 'We use 42 vendors'): BannerData { return { otConsentString: '', @@ -129,6 +141,50 @@ describe('OnetrustCmpAdapter', () => { await expect(adapter.hasFullConsent()).resolves.toBe(false); }); + it('should ignore an initial status read that resolves after accept all', async () => { + jest.useFakeTimers(); + try { + const initialShouldShowBanner = createDeferred(); + const shouldShowBanner = jest + .fn() + .mockReturnValueOnce(initialShouldShowBanner.promise) + .mockResolvedValue(false); + const { sdk } = createMockSdk({ shouldShowBanner }); + const adapter = await createOnetrustCmpAdapter(sdk); + const listener = jest.fn(); + + adapter.onConsentStatusChange(listener); + jest.advanceTimersByTime(0); + await Promise.resolve(); + + await adapter.acceptAll(); + expect(listener).toHaveBeenCalledWith(true); + listener.mockClear(); + + initialShouldShowBanner.resolve(true); + await Promise.resolve(); + await Promise.resolve(); + + expect(listener).not.toHaveBeenCalled(); + } finally { + jest.clearAllTimers(); + jest.useRealTimers(); + } + }); + + it('should not require ATT-linked groups for full Contentpass consent', async () => { + const { sdk } = createMockSdk({ + getConsentStatusForCategory: jest.fn((groupId: string) => + Promise.resolve(groupId === 'C0002' ? 0 : 1) + ), + }); + const adapter = await createOnetrustCmpAdapter(sdk, { + attGroupIds: ['C0002'], + }); + + await expect(adapter.hasFullConsent()).resolves.toBe(true); + }); + it('should emit consent status when OneTrust confirms preference-center choices', async () => { const { sdk, eventHandlers } = createMockSdk({ getConsentStatusForCategory: jest diff --git a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts index 9b64ecb..9a6d118 100644 --- a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts +++ b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts @@ -11,28 +11,70 @@ const CONSENT_CHANGE_EVENTS = new Set([ OTEventName.vendorConfirmChoices, ]); +const CONSENT_STATUS_REFRESH_DELAYS_MS = [100, 500, 1000]; + +export type OnetrustCmpAdapterOptions = { + /** + * OneTrust group IDs whose state is controlled by App Tracking Transparency. + * + * These groups are still logged, but do not determine whether Contentpass can + * dismiss its consent layer. Apple can keep an ATT-linked group disabled even + * after a user accepts all CMP purposes. + */ + attGroupIds?: string[]; +}; + +type ConsentStatus = { + groupId: string; + status: number; + isAttGroup: boolean; +}; + +type ConsentState = { + shouldShowBanner: boolean; + consentStatuses: ConsentStatus[]; +}; + export async function createOnetrustCmpAdapter( - sdk: OTPublishersNativeSDK + sdk: OTPublishersNativeSDK, + options: OnetrustCmpAdapterOptions = {} ): Promise { + console.debug('[OnetrustCmpAdapter::create] fetching CMP API data'); + try { await sdk.fetchPreferencesCmpApiData(); const bannerData = await sdk.getBannerData(); - const preferenceCenterData: PreferenceCenterData = await sdk.getPreferenceCenterData(); - return new OnetrustCmpAdapter(sdk, bannerData, preferenceCenterData); + console.debug('[OnetrustCmpAdapter::create] CMP API data fetched', { + purposeCount: preferenceCenterData?.purposes?.length ?? 0, + shouldResetLocalState: + preferenceCenterData?.appConfig?.shouldResetLocalState, + templateChanged: preferenceCenterData?.appConfig?.templateChanged, + showBanner: preferenceCenterData?.appConfig?.showBanner, + bannerReason: preferenceCenterData?.appConfig?.bannerReason, + bannerReasonCode: preferenceCenterData?.appConfig?.bannerReasonCode, + }); + + return new OnetrustCmpAdapter( + sdk, + bannerData, + preferenceCenterData, + options + ); } catch (error: any) { - console.error('Error getting banner or preference center data', error); + console.error('[OnetrustCmpAdapter::create] failed', error); throw error; } } export default class OnetrustCmpAdapter implements CmpAdapter { - private readonly groupIds: string[] = []; - private readonly numVendors: number = 0; - private readonly tcfPurposes: string[] = []; + private readonly groupIds: string[]; + private readonly attGroupIds: ReadonlySet; + private readonly numVendors: number; + private readonly tcfPurposes: string[]; private readonly eventListeners = new Set< (eventName: OTEventName, data?: any) => void >(); @@ -40,19 +82,39 @@ export default class OnetrustCmpAdapter implements CmpAdapter { private readonly consentStatusChangeListeners = new Set< (fullConsent: boolean) => void >(); + private consentStatusRevision = 0; constructor( private readonly sdk: OTPublishersNativeSDK, bannerData: BannerData, - preferenceCenterData: PreferenceCenterData + preferenceCenterData: PreferenceCenterData, + options: OnetrustCmpAdapterOptions = {} ) { this.groupIds = preferenceCenterData.purposes .map(({ groupId }) => groupId) .filter(Boolean); + this.attGroupIds = new Set(options.attGroupIds ?? []); this.numVendors = OnetrustCmpAdapter.getNumVendors( bannerData.bannerUIData?.summary?.description?.text ?? '' ); this.tcfPurposes = getTcfPurposes(preferenceCenterData.purposes); + + const unknownAttGroupIds = [...this.attGroupIds].filter( + (groupId) => !this.groupIds.includes(groupId) + ); + console.debug('[OnetrustCmpAdapter::constructor] configured', { + groupIds: this.groupIds, + attGroupIds: [...this.attGroupIds], + unknownAttGroupIds, + tcfPurposes: this.tcfPurposes, + numVendors: this.numVendors, + }); + if (unknownAttGroupIds.length > 0) { + console.warn('[OnetrustCmpAdapter::constructor] unknown ATT group IDs', { + unknownAttGroupIds, + }); + } + this.initializeEventBridge(); } @@ -62,33 +124,73 @@ export default class OnetrustCmpAdapter implements CmpAdapter { } async waitForInit(): Promise { - return Promise.resolve(); + console.debug('[OnetrustCmpAdapter::waitForInit] already initialized'); } async acceptAll(): Promise { - console.debug('[OnetrustCmpAdapter::acceptAll]'); - await this.sdk.saveConsent(OTConsentInteraction.bannerAllowAll); - const hasFullConsent = await this.hasFullConsent(); - this.emitConsentStatusChange(hasFullConsent); + const revision = this.beginConsentStatusOperation('acceptAll'); + console.debug('[OnetrustCmpAdapter::acceptAll] saving banner consent', { + revision, + }); + this.logConsentSnapshot('acceptAll: before saveConsent'); + + try { + await this.sdk.saveConsent(OTConsentInteraction.bannerAllowAll); + console.debug('[OnetrustCmpAdapter::acceptAll] saveConsent resolved', { + revision, + }); + this.logConsentSnapshot('acceptAll: after saveConsent'); + this.scheduleConsentStatusRefreshes(revision, 'acceptAll'); + await this.emitConsentStatusForRevision(revision, 'acceptAll'); + } catch (error) { + console.error('[OnetrustCmpAdapter::acceptAll] saveConsent failed', { + revision, + error, + }); + throw error; + } } async denyAll(): Promise { - console.debug('[OnetrustCmpAdapter::denyAll]'); - await this.sdk.saveConsent(OTConsentInteraction.bannerRejectAll); - const hasFullConsent = await this.hasFullConsent(); - this.emitConsentStatusChange(hasFullConsent); + const revision = this.beginConsentStatusOperation('denyAll'); + console.debug('[OnetrustCmpAdapter::denyAll] saving banner consent', { + revision, + }); + this.logConsentSnapshot('denyAll: before saveConsent'); + + try { + await this.sdk.saveConsent(OTConsentInteraction.bannerRejectAll); + console.debug('[OnetrustCmpAdapter::denyAll] saveConsent resolved', { + revision, + }); + this.logConsentSnapshot('denyAll: after saveConsent'); + this.scheduleConsentStatusRefreshes(revision, 'denyAll'); + await this.emitConsentStatusForRevision(revision, 'denyAll'); + } catch (error) { + console.error('[OnetrustCmpAdapter::denyAll] saveConsent failed', { + revision, + error, + }); + throw error; + } } getNumberOfVendors(): Promise { + console.debug('[OnetrustCmpAdapter::getNumberOfVendors]', { + numVendors: this.numVendors, + }); return Promise.resolve(this.numVendors); } getRequiredPurposes(): Promise { + console.debug('[OnetrustCmpAdapter::getRequiredPurposes]', { + tcfPurposes: this.tcfPurposes, + }); return Promise.resolve(this.tcfPurposes); } showSecondLayer(view: 'vendor' | 'purpose'): Promise { - console.debug('[OnetrustCmpAdapter::showSecondLayer]', view); + console.debug('[OnetrustCmpAdapter::showSecondLayer] opening', { view }); return new Promise((resolve) => { const remove = this.onEvent((eventName: OTEventName, _?: any) => { switch (eventName) { @@ -99,6 +201,10 @@ export default class OnetrustCmpAdapter implements CmpAdapter { case OTEventName.hideVendorList: case OTEventName.vendorConfirmChoices: case OTEventName.allSDKViewsDismissed: + console.debug('[OnetrustCmpAdapter::showSecondLayer] closed', { + view, + eventName, + }); remove(); resolve(); break; @@ -112,36 +218,57 @@ export default class OnetrustCmpAdapter implements CmpAdapter { } hasFullConsent = async (): Promise => { - console.debug('[OnetrustCmpAdapter::hasFullConsent]'); - const [shouldShowBanner, consentStatuses] = await Promise.all([ - this.sdk.shouldShowBanner(), - Promise.all( - this.groupIds.map((groupId) => - this.sdk.getConsentStatusForCategory(groupId) - ) - ), - ]); + const consentState = await this.getConsentState(); + const fullConsent = this.hasFullConsentForState(consentState); - return ( - !shouldShowBanner && - consentStatuses.every((consentStatus: number) => consentStatus === 1) - ); + console.debug('[OnetrustCmpAdapter::hasFullConsent]', { + fullConsent, + ...consentState, + }); + + return fullConsent; }; onConsentStatusChange(callback: (fullConsent: boolean) => void): () => void { this.consentStatusChangeListeners.add(callback); + console.debug('[OnetrustCmpAdapter::onConsentStatusChange] subscribed', { + listenerCount: this.consentStatusChangeListeners.size, + }); + setTimeout(() => { - this.hasFullConsent().then((fullConsent) => - this.emitConsentStatusChangeEventSingle(fullConsent, callback) + const revision = this.consentStatusRevision; + console.debug( + '[OnetrustCmpAdapter::onConsentStatusChange] initial status check', + { revision } + ); + this.emitConsentStatusForRevision( + revision, + 'initial subscription', + callback ); }, 0); - return () => this.consentStatusChangeListeners.delete(callback); + + return () => { + this.consentStatusChangeListeners.delete(callback); + console.debug( + '[OnetrustCmpAdapter::onConsentStatusChange] unsubscribed', + { + listenerCount: this.consentStatusChangeListeners.size, + } + ); + }; } onEvent(callback: (eventName: OTEventName, data?: any) => void): () => void { this.eventListeners.add(callback); + console.debug('[OnetrustCmpAdapter::onEvent] subscribed', { + listenerCount: this.eventListeners.size, + }); return () => { this.eventListeners.delete(callback); + console.debug('[OnetrustCmpAdapter::onEvent] unsubscribed', { + listenerCount: this.eventListeners.size, + }); }; } @@ -155,11 +282,19 @@ export default class OnetrustCmpAdapter implements CmpAdapter { ); this.eventSubscriptions.push(subscription); }); + console.debug('[OnetrustCmpAdapter::initializeEventBridge] ready', { + subscriptionCount: this.eventSubscriptions.length, + }); } private emitEvent(eventName: OTEventName, data?: any): void { + console.debug('[OnetrustCmpAdapter::onEvent] received', { + eventName, + data, + }); if (CONSENT_CHANGE_EVENTS.has(eventName)) { - this.emitCurrentConsentStatus(); + const revision = this.beginConsentStatusOperation(`event:${eventName}`); + this.emitConsentStatusForRevision(revision, `event:${eventName}`); } this.eventListeners.forEach((listener) => { @@ -171,23 +306,148 @@ export default class OnetrustCmpAdapter implements CmpAdapter { }); } - private emitCurrentConsentStatus(): void { - this.hasFullConsent() - .then((fullConsent) => this.emitConsentStatusChange(fullConsent)) - .catch((error) => { - console.error( - '[OnetrustCmpAdapter::emitCurrentConsentStatus] failed', - error + private beginConsentStatusOperation(context: string): number { + this.consentStatusRevision += 1; + console.debug('[OnetrustCmpAdapter::consentStatusOperation] started', { + context, + revision: this.consentStatusRevision, + }); + return this.consentStatusRevision; + } + + private scheduleConsentStatusRefreshes( + revision: number, + context: string + ): void { + CONSENT_STATUS_REFRESH_DELAYS_MS.forEach((delay) => { + setTimeout(() => { + this.logConsentSnapshot(`${context}: +${delay}ms`); + this.emitConsentStatusForRevision(revision, `${context}: +${delay}ms`); + }, delay); + }); + } + + private async emitConsentStatusForRevision( + revision: number, + context: string, + listener?: (fullConsent: boolean) => void + ): Promise { + try { + const fullConsent = await this.hasFullConsent(); + if (revision !== this.consentStatusRevision) { + console.debug( + '[OnetrustCmpAdapter::emitConsentStatus] ignored stale status', + { + context, + revision, + currentRevision: this.consentStatusRevision, + fullConsent, + } ); + return; + } + + if (listener) { + if (!this.consentStatusChangeListeners.has(listener)) { + console.debug( + '[OnetrustCmpAdapter::emitConsentStatus] listener unsubscribed before initial status', + { context, revision } + ); + return; + } + this.emitConsentStatusChangeEventSingle(fullConsent, listener); + return; + } + + this.emitConsentStatusChange(fullConsent, context, revision); + } catch (error) { + console.error('[OnetrustCmpAdapter::emitConsentStatus] failed', { + context, + revision, + error, }); + } } - private emitConsentStatusChange(fullConsent: boolean): void { + private emitConsentStatusChange( + fullConsent: boolean, + context: string, + revision: number + ): void { + console.debug('[OnetrustCmpAdapter::emitConsentStatusChange]', { + context, + revision, + fullConsent, + listenerCount: this.consentStatusChangeListeners.size, + }); this.consentStatusChangeListeners.forEach((listener) => this.emitConsentStatusChangeEventSingle(fullConsent, listener) ); } + private async getConsentState(): Promise { + const [shouldShowBanner, statuses] = await Promise.all([ + this.sdk.shouldShowBanner(), + Promise.all( + this.groupIds.map((groupId) => + this.sdk.getConsentStatusForCategory(groupId).then((status) => ({ + groupId, + status, + isAttGroup: this.attGroupIds.has(groupId), + })) + ) + ), + ]); + + return { shouldShowBanner, consentStatuses: statuses }; + } + + private hasFullConsentForState({ + shouldShowBanner, + consentStatuses, + }: ConsentState): boolean { + return ( + !shouldShowBanner && + consentStatuses + .filter(({ isAttGroup }) => !isAttGroup) + .every(({ status }) => status === 1) + ); + } + + private async logConsentSnapshot(context: string): Promise { + try { + const [consentState, attStatus] = await Promise.all([ + this.getConsentState(), + this.getAttStatus(), + ]); + console.debug('[OnetrustCmpAdapter::consentSnapshot]', { + context, + timestamp: new Date().toISOString(), + fullConsent: this.hasFullConsentForState(consentState), + attStatus, + ...consentState, + }); + } catch (error) { + console.error('[OnetrustCmpAdapter::consentSnapshot] failed', { + context, + error, + }); + } + } + + private async getAttStatus(): Promise { + if (!this.sdk.getATTStatus) { + return 'not exposed by react-native-onetrust-cmp'; + } + + try { + return String(await this.sdk.getATTStatus()); + } catch (error) { + console.error('[OnetrustCmpAdapter::getAttStatus] failed', error); + return 'failed to read'; + } + } + private emitConsentStatusChangeEventSingle( fullConsent: boolean, listener: (fullConsent: boolean) => void diff --git a/packages/react-native-contentpass-cmp-onetrust/src/index.ts b/packages/react-native-contentpass-cmp-onetrust/src/index.ts index 5805db0..d12a8da 100644 --- a/packages/react-native-contentpass-cmp-onetrust/src/index.ts +++ b/packages/react-native-contentpass-cmp-onetrust/src/index.ts @@ -1,6 +1,7 @@ import OnetrustCmpAdapter, { createOnetrustCmpAdapter, } from './OnetrustCmpAdapter'; +export type { OnetrustCmpAdapterOptions } from './OnetrustCmpAdapter'; export default OnetrustCmpAdapter; diff --git a/packages/react-native-contentpass-cmp-onetrust/src/react-native-onetrust-cmp.d.ts b/packages/react-native-contentpass-cmp-onetrust/src/react-native-onetrust-cmp.d.ts index 9bda601..eb7b97e 100644 --- a/packages/react-native-contentpass-cmp-onetrust/src/react-native-onetrust-cmp.d.ts +++ b/packages/react-native-contentpass-cmp-onetrust/src/react-native-onetrust-cmp.d.ts @@ -21,6 +21,7 @@ declare module 'react-native-onetrust-cmp' { saveConsent(interaction: OTConsentInteraction): Promise; shouldShowBanner(): Promise; getConsentStatusForCategory(categoryId: string): Promise; + getATTStatus?(): Promise; addEventListener( eventName: OTEventName, handler: (data?: any) => void diff --git a/packages/react-native-contentpass-ui/src/components/ContentpassConsentGate.tsx b/packages/react-native-contentpass-ui/src/components/ContentpassConsentGate.tsx index 5fa9ff0..fb37c4b 100644 --- a/packages/react-native-contentpass-ui/src/components/ContentpassConsentGate.tsx +++ b/packages/react-native-contentpass-ui/src/components/ContentpassConsentGate.tsx @@ -45,7 +45,13 @@ export default function ContentpassConsentGate({ return { acceptAll: async () => { try { + console.debug( + '[ContentpassConsentGate::acceptAll] forwarding to CMP' + ); await cmpAdapter.acceptAll(); + console.debug( + '[ContentpassConsentGate::acceptAll] CMP action resolved' + ); } catch (error) { console.error('Failed to accept all in CMP', error); } @@ -98,10 +104,33 @@ export default function ContentpassConsentGate({ return; } - cmpAdapter?.onConsentStatusChange?.((v: boolean) => setHasFullConsent(v)); - cmpAdapter.getRequiredPurposes().then((v: string[]) => setPurposesList(v)); - cmpAdapter.getNumberOfVendors().then((v: number) => setVendorCount(v)); - }, [cmpReady, cmpAdapter, onVisibilityChange, isVisible]); + let active = true; + const unsubscribe = cmpAdapter.onConsentStatusChange((v: boolean) => { + console.debug('[ContentpassConsentGate::onConsentStatusChange]', { + fullConsent: v, + active, + }); + if (active) { + setHasFullConsent(v); + } + }); + cmpAdapter.getRequiredPurposes().then((v: string[]) => { + if (active) { + setPurposesList(v); + } + }); + cmpAdapter.getNumberOfVendors().then((v: number) => { + if (active) { + setVendorCount(v); + } + }); + + return () => { + active = false; + console.debug('[ContentpassConsentGate::onConsentStatusChange] cleanup'); + unsubscribe?.(); + }; + }, [cmpReady, cmpAdapter]); // Monitor the contentpass auth state useEffect(() => { @@ -135,6 +164,14 @@ export default function ContentpassConsentGate({ cpAuthState.state === ContentpassStateType.AUTHENTICATED || hasFullConsent; const visible = !isFine; + console.debug('[ContentpassConsentGate::visibility]', { + cmpReady, + contentpassState: cpAuthState.state, + hasFullConsent, + isShowingContentpass, + isShowingSecondLayer, + visible, + }); if (visible !== isVisible) { onVisibilityChange?.(visible); } diff --git a/packages/react-native-contentpass/src/types/CmpAdapter.ts b/packages/react-native-contentpass/src/types/CmpAdapter.ts index 3f6faa5..e3e10f7 100644 --- a/packages/react-native-contentpass/src/types/CmpAdapter.ts +++ b/packages/react-native-contentpass/src/types/CmpAdapter.ts @@ -7,7 +7,9 @@ export type CmpAdapter = { getNumberOfVendors: () => Promise; getRequiredPurposes: () => Promise; hasFullConsent: () => Promise; - onConsentStatusChange: (callback: (fullConsent: boolean) => void) => void; + onConsentStatusChange: ( + callback: (fullConsent: boolean) => void + ) => void | (() => void); onAdapterEvent?: ( eventName: string, callback: (data?: any) => void From f1393e086d0d7367cd0c498ad60d23429e85399a Mon Sep 17 00:00:00 2001 From: Maximilian Haupt Date: Fri, 17 Jul 2026 09:50:46 +0200 Subject: [PATCH 2/2] chore: bump versions --- examples/consentmanager/CHANGELOG.md | 9 +++++++++ examples/consentmanager/package.json | 2 +- .../react-native-contentpass-cmp-onetrust/CHANGELOG.md | 6 ++++++ .../react-native-contentpass-cmp-onetrust/package.json | 2 +- packages/react-native-contentpass-ui/CHANGELOG.md | 6 ++++++ packages/react-native-contentpass-ui/package.json | 2 +- packages/react-native-contentpass/CHANGELOG.md | 6 ++++++ packages/react-native-contentpass/package.json | 2 +- 8 files changed, 31 insertions(+), 4 deletions(-) diff --git a/examples/consentmanager/CHANGELOG.md b/examples/consentmanager/CHANGELOG.md index da36e29..0e4e29a 100644 --- a/examples/consentmanager/CHANGELOG.md +++ b/examples/consentmanager/CHANGELOG.md @@ -1,5 +1,14 @@ # @contentpass/examples-consentmanager +## 0.0.4 + +### Patch Changes + +- Updated dependencies []: + - @contentpass/react-native-contentpass-ui@0.6.1 + - @contentpass/react-native-contentpass@0.7.2 + - @contentpass/react-native-contentpass-cmp-consentmanager@0.1.1 + ## 0.0.3 ### Patch Changes diff --git a/examples/consentmanager/package.json b/examples/consentmanager/package.json index 6d67b6a..14bc1cc 100644 --- a/examples/consentmanager/package.json +++ b/examples/consentmanager/package.json @@ -1,6 +1,6 @@ { "name": "@contentpass/examples-consentmanager", - "version": "0.0.3", + "version": "0.0.4", "main": "index.ts", "scripts": { "start": "expo start", diff --git a/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md b/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md index 42a9922..94817f0 100644 --- a/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md +++ b/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md @@ -1,5 +1,11 @@ # @contentpass/react-native-contentpass-cmp-onetrust +## 0.4.0 + +### Minor Changes + +- Harden OneTrust consent status + ## 0.3.2 ### Patch Changes diff --git a/packages/react-native-contentpass-cmp-onetrust/package.json b/packages/react-native-contentpass-cmp-onetrust/package.json index ff4ae1a..ffd7959 100644 --- a/packages/react-native-contentpass-cmp-onetrust/package.json +++ b/packages/react-native-contentpass-cmp-onetrust/package.json @@ -1,6 +1,6 @@ { "name": "@contentpass/react-native-contentpass-cmp-onetrust", - "version": "0.3.2", + "version": "0.4.0", "description": "Contentpass OneTrust CMP adapter", "source": "./src/index.ts", "main": "./lib/commonjs/index.js", diff --git a/packages/react-native-contentpass-ui/CHANGELOG.md b/packages/react-native-contentpass-ui/CHANGELOG.md index 943ec95..7274b93 100644 --- a/packages/react-native-contentpass-ui/CHANGELOG.md +++ b/packages/react-native-contentpass-ui/CHANGELOG.md @@ -1,5 +1,11 @@ # @contentpass/react-native-contentpass-ui +## 0.6.1 + +### Patch Changes + +- Harden OneTrust consent status + ## 0.6.0 ### Minor Changes diff --git a/packages/react-native-contentpass-ui/package.json b/packages/react-native-contentpass-ui/package.json index dd5da53..e2f6989 100644 --- a/packages/react-native-contentpass-ui/package.json +++ b/packages/react-native-contentpass-ui/package.json @@ -1,6 +1,6 @@ { "name": "@contentpass/react-native-contentpass-ui", - "version": "0.6.0", + "version": "0.6.1", "description": "Contentpass React Native UI Components", "source": "./src/index.tsx", "main": "./lib/commonjs/index.js", diff --git a/packages/react-native-contentpass/CHANGELOG.md b/packages/react-native-contentpass/CHANGELOG.md index 504fd1f..73d58c6 100644 --- a/packages/react-native-contentpass/CHANGELOG.md +++ b/packages/react-native-contentpass/CHANGELOG.md @@ -1,5 +1,11 @@ # @contentpass/react-native-contentpass +## 0.7.2 + +### Patch Changes + +- Harden OneTrust consent status + ## 0.7.1 ### Patch Changes diff --git a/packages/react-native-contentpass/package.json b/packages/react-native-contentpass/package.json index 16d0f10..0d281bc 100644 --- a/packages/react-native-contentpass/package.json +++ b/packages/react-native-contentpass/package.json @@ -1,6 +1,6 @@ { "name": "@contentpass/react-native-contentpass", - "version": "0.7.1", + "version": "0.7.2", "description": "Contentpass React Native SDK", "source": "./src/index.tsx", "main": "./lib/commonjs/index.js",