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
2 changes: 1 addition & 1 deletion RNAmazonPublisherServices.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
72 changes: 72 additions & 0 deletions android/src/main/java/com/adversport/rnaps/RNAPSAdsModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@
* along with Foobar. If not, see <https://www.gnu.org/licenses/>.
*/

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;
import com.amazon.device.ads.MRAIDPolicy;
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 {
Expand Down Expand Up @@ -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<ApsExternalUserId> 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<String, String> readStringMap(ReadableMap parent, String key) {
if (!parent.hasKey(key)) {
return null;
}
ReadableMap map = parent.getMap(key);
if (map == null) {
return null;
}
Map<String, String> 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;
}
}
2 changes: 2 additions & 0 deletions ios/RNAPS/RNAPSAdsModule.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 35 additions & 1 deletion ios/RNAPS/RNAPSAdsModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,19 +98,53 @@ 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
}

@objc(addCustomAttribute:value:)
func addCustomAttribute(key: String, value: String) {
DTBAds.sharedInstance().addCustomAttribute(key, value: value)
APS.setCustomAttribute(value, forKey: key)
}

@objc(removeCustomAttribute:)
func removeCustomAttribute(key: String) {
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)
}

}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "react-native-aps",
"packageManager": "yarn@4.3.1",
"version": "2.3.0",
"version": "2.4.0",
"author": "AdversportTeam <rm@adversport.com> (https://github.com/AdversportTeam)",
"contributors": [
"Jay Kim <me@wjay.kim> (https://github.com/wjaykim)"
Expand Down
22 changes: 22 additions & 0 deletions src/APSAds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
48 changes: 48 additions & 0 deletions src/__tests__/APSAds.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { APSAds } from '../APSAds';
import AdsModule from '../internal/AdsModule';
import { AdNetwork, MRAIDPolicy } from '../types';

jest.mock('../internal/AdsModule');
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/internal/__mocks__/AdsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const AdsModule = {
setUseGeoLocation: jest.fn(),
addCustomAttribute: jest.fn(),
removeCustomAttribute: jest.fn(),
setExternalUserIds: jest.fn(),
};

export default AdsModule;
2 changes: 2 additions & 0 deletions src/turbomodules/NativeRNAPSAdsModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Spec>('RNAPSAdsModule');
44 changes: 44 additions & 0 deletions src/types/ExternalUserId.ts
Original file line number Diff line number Diff line change
@@ -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<string, string>;
}>;
}

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`
);
}
});
});
}
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export { AdNetwork } from './AdNetwork';
export type { AdNetworkInfo } from './AdNetworkInfo';
export { AdType } from './AdType';
export { MRAIDPolicy } from './MRAIDPolicy';
export * from './ExternalUserId';
Loading