diff --git a/RNAmazonPublisherServices.podspec b/RNAmazonPublisherServices.podspec
index ec2c46f..4ea3314 100644
--- a/RNAmazonPublisherServices.podspec
+++ b/RNAmazonPublisherServices.podspec
@@ -17,5 +17,5 @@ Pod::Spec.new do |s|
install_modules_dependencies(s)
- s.dependency "AmazonPublisherServicesSDK", "5.3.1"
+ s.dependency "AmazonPublisherServicesSDK", "5.6.4"
end
diff --git a/android/build.gradle b/android/build.gradle
index 97d45d4..d345b35 100644
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -45,7 +45,7 @@ dependencies {
} else {
implementation 'com.facebook.react:react-native:+'
}
- implementation("com.amazon.android:aps-sdk:11.1.1")
+ implementation("com.amazon.android:aps-sdk:12.0.1")
implementation("com.iabtcf:iabtcf-decoder:2.0.10")
}
diff --git a/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java b/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java
index 65e1ffb..af0df76 100644
--- a/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java
+++ b/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java
@@ -257,7 +257,7 @@ public void loadAd(int loaderId, String adType, ReadableMap options, Promise pro
// NOTE: options.contentUrl is intentionally ignored here.
//
// Amazon DSP asks for the public web URL of the content being viewed, but
- // the Android APS SDK (com.amazon.android:aps-sdk 11.1.1) exposes no
+ // the Android APS SDK (com.amazon.android:aps-sdk 12.0.1) exposes no
// equivalent of the iOS +[APS setContentUrl:] — verified by inspecting every
// public member of AdRegistration and DTBAdRequest. There is no supported
// way to attach it to an Android bid request today.
diff --git a/android/src/main/java/com/adversport/rnaps/RNAPSAdsModule.java b/android/src/main/java/com/adversport/rnaps/RNAPSAdsModule.java
index e14ee68..9c7b208 100644
--- a/android/src/main/java/com/adversport/rnaps/RNAPSAdsModule.java
+++ b/android/src/main/java/com/adversport/rnaps/RNAPSAdsModule.java
@@ -18,6 +18,7 @@
* along with Foobar. If not, see .
*/
+import com.amazon.aps.ads.common.ApsExternalUserId;
import com.amazon.device.ads.AdRegistration;
import com.amazon.device.ads.DTBAdNetwork;
import com.amazon.device.ads.DTBAdNetworkInfo;
@@ -25,6 +26,9 @@
import com.facebook.react.bridge.*;
import com.facebook.react.module.annotations.ReactModule;
import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
@ReactModule(name = RNAPSAdsModule.MODULE_NAME)
public class RNAPSAdsModule extends ReactContextBaseJavaModule {
@@ -153,4 +157,72 @@ public void addCustomAttribute(String key, String value) {
public void removeCustomAttribute(String key) {
AdRegistration.removeCustomAttribute(key);
}
+
+ /**
+ * Third-party identifiers (ID5, LiveRamp...), forwarded by Amazon to the TAM/UAM bidders the
+ * publisher has enabled. Set once per user session; call again when an id changes. Passing an
+ * empty array clears them, which is what a consent withdrawal must do.
+ */
+ @ReactMethod
+ public void setExternalUserIds(ReadableArray externalUserIds) {
+ List ids = new ArrayList<>();
+
+ for (int i = 0; i < externalUserIds.size(); i++) {
+ ReadableMap entry = externalUserIds.getMap(i);
+ if (entry == null || !entry.hasKey("source")) {
+ continue;
+ }
+ String source = entry.getString("source");
+ ReadableArray uids = entry.hasKey("uids") ? entry.getArray("uids") : null;
+ if (source == null || uids == null || uids.size() == 0) {
+ continue;
+ }
+
+ ApsExternalUserId.Builder builder = ApsExternalUserId.Companion.builder().addSource(source);
+ boolean hasUid = false;
+
+ for (int j = 0; j < uids.size(); j++) {
+ ReadableMap uid = uids.getMap(j);
+ if (uid == null || !uid.hasKey("id")) {
+ continue;
+ }
+ String id = uid.getString("id");
+ if (id == null) {
+ continue;
+ }
+ Integer atype = uid.hasKey("atype") ? uid.getInt("atype") : null;
+ builder.addUniqueId(id, atype, readStringMap(uid, "ext"));
+ hasUid = true;
+ }
+
+ if (hasUid) {
+ ids.add(builder.build());
+ }
+ }
+
+ AdRegistration.setExternalUserIds(ids);
+ }
+
+ /**
+ * Reads a nested string map, skipping any non-string value rather than failing the whole call.
+ */
+ private static Map readStringMap(ReadableMap parent, String key) {
+ if (!parent.hasKey(key)) {
+ return null;
+ }
+ ReadableMap map = parent.getMap(key);
+ if (map == null) {
+ return null;
+ }
+ Map out = new HashMap<>();
+ ReadableMapKeySetIterator iterator = map.keySetIterator();
+ while (iterator.hasNextKey()) {
+ String k = iterator.nextKey();
+ String v = map.getString(k);
+ if (v != null) {
+ out.put(k, v);
+ }
+ }
+ return out.isEmpty() ? null : out;
+ }
}
diff --git a/ios/RNAPS/RNAPSAdsModule.m b/ios/RNAPS/RNAPSAdsModule.m
index 3b37408..b3f37af 100644
--- a/ios/RNAPS/RNAPSAdsModule.m
+++ b/ios/RNAPS/RNAPSAdsModule.m
@@ -39,4 +39,6 @@ @interface RCT_EXTERN_MODULE (RNAPSAdsModule, NSObject)
RCT_EXTERN_METHOD(removeCustomAttribute : (nonnull NSString *)key)
+RCT_EXTERN_METHOD(setExternalUserIds : (nonnull NSArray *)externalUserIds)
+
@end
diff --git a/ios/RNAPS/RNAPSAdsModule.swift b/ios/RNAPS/RNAPSAdsModule.swift
index c195224..9ae2f09 100644
--- a/ios/RNAPS/RNAPSAdsModule.swift
+++ b/ios/RNAPS/RNAPSAdsModule.swift
@@ -98,6 +98,18 @@ class RNAPSAdsModule: NSObject {
DTBAds.sharedInstance().testMode = enabled
}
+ // The setters below still go through the deprecated DTBAds singleton on purpose.
+ //
+ // Their replacements are not setters at all: APS 5.6.4 moved testMode, useGeolocation,
+ // mraidPolicy and mraidSupportedVersions onto APSInitConfig, an object handed once to
+ // +[APS initializeWithAppKey:config:completion:]. Adopting them means reshaping this
+ // bridge's contract — JS calls initialize() first and these setters afterwards, which
+ // the config model cannot express — so it belongs in its own change, not here.
+ //
+ // Do not trust the deprecation text: it points at +[APS setTestMode:] and friends, which
+ // are declared nowhere in APS.h (the selectors do exist in the binary). Read APSInitConfig.h.
+ //
+ // removeCustomAttribute has no replacement at all, on APS or on APSInitConfig.
@objc(setUseGeoLocation:)
func setUseGeoLocation(enabled: Bool) -> Void {
DTBAds.sharedInstance().useGeoLocation = enabled
@@ -105,7 +117,7 @@ class RNAPSAdsModule: NSObject {
@objc(addCustomAttribute:value:)
func addCustomAttribute(key: String, value: String) {
- DTBAds.sharedInstance().addCustomAttribute(key, value: value)
+ APS.setCustomAttribute(value, forKey: key)
}
@objc(removeCustomAttribute:)
@@ -113,4 +125,26 @@ class RNAPSAdsModule: NSObject {
DTBAds.sharedInstance().removeCustomAttribute(key)
}
+ // Third-party identifiers (ID5, LiveRamp...), forwarded by Amazon to the TAM/UAM bidders
+ // the publisher has enabled. Set once per user session; call again when an id changes.
+ // Passing an empty array clears them, which is what a consent withdrawal must do.
+ @objc(setExternalUserIds:)
+ func setExternalUserIds(externalUserIds: [[String: Any]]) {
+ let ids: [APSExternalUserId] = externalUserIds.compactMap { entry in
+ guard let source = entry["source"] as? String,
+ let uids = entry["uids"] as? [[String: Any]], !uids.isEmpty else {
+ return nil
+ }
+ // Swift imports the +builder factory as an initializer, so `.builder()` does not exist here.
+ let builder = APSExternalUserIdBuilder()
+ _ = builder.addSource(source)
+ for uid in uids {
+ guard let id = uid["id"] as? String else { continue }
+ _ = builder.addUniqueId(id, atype: uid["atype"] as? NSNumber, ext: uid["ext"] as? [String: String])
+ }
+ return builder.build()
+ }
+ APS.setExternalUserIds(ids)
+ }
+
}
diff --git a/package.json b/package.json
index 50e4f14..27b77ad 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "react-native-aps",
"packageManager": "yarn@4.3.1",
- "version": "2.3.0",
+ "version": "2.4.0",
"author": "AdversportTeam (https://github.com/AdversportTeam)",
"contributors": [
"Jay Kim (https://github.com/wjaykim)"
diff --git a/src/APSAds.ts b/src/APSAds.ts
index 77f668d..a94c2a5 100644
--- a/src/APSAds.ts
+++ b/src/APSAds.ts
@@ -22,6 +22,10 @@ import {
validateAdNetworkInfo,
} from './types/AdNetworkInfo';
import { isMRAIDPolicy, MRAIDPolicy } from './types/MRAIDPolicy';
+import {
+ type ExternalUserId,
+ validateExternalUserIds,
+} from './types/ExternalUserId';
export class APSAds {
private static _nativeModule = AdsModule;
@@ -129,4 +133,22 @@ export class APSAds {
}
return this._nativeModule.removeCustomAttribute(key);
}
+
+ /**
+ * Sets the third-party user identifiers (ID5, LiveRamp...) sent with every bid request.
+ * Amazon forwards them to the TAM/UAM bidders the publisher has enabled.
+ *
+ * Set once per user session, and call again whenever an id changes. Pass an empty array to
+ * clear them, which is what a consent withdrawal must do.
+ */
+ static setExternalUserIds(externalUserIds: ExternalUserId[]): void {
+ try {
+ validateExternalUserIds(externalUserIds);
+ } catch (e) {
+ if (e instanceof Error) {
+ throw new Error(`APSAds.setExternalUserIds(*) ${e.message}`);
+ }
+ }
+ return this._nativeModule.setExternalUserIds(externalUserIds);
+ }
}
diff --git a/src/__tests__/APSAds.test.ts b/src/__tests__/APSAds.test.ts
index 4b2995a..431456e 100644
--- a/src/__tests__/APSAds.test.ts
+++ b/src/__tests__/APSAds.test.ts
@@ -1,4 +1,5 @@
import { APSAds } from '../APSAds';
+import AdsModule from '../internal/AdsModule';
import { AdNetwork, MRAIDPolicy } from '../types';
jest.mock('../internal/AdsModule');
@@ -108,6 +109,53 @@ describe('APSAds', function () {
expect(APSAds.addCustomAttribute('key', 'value')).toBeUndefined();
});
});
+ describe('setExternalUserIds', function () {
+ const anId5Eid = {
+ source: 'id5-sync.com',
+ uids: [{ id: 'ID5*abc', atype: 2, ext: { linkType: '2' } }],
+ };
+
+ it('throws if externalUserIds is not an array', function () {
+ // @ts-ignore
+ expect(() => APSAds.setExternalUserIds('nope')).toThrowError(
+ "APSAds.setExternalUserIds(*) 'externalUserIds' expected an array value"
+ );
+ });
+ it('throws if source is missing', function () {
+ expect(() =>
+ // @ts-ignore
+ APSAds.setExternalUserIds([{ uids: [{ id: 'a' }] }])
+ ).toThrowError(
+ "APSAds.setExternalUserIds(*) 'externalUserIds[0].source' expected a string value"
+ );
+ });
+ it('throws if uids is empty', function () {
+ expect(() =>
+ APSAds.setExternalUserIds([{ source: 'id5-sync.com', uids: [] }])
+ ).toThrowError(
+ "APSAds.setExternalUserIds(*) 'externalUserIds[0].uids' expected a non-empty array value"
+ );
+ });
+ it('throws if a uid has no id', function () {
+ expect(() =>
+ APSAds.setExternalUserIds([
+ // @ts-ignore
+ { source: 'id5-sync.com', uids: [{ atype: 2 }] },
+ ])
+ ).toThrowError(
+ "APSAds.setExternalUserIds(*) 'externalUserIds[0].uids[0].id' expected a string value"
+ );
+ });
+ it('accepts an eid as returned by the issuer, untouched', function () {
+ expect(APSAds.setExternalUserIds([anId5Eid])).toBeUndefined();
+ expect(AdsModule.setExternalUserIds).toHaveBeenCalledWith([anId5Eid]);
+ });
+ it('accepts an empty array, which clears the ids', function () {
+ expect(APSAds.setExternalUserIds([])).toBeUndefined();
+ expect(AdsModule.setExternalUserIds).toHaveBeenCalledWith([]);
+ });
+ });
+
describe('removeCustomAttribute', function () {
it('throws if key is invalid', function () {
// @ts-ignore
diff --git a/src/internal/__mocks__/AdsModule.ts b/src/internal/__mocks__/AdsModule.ts
index 577ecf0..205ed84 100644
--- a/src/internal/__mocks__/AdsModule.ts
+++ b/src/internal/__mocks__/AdsModule.ts
@@ -7,6 +7,7 @@ const AdsModule = {
setUseGeoLocation: jest.fn(),
addCustomAttribute: jest.fn(),
removeCustomAttribute: jest.fn(),
+ setExternalUserIds: jest.fn(),
};
export default AdsModule;
diff --git a/src/turbomodules/NativeRNAPSAdsModule.ts b/src/turbomodules/NativeRNAPSAdsModule.ts
index bbcbf61..98808a9 100644
--- a/src/turbomodules/NativeRNAPSAdsModule.ts
+++ b/src/turbomodules/NativeRNAPSAdsModule.ts
@@ -45,6 +45,8 @@ export interface Spec extends TurboModule {
addCustomAttribute: (key: string, value: string) => void;
removeCustomAttribute: (key: string) => void;
+
+ setExternalUserIds: (externalUserIds: Object[]) => void;
}
export default TurboModuleRegistry.getEnforcing('RNAPSAdsModule');
diff --git a/src/types/ExternalUserId.ts b/src/types/ExternalUserId.ts
new file mode 100644
index 0000000..0633126
--- /dev/null
+++ b/src/types/ExternalUserId.ts
@@ -0,0 +1,44 @@
+/**
+ * A third-party user identifier, in the OpenRTB `user.eids` shape.
+ *
+ * Amazon forwards these to the TAM/UAM bidders the publisher has enabled. The object is
+ * meant to be passed through untouched from whoever issued it (ID5, LiveRamp...): `atype`
+ * and `ext` are opaque to us and must not be rebuilt.
+ *
+ * @see https://resources.ams.amazon.com/s/article/external-user-ids
+ */
+export interface ExternalUserId {
+ /** Identifier domain, e.g. `id5-sync.com`. */
+ source: string;
+ uids: Array<{
+ id: string;
+ /** OpenRTB agent type. 1 = probabilistic, 2 = device advertising id. */
+ atype?: number;
+ ext?: Record;
+ }>;
+}
+
+export function validateExternalUserIds(externalUserIds: ExternalUserId[]) {
+ if (!Array.isArray(externalUserIds)) {
+ throw new Error("'externalUserIds' expected an array value");
+ }
+ externalUserIds.forEach((externalUserId, index) => {
+ if (typeof externalUserId?.source !== 'string') {
+ throw new Error(
+ `'externalUserIds[${index}].source' expected a string value`
+ );
+ }
+ if (!Array.isArray(externalUserId.uids) || !externalUserId.uids.length) {
+ throw new Error(
+ `'externalUserIds[${index}].uids' expected a non-empty array value`
+ );
+ }
+ externalUserId.uids.forEach((uid, uidIndex) => {
+ if (typeof uid?.id !== 'string') {
+ throw new Error(
+ `'externalUserIds[${index}].uids[${uidIndex}].id' expected a string value`
+ );
+ }
+ });
+ });
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index 6841042..b1045cd 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -23,3 +23,4 @@ export { AdNetwork } from './AdNetwork';
export type { AdNetworkInfo } from './AdNetworkInfo';
export { AdType } from './AdType';
export { MRAIDPolicy } from './MRAIDPolicy';
+export * from './ExternalUserId';