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
29 changes: 29 additions & 0 deletions .commitmsg
Original file line number Diff line number Diff line change
@@ -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.
39 changes: 39 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

Expand Down
19 changes: 19 additions & 0 deletions ios/RNAPS/RNAPSAdLoaderModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
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.2.1",
"version": "2.3.0",
"author": "AdversportTeam <rm@adversport.com> (https://github.com/AdversportTeam)",
"contributors": [
"Jay Kim <me@wjay.kim> (https://github.com/wjaykim)"
Expand Down
44 changes: 44 additions & 0 deletions src/__tests__/AdLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 20 additions & 0 deletions src/types/AdLoaderOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand Down
Loading