From 8d6b0b380aa1aef5d8d586eb25f5da51109fab3c Mon Sep 17 00:00:00 2001 From: Maximilian Haupt Date: Mon, 20 Jul 2026 23:44:09 +0200 Subject: [PATCH 1/4] fix(onetrust): handle preference center delay OneTrust can keep its banner state stale after consent is saved in the preference center. Settle explicit Accept All actions and refresh after the center closes. This recognizes manual all-category selections without bypassing partial or rejected choices. --- .../src/OnetrustCmpAdapter.test.ts | 49 +++++++++++++++++++ .../src/OnetrustCmpAdapter.ts | 23 +++++++++ 2 files changed, 72 insertions(+) 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 528d204..136caab 100644 --- a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.test.ts +++ b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.test.ts @@ -266,6 +266,55 @@ describe('OnetrustCmpAdapter', () => { expect(listener).toHaveBeenCalledWith(false); }); + it('should accept consent during the banner settlement period after accepting all in the preference center', async () => { + const { sdk, eventHandlers } = createMockSdk({ + shouldShowBanner: jest.fn().mockResolvedValue(true), + }); + const adapter = await createOnetrustCmpAdapter(sdk); + const listener = jest.fn(); + + adapter.onConsentStatusChange(listener); + await flushPromises(); + listener.mockClear(); + + eventHandlers.get(OTEventName.preferenceCenterAcceptAll)?.(); + await flushPromises(); + + expect(listener).toHaveBeenCalledWith(true); + }); + + it('should refresh consent after the preference center closes', async () => { + jest.useFakeTimers(); + try { + let bannerReads = 0; + let statusReads = 0; + const { sdk, eventHandlers } = createMockSdk({ + shouldShowBanner: jest.fn(() => + Promise.resolve(bannerReads++ >= 3 ? false : true) + ), + getConsentStatusForCategory: jest.fn(() => + Promise.resolve(Math.floor(statusReads++ / 2) >= 3 ? 1 : 0) + ), + }); + const adapter = await createOnetrustCmpAdapter(sdk); + const listener = jest.fn(); + + adapter.onConsentStatusChange(listener); + await jest.advanceTimersByTimeAsync(0); + listener.mockClear(); + + const secondLayer = adapter.showSecondLayer('purpose'); + eventHandlers.get(OTEventName.preferenceCenterConfirmChoices)?.(); + await secondLayer; + await jest.advanceTimersByTimeAsync(100); + + expect(listener).toHaveBeenCalledWith(true); + } finally { + jest.clearAllTimers(); + jest.useRealTimers(); + } + }); + it('should emit consent status when OneTrust rejects all in the preference center', async () => { const { sdk, eventHandlers } = createMockSdk({ getConsentStatusForCategory: jest.fn().mockResolvedValue(0), diff --git a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts index 5a4c2fb..c49a29b 100644 --- a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts +++ b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts @@ -216,6 +216,18 @@ export default class OnetrustCmpAdapter implements CmpAdapter { eventName, }); remove(); + const revision = this.beginConsentStatusOperation( + `showSecondLayer: closed:${eventName}` + ); + this.logConsentSnapshot(`showSecondLayer: closed:${eventName}`); + this.scheduleConsentStatusRefreshes( + revision, + `showSecondLayer: closed:${eventName}` + ); + this.emitConsentStatusForRevision( + revision, + `showSecondLayer: closed:${eventName}` + ); resolve(); break; default: @@ -304,6 +316,17 @@ export default class OnetrustCmpAdapter implements CmpAdapter { }); if (CONSENT_CHANGE_EVENTS.has(eventName)) { const revision = this.beginConsentStatusOperation(`event:${eventName}`); + if (eventName === OTEventName.preferenceCenterAcceptAll) { + this.bannerAcknowledgedUntil = + Date.now() + BANNER_SETTLEMENT_TIMEOUT_MS; + console.debug( + '[OnetrustCmpAdapter::onEvent] started banner settlement', + { + eventName, + bannerAcknowledgedUntil: this.bannerAcknowledgedUntil, + } + ); + } this.emitConsentStatusForRevision(revision, `event:${eventName}`); } From ffe789313486d552c571bb3de58f6879c31c8df5 Mon Sep 17 00:00:00 2001 From: Maximilian Haupt Date: Mon, 20 Jul 2026 23:45:01 +0200 Subject: [PATCH 2/4] chore: bump OneTrust adapter to 0.4.2 --- packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md | 6 ++++++ packages/react-native-contentpass-cmp-onetrust/package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md b/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md index 2cc30e6..5ea04e7 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.2 + +### Patch Changes + +- Handle OneTrust's short banner-state delay after accepting all consent from either the Contentpass layer or the OneTrust preference center. + ## 0.4.1 ### Patch Changes diff --git a/packages/react-native-contentpass-cmp-onetrust/package.json b/packages/react-native-contentpass-cmp-onetrust/package.json index f31f0fb..4351a72 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.4.1", + "version": "0.4.2", "description": "Contentpass OneTrust CMP adapter", "source": "./src/index.ts", "main": "./lib/commonjs/index.js", From 8e16a1ae5cbad52b07d250dea1cb83a59ce5529f Mon Sep 17 00:00:00 2001 From: Maximilian Haupt Date: Tue, 21 Jul 2026 15:45:58 +0200 Subject: [PATCH 3/4] fix(onetrust): fetch Android preference data --- .../react-native-onetrust-cmp+202602.1.2.patch | 17 +++++++++++++++-- .../CHANGELOG.md | 1 + .../src/OnetrustCmpAdapter.test.ts | 10 ++++++++++ .../src/OnetrustCmpAdapter.ts | 8 +++++++- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/examples/onetrust/patches/react-native-onetrust-cmp+202602.1.2.patch b/examples/onetrust/patches/react-native-onetrust-cmp+202602.1.2.patch index bdefd44..9aa61ab 100644 --- a/examples/onetrust/patches/react-native-onetrust-cmp+202602.1.2.patch +++ b/examples/onetrust/patches/react-native-onetrust-cmp+202602.1.2.patch @@ -2,13 +2,26 @@ diff --git a/node_modules/react-native-onetrust-cmp/android/src/main/java/com/on index 23e6beb..8c85220 100644 --- a/node_modules/react-native-onetrust-cmp/android/src/main/java/com/onetrust/OneTrust.java +++ b/node_modules/react-native-onetrust-cmp/android/src/main/java/com/onetrust/OneTrust.java -@@ -313,6 +313,39 @@ public class OneTrust extends ReactContextBaseJavaModule { +@@ -313,6 +313,52 @@ public class OneTrust extends ReactContextBaseJavaModule { promise.resolve(googleConsentData.toString()); } + @ReactMethod + public void fetchPreferencesCmpApiData(Promise promise){ -+ promise.resolve(null); ++ ot.fetchPreferencesCmpApiData(new OTCallback() { ++ @Override ++ public void onSuccess(@NonNull OTResponse otResponse) { ++ promise.resolve(null); ++ } ++ ++ @Override ++ public void onFailure(@NonNull OTResponse otResponse) { ++ promise.reject( ++ "OT_FETCH_PREFERENCE_CENTER_DATA", ++ otResponse.getResponseMessage() ++ ); ++ } ++ }); + } + + @ReactMethod diff --git a/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md b/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md index 5ea04e7..708f60e 100644 --- a/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md +++ b/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md @@ -5,6 +5,7 @@ ### Patch Changes - Handle OneTrust's short banner-state delay after accepting all consent from either the Contentpass layer or the OneTrust preference center. +- Fetch Android preference-center data before creating the adapter. ## 0.4.1 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 136caab..7a7effc 100644 --- a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.test.ts +++ b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.test.ts @@ -122,6 +122,16 @@ describe('createOnetrustCmpAdapter', () => { expect(sdk.getBannerData).toHaveBeenCalled(); expect(sdk.getPreferenceCenterData).toHaveBeenCalled(); }); + + it('should fail clearly when OneTrust returns no preference center data', async () => { + const { sdk } = createMockSdk({ + getPreferenceCenterData: jest.fn().mockResolvedValue(null), + }); + + await expect(createOnetrustCmpAdapter(sdk)).rejects.toThrow( + 'OneTrust returned no preference center data after fetching CMP API data' + ); + }); }); describe('OnetrustCmpAdapter', () => { diff --git a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts index c49a29b..ebceb9e 100644 --- a/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts +++ b/packages/react-native-contentpass-cmp-onetrust/src/OnetrustCmpAdapter.ts @@ -52,9 +52,15 @@ export async function createOnetrustCmpAdapter( await sdk.fetchPreferencesCmpApiData(); const bannerData = await sdk.getBannerData(); - const preferenceCenterData: PreferenceCenterData = + const preferenceCenterData: PreferenceCenterData | null = await sdk.getPreferenceCenterData(); + if (!Array.isArray(preferenceCenterData?.purposes)) { + throw new Error( + 'OneTrust returned no preference center data after fetching CMP API data' + ); + } + console.debug('[OnetrustCmpAdapter::create] CMP API data fetched', { purposeCount: preferenceCenterData?.purposes?.length ?? 0, shouldResetLocalState: From 7d9a05b5708a0aae44e2bbee8f3080b44b85d2e1 Mon Sep 17 00:00:00 2001 From: Maximilian Haupt Date: Tue, 21 Jul 2026 16:55:25 +0200 Subject: [PATCH 4/4] chore: bump OneTrust adapter to 0.4.3 --- .../react-native-contentpass-cmp-onetrust/CHANGELOG.md | 7 ++++++- .../react-native-contentpass-cmp-onetrust/package.json | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md b/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md index 708f60e..f67d013 100644 --- a/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md +++ b/packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md @@ -1,11 +1,16 @@ # @contentpass/react-native-contentpass-cmp-onetrust +## 0.4.3 + +### Patch Changes + +- Fetch Android preference-center data before creating the adapter. + ## 0.4.2 ### Patch Changes - Handle OneTrust's short banner-state delay after accepting all consent from either the Contentpass layer or the OneTrust preference center. -- Fetch Android preference-center data before creating the adapter. ## 0.4.1 diff --git a/packages/react-native-contentpass-cmp-onetrust/package.json b/packages/react-native-contentpass-cmp-onetrust/package.json index 4351a72..2fe139e 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.4.2", + "version": "0.4.3", "description": "Contentpass OneTrust CMP adapter", "source": "./src/index.ts", "main": "./lib/commonjs/index.js",