From f9805c81fe5329856726332c5be9fbf9e0bf70e7 Mon Sep 17 00:00:00 2001 From: Jerome Bonfort Date: Wed, 9 Sep 2026 17:22:43 +0200 Subject: [PATCH 1/2] feat: support instream video ad requests (Amazon TAM) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rescues work that only ever existed as a patch file in the app repo (footmercatomobile2, branch feat/prebid-video-native-bridge, June 2026). It was never opened here as a branch or a PR, so it was one branch deletion away from being lost. AdLoader.createVideoAdLoader({ slotUUID, playerWidth, playerHeight }) issues an instream video bid request. Defaults to a 640x480 player, matching the web adManager player size. The non-obvious part is Android, and it is why the Java file gets a helper extracted: banner and interstitial read their targeting from getDefaultDisplayAdsRequestCustomParams(), a Map>, while instream video exposes a flat Map through a different accessor. Reading the display one for a video response silently drops every amzn* keyword. Merged on top of the contentUrl work from #16: both touch the loadAd call site, so the callback now carries the video flag while keeping the note explaining why options.contentUrl has no Android equivalent. Only src/, android/ and ios/ are carried over. The original patch also touched lib/typescript, but lib/ is generated by bob at publish time and is not tracked here — which also fixes a latent defect of that patch, whose lib/commonjs and lib/module were never updated, leaving the API typed but absent at runtime for any consumer resolving through main. Adds 5 tests, which the original patch had none of. 41 tests green. Co-Authored-By: Claude Opus 5 --- .../adversport/rnaps/RNAPSAdLoaderModule.java | 60 ++++++++++++------- ios/RNAPS/RNAPSAdLoaderModule.swift | 10 ++++ src/AdLoader.ts | 17 ++++++ src/__tests__/AdLoader.test.ts | 47 +++++++++++++++ src/types/AdLoaderOptions.ts | 26 ++++++++ src/types/AdType.ts | 1 + src/types/index.ts | 6 +- 7 files changed, 143 insertions(+), 24 deletions(-) diff --git a/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java b/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java index 65e1ffb..4dbcaf5 100644 --- a/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java +++ b/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java @@ -32,6 +32,7 @@ public class RNAPSAdLoaderModule extends ReactContextBaseJavaModule { public static final String MODULE_NAME = "RNAPSAdLoaderModule"; public static final String AD_TYPE_BANNER = "banner"; public static final String AD_TYPE_INTERSTITIAL = "interstitial"; + public static final String AD_TYPE_VIDEO = "video"; public static final String EVENT_SUCCESS = "onSuccess"; public static final String EVENT_FAILURE = "onFailure"; @@ -64,10 +65,12 @@ public void removeListeners(double count) { private class AdCallback implements DTBAdCallback { private final int loaderId; + private final boolean isVideo; private Promise promise; - public AdCallback(int loaderId, Promise promise) { + public AdCallback(int loaderId, boolean isVideo, Promise promise) { this.loaderId = loaderId; + this.isVideo = isVideo; this.promise = promise; } @@ -143,29 +146,32 @@ public void onFailure(AdError adError) { @Override public void onSuccess(DTBAdResponse response) { - WritableMap responseMap = Arguments.createMap(); - Map> customParams = response.getDefaultDisplayAdsRequestCustomParams(); - - for (Map.Entry> entry : customParams.entrySet()) { - List values = entry.getValue(); - StringBuilder valueBuilder = new StringBuilder(); - for (int i = 0; i < values.size(); i++) { - valueBuilder.append(values.get(i)); - if (i < values.size() - 1) { - valueBuilder.append(","); - } - } - responseMap.putString(entry.getKey(), valueBuilder.toString()); - } - WritableMap payload = Arguments.createMap(); payload.putInt("loaderId", loaderId); - payload.putMap("response", responseMap); + payload.putMap("response", buildTargetingMap(response)); sendEvent(EVENT_SUCCESS, payload); if (promise != null) { - // Créer une copie pour promise.resolve car responseMap est déjà utilisé - WritableMap responseCopy = Arguments.createMap(); + // Fresh copy — the event payload above already consumed a WritableMap. + promise.resolve(buildTargetingMap(response)); + promise = null; + } + } + + // Banner/interstitial expose Map> (comma-joined here); + // instream video exposes a flat Map via a distinct SDK + // accessor — pick the right one so the amzn* video keywords aren't dropped. + private WritableMap buildTargetingMap(DTBAdResponse response) { + WritableMap map = Arguments.createMap(); + if (isVideo) { + Map videoParams = response.getDefaultVideoAdsRequestCustomParams(); + if (videoParams != null) { + for (Map.Entry entry : videoParams.entrySet()) { + map.putString(entry.getKey(), entry.getValue()); + } + } + } else { + Map> customParams = response.getDefaultDisplayAdsRequestCustomParams(); for (Map.Entry> entry : customParams.entrySet()) { List values = entry.getValue(); StringBuilder valueBuilder = new StringBuilder(); @@ -175,11 +181,10 @@ public void onSuccess(DTBAdResponse response) { valueBuilder.append(","); } } - responseCopy.putString(entry.getKey(), valueBuilder.toString()); + map.putString(entry.getKey(), valueBuilder.toString()); } - promise.resolve(responseCopy); - promise = null; } + return map; } } @@ -224,6 +229,14 @@ public void loadAd(int loaderId, String adType, ReadableMap options, Promise pro case AD_TYPE_INTERSTITIAL: adSize = new DTBAdSize.DTBInterstitialAdSize(slotUUID); break; + case AD_TYPE_VIDEO: { + // Instream video bid request — player size mirrors the web adManager + // (assets/js/adManager.js playerSize [640, 480]). + int playerWidth = options.hasKey("playerWidth") ? options.getInt("playerWidth") : 640; + int playerHeight = options.hasKey("playerHeight") ? options.getInt("playerHeight") : 480; + adSize = new DTBAdSize.DTBVideo(playerWidth, playerHeight, slotUUID); + break; + } default: promise.reject("invalid_ad_type", "unsupported ad type: " + adType); return; @@ -254,6 +267,7 @@ 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 @@ -266,7 +280,7 @@ public void loadAd(int loaderId, String adType, ReadableMap options, Promise pro // 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)); + adLoader.loadAd(new AdCallback(loaderId, AD_TYPE_VIDEO.equals(adType), promise)); } @ReactMethod diff --git a/ios/RNAPS/RNAPSAdLoaderModule.swift b/ios/RNAPS/RNAPSAdLoaderModule.swift index a0329eb..9a84dcd 100644 --- a/ios/RNAPS/RNAPSAdLoaderModule.swift +++ b/ios/RNAPS/RNAPSAdLoaderModule.swift @@ -23,6 +23,7 @@ import Foundation class RNAPSAdLoaderModule: RCTEventEmitter { static let AD_TYPE_BANNER = "banner" static let AD_TYPE_INTERSTITIAL = "interstitial" + static let AD_TYPE_VIDEO = "video" static let EVENT_SUCCESS = "onSuccess" static let EVENT_FAILURE = "onFailure" static let ERROR_DOMAIN = "RNAPS" @@ -171,6 +172,15 @@ class RNAPSAdLoaderModule: RCTEventEmitter { case RNAPSAdLoaderModule.AD_TYPE_INTERSTITIAL: adSize = DTBAdSize(interstitialAdSizeWithSlotUUID: slotUUID) break + case RNAPSAdLoaderModule.AD_TYPE_VIDEO: + // Instream video bid request — player size mirrors the web adManager + // (assets/js/adManager.js playerSize [640, 480]). Same legacy DTB path the + // banner/interstitial cases use; DTBAdResponse.customTargeting() (in the + // shared onSuccess) returns the amzn* keywords for the video slot. + let playerWidth = options["playerWidth"] as? Int ?? 640 + let playerHeight = options["playerHeight"] as? Int ?? 480 + adSize = DTBAdSize(videoAdSizeWithPlayerWidth: playerWidth, height: playerHeight, andSlotUUID: slotUUID) + break default: // Original code just returned, no error reject return diff --git a/src/AdLoader.ts b/src/AdLoader.ts index dbdf24d..9703a35 100644 --- a/src/AdLoader.ts +++ b/src/AdLoader.ts @@ -24,8 +24,10 @@ import type { AdLoaderListener } from './types/AdLoaderListener'; import { type AdLoaderOptions, type BannerAdLoaderOptions, + type VideoAdLoaderOptions, validateAdLoaderOptions, validateBannerAdLoaderOptions, + validateVideoAdLoaderOptions, } from './types/AdLoaderOptions'; import { AdType } from './types'; @@ -74,6 +76,21 @@ export class AdLoader { return adLoader; } + /** + * Create a video (instream) AdLoader instance. + */ + static createVideoAdLoader(adLoaderOptions: VideoAdLoaderOptions) { + try { + validateVideoAdLoaderOptions(adLoaderOptions); + } catch (e) { + if (e instanceof Error) { + throw new Error(`AdLoader.createVideoAdLoader(*) ${e.message}`); + } + } + const adLoader = new AdLoader(AdType.VIDEO, adLoaderOptions); + return adLoader; + } + /** * Add a listener for the bid response. Supported events are: * - `AdLoaderEvent.SUCCESS` diff --git a/src/__tests__/AdLoader.test.ts b/src/__tests__/AdLoader.test.ts index 8629bf7..e67c9c8 100644 --- a/src/__tests__/AdLoader.test.ts +++ b/src/__tests__/AdLoader.test.ts @@ -55,6 +55,53 @@ describe('AdLoader', function () { }); }); + describe('createVideoAdLoader', function () { + it('throws if adLoaderOptions is invalid', function () { + expect(() => + // @ts-ignore + AdLoader.createVideoAdLoader(123) + ).toThrowError( + "AdLoader.createVideoAdLoader(*) 'adLoaderOptions' expected an object value" + ); + }); + it('throws if slotUUID is invalid', function () { + expect(() => + // @ts-ignore + AdLoader.createVideoAdLoader({ slotUUID: 123 }) + ).toThrowError( + "AdLoader.createVideoAdLoader(*) 'adLoaderOptions.slotUUID' expected a string value" + ); + }); + it('throws if the player size is missing', function () { + expect(() => + // @ts-ignore + AdLoader.createVideoAdLoader({ slotUUID: 'uuid' }) + ).toThrowError( + "AdLoader.createVideoAdLoader(*) 'adLoaderOptions.playerWidth' and 'adLoaderOptions.playerHeight' expected number values" + ); + }); + it('throws if the player size is not numeric', function () { + expect(() => + AdLoader.createVideoAdLoader({ + slotUUID: 'uuid', + // @ts-ignore + playerWidth: '640', + playerHeight: 480, + }) + ).toThrowError( + "AdLoader.createVideoAdLoader(*) 'adLoaderOptions.playerWidth' and 'adLoaderOptions.playerHeight' expected number values" + ); + }); + it('returns an AdLoader for a valid player size', function () { + const adLoader = AdLoader.createVideoAdLoader({ + slotUUID: 'uuid', + playerWidth: 640, + playerHeight: 480, + }); + expect(adLoader).toBeInstanceOf(AdLoader); + }); + }); + describe('contentUrl', function () { it('throws if contentUrl is not a string', function () { expect(() => diff --git a/src/types/AdLoaderOptions.ts b/src/types/AdLoaderOptions.ts index 09db851..d0b8fbf 100644 --- a/src/types/AdLoaderOptions.ts +++ b/src/types/AdLoaderOptions.ts @@ -83,3 +83,29 @@ export function validateBannerAdLoaderOptions( throw new Error("'adLoaderOptions.size' expected a valid size string"); } } + +export interface VideoAdLoaderOptions extends AdLoaderOptions { + /** + * The width of the video player. Required for video (instream) ad slots. + */ + playerWidth: number; + + /** + * The height of the video player. Required for video (instream) ad slots. + */ + playerHeight: number; +} + +export function validateVideoAdLoaderOptions( + adLoaderOptions: VideoAdLoaderOptions +) { + validateAdLoaderOptions(adLoaderOptions); + if ( + typeof adLoaderOptions.playerWidth !== 'number' || + typeof adLoaderOptions.playerHeight !== 'number' + ) { + throw new Error( + "'adLoaderOptions.playerWidth' and 'adLoaderOptions.playerHeight' expected number values" + ); + } +} diff --git a/src/types/AdType.ts b/src/types/AdType.ts index 1e960db..ab52482 100644 --- a/src/types/AdType.ts +++ b/src/types/AdType.ts @@ -19,6 +19,7 @@ export enum AdType { BANNER = 'banner', INTERSTITIAL = 'interstitial', + VIDEO = 'video', } export function isAdType(value: any): value is AdType { diff --git a/src/types/index.ts b/src/types/index.ts index 6841042..00c503b 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -18,7 +18,11 @@ export { AdLoaderEvent } from './AdLoaderEvent'; export type { AdLoaderListener } from './AdLoaderListener'; -export type { AdLoaderOptions, BannerAdLoaderOptions } from './AdLoaderOptions'; +export type { + AdLoaderOptions, + BannerAdLoaderOptions, + VideoAdLoaderOptions, +} from './AdLoaderOptions'; export { AdNetwork } from './AdNetwork'; export type { AdNetworkInfo } from './AdNetworkInfo'; export { AdType } from './AdType'; From a170d89d18c53c846f79cb1a3a8fb75b1683c7b1 Mon Sep 17 00:00:00 2001 From: Jerome Bonfort Date: Wed, 9 Sep 2026 18:47:35 +0200 Subject: [PATCH 2/2] style: apply google-java-format to the video case block Same miss as on the SDK branch: I ran lint:js:check locally instead of lint:code, which the CI uses and which chains the Java and Objective-C formatters. The braced switch case carried over from the app patch is not the style google-java-format wants. Co-Authored-By: Claude Opus 5 --- .../adversport/rnaps/RNAPSAdLoaderModule.java | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java b/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java index 4dbcaf5..2b5d37c 100644 --- a/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java +++ b/android/src/main/java/com/adversport/rnaps/RNAPSAdLoaderModule.java @@ -229,14 +229,15 @@ public void loadAd(int loaderId, String adType, ReadableMap options, Promise pro case AD_TYPE_INTERSTITIAL: adSize = new DTBAdSize.DTBInterstitialAdSize(slotUUID); break; - case AD_TYPE_VIDEO: { - // Instream video bid request — player size mirrors the web adManager - // (assets/js/adManager.js playerSize [640, 480]). - int playerWidth = options.hasKey("playerWidth") ? options.getInt("playerWidth") : 640; - int playerHeight = options.hasKey("playerHeight") ? options.getInt("playerHeight") : 480; - adSize = new DTBAdSize.DTBVideo(playerWidth, playerHeight, slotUUID); - break; - } + case AD_TYPE_VIDEO: + { + // Instream video bid request — player size mirrors the web adManager + // (assets/js/adManager.js playerSize [640, 480]). + int playerWidth = options.hasKey("playerWidth") ? options.getInt("playerWidth") : 640; + int playerHeight = options.hasKey("playerHeight") ? options.getInt("playerHeight") : 480; + adSize = new DTBAdSize.DTBVideo(playerWidth, playerHeight, slotUUID); + break; + } default: promise.reject("invalid_ad_type", "unsupported ad type: " + adType); return;