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
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
diff --git 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
index 23e6beb..8c85220 100644
index 23e6beb..74a8398 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,52 @@ public class OneTrust extends ReactContextBaseJavaModule {
@@ -245,6 +245,12 @@ public class OneTrust extends ReactContextBaseJavaModule {
promise.resolve(consentValue);
}

+ @ReactMethod
+ public void getPurposeConsentLocal(String categoryId, Promise promise){
+ Integer consentValue = ot.getPurposeConsentLocal(categoryId);
+ promise.resolve(consentValue);
+ }
+
@ReactMethod
public void getConsentStatusForSDKId(String SDKId, Promise promise){
Integer consentValue = ot.getConsentStatusForSDKId(SDKId);
@@ -313,6 +319,52 @@ public class OneTrust extends ReactContextBaseJavaModule {
promise.resolve(googleConsentData.toString());
}

Expand Down Expand Up @@ -56,7 +69,7 @@ index 23e6beb..8c85220 100644
public void showConsentPurposesUI(){
final Activity currentActivity = getCurrentActivity();
diff --git a/node_modules/react-native-onetrust-cmp/iOS/OneTrust.m b/node_modules/react-native-onetrust-cmp/iOS/OneTrust.m
index 2c1a676..c84294f 100644
index 2c1a676..ba9c9b3 100644
--- a/node_modules/react-native-onetrust-cmp/iOS/OneTrust.m
+++ b/node_modules/react-native-onetrust-cmp/iOS/OneTrust.m
@@ -36,6 +36,21 @@ + (BOOL)requiresMainQueueSetup{
Expand All @@ -81,7 +94,19 @@ index 2c1a676..c84294f 100644
RCT_EXTERN_METHOD(shouldShowBanner:
(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject
@@ -65,6 +80,11 @@ + (BOOL)requiresMainQueueSetup{
@@ -46,6 +61,11 @@ + (BOOL)requiresMainQueueSetup{
resolve:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)

+RCT_EXTERN_METHOD(getPurposeConsentLocal:
+ (NSString*)categoryId
+ resolve:(RCTPromiseResolveBlock)resolve
+ rejecter:(RCTPromiseRejectBlock)reject)
+
RCT_EXTERN_METHOD(getConsentStatusForSDKId:
(NSString*)sdkId
resolve:(RCTPromiseResolveBlock)resolve
@@ -65,6 +85,11 @@ + (BOOL)requiresMainQueueSetup{
rejecter:(RCTPromiseRejectBlock)reject
)

Expand All @@ -94,7 +119,7 @@ index 2c1a676..c84294f 100644
RCT_EXTERN_METHOD(showConsentPurposesUI)

diff --git a/node_modules/react-native-onetrust-cmp/iOS/OneTrust.swift b/node_modules/react-native-onetrust-cmp/iOS/OneTrust.swift
index 0030d64..894862f 100644
index 0030d64..7d4cb67 100644
--- a/node_modules/react-native-onetrust-cmp/iOS/OneTrust.swift
+++ b/node_modules/react-native-onetrust-cmp/iOS/OneTrust.swift
@@ -81,31 +81,64 @@ import AppTrackingTransparency
Expand Down Expand Up @@ -151,19 +176,19 @@ index 0030d64..894862f 100644
+ resolve(json)
+ } else {
+ reject("OT_GET_PREFERENCE_CENTER_DATA","OneTrust getPreferenceCenterData returned unsupported type.",nil)
}
+ }
+ return
+ }
+ OTPublishersHeadlessSDK.shared.fetchPreferencesCmpApiData { [weak self] _ in
+ guard let data = OTPublishersHeadlessSDK.shared.getPreferenceCenterData(),
+ let json = self?.serializeToJSON(data) else {
+ resolve(nil)
+ return
+ }
}
+ resolve(json)
+ }
+ }

+ @objc(getBannerData:rejecter:)
+ public func getBannerData(_ resolve:@escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock){
+ if let data = OTPublishersHeadlessSDK.shared.getBannerData() {
Expand All @@ -185,7 +210,20 @@ index 0030d64..894862f 100644
}

@objc(shouldShowBanner:rejecter:)
@@ -153,6 +186,28 @@ import AppTrackingTransparency
@@ -119,6 +152,12 @@ import AppTrackingTransparency
let consentStatus = OTPublishersHeadlessSDK.shared.getConsentStatus(forCategory: categoryId as String)
resolve(consentStatus)
}
+
+ @objc(getPurposeConsentLocal:resolve:rejecter:)
+ public func getPurposeConsentLocal(_ categoryId:NSString, resolve:@escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) -> Void{
+ let consentStatus = OTPublishersHeadlessSDK.shared.getPurposeConsentLocal(forCustomGroupId: categoryId as String)
+ resolve(consentStatus)
+ }

@objc(getConsentStatusForSDKId:resolve:rejecter:)
public func getConsentStatusForSDKId(_ sdkId:NSString, resolve:@escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) -> Void{
@@ -153,6 +192,28 @@ import AppTrackingTransparency
public func getATTStatus(_ resolve:@escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock){
resolve(getATTStatusAsString())
}
Expand Down Expand Up @@ -214,7 +252,7 @@ index 0030d64..894862f 100644
#endif

@objc(showConsentUI:resolve:rejecter:)
@@ -177,6 +232,8 @@ import AppTrackingTransparency
@@ -177,6 +238,8 @@ import AppTrackingTransparency
OTPublishersHeadlessSDK.shared.showConsentUI(for: type, from: vc, completion: {
resolve(nil)
})
Expand All @@ -223,11 +261,32 @@ index 0030d64..894862f 100644
}
}
}
@@ -464,4 +527,4 @@ extension OneTrust: OTEventListener {
let params: [String: Any] = ["sdkId": sdkId, "consentStatus": consentStatus]
sendEventToJS("onSDKListSDKConsentChanged", params)
}
-}
+}
\ No newline at end of file
diff --git a/node_modules/react-native-onetrust-cmp/index.ts b/node_modules/react-native-onetrust-cmp/index.ts
index f99e8c5..33c48c8 100644
index f99e8c5..5ab246a 100644
--- a/node_modules/react-native-onetrust-cmp/index.ts
+++ b/node_modules/react-native-onetrust-cmp/index.ts
@@ -149,6 +149,36 @@ export default class OTPublishersNativeSDK{
@@ -73,6 +73,13 @@ export default class OTPublishersNativeSDK{
return OneTrust.getConsentStatusForCategory(categoryId);
}

+ /**
+ * Gets the latest local consent status for a category.
+ */
+ static getPurposeConsentLocal(categoryId:string):Promise<number | null> {
+ return OneTrust.getPurposeConsentLocal(categoryId);
+ }
+
/**
* Get the current consent status for the given category
* @param {string} SDKId String of the SDK GUID for which you'd like to retrieve consent
@@ -149,6 +156,36 @@ export default class OTPublishersNativeSDK{
return JSON.parse(googleConsentModeData);
}

Expand Down
6 changes: 6 additions & 0 deletions packages/react-native-contentpass-cmp-onetrust/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# @contentpass/react-native-contentpass-cmp-onetrust

## 0.4.4

### Patch Changes

- Read local OneTrust consent

## 0.4.3

### Patch Changes
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@contentpass/react-native-contentpass-cmp-onetrust",
"version": "0.4.3",
"version": "0.4.4",
"description": "Contentpass OneTrust CMP adapter",
"source": "./src/index.ts",
"main": "./lib/commonjs/index.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,41 @@ describe('OnetrustCmpAdapter', () => {
await expect(adapter.hasFullConsent()).resolves.toBe(true);
});

it('should prefer the latest local purpose consent status', async () => {
const { sdk } = createMockSdk({
getConsentStatusForCategory: jest.fn().mockResolvedValue(0),
getPurposeConsentLocal: jest.fn().mockResolvedValue(1),
});
const adapter = await createOnetrustCmpAdapter(sdk);

await expect(adapter.hasFullConsent()).resolves.toBe(true);
});

it('should wait for local purpose consent to settle during initialization', async () => {
jest.useFakeTimers();
try {
const { sdk } = createMockSdk({
shouldShowBanner: jest.fn().mockResolvedValue(false),
getConsentStatusForCategory: jest.fn().mockResolvedValue(0),
getPurposeConsentLocal: jest
.fn()
.mockResolvedValueOnce(0)
.mockResolvedValueOnce(0)
.mockResolvedValue(1),
});
const adapter = await createOnetrustCmpAdapter(sdk);

const initialization = adapter.waitForInit();
await jest.runAllTimersAsync();

await expect(initialization).resolves.toBeUndefined();
await expect(adapter.hasFullConsent()).resolves.toBe(true);
} finally {
jest.clearAllTimers();
jest.useRealTimers();
}
});

it('should report missing consent when OneTrust requires reconsent', async () => {
const { sdk } = createMockSdk({
shouldShowBanner: jest.fn().mockResolvedValue(true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const CONSENT_CHANGE_EVENTS = new Set<OTEventName>([
]);

const CONSENT_STATUS_REFRESH_DELAYS_MS = [100, 500, 1000];
const INITIAL_CONSENT_STATUS_REFRESH_DELAYS_MS = [0, 100, 500, 1000];
const BANNER_SETTLEMENT_TIMEOUT_MS = 10_000;

export type OnetrustCmpAdapterOptions = {
Expand All @@ -28,6 +29,9 @@ export type OnetrustCmpAdapterOptions = {
type ConsentStatus = {
groupId: string;
status: number;
savedStatus: number;
localStatus: number | null;
statusSource: 'saved' | 'local';
isAttGroup: boolean;
};

Expand Down Expand Up @@ -69,6 +73,17 @@ export async function createOnetrustCmpAdapter(
showBanner: preferenceCenterData?.appConfig?.showBanner,
bannerReason: preferenceCenterData?.appConfig?.bannerReason,
bannerReasonCode: preferenceCenterData?.appConfig?.bannerReasonCode,
preferenceCenterGroupConsents:
preferenceCenterData?.storageKeys?.OT_GroupConsents,
preferenceCenterPurposeStates: Object.fromEntries(
preferenceCenterData.purposes.map(
({ consentStatus, consentToggleStatus, groupId }) => [
groupId,
{ consentStatus, consentToggleStatus },
]
)
),
bannerGroupConsents: bannerData?.storageKeys?.OT_GroupConsents,
});

return new OnetrustCmpAdapter(
Expand Down Expand Up @@ -97,6 +112,7 @@ export default class OnetrustCmpAdapter implements CmpAdapter {
>();
private consentStatusRevision = 0;
private bannerAcknowledgedUntil = 0;
private initialConsentStatusReady: Promise<void> | null = null;

constructor(
private readonly sdk: OTPublishersNativeSDK,
Expand Down Expand Up @@ -138,7 +154,11 @@ export default class OnetrustCmpAdapter implements CmpAdapter {
}

async waitForInit(): Promise<void> {
console.debug('[OnetrustCmpAdapter::waitForInit] already initialized');
if (!this.initialConsentStatusReady) {
this.initialConsentStatusReady = this.waitForInitialConsentStatus();
}

await this.initialConsentStatusReady;
}

async acceptAll(): Promise<void> {
Expand Down Expand Up @@ -428,19 +448,73 @@ export default class OnetrustCmpAdapter implements CmpAdapter {
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),
}))
)
this.groupIds.map((groupId) => this.getConsentStatus(groupId))
),
]);

return { shouldShowBanner, consentStatuses: statuses };
}

private async getConsentStatus(groupId: string): Promise<ConsentStatus> {
const savedStatusPromise = this.sdk.getConsentStatusForCategory(groupId);
const localStatusPromise = this.sdk.getPurposeConsentLocal
? this.sdk.getPurposeConsentLocal(groupId).catch((error) => {
console.warn(
'[OnetrustCmpAdapter::getConsentStatus] local status unavailable',
{ groupId, error }
);
return null;
})
: Promise.resolve(null);
const [savedStatus, localStatus] = await Promise.all([
savedStatusPromise,
localStatusPromise,
]);
const hasLocalStatus = localStatus !== null && localStatus >= 0;

return {
groupId,
status: hasLocalStatus ? localStatus : savedStatus,
savedStatus,
localStatus,
statusSource: hasLocalStatus ? 'local' : 'saved',
isAttGroup: this.attGroupIds.has(groupId),
};
}

private async waitForInitialConsentStatus(): Promise<void> {
const startedAt = Date.now();

for (const delay of INITIAL_CONSENT_STATUS_REFRESH_DELAYS_MS) {
const remainingDelay = Math.max(0, delay - (Date.now() - startedAt));
if (remainingDelay > 0) {
await new Promise<void>((resolve) =>
setTimeout(resolve, remainingDelay)
);
}

try {
const consentState = await this.getConsentState();
const consentDecision = this.getFullConsentDecision(consentState);
console.debug('[OnetrustCmpAdapter::waitForInit] consent status', {
delay,
...consentDecision,
...consentState,
});

if (consentState.shouldShowBanner || consentDecision.fullConsent) {
return;
}
} catch (error) {
console.error('[OnetrustCmpAdapter::waitForInit] status check failed', {
delay,
error,
});
return;
}
}
}

private getFullConsentDecision({
shouldShowBanner,
consentStatuses,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ declare module 'react-native-onetrust-cmp' {
saveConsent(interaction: OTConsentInteraction): Promise<void>;
shouldShowBanner(): Promise<boolean>;
getConsentStatusForCategory(categoryId: string): Promise<number>;
getPurposeConsentLocal?(categoryId: string): Promise<number | null>;
getATTStatus?(): Promise<string>;
addEventListener(
eventName: OTEventName,
Expand Down
Loading