From 62b44e84b6d44a24e0e9853306a079f272559f3a Mon Sep 17 00:00:00 2001 From: 0xRomain Date: Thu, 6 Aug 2026 11:31:17 +0200 Subject: [PATCH] feat: support Amazon DSP content URL on bid requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Amazon DSP now requires in-app bid requests to carry the public web URL of the content being viewed, so AmazonAdBot can crawl the page and verify what surrounds the ad. Inventory it cannot verify progressively loses eligibility with Amazon advertisers. Adds an optional `contentUrl` to AdLoaderOptions, applied per bid request. iOS: forwarded to +[APS setContentUrl:], called immediately before loadAd and inside the same native method. That setter is a class method, so the value is process-global; setting it there means another slot cannot overwrite it between the two, since React Native serialises module methods on a single queue. An empty value is skipped rather than forwarded — the SDK header states it throws an NSException in development builds otherwise. Android: ignored, and documented as such. The Android APS SDK (com.amazon.android:aps-sdk 11.1.1) exposes no equivalent of setContentUrl — verified against every public member of AdRegistration and DTBAdRequest. Rejecting the promise would break the bidding chain for callers that legitimately pass the option for iOS, so the option is accepted and dropped. Two known limitations are documented in the README and have been raised with Amazon APS: the Android gap above, and the fact that the iOS setter is sticky — it cannot be cleared, so the last URL set stays attached to later requests until another one replaces it. Validation rejects a non-string contentUrl, consistent with the other options. 36 tests green. --- .commitmsg | 29 ++++++++++++ README.md | 39 ++++++++++++++++ .../adversport/rnaps/RNAPSAdLoaderModule.java | 12 +++++ ios/RNAPS/RNAPSAdLoaderModule.swift | 19 ++++++++ package.json | 2 +- src/__tests__/AdLoader.test.ts | 44 +++++++++++++++++++ src/types/AdLoaderOptions.ts | 20 +++++++++ 7 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 .commitmsg diff --git a/.commitmsg b/.commitmsg new file mode 100644 index 0000000..eba5d2a --- /dev/null +++ b/.commitmsg @@ -0,0 +1,29 @@ +feat: support Amazon DSP content URL on bid requests + +Amazon DSP now requires in-app bid requests to carry the public web URL of the +content being viewed, so AmazonAdBot can crawl the page and verify what +surrounds the ad. Inventory it cannot verify progressively loses eligibility +with Amazon advertisers. + +Adds an optional `contentUrl` to AdLoaderOptions, applied per bid request. + +iOS: forwarded to +[APS setContentUrl:], called immediately before loadAd and +inside the same native method. That setter is a class method, so the value is +process-global; setting it there means another slot cannot overwrite it between +the two, since React Native serialises module methods on a single queue. An +empty value is skipped rather than forwarded — the SDK header states it throws +an NSException in development builds otherwise. + +Android: ignored, and documented as such. The Android APS SDK +(com.amazon.android:aps-sdk 11.1.1) exposes no equivalent of setContentUrl — +verified against every public member of AdRegistration and DTBAdRequest. +Rejecting the promise would break the bidding chain for callers that +legitimately pass the option for iOS, so the option is accepted and dropped. + +Two known limitations are documented in the README and have been raised with +Amazon APS: the Android gap above, and the fact that the iOS setter is sticky — +it cannot be cleared, so the last URL set stays attached to later requests until +another one replaces it. + +Validation rejects a non-string contentUrl, consistent with the other options. +36 tests green. diff --git a/README.md b/README.md index fc45104..d3b556c 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,45 @@ - [Guides](https://AdversportTeam.github.io/react-native-aps/docs/guides) - [API Reference](https://AdversportTeam.github.io/react-native-aps/docs/api) +## Content URL (Amazon DSP) + +Amazon DSP requires in-app bid requests to carry the public web URL of the +content the user is viewing, so that AmazonAdBot can crawl the page and verify +what surrounds the ad. Inventory it cannot verify progressively loses +eligibility with Amazon advertisers. + +Pass it per bid request through `contentUrl`: + +```ts +const adLoader = AdLoader.createBannerAdLoader({ + slotUUID: 'your-slot-uuid', + size: '320x50', + contentUrl: 'https://yourdomain.com/article/12345', +}); +``` + +The URL must match the in-app content — not a deep link, not a store URL, not +the app home page — and must be publicly reachable. Omit the option entirely +when you have no reliable URL: a missing field is neutral, a wrong one is an +active non-compliance signal. + +### Platform support + +| Platform | Behaviour | +| --- | --- | +| iOS | Applied through `+[APS setContentUrl:]`, immediately before `loadAd` and inside the same native call. | +| Android | **Ignored.** The Android APS SDK exposes no equivalent. | + +Two caveats worth knowing on iOS: + +- `+[APS setContentUrl:]` is a class method, so the value is process-global and + **sticky** — it cannot be cleared, and the last URL set stays attached to + subsequent requests until another one replaces it. +- The SDK throws an `NSException` in development builds when given an empty or + nil value, so an empty `contentUrl` is skipped rather than forwarded. + +Both the Android gap and the iOS stickiness have been raised with Amazon APS. + ## Contributing See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. diff --git a/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java b/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java index 7412c69..65e1ffb 100644 --- a/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java +++ b/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java @@ -254,6 +254,18 @@ public void loadAd(int loaderId, String adType, ReadableMap options, Promise pro } adLoaders.put(loaderId, adLoader); + // 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 + // 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. + // + // Ignoring it is deliberate: rejecting the promise would break the whole + // bidding chain for callers that legitimately pass the option for iOS. The + // limitation has been raised with Amazon APS; revisit when they ship an API. + adLoader.loadAd(new AdCallback(loaderId, promise)); } diff --git a/ios/RNAPS/RNAPSAdLoaderModule.swift b/ios/RNAPS/RNAPSAdLoaderModule.swift index 6d19ec9..a0329eb 100644 --- a/ios/RNAPS/RNAPSAdLoaderModule.swift +++ b/ios/RNAPS/RNAPSAdLoaderModule.swift @@ -192,6 +192,25 @@ class RNAPSAdLoaderModule: RCTEventEmitter { adLoader.setAutoRefresh(refreshInterval) } + // Amazon DSP requires the public web URL of the content being viewed, so + // that AmazonAdBot can crawl it and verify the surroundings of the ad. + // + // +[APS setContentUrl:] is a CLASS method, i.e. process-global state. We set + // it here, immediately before loadAd and inside the same native call, so it + // cannot be overwritten by another slot between the two — React Native + // serialises module methods on a single queue. + // + // Empty or nil is skipped on purpose: the SDK header states it "will throw + // an NSException in development" in that case. Callers with no reliable URL + // simply omit the option. + // + // Known limitation: the setter is sticky and cannot be cleared, so the last + // URL set stays attached to subsequent requests until another one replaces + // it. Raised with Amazon APS. + if let contentUrl = options["contentUrl"] as? String, !contentUrl.isEmpty { + APS.setContentUrl(contentUrl) + } + adLoaders.updateValue(adLoader, forKey: loaderId) adLoader.loadAd(AdLoadCallback(adLoaderModule: self, loaderId: loaderId, resolve: resolve, reject: reject)) } diff --git a/package.json b/package.json index e2dbe3d..50e4f14 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-native-aps", "packageManager": "yarn@4.3.1", - "version": "2.2.1", + "version": "2.3.0", "author": "AdversportTeam (https://github.com/AdversportTeam)", "contributors": [ "Jay Kim (https://github.com/wjaykim)" diff --git a/src/__tests__/AdLoader.test.ts b/src/__tests__/AdLoader.test.ts index f6d6467..8629bf7 100644 --- a/src/__tests__/AdLoader.test.ts +++ b/src/__tests__/AdLoader.test.ts @@ -55,6 +55,50 @@ describe('AdLoader', function () { }); }); + describe('contentUrl', function () { + it('throws if contentUrl is not a string', function () { + expect(() => + AdLoader.createBannerAdLoader({ + slotUUID: 'uuid', + size: '320x50', + // @ts-ignore + contentUrl: 123, + }) + ).toThrowError( + "AdLoader.createBannerAdLoader(*) 'adLoaderOptions.contentUrl' expected a string value" + ); + }); + + it('accepts a valid contentUrl and keeps it on the loader options', function () { + const adLoader = AdLoader.createBannerAdLoader({ + slotUUID: 'uuid', + size: '320x50', + contentUrl: 'https://www.example.com/article/12345', + }); + expect(adLoader.adLoaderOptions.contentUrl).toBe( + 'https://www.example.com/article/12345' + ); + }); + + it('is optional — omitting it stays valid', function () { + const adLoader = AdLoader.createBannerAdLoader({ + slotUUID: 'uuid', + size: '320x50', + }); + expect(adLoader.adLoaderOptions.contentUrl).toBeUndefined(); + }); + + it('is accepted on interstitial loaders too', function () { + const adLoader = AdLoader.createInterstitialAdLoader({ + slotUUID: 'uuid', + contentUrl: 'https://www.example.com/article/12345', + }); + expect(adLoader.adLoaderOptions.contentUrl).toBe( + 'https://www.example.com/article/12345' + ); + }); + }); + describe('addListener', function () { const adLoader = AdLoader.createBannerAdLoader({ slotUUID: TestIds.APS_SLOT_BANNER_320x50, diff --git a/src/types/AdLoaderOptions.ts b/src/types/AdLoaderOptions.ts index ec13c6e..09db851 100644 --- a/src/types/AdLoaderOptions.ts +++ b/src/types/AdLoaderOptions.ts @@ -25,6 +25,20 @@ export interface AdLoaderOptions { * The optional custom targeting key value pairs for the bid request. */ customTargeting?: { [key: string]: string }; + /** + * The public web URL of the content the user is currently viewing. + * + * Amazon DSP requires in-app bid requests to carry it so that AmazonAdBot can + * crawl the page and verify the content surrounding the ad. Inventory it + * cannot verify progressively loses eligibility with Amazon advertisers. + * + * It must be the web URL matching the in-app content — not a deep link, not a + * store URL, not the app home page — and it must be publicly reachable. + * + * iOS only: the Android APS SDK exposes no equivalent, where the value is + * ignored. See the README for the current status of that limitation. + */ + contentUrl?: string; } export interface BannerAdLoaderOptions extends AdLoaderOptions { @@ -53,6 +67,12 @@ export function validateAdLoaderOptions(adLoaderOptions: AdLoaderOptions) { if (typeof adLoaderOptions.slotUUID !== 'string') { throw new Error("'adLoaderOptions.slotUUID' expected a string value"); } + if ( + adLoaderOptions.contentUrl !== undefined && + typeof adLoaderOptions.contentUrl !== 'string' + ) { + throw new Error("'adLoaderOptions.contentUrl' expected a string value"); + } } export function validateBannerAdLoaderOptions(