Skip to content
Open
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
Expand Up @@ -85,6 +85,16 @@ internal class RNUsercentricsModule(
promise.resolve(usercentricsProxy.instance.getABTestingVariant())
}

@ReactMethod
override fun getDpsMetadata(templateId: String, promise: Promise) {
val metadata = usercentricsProxy.instance.getDpsMetadata(templateId)
if (metadata == null) {
promise.resolve(null)
} else {
promise.resolve(metadata.toWritableMap())
}
Comment on lines +89 to +95

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Metadata serialization loses fields 🐞 Bug ✓ Correctness

Android getDpsMetadata resolves SDK metadata via Map.toWritableMap(), but that converter ignores
nulls and unsupported Number types (e.g., Long/Float) and does unsafe nested-map casts, which can
lead to missing metadata fields or runtime exceptions when DPS metadata contains non-Int/Double
values.
Agent Prompt
### Issue description
`getDpsMetadata` converts the SDK-returned metadata with `Map<String, Any?>.toWritableMap()`.

That converter:
- does not write `null` values (it drops keys)
- does not support generic `Number` (only `Int`/`Double`), so values like `Long`/`Float` are dropped
- uses unsafe casts for nested maps (`value as Map<String, Any>`) which can throw at runtime

This can cause missing fields or bridge crashes for real-world DPS metadata.

### Issue Context
The repository already includes a robust dynamic serialization approach in `GppDataExtensions.kt` (handles `null`, `Number`, `Iterable`, arrays, and has safe key handling). DPS metadata is similarly dynamic and should use the same robustness.

### Fix Focus Areas
- android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt[88-96]
- android/src/main/java/com/usercentrics/reactnative/extensions/ReadableMapExtensions.kt[74-115]
- android/src/main/java/com/usercentrics/reactnative/extensions/GppDataExtensions.kt[17-46]

### Implementation notes
- Prefer extracting a shared `Any?.toWritableValue()` / `Map<*, *>.toWritableMap()` / `Iterable<*>.toWritableArray()` utility (based on `GppDataExtensions.kt`) and use it for DPS metadata.
- Ensure `null` values become `putNull` / `pushNull` rather than being dropped.
- Normalize `Number` similarly to `GppDataExtensions.normalizeNumber` to avoid losing integer-ness.
- Avoid unchecked casts for nested maps; convert keys via `key?.toString() ?: continue`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}
Comment on lines +88 to +96
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL_BUG] Wrap the new getDpsMetadata implementation in a try/catch and reject the promise on failure. Converting native Maps to WritableMap (metadata.toWritableMap()) can throw if values are not directly serializable to the bridge. Example actionable fixes: (1) surround the conversion with try { ... } catch (e: Exception) { promise.reject(e) } or (2) defensively validate/normalize the map values before calling toWritableMap(). This prevents unhandled exceptions that would crash the bridge or leave the JS promise unresolved.

@ReactMethod
override fun getDpsMetadata(templateId: String, promise: Promise) {
    try {
        val metadata = usercentricsProxy.instance.getDpsMetadata(templateId)
        if (metadata == null) {
            promise.resolve(null)
        } else {
            promise.resolve(metadata.toWritableMap())
        }
    } catch (e: Exception) {
        promise.reject("usercentrics_getDpsMetadata_error", e.message, e)
    }
}

Comment on lines +88 to +96
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for any documentation or usage of getDpsMetadata in the codebase
rg -n "getDpsMetadata" --type-add 'kotlin:*.kt' --type kotlin

Repository: Usercentrics/react-native-sdk

Length of output: 529


🏁 Script executed:

find . -name "build.gradle*" -type f | head -20

Repository: Usercentrics/react-native-sdk

Length of output: 288


🏁 Script executed:

rg -n "usercentrics" --type-add 'gradle:*.gradle' --type gradle -i | head -30

Repository: Usercentrics/react-native-sdk

Length of output: 1278


🏁 Script executed:

rg -n "2\.25\." --type-add 'gradle:*.gradle' --type gradle

Repository: Usercentrics/react-native-sdk

Length of output: 138


🏁 Script executed:

cat -n android/build.gradle.kts | head -100

Repository: Usercentrics/react-native-sdk

Length of output: 3728


🏁 Script executed:

rg -n "usercentrics-ui\|usercentrics_version" android/build.gradle.kts

Repository: Usercentrics/react-native-sdk

Length of output: 55


🏁 Script executed:

rg -n "usercentricsProxy" --type kotlin

Repository: Usercentrics/react-native-sdk

Length of output: 20394


🏁 Script executed:

find . -name "UsercentricsProxy*" -type f

Repository: Usercentrics/react-native-sdk

Length of output: 150


🏁 Script executed:

rg -n "interface UsercentricsProxy\|class.*UsercentricsProxy" --type kotlin -A 5

Repository: Usercentrics/react-native-sdk

Length of output: 55


🏁 Script executed:

rg -n "getDpsMetadata" --type kotlin -B 2 -A 2

Repository: Usercentrics/react-native-sdk

Length of output: 1292


🏁 Script executed:

cat -n android/src/main/java/com/usercentrics/reactnative/api/UsercentricsProxy.kt

Repository: Usercentrics/react-native-sdk

Length of output: 2289


🏁 Script executed:

rg -n "getDpsMetadata" --type kotlin -B 5 -A 5

Repository: Usercentrics/react-native-sdk

Length of output: 2599


🏁 Script executed:

find . -name "*Test*.kt" -type f -exec grep -l "getDpsMetadata" {} \;

Repository: Usercentrics/react-native-sdk

Length of output: 55


🌐 Web query:

Usercentrics Android SDK 2.25.1 getDpsMetadata API changelog

💡 Result:

No specific changelog entry found for the getDpsMetadata API in Usercentrics Android SDK version 2.25.1. The release notes for 2.25.1 (released March 2, 2026) mention only "UI improvements and fixes" and "Patch with security fixes". No mention of getDpsMetadata or DPS-related API changes in 2.25.1, 2.25.0, or recent versions. Official API documentation lists methods like getCMPData, getConsents, but no getDpsMetadata. DPS (Data Processing Services) metadata is likely accessed via UsercentricsService.deviceStorage in getCMPData response, which received TCF Vendor Device Storage enhancements in 2.25.1 (new fields: sdks, specialPurposes, optOut in disclosures).

Citations:


🌐 Web query:

Usercentrics SDK getDpsMetadata method documentation

💡 Result:

No official documentation for a "getDpsMetadata" method was found in Usercentrics SDKs across browser, mobile (iOS/Android/Flutter/Unity/React Native), or related APIs after extensive searches of official docs.usercentrics.com pages, pub.dev packages, and GitHub repos. Usercentrics SDKs provide similar functionality through methods like: - getServices or getServicesBaseInfo / getServicesFullInfo in browser SDK (returns Service[] with data processing service details) - getCMPData in Apps SDK Core API (includes services: [UsercentricsService]) - consents (Future<List>) in Flutter SDK "DPS" refers to Data Processing Services in Usercentrics terminology. If this is from a private/internal SDK version or custom wrapper, check your specific implementation or contact Usercentrics support. Primary docs: https://docs.usercentrics.com/

Citations:


Remove getDpsMetadata method or verify API existence with Usercentrics support.

The getDpsMetadata(templateId: String) method is not documented in the official Usercentrics Android SDK v2.25.1 API. Official documentation and recent changelogs (including 2.25.1 released March 2, 2026) do not list this method. Similar functionality appears to be accessed through getCMPData() or getConsents() instead. Calling this non-existent method at runtime will cause a crash. Verify with Usercentrics whether this API exists in v2.25.1 or if an alternative method should be used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/src/main/java/com/usercentrics/reactnative/RNUsercentricsModule.kt`
around lines 88 - 96, The getDpsMetadata ReactMethod references a non-existent
Usercentrics SDK method and will crash; either remove the entire getDpsMetadata
method from RNUsercentricsModule or replace its implementation by calling a
supported API (e.g., usercentricsProxy.instance.getCMPData() or
usercentricsProxy.instance.getConsents()) after confirming which API returns the
equivalent DPS metadata with Usercentrics support, then convert that result to a
WritableMap (using metadata.toWritableMap() or equivalent) and resolve the
promise; if you cannot confirm immediately, remove the method to prevent runtime
crashes and open a follow-up to reintroduce it once Usercentrics confirms the
correct API.


@ReactMethod
override fun getConsents(promise: Promise) {
promise.resolve(usercentricsProxy.instance.getConsents().toWritableArray())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ abstract class RNUsercentricsModuleSpec internal constructor(context: ReactAppli
@ReactMethod
abstract fun getABTestingVariant(promise: Promise)

@ReactMethod
abstract fun getDpsMetadata(templateId: String, promise: Promise)

Comment on lines +63 to +65
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[REFACTORING] You added a new abstract method to the TurboModule spec. Make sure to run the native codegen/prepare steps so generated JNI and TurboModule bindings are updated accordingly (e.g. run the project prepare/script that triggers generate-codegen-jni or run yarn prepare). Without regenerating, native binding mismatches can cause runtime crashes or missing methods in the bridge.

@ReactMethod
abstract fun setCMPId(id: Double)

Expand Down
7 changes: 7 additions & 0 deletions example/ios/exampleTests/Fake/FakeUsercentricsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,13 @@ final class FakeUsercentricsManager: UsercentricsManager {
callback(getTCFDataResponse!)
}

var getDpsMetadataResponse: [String: Any]?
var getDpsMetadataTemplateId: String?
func getDpsMetadata(templateId: String) -> [String: Any]? {
getDpsMetadataTemplateId = templateId
return getDpsMetadataResponse
}

var getAdditionalConsentModeDataResponse: AdditionalConsentModeData?
func getAdditionalConsentModeData() -> AdditionalConsentModeData {
return getAdditionalConsentModeDataResponse!
Expand Down
37 changes: 37 additions & 0 deletions example/ios/exampleTests/RNUsercentricsModuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,43 @@ class RNUsercentricsModuleTests: XCTestCase {
}
}

func testGetDpsMetadataWithValidData() {
fakeUsercentrics.getDpsMetadataResponse = ["partner": "appsflyer", "source": "campaign_1"]
module.getDpsMetadata("template123") { result in
guard let result = result as? NSDictionary else {
XCTFail()
return
}
XCTAssertEqual("appsflyer", result["partner"] as! String)
XCTAssertEqual("campaign_1", result["source"] as! String)
} reject: { _, _, _ in
XCTFail("Should not go here")
}
XCTAssertEqual("template123", fakeUsercentrics.getDpsMetadataTemplateId)
}

func testGetDpsMetadataWhenNull() {
fakeUsercentrics.getDpsMetadataResponse = nil
module.getDpsMetadata("nonExistent") { result in
XCTAssertNil(result)
} reject: { _, _, _ in
XCTFail("Should not go here")
}
}

func testGetDpsMetadataWithEmptyMap() {
fakeUsercentrics.getDpsMetadataResponse = [:]
module.getDpsMetadata("template123") { result in
guard let result = result as? NSDictionary else {
XCTFail()
return
}
XCTAssertEqual(0, result.count)
} reject: { _, _, _ in
XCTFail("Should not go here")
}
}

func testGetAdditionalConsentModeData() {
let expected = AdditionalConsentModeData(acString: "2~43.46.55~dv.",
adTechProviders: [AdTechProvider(id: 43, name: "AdPredictive", privacyPolicyUrl: "https://adpredictive.com/privacy", consent: true)])
Expand Down
5 changes: 5 additions & 0 deletions ios/Manager/UsercentricsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public protocol UsercentricsManager {
func setGPPConsent(sectionName: String, fieldName: String, value: Any)
func getTCFData(callback: @escaping (TCFData) -> Void)
func getABTestingVariant() -> String?
func getDpsMetadata(templateId: String) -> [String: Any]?
func getAdditionalConsentModeData() -> AdditionalConsentModeData
func onGppSectionChange(callback: @escaping (GppSectionChangePayload) -> Void) -> UsercentricsDisposableEvent<GppSectionChangePayload>

Expand Down Expand Up @@ -116,6 +117,10 @@ final class UsercentricsManagerImplementation: UsercentricsManager {
UsercentricsCore.shared.getTCFData(callback: callback)
}

func getDpsMetadata(templateId: String) -> [String: Any]? {
return UsercentricsCore.shared.getDpsMetadata(templateId: templateId)
}

func getAdditionalConsentModeData() -> AdditionalConsentModeData {
return UsercentricsCore.shared.getAdditionalConsentModeData()
}
Expand Down
5 changes: 5 additions & 0 deletions ios/RNUsercentricsModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@ class RNUsercentricsModule: RCTEventEmitter {
resolve(usercentricsManager.getABTestingVariant())
}

@objc func getDpsMetadata(_ templateId: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
let metadata = usercentricsManager.getDpsMetadata(templateId: templateId)
resolve(metadata as NSDictionary?)
}
Comment on lines +162 to +165
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL_BUG] Avoid force/unsafe bridging when resolving the metadata. resolve(metadata as NSDictionary?) may crash or produce unexpected results if the dictionary contains values that aren't bridgeable to Objective-C. Convert safely, e.g. guard let metadata = usercentricsManager.getDpsMetadata(templateId: templateId) else { resolve(nil); return } then create an NSDictionary via NSDictionary(dictionary: metadata) or use JSONSerialization to ensure bridgeable types. Also consider catching exceptions and rejecting with an error when conversion fails.

@objc func getDpsMetadata(_ templateId: String,
                         resolve: @escaping RCTPromiseResolveBlock,
                         reject: @escaping RCTPromiseRejectBlock) -> Void {
    guard let metadata = usercentricsManager.getDpsMetadata(templateId: templateId) else {
        resolve(nil)
        return
    }

    // Ensure only bridgeable Foundation types are passed to React Native
    if JSONSerialization.isValidJSONObject(metadata) {
        resolve(metadata as NSDictionary)
    } else {
        // Fallback: attempt to sanitize via JSON round‑trip
        do {
            let data = try JSONSerialization.data(withJSONObject: metadata, options: [])
            if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
                resolve(jsonObject as NSDictionary)
            } else {
                reject("usercentrics_reactNative_getDpsMetadata_error",
                       "Failed to serialize DPS metadata",
                       nil)
            }
        } catch {
            reject("usercentrics_reactNative_getDpsMetadata_error",
                   "Failed to serialize DPS metadata: \(error.localizedDescription)",
                   error)
        }
    }
}


@objc func getAdditionalConsentModeData(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
resolve(usercentricsManager.getAdditionalConsentModeData().toDictionary())
}
Expand Down
4 changes: 4 additions & 0 deletions ios/RNUsercentricsModuleSpec.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ NS_ASSUME_NONNULL_BEGIN
- (void)getCMPData:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

- (void)getDpsMetadata:(NSString *)templateId
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

- (void)getAdditionalConsentModeData:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

Expand Down
7 changes: 7 additions & 0 deletions sample/ios/sampleTests/Fake/FakeUsercentricsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ final class FakeUsercentricsManager: UsercentricsManager {
callback(getTCFDataResponse!)
}

var getDpsMetadataResponse: [String: Any]?
var getDpsMetadataTemplateId: String?
func getDpsMetadata(templateId: String) -> [String: Any]? {
getDpsMetadataTemplateId = templateId
return getDpsMetadataResponse
}

var getAdditionalConsentModeDataResponse: AdditionalConsentModeData?
func getAdditionalConsentModeData() -> AdditionalConsentModeData {
return getAdditionalConsentModeDataResponse!
Expand Down
37 changes: 37 additions & 0 deletions sample/ios/sampleTests/RNUsercentricsModuleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,43 @@ class RNUsercentricsModuleTests: XCTestCase {
}
}

func testGetDpsMetadataWithValidData() {
fakeUsercentrics.getDpsMetadataResponse = ["partner": "appsflyer", "source": "campaign_1"]
module.getDpsMetadata("template123") { result in
guard let result = result as? NSDictionary else {
XCTFail()
return
Comment on lines +570 to +572
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add assertion messages to XCTFail() calls.

Two XCTFail() calls have no message, which hurts debugging clarity (also matches SwiftLint warning).

Suggested patch
-      guard let result = result as? NSDictionary else {
-        XCTFail()
+      guard let result = result as? NSDictionary else {
+        XCTFail("Expected NSDictionary result for non-empty DPS metadata")
         return
       }
@@
-      guard let result = result as? NSDictionary else {
-        XCTFail()
+      guard let result = result as? NSDictionary else {
+        XCTFail("Expected NSDictionary result for empty DPS metadata map")
         return
       }

Also applies to: 594-596

🧰 Tools
🪛 SwiftLint (0.63.2)

[Warning] 571-571: An XCTFail call should include a description of the assertion

(xctfail_message)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@sample/ios/sampleTests/RNUsercentricsModuleTests.swift` around lines 570 -
572, Update the bare XCTFail() calls in RNUsercentricsModuleTests.swift to
include descriptive failure messages so failures are actionable; specifically,
modify the guard blocks that check "guard let result = result as? NSDictionary
else { XCTFail(); return }" (and the similar guard at the other occurrence) to
call XCTFail(...) with a short message such as "expected 'result' to be
NSDictionary but was nil or wrong type" or test-specific context so the test
output identifies what went wrong.

}
XCTAssertEqual("appsflyer", result["partner"] as! String)
XCTAssertEqual("campaign_1", result["source"] as! String)
} reject: { _, _, _ in
XCTFail("Should not go here")
}
XCTAssertEqual("template123", fakeUsercentrics.getDpsMetadataTemplateId)
}

func testGetDpsMetadataWhenNull() {
fakeUsercentrics.getDpsMetadataResponse = nil
module.getDpsMetadata("nonExistent") { result in
XCTAssertNil(result)
} reject: { _, _, _ in
XCTFail("Should not go here")
}
}

func testGetDpsMetadataWithEmptyMap() {
fakeUsercentrics.getDpsMetadataResponse = [:]
module.getDpsMetadata("template123") { result in
guard let result = result as? NSDictionary else {
XCTFail()
return
}
XCTAssertEqual(0, result.count)
} reject: { _, _, _ in
XCTFail("Should not go here")
}
}

func testGetAdditionalConsentModeData() {
let expected = AdditionalConsentModeData(acString: "2~43.46.55~dv.",
adTechProviders: [AdTechProvider(id: 43, name: "AdPredictive", privacyPolicyUrl: "https://adpredictive.com/privacy", consent: true)])
Expand Down
1 change: 1 addition & 0 deletions src/NativeUsercentrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface Spec extends TurboModule {
getGPPData(): Promise<GppData>;
getGPPString(): Promise<string | null>;
getABTestingVariant(): Promise<string>;
getDpsMetadata(templateId: string): Promise<Record<string, unknown> | null>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[REFACTORING] Type mismatch/consistency: you added getDpsMetadata(templateId: string): Promise<Record<string, unknown> | null>. The fabric TurboModule spec uses Promise<Object | null>. Align the types across both files to a single, specific shape (prefer Record<string, unknown> | null) to keep type safety consistent and avoid confusion when consuming the API.

// src/fabric/NativeUsercentricsModule.ts
export interface Spec extends TurboModule {
  // ...existing methods...
-  getDpsMetadata(templateId: string): Promise<Object | null>;
+  getDpsMetadata(templateId: string): Promise<Record<string, unknown> | null>;
}


// Configuration Setters
setCMPId(id: number): void;
Expand Down
5 changes: 5 additions & 0 deletions src/Usercentrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ export const Usercentrics = {
return RNUsercentricsModule.getAdditionalConsentModeData();
},

getDpsMetadata: async (templateId: string): Promise<Record<string, unknown> | null> => {
await RNUsercentricsModule.isReady();
return RNUsercentricsModule.getDpsMetadata(templateId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: This new call assumes getDpsMetadata always exists on the native module, but on iOS legacy bridge builds the method is not exported, so this will throw undefined is not a function at runtime. Guard the method before calling it and fail with a controlled error (or safe fallback) instead of invoking an undefined function. [possible bug]

Severity Level: Major ⚠️
-`Usercentrics.getDpsMetadata()` crashes on legacy iOS bridge.
- ⚠️ DPS metadata feature unavailable for legacy iOS consumers.
- ⚠️ Runtime failure occurs after successful `isReady()` call.
Suggested change
return RNUsercentricsModule.getDpsMetadata(templateId);
if (typeof RNUsercentricsModule.getDpsMetadata !== 'function') {
throw new Error('Usercentrics React Native SDK: getDpsMetadata is not available in this native build.');
}
Steps of Reproduction ✅
1. Build iOS with legacy bridge (non-new-architecture); JS resolves module through
fallback at `src/Usercentrics.tsx:25` and `src/NativeUsercentrics.ts:74`
(`TurboModuleRegistry.get(...) || NativeModules.RNUsercentricsModule`).

2. Call exported API `Usercentrics.getDpsMetadata("template123")` (public export at
`src/index.tsx:1`, implementation at `src/Usercentrics.tsx:103-105`).

3. On iOS legacy bridge, `ios/RNUsercentricsModule.mm:10-118` defines exported methods via
`RCT_EXTERN_METHOD(...)`, and this file has no `getDpsMetadata` export entry.

4. Because legacy `NativeModules.RNUsercentricsModule` lacks that function, call site
`src/Usercentrics.tsx:105` invokes `undefined`, causing runtime `TypeError:
...getDpsMetadata is not a function`.
Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/Usercentrics.tsx
**Line:** 105:105
**Comment:**
	*Possible Bug: This new call assumes `getDpsMetadata` always exists on the native module, but on iOS legacy bridge builds the method is not exported, so this will throw `undefined is not a function` at runtime. Guard the method before calling it and fail with a controlled error (or safe fallback) instead of invoking an undefined function.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
👍 | 👎

},
Comment on lines +103 to +106
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[REFACTORING] Public wrapper getDpsMetadata is added — ensure its returned runtime type is documented in public typings and that callers know it can be null. Also consider normalizing the returned value (e.g. return {} instead of null) only if that matches product requirements; otherwise keep current null semantics but document them.

// src/Usercentrics.tsx
export interface UsercentricsPublicAPI {
  // ...existing methods
  /**
   * Returns DPS metadata for the given template.
   *
   * When metadata is available, a flat key/value object is returned.
   * When no metadata exists for the given templateId, this resolves to `null`.
   */
  getDpsMetadata(templateId: string): Promise<Record<string, unknown> | null>;
}

export const Usercentrics: UsercentricsPublicAPI = {
  // ...existing methods
  getDpsMetadata: async (
    templateId: string,
  ): Promise<Record<string, unknown> | null> => {
    await RNUsercentricsModule.isReady();
    const metadata = await RNUsercentricsModule.getDpsMetadata(templateId);
    // keep native null semantics; callers must handle `null`
    return metadata;
  },
};


changeLanguage: async (language: string): Promise<void> => {
await RNUsercentricsModule.isReady();
return RNUsercentricsModule.changeLanguage(language);
Expand Down
32 changes: 32 additions & 0 deletions src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jest.mock("react-native", () => {
setGPPConsent: jest.fn(),
track: jest.fn(),
reset: jest.fn(),
getDpsMetadata: jest.fn(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[VALIDATION] You added getDpsMetadata to the NativeModules mock. The JS implementation awaits RNUsercentricsModule.isReady() before calling the method; please ensure RNUsercentricsModule.isReady is mocked to a resolved value (e.g. jest.fn().mockResolvedValue(undefined)) in tests that call getDpsMetadata so the await doesn't produce surprising behavior. Also, if your production code prefers TurboModuleRegistry.get over NativeModules, make sure tests mock the TurboModule path as well (or explicitly import the same module path your code uses).

// At top-level in src/__tests__/index.test.ts, after defining RNUsercentricsModule mock
beforeEach(() => {
  RNUsercentricsModule.isReady.mockResolvedValue(undefined);
});

// Or, inside each DPS metadata test:
RNUsercentricsModule.isReady.mockResolvedValueOnce(undefined);

// Additionally, if your production code ever switches to TurboModuleRegistry:
jest.mock('../fabric/NativeUsercentricsModule', () => {
  return {
    __esModule: true,
    default: {
      isReady: jest.fn().mockResolvedValue(undefined),
      getDpsMetadata: jest.fn(),
      // ...other mocked methods as needed
    },
  };
});

clearUserSession: jest.fn(),
addListener: jest.fn(),
removeListeners: jest.fn()
Expand Down Expand Up @@ -455,6 +456,37 @@ describe('Test Usercentrics Module', () => {
expect(data).toStrictEqual(response)
})

test('testGetDpsMetadataWithValidData', async () => {
const metadata = { partner: "appsflyer", source: "campaign_1" };
RNUsercentricsModule.getDpsMetadata.mockImplementationOnce(
(): Promise<any> => Promise.resolve(metadata)
)

const data = await Usercentrics.getDpsMetadata("templateId123");
expect(data).toStrictEqual(metadata);

const call = RNUsercentricsModule.getDpsMetadata.mock.calls[0][0];
expect(call).toBe("templateId123");
})

test('testGetDpsMetadataWhenNull', async () => {
RNUsercentricsModule.getDpsMetadata.mockImplementationOnce(
(): Promise<any> => Promise.resolve(null)
)

const data = await Usercentrics.getDpsMetadata("nonExistentId");
expect(data).toBe(null);
})

test('testGetDpsMetadataWithEmptyObject', async () => {
RNUsercentricsModule.getDpsMetadata.mockImplementationOnce(
(): Promise<any> => Promise.resolve({})
)

const data = await Usercentrics.getDpsMetadata("templateId123");
expect(data).toStrictEqual({});
})

test('testClearUserSession', async () => {
const readyStatus = new UsercentricsReadyStatus(
true,
Expand Down
1 change: 1 addition & 0 deletions src/fabric/NativeUsercentricsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface Spec extends TurboModule {
getGPPData(): Promise<Object>;
getGPPString(): Promise<string | null>;
getABTestingVariant(): Promise<string>;
getDpsMetadata(templateId: string): Promise<Object | null>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[REFACTORING] Use a consistent, explicit type for the new method signature instead of generic Object. Change getDpsMetadata(templateId: string): Promise<Object | null> to use the same type as the JS spec (e.g. Promise<Record<string, unknown> | null>) so TS types match between the TurboModule spec and the public TS declaration.

// src/fabric/NativeUsercentricsModule.ts
export interface Spec extends TurboModule {
  // ...
  getABTestingVariant(): Promise<string>;
  getDpsMetadata(templateId: string): Promise<Record<string, unknown> | null>;
  // ...
}


// Configuration Setters
setCMPId(id: number): void;
Expand Down
Loading