diff --git a/PrebidMobile/Swift/PrebidMobileRendering/AdTypes/AdView/AdConfiguration.swift b/PrebidMobile/Swift/PrebidMobileRendering/AdTypes/AdView/AdConfiguration.swift index 84c6e32ed..ea0a489bd 100644 --- a/PrebidMobile/Swift/PrebidMobileRendering/AdTypes/AdView/AdConfiguration.swift +++ b/PrebidMobile/Swift/PrebidMobileRendering/AdTypes/AdView/AdConfiguration.swift @@ -76,6 +76,21 @@ public class AdConfiguration: AutoRefreshCountConfig { Sets an ad unit as an rewarded */ public var isRewarded = false + + /// The fallback rewarded-ad completion time, in seconds, used when the bid does not provide + /// `completion.banner.time`, `completion.video.time`, or `completion.video.endcard.time`. + /// + /// The default value is 120 seconds. Assigning a negative value is ignored. + public var rewardedCompletionTime: TimeInterval { + get { _rewardedCompletionTime } + set { + if newValue >= 0 { + _rewardedCompletionTime = newValue + } else { + Log.warn("The rewarded completion time must not be negative.") + } + } + } /** Indicates whether the ad is built-in video e.g. 300x250. @@ -134,6 +149,7 @@ public class AdConfiguration: AutoRefreshCountConfig { // MARK: Private properties private var _autoRefreshDelay: TimeInterval? = PrebidConstants.AUTO_REFRESH_DELAY_DEFAULT + private var _rewardedCompletionTime = RewardedConfig.fallbackCompletionTime public var impORTBConfig: String? public var globalORTBConfig: String? diff --git a/PrebidMobile/Swift/PrebidMobileRendering/AdTypes/AdView/VideoControlsConfiguration.swift b/PrebidMobile/Swift/PrebidMobileRendering/AdTypes/AdView/VideoControlsConfiguration.swift index b0503f2c7..5ead69621 100644 --- a/PrebidMobile/Swift/PrebidMobileRendering/AdTypes/AdView/VideoControlsConfiguration.swift +++ b/PrebidMobile/Swift/PrebidMobileRendering/AdTypes/AdView/VideoControlsConfiguration.swift @@ -90,7 +90,7 @@ public class VideoControlsConfiguration: NSObject { /// This property indicates whether mute controls is visible on the screen. public var isSoundButtonVisible = false - + /// Use to initialize video controls with server values. public func initialize(with ortbAdConfiguration: ORTBAdConfiguration?) { @@ -136,4 +136,5 @@ public class VideoControlsConfiguration: NSObject { private var _skipButtonArea = PrebidConstants.BUTTON_AREA_DEFAULT.doubleValue private var _skipButtonPosition = Position.topLeft + } diff --git a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/GAM/RewardedAdUnit.swift b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/GAM/RewardedAdUnit.swift index 1ba71fdf0..8143da071 100644 --- a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/GAM/RewardedAdUnit.swift +++ b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/GAM/RewardedAdUnit.swift @@ -82,6 +82,13 @@ public class RewardedAdUnit: NSObject, BaseInterstitialAdUnitProtocol { get { adUnitConfig.adConfiguration.videoControlsConfig.isSoundButtonVisible } set { adUnitConfig.adConfiguration.videoControlsConfig.isSoundButtonVisible = newValue } } + + /// The fallback completion time, in seconds, used when the bid omits rewarded completion criteria. + /// The default value is 120 seconds. Assigning a negative value is ignored. + public var rewardedCompletionTime: TimeInterval { + get { adUnitConfig.adConfiguration.rewardedCompletionTime } + set { adUnitConfig.adConfiguration.rewardedCompletionTime = newValue } + } // MARK: Internal Properties diff --git a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/GAM/RewardedConfig.swift b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/GAM/RewardedConfig.swift index dafe5e19f..3efaa53e2 100644 --- a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/GAM/RewardedConfig.swift +++ b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/GAM/RewardedConfig.swift @@ -17,6 +17,8 @@ import Foundation @objc(PBMRewardedConfig) @objcMembers public class RewardedConfig: NSObject { + + static let fallbackCompletionTime: TimeInterval = 120 // MARK: - Reward @@ -67,15 +69,26 @@ public class RewardedConfig: NSObject { // MARK: - Default Values - /// The timeout duration for rewarded completion, measured in seconds. - public let defaultCompletionTime: NSNumber = 120 + /// The fallback rewarded completion time, measured in seconds. + public let defaultCompletionTime: NSNumber /// The playback event when the SDK should send a signal to the application that the user has earned the reward public let defaultVideoPlaybackEvent = "complete" private let ortbRewarded: ORTBRewardedConfiguration? - public init(ortbRewarded: ORTBRewardedConfiguration?) { + public convenience init(ortbRewarded: ORTBRewardedConfiguration?) { + self.init( + ortbRewarded: ortbRewarded, + defaultCompletionTime: Self.fallbackCompletionTime + ) + } + + public init( + ortbRewarded: ORTBRewardedConfiguration?, + defaultCompletionTime: TimeInterval + ) { self.ortbRewarded = ortbRewarded + self.defaultCompletionTime = NSNumber(value: defaultCompletionTime) } } diff --git a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/MediationAPI/MediationBaseInterstitialAdUnit.swift b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/MediationAPI/MediationBaseInterstitialAdUnit.swift index 4ee477aae..dd30064ea 100644 --- a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/MediationAPI/MediationBaseInterstitialAdUnit.swift +++ b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/MediationAPI/MediationBaseInterstitialAdUnit.swift @@ -45,7 +45,7 @@ public class MediationBaseInterstitialAdUnit : NSObject { get { adUnitConfig.adConfiguration.videoControlsConfig.isSoundButtonVisible } set { adUnitConfig.adConfiguration.videoControlsConfig.isSoundButtonVisible = newValue } } - + /// The area for the close button in the video ad. public var closeButtonArea: Double { get { adUnitConfig.adConfiguration.videoControlsConfig.closeButtonArea } diff --git a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/MediationAPI/MediationRewardedAdUnit.swift b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/MediationAPI/MediationRewardedAdUnit.swift index cec66e397..993e27ad0 100644 --- a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/MediationAPI/MediationRewardedAdUnit.swift +++ b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/Integrations/MediationAPI/MediationRewardedAdUnit.swift @@ -19,6 +19,13 @@ import Foundation /// This class is a part of Mediation API. @objcMembers public class MediationRewardedAdUnit : MediationBaseInterstitialAdUnit { + + /// The fallback completion time, in seconds, used when the bid omits rewarded completion criteria. + /// The default value is 120 seconds. Assigning a negative value is ignored. + public var rewardedCompletionTime: TimeInterval { + get { adUnitConfig.adConfiguration.rewardedCompletionTime } + set { adUnitConfig.adConfiguration.rewardedCompletionTime = newValue } + } // - MARK: Public Methods diff --git a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/PBMCacheRenderers/DisplayView.swift b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/PBMCacheRenderers/DisplayView.swift index fb746199d..0e1acc74e 100644 --- a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/PBMCacheRenderers/DisplayView.swift +++ b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/PBMCacheRenderers/DisplayView.swift @@ -28,6 +28,13 @@ class DisplayView: UIView, PrebidMobileDisplayViewProtocol, AdViewManagerDelegat adViewManager?.isCreativeOpened ?? false } + /// The fallback completion time, in seconds, used when the bid omits rewarded completion criteria. + /// The default value is 120 seconds. Assigning a negative value is ignored. + public var rewardedCompletionTime: TimeInterval { + get { adConfiguration.adConfiguration.rewardedCompletionTime } + set { adConfiguration.adConfiguration.rewardedCompletionTime = newValue } + } + // MARK: - Internal Properties let bid: Bid @@ -60,7 +67,11 @@ class DisplayView: UIView, PrebidMobileDisplayViewProtocol, AdViewManagerDelegat guard transactionFactory == nil else { return } adConfiguration.adConfiguration.winningBidAdFormat = bid.adFormat - adConfiguration.adConfiguration.rewardedConfig = RewardedConfig(ortbRewarded: bid.rewardedConfig) + let rewardedConfig = RewardedConfig( + ortbRewarded: bid.rewardedConfig, + defaultCompletionTime: rewardedCompletionTime + ) + adConfiguration.adConfiguration.rewardedConfig = rewardedConfig transactionFactory = Factory.createTransactionFactory( bid: bid, diff --git a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/PBMCacheRenderers/InterstitialController.swift b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/PBMCacheRenderers/InterstitialController.swift index 910074106..07e72fede 100644 --- a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/PBMCacheRenderers/InterstitialController.swift +++ b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/PBMCacheRenderers/InterstitialController.swift @@ -35,6 +35,13 @@ public class InterstitialController: get { adConfiguration.adConfiguration.videoControlsConfig } set { adConfiguration.adConfiguration.videoControlsConfig = newValue } } + + /// The fallback completion time, in seconds, used when the bid omits rewarded completion criteria. + /// The default value is 120 seconds. Assigning a negative value is ignored. + public var rewardedCompletionTime: TimeInterval { + get { adConfiguration.adConfiguration.rewardedCompletionTime } + set { adConfiguration.adConfiguration.rewardedCompletionTime = newValue } + } public var videoParameters: VideoParameters { get { adConfiguration.adConfiguration.videoParameters } @@ -72,7 +79,6 @@ public class InterstitialController: } adConfiguration.adConfiguration.winningBidAdFormat = bid.adFormat - adConfiguration.adConfiguration.rewardedConfig = RewardedConfig(ortbRewarded: bid.rewardedConfig) videoControlsConfig.initialize(with: bid.videoAdConfiguration) // This part is dedicating to test server-side ad configurations. @@ -80,6 +86,12 @@ public class InterstitialController: #if DEBUG adConfiguration.adConfiguration.videoControlsConfig.initialize(with: bid.testVideoAdConfiguration) #endif + + let rewardedConfig = RewardedConfig( + ortbRewarded: bid.rewardedConfig, + defaultCompletionTime: rewardedCompletionTime + ) + adConfiguration.adConfiguration.rewardedConfig = rewardedConfig transactionFactory = Factory.createTransactionFactory( bid: bid, diff --git a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/PBMCore/AdUnitConfig.swift b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/PBMCore/AdUnitConfig.swift index 9034844cc..327112c46 100644 --- a/PrebidMobile/Swift/PrebidMobileRendering/Prebid/PBMCore/AdUnitConfig.swift +++ b/PrebidMobile/Swift/PrebidMobileRendering/Prebid/PBMCore/AdUnitConfig.swift @@ -151,6 +151,7 @@ public class AdUnitConfig: NSObject, NSCopying { clone.adConfiguration.isBuiltInVideo = self.adConfiguration.isBuiltInVideo clone.adConfiguration.isInterstitialAd = self.adConfiguration.isInterstitialAd clone.adConfiguration.isRewarded = self.adConfiguration.isRewarded + clone.adConfiguration.rewardedCompletionTime = self.adConfiguration.rewardedCompletionTime clone.adConfiguration.forceInterstitialPresentation = self.adConfiguration.forceInterstitialPresentation clone.adConfiguration.interstitialLayout = self.adConfiguration.interstitialLayout clone.nativeAdConfiguration = self.nativeAdConfiguration diff --git a/PrebidMobileTests/RenderingTests/Tests/PBMHTMLCreativeTest.swift b/PrebidMobileTests/RenderingTests/Tests/PBMHTMLCreativeTest.swift index e132474a9..e21184e88 100644 --- a/PrebidMobileTests/RenderingTests/Tests/PBMHTMLCreativeTest.swift +++ b/PrebidMobileTests/RenderingTests/Tests/PBMHTMLCreativeTest.swift @@ -495,6 +495,41 @@ class PBMHTMLCreativeTest : XCTestCase, CreativeResolutionDelegate, CreativeView waitForExpectations(timeout: 10) } + + func testRewardEvent_EndcardUsesConfiguredFallback() { + let exp = expectation(description: "Reward completion") + + let rewardedConfig = RewardedConfig( + ortbRewarded: ORTBRewardedConfiguration(), + defaultCompletionTime: 1 + ) + + let adConfiguration = AdConfiguration() + adConfiguration.rewardedConfig = rewardedConfig + adConfiguration.isRewarded = true + let creativeModel = CreativeModel(adConfiguration: adConfiguration) + self.htmlCreative = MockPBMHTMLCreative( + creativeModel: creativeModel, + transaction: UtilitiesForTesting.createEmptyTransaction() + ) + + htmlCreative.creativeModel.isCompanionAd = true + htmlCreative.onViewabilityChanged( + true, + viewExposure: Factory.createViewExposure( + exposureFactor: 5, + visibleRectangle: CGRect(origin: .zero, size: CGSize(width: 300, height: 250)) + ) + ) + + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + if self.htmlCreative.creativeModel.userHasEarnedReward == true { + exp.fulfill() + } + } + + waitForExpectations(timeout: 3) + } func testPostRewardEvent_Banner() { let rewardTime: NSNumber = 2 diff --git a/PrebidMobileTests/RenderingTests/Tests/VideoControlsConfigTests.swift b/PrebidMobileTests/RenderingTests/Tests/VideoControlsConfigTests.swift index 39b5addbb..315eb3cc6 100644 --- a/PrebidMobileTests/RenderingTests/Tests/VideoControlsConfigTests.swift +++ b/PrebidMobileTests/RenderingTests/Tests/VideoControlsConfigTests.swift @@ -27,6 +27,50 @@ class VideoControlsConfigTests: XCTestCase { XCTAssertTrue(adConfiguration.isSoundButtonVisible == false) } + func testRewardedCompletionTime() { + let adConfiguration = AdConfiguration() + XCTAssertEqual(adConfiguration.rewardedCompletionTime, 120) + + adConfiguration.rewardedCompletionTime = 15 + XCTAssertEqual(adConfiguration.rewardedCompletionTime, 15) + + adConfiguration.rewardedCompletionTime = -1 + XCTAssertEqual(adConfiguration.rewardedCompletionTime, 15) + } + + func testInterstitialControllerPropagatesRewardedCompletionTime() { + let adConfiguration = AdUnitConfig(configId: "test") + let controller = InterstitialController( + bid: makeBid(), + adConfiguration: adConfiguration + ) + controller.rewardedCompletionTime = 15 + + controller.loadAd() + + XCTAssertEqual( + adConfiguration.adConfiguration.rewardedConfig?.defaultCompletionTime, + 15 + ) + } + + func testDisplayViewPropagatesRewardedCompletionTime() { + let adConfiguration = AdUnitConfig(configId: "test") + let displayView = DisplayView( + frame: .zero, + bid: makeBid(), + adConfiguration: adConfiguration + ) + displayView.rewardedCompletionTime = 20 + + displayView.loadAd() + + XCTAssertEqual( + adConfiguration.adConfiguration.rewardedConfig?.defaultCompletionTime, + 20 + ) + } + func testCloseButtonArea() { let adConfiguration = VideoControlsConfiguration() XCTAssertEqual(adConfiguration.closeButtonArea, PrebidConstants.BUTTON_AREA_DEFAULT.doubleValue) @@ -73,4 +117,8 @@ class VideoControlsConfigTests: XCTestCase { XCTAssertEqual(adConfiguration.closeButtonPosition, .topLeft) } + + private func makeBid() -> Bid { + Bid(bid: ORTBBid(bidID: "test", impid: "imp1", price: 1)) + } }