From fd905b7607e5657dd42e24ed70741adab5660341 Mon Sep 17 00:00:00 2001 From: Maximilian Haupt Date: Thu, 9 Jul 2026 10:55:55 +0200 Subject: [PATCH 1/5] fix: handle OneTrust consent change events --- .../src/OnetrustCmpAdapter.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts index 6025d0c..ac40eaf 100644 --- a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts +++ b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts @@ -4,6 +4,13 @@ import type { CmpAdapter } from '@contentpass/react-native-contentpass'; import type { BannerData, PreferenceCenterData } from './types'; import { getTcfPurposes } from './purposes'; +const CONSENT_CHANGE_EVENTS = new Set([ + OTEventName.preferenceCenterAcceptAll, + OTEventName.preferenceCenterRejectAll, + OTEventName.preferenceCenterConfirmChoices, + OTEventName.vendorConfirmChoices, +]); + export async function createOnetrustCmpAdapter( sdk: OTPublishersNativeSDK ): Promise { @@ -148,6 +155,10 @@ export default class OnetrustCmpAdapter implements CmpAdapter { } private emitEvent(eventName: OTEventName, data?: any): void { + if (CONSENT_CHANGE_EVENTS.has(eventName)) { + this.emitCurrentConsentStatus(); + } + this.eventListeners.forEach((listener) => { try { listener(eventName, data); @@ -157,6 +168,17 @@ 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 emitConsentStatusChange(fullConsent: boolean): void { this.consentStatusChangeListeners.forEach((listener) => this.emitConsentStatusChangeEventSingle(fullConsent, listener) From 67aa1e216dd1e057323da01be948244a93bc0148 Mon Sep 17 00:00:00 2001 From: Maximilian Haupt Date: Thu, 9 Jul 2026 10:56:24 +0200 Subject: [PATCH 2/5] chore: add tests for OneTrust adapter --- .../src/OnetrustCmpAdapter.test.ts | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.test.ts diff --git a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.test.ts b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.test.ts new file mode 100644 index 0000000..a53bf52 --- /dev/null +++ b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.test.ts @@ -0,0 +1,168 @@ +import type OTPublishersNativeSDK from 'react-native-onetrust-cmp'; +import { OTEventName } from 'react-native-onetrust-cmp'; +import OnetrustCmpAdapter, { + createOnetrustCmpAdapter, +} from './OnetrustCmpAdapter'; +import type { BannerData, PreferenceCenterData } from './types'; + +jest.mock('react-native-onetrust-cmp', () => ({ + OTConsentInteraction: { + bannerAllowAll: 'bannerAllowAll', + bannerRejectAll: 'bannerRejectAll', + }, + OTEventName: { + hidePreferenceCenter: 'hidePreferenceCenter', + preferenceCenterAcceptAll: 'preferenceCenterAcceptAll', + preferenceCenterRejectAll: 'preferenceCenterRejectAll', + preferenceCenterConfirmChoices: 'preferenceCenterConfirmChoices', + hideVendorList: 'hideVendorList', + vendorConfirmChoices: 'vendorConfirmChoices', + allSDKViewsDismissed: 'allSDKViewsDismissed', + }, +})); + +type EventHandler = (data?: any) => void; + +function createBannerData(description = 'We use 42 vendors'): BannerData { + return { + otConsentString: '', + bannerUIData: { + summary: { + description: { + text: description, + textAlign: '', + textColor: '', + textColorDark: '', + fontSize: '', + }, + dpdTitle: {} as any, + title: {} as any, + dpdDescription: {} as any, + }, + buttons: {} as any, + general: {} as any, + logo: {} as any, + }, + appConfig: {}, + storageKeys: {}, + }; +} + +function createPreferenceCenterData(): PreferenceCenterData { + return { + purposes: [ + { + groupId: 'C0001', + isIabPurpose: true, + purposeId: '1', + }, + { + groupId: 'C0002', + isIabPurpose: true, + purposeId: '2', + }, + ] as any, + pcUIData: {}, + appConfig: {}, + otConsentString: '', + storageKeys: {}, + }; +} + +function createMockSdk(overrides: Partial = {}): { + sdk: OTPublishersNativeSDK; + eventHandlers: Map; +} { + const eventHandlers = new Map(); + const sdk = { + fetchPreferencesCmpApiData: jest.fn().mockResolvedValue(undefined), + getBannerData: jest.fn().mockResolvedValue(createBannerData()), + getPreferenceCenterData: jest + .fn() + .mockResolvedValue(createPreferenceCenterData()), + saveConsent: jest.fn().mockResolvedValue(undefined), + getConsentStatusForCategory: jest.fn().mockResolvedValue(1), + addEventListener: jest.fn( + (eventName: OTEventName, handler: EventHandler) => { + eventHandlers.set(eventName, handler); + return { remove: jest.fn() }; + } + ), + showPreferenceCenterUI: jest.fn(), + showConsentPurposesUI: jest.fn(), + ...overrides, + } as OTPublishersNativeSDK; + + return { sdk, eventHandlers }; +} + +async function flushPromises(): Promise { + await new Promise((resolve) => setTimeout(resolve, 0)); +} + +describe('createOnetrustCmpAdapter', () => { + it('should create an adapter from the SDK data', async () => { + const { sdk } = createMockSdk(); + const adapter = await createOnetrustCmpAdapter(sdk); + expect(adapter).toBeInstanceOf(OnetrustCmpAdapter); + expect(sdk.fetchPreferencesCmpApiData).toHaveBeenCalled(); + expect(sdk.getBannerData).toHaveBeenCalled(); + expect(sdk.getPreferenceCenterData).toHaveBeenCalled(); + }); +}); + +describe('OnetrustCmpAdapter', () => { + it('should emit consent status when OneTrust confirms preference-center choices', async () => { + const { sdk, eventHandlers } = createMockSdk({ + getConsentStatusForCategory: jest + .fn() + .mockResolvedValueOnce(1) + .mockResolvedValueOnce(1) + .mockResolvedValueOnce(1) + .mockResolvedValueOnce(0), + }); + const adapter = await createOnetrustCmpAdapter(sdk); + const listener = jest.fn(); + + adapter.onConsentStatusChange(listener); + await flushPromises(); + listener.mockClear(); + + eventHandlers.get(OTEventName.preferenceCenterConfirmChoices)?.(); + await flushPromises(); + + expect(listener).toHaveBeenCalledWith(false); + }); + + it('should emit consent status when OneTrust rejects all in the preference center', async () => { + const { sdk, eventHandlers } = createMockSdk({ + getConsentStatusForCategory: jest.fn().mockResolvedValue(0), + }); + const adapter = await createOnetrustCmpAdapter(sdk); + const listener = jest.fn(); + + adapter.onConsentStatusChange(listener); + await flushPromises(); + listener.mockClear(); + + eventHandlers.get(OTEventName.preferenceCenterRejectAll)?.(); + await flushPromises(); + + expect(listener).toHaveBeenCalledWith(false); + }); + + it('should not emit consent status for close-only OneTrust events', async () => { + const { sdk, eventHandlers } = createMockSdk(); + const adapter = await createOnetrustCmpAdapter(sdk); + const listener = jest.fn(); + + adapter.onConsentStatusChange(listener); + await flushPromises(); + listener.mockClear(); + + eventHandlers.get(OTEventName.hidePreferenceCenter)?.(); + await flushPromises(); + + expect(listener).not.toHaveBeenCalled(); + }); +}); From c9a4e174f87a2d609e0e0b838686cbfc39e1dc6c Mon Sep 17 00:00:00 2001 From: Maximilian Haupt Date: Thu, 9 Jul 2026 10:56:40 +0200 Subject: [PATCH 3/5] chore: add privacy settings button to OneTrust demo --- examples/onetrust/src/Content.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/examples/onetrust/src/Content.tsx b/examples/onetrust/src/Content.tsx index 4af1f9a..fc59ece 100644 --- a/examples/onetrust/src/Content.tsx +++ b/examples/onetrust/src/Content.tsx @@ -24,10 +24,19 @@ export default function Content({ cmpAdapter }: { cmpAdapter: CmpAdapter }) { }); }, [sdk]); + async function openOneTrustSettings() { + try { + await cmpAdapter.showSecondLayer('purpose'); + } catch (error) { + console.error('Failed to open OneTrust settings', error); + } + } + return ( Hello World! Has full consent: {hasFullConsent ? 'Yes' : 'No'} +