diff --git a/InternalTestApp/InternalTestApp.xcodeproj/project.pbxproj b/InternalTestApp/InternalTestApp.xcodeproj/project.pbxproj index 058cadabe..f923c4532 100644 --- a/InternalTestApp/InternalTestApp.xcodeproj/project.pbxproj +++ b/InternalTestApp/InternalTestApp.xcodeproj/project.pbxproj @@ -25,6 +25,12 @@ 34E5698A237DB96900B47B01 /* UtilitiesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34E56989237DB96900B47B01 /* UtilitiesViewController.swift */; }; 34FC0CF424ADD3A80045553E /* PrebidInterstitialController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34FC0CF324ADD3A80045553E /* PrebidInterstitialController.swift */; }; 34FC0CF624ADD5640045553E /* PrebidGAMRewardedController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34FC0CF524ADD5640045553E /* PrebidGAMRewardedController.swift */; }; + 3C28C9D82C35713B00D0A7DB /* CustomRendererInterstitialController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C28C9D72C35713B00D0A7DB /* CustomRendererInterstitialController.swift */; }; + 3C28C9D92C35713B00D0A7DB /* CustomRendererInterstitialController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C28C9D72C35713B00D0A7DB /* CustomRendererInterstitialController.swift */; }; + 3C28C9DB2C357E3500D0A7DB /* SampleCustomRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C28C9DA2C357E3500D0A7DB /* SampleCustomRenderer.swift */; }; + 3C28C9DC2C357E3500D0A7DB /* SampleCustomRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C28C9DA2C357E3500D0A7DB /* SampleCustomRenderer.swift */; }; + 3CC4A3E92C11F96800B97128 /* CustomRendererBannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC4A3E82C11F96800B97128 /* CustomRendererBannerController.swift */; }; + 3CC4A3EA2C11F96800B97128 /* CustomRendererBannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CC4A3E82C11F96800B97128 /* CustomRendererBannerController.swift */; }; 457FD34BA45C3840CC31A8F5 /* Pods_InternalTestApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5C11D97034E668BBFDD0DDCA /* Pods_InternalTestApp.framework */; }; 530E722C292FACB20025B44D /* UIImageView+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 530E722B292FACB20025B44D /* UIImageView+Extensions.swift */; }; 5397BD142936185400ABDA22 /* PrebidOriginalAPIDisplayBannerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5397BD132936185400ABDA22 /* PrebidOriginalAPIDisplayBannerController.swift */; }; @@ -374,6 +380,9 @@ 34FC0CF524ADD5640045553E /* PrebidGAMRewardedController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidGAMRewardedController.swift; sourceTree = ""; }; 35F94D131F93F85D00CF46DB /* InternalTestAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = InternalTestAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 35F94D171F93F85D00CF46DB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3C28C9D72C35713B00D0A7DB /* CustomRendererInterstitialController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomRendererInterstitialController.swift; sourceTree = ""; }; + 3C28C9DA2C357E3500D0A7DB /* SampleCustomRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleCustomRenderer.swift; sourceTree = ""; }; + 3CC4A3E82C11F96800B97128 /* CustomRendererBannerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomRendererBannerController.swift; sourceTree = ""; }; 530E722B292FACB20025B44D /* UIImageView+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImageView+Extensions.swift"; sourceTree = ""; }; 5397BD132936185400ABDA22 /* PrebidOriginalAPIDisplayBannerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidOriginalAPIDisplayBannerController.swift; sourceTree = ""; }; 5397BD25293760F500ABDA22 /* PrebidOriginalAPIDisplayInterstitialController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidOriginalAPIDisplayInterstitialController.swift; sourceTree = ""; }; @@ -634,6 +643,7 @@ 349302192473F2EF004A6086 /* Prebid */ = { isa = PBXGroup; children = ( + 3CC4A3E72C11F93A00B97128 /* CustomRenderer */, 3461835425A6020500783A2C /* NativeAdViewBox.swift */, AC4F253825DFF6300095C601 /* NativeAdViewBoxLinks.swift */, AC4F254A25E0112C0095C601 /* NativeAdViewBoxProtocol.swift */, @@ -691,6 +701,15 @@ path = PrebidMobileDemoRenderingUITests; sourceTree = ""; }; + 3CC4A3E72C11F93A00B97128 /* CustomRenderer */ = { + isa = PBXGroup; + children = ( + 3CC4A3E82C11F96800B97128 /* CustomRendererBannerController.swift */, + 3C28C9D72C35713B00D0A7DB /* CustomRendererInterstitialController.swift */, + ); + path = CustomRenderer; + sourceTree = ""; + }; 5397BD122936180600ABDA22 /* OriginalAPI */ = { isa = PBXGroup; children = ( @@ -828,6 +847,7 @@ 34B10A2F255017DE00B5FE09 /* ThreadCheckingButton.swift */, 34C25816255D69E1000A4A73 /* EventReportContainer.swift */, 530E722B292FACB20025B44D /* UIImageView+Extensions.swift */, + 3C28C9DA2C357E3500D0A7DB /* SampleCustomRenderer.swift */, ); path = Utilities; sourceTree = ""; @@ -1166,6 +1186,7 @@ }; 93FED9C21A376EFE009A6E9F = { CreatedOnToolsVersion = 6.1.1; + DevelopmentTeam = 9TN4TUCG8N; LastSwiftMigration = 1020; ProvisioningStyle = Automatic; }; @@ -1536,6 +1557,7 @@ 53A651852AC200AA000D42E4 /* PrebidOriginalAPIMultiformatNativeStylesController.swift in Sources */, 53ED2FD729798104007D13EE /* UtilitiesViewController.swift in Sources */, 53ED2FD829798104007D13EE /* AdMobNativeAdView.swift in Sources */, + 3CC4A3EA2C11F96800B97128 /* CustomRendererBannerController.swift in Sources */, 53ED2FD929798104007D13EE /* PrebidGAMInterstitialController.swift in Sources */, 53ED2FDA29798104007D13EE /* PrebidMAXRewardedController.swift in Sources */, 53ED2FDB29798104007D13EE /* PrebidMAXNativeController.swift in Sources */, @@ -1555,6 +1577,7 @@ 53ED2FE829798104007D13EE /* PrebidBannerController.swift in Sources */, 53ED2FE929798104007D13EE /* PrebidBannerConfigurationController.swift in Sources */, 53ED2FEA29798104007D13EE /* TestCase.swift in Sources */, + 3C28C9DC2C357E3500D0A7DB /* SampleCustomRenderer.swift in Sources */, 53ED2FEB29798104007D13EE /* PrebidAdMobNativeViewController.swift in Sources */, 53ED2FEC29798104007D13EE /* PrebidInterstitialController.swift in Sources */, 53ED2FED29798104007D13EE /* PrebidOriginalAPIDisplayInterstitialController.swift in Sources */, @@ -1600,6 +1623,7 @@ 53ED301229798104007D13EE /* ReactiveSdkInitFlag.swift in Sources */, 53ED301329798104007D13EE /* PrebidOriginalAPINativeBannerController.swift in Sources */, 53ED301429798104007D13EE /* TestCasesSectionsViewController.swift in Sources */, + 3C28C9D92C35713B00D0A7DB /* CustomRendererInterstitialController.swift in Sources */, 53ED301529798104007D13EE /* AppSettingsKeys.swift in Sources */, 53ED301729798104007D13EE /* NativeEventTracker+Extensions.swift in Sources */, 53ED301829798104007D13EE /* PrebidRewardedController.swift in Sources */, @@ -1630,6 +1654,7 @@ 53A651842AC200AA000D42E4 /* PrebidOriginalAPIMultiformatNativeStylesController.swift in Sources */, 34E5698A237DB96900B47B01 /* UtilitiesViewController.swift in Sources */, 92ABD58C279DA7E200AFDFF6 /* AdMobNativeAdView.swift in Sources */, + 3CC4A3E92C11F96800B97128 /* CustomRendererBannerController.swift in Sources */, 34BA95FE249CBF05006AE372 /* PrebidGAMInterstitialController.swift in Sources */, 92221CE52804BBB6005DF671 /* PrebidMAXRewardedController.swift in Sources */, 92986537280604E4007A2F34 /* PrebidMAXNativeController.swift in Sources */, @@ -1649,6 +1674,7 @@ 3493022224740BEE004A6086 /* PrebidBannerController.swift in Sources */, 5BDB85DA2739794900A529F6 /* PrebidBannerConfigurationController.swift in Sources */, 5B3EEDE52101FB8800BAA0C4 /* TestCase.swift in Sources */, + 3C28C9DB2C357E3500D0A7DB /* SampleCustomRenderer.swift in Sources */, 92C475122796FF8400C26E27 /* PrebidAdMobNativeViewController.swift in Sources */, 34FC0CF424ADD3A80045553E /* PrebidInterstitialController.swift in Sources */, 5397BD26293760F500ABDA22 /* PrebidOriginalAPIDisplayInterstitialController.swift in Sources */, @@ -1694,6 +1720,7 @@ 346C6F5725DFAB7E00457DD3 /* ReactiveSdkInitFlag.swift in Sources */, 5397BD2C2937764100ABDA22 /* PrebidOriginalAPINativeBannerController.swift in Sources */, 5B3EEDE22101EDA200BAA0C4 /* TestCasesSectionsViewController.swift in Sources */, + 3C28C9D82C35713B00D0A7DB /* CustomRendererInterstitialController.swift in Sources */, ACC41ACD2444EADB00B9A3A7 /* AppSettingsKeys.swift in Sources */, 92C4E60D27A2F6D700738370 /* NativeEventTracker+Extensions.swift in Sources */, 34BA9600249CC118006AE372 /* PrebidRewardedController.swift in Sources */, @@ -2092,10 +2119,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 9TN4TUCG8N; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -2125,10 +2151,9 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO; - CODE_SIGN_IDENTITY = "iPhone Developer"; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = 9TN4TUCG8N; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift b/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift index 6083d3e60..72106a742 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift @@ -583,6 +583,24 @@ struct TestCaseManager { setupCustomParams(for: bannerController.prebidConfigId) }), + TestCase(title: "Banner 320x50 (CustomRenderer)", + tags: [.banner, .inapp, .server], + exampleVCStoryboardID: "AdapterViewController", + configurationClosure: { vc in + guard let adapterVC = vc as? AdapterViewController else { + return + } + + let bannerController = CustomRendererBannerController(rootController: adapterVC) + bannerController.adSizes = [CGSize(width: 320, height: 50)] + + bannerController.prebidConfigId = "prebid-ita-banner-320-50-meta-custom-renderer"; + + adapterVC.setup(adapter: bannerController) + + setupCustomParams(for: bannerController.prebidConfigId) + }), + TestCase(title: "Banner 320x50 Server-Side Creative Factory Timeout (In-App)", tags: [.banner, .inapp, .server], exampleVCStoryboardID: "AdapterViewController", diff --git a/InternalTestApp/PrebidMobileDemoRendering/Utilities/SampleCustomRenderer.swift b/InternalTestApp/PrebidMobileDemoRendering/Utilities/SampleCustomRenderer.swift new file mode 100644 index 000000000..5fd7d86b3 --- /dev/null +++ b/InternalTestApp/PrebidMobileDemoRendering/Utilities/SampleCustomRenderer.swift @@ -0,0 +1,83 @@ +/*   Copyright 2018-2021 Prebid.org, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import Foundation +import PrebidMobile +import UIKit + +public class SampleCustomRenderer: NSObject, PrebidMobilePluginRenderer { + + public let name = "SampleCustomRenderer" + + public let version = "1.0.0" + + public var data: [AnyHashable: Any]? = nil + + private var adViewManager: PBMAdViewManager? + + public func isSupportRendering(for format: AdFormat?) -> Bool { + AdFormat.allCases.contains(where: { $0 == format }) + } + + public func setupBid(_ bid: Bid, adConfiguration: AdUnitConfig, connection: PrebidServerConnectionProtocol) { + + } + + public func createBannerAdView(with frame: CGRect, bid: Bid, adConfiguration: AdUnitConfig, + connection: PrebidServerConnectionProtocol, adViewDelegate: (any PBMAdViewDelegate)?) { + DispatchQueue.main.async { + if let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }), + let rootView = window.rootViewController?.view { + if let prebidBannerView = self.findPrebidBannerView(in: rootView) { + print("Found PrebidBannerView: \(prebidBannerView)") + + let label = UILabel() + label.text = "Prebid SDK - Custom Renderer" + label.textAlignment = .center + label.textColor = .black + label.backgroundColor = .yellow + label.translatesAutoresizingMaskIntoConstraints = false + + prebidBannerView.addSubview(label) + + NSLayoutConstraint.activate([ + label.centerXAnchor.constraint(equalTo: prebidBannerView.centerXAnchor), + label.centerYAnchor.constraint(equalTo: prebidBannerView.centerYAnchor), + label.widthAnchor.constraint(equalTo: prebidBannerView.widthAnchor), + label.heightAnchor.constraint(equalTo: prebidBannerView.heightAnchor) + ]) + + } else { + print("PrebidBannerView not found.") + } + } + } + } + + private func findPrebidBannerView(in view: UIView) -> UIView? { + if view.accessibilityIdentifier == "PrebidBannerView" { + return view + } + for subview in view.subviews { + if let foundView = findPrebidBannerView(in: subview) { + return foundView + } + } + return nil + } + + public func createInterstitialController(bid: Bid, adConfiguration: AdUnitConfig, connection: PrebidServerConnectionProtocol, + adViewManagerDelegate adViewDelegate: InterstitialController?, videoControlsConfig: VideoControlsConfiguration?) { + } +} diff --git a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/CustomRenderer/CustomRendererBannerController.swift b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/CustomRenderer/CustomRendererBannerController.swift new file mode 100644 index 000000000..8370e7b31 --- /dev/null +++ b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/CustomRenderer/CustomRendererBannerController.swift @@ -0,0 +1,250 @@ +/*   Copyright 2018-2021 Prebid.org, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit +import PrebidMobile + +class CustomRendererBannerController: NSObject, AdaptedController, PrebidConfigurableBannerController, BannerViewDelegate { + + var refreshInterval: TimeInterval = 0 + + var prebidConfigId = "" + + var adSizes = [CGSize]() + var adFormat: AdFormat? + + var adBannerView: BannerView? + + var testPBSSDKConfig = false + + weak var rootController: AdapterViewController? + + private let adViewDidReceiveAdButton = EventReportContainer() + private let adViewDidFailToLoadAdButton = EventReportContainer() + private let adViewWillPresentScreenButton = EventReportContainer() + private let adViewDidDismissScreenButton = EventReportContainer() + private let adViewWillLeaveApplicationButton = EventReportContainer() + + private let reloadButton = ThreadCheckingButton() + private let stopRefreshButton = ThreadCheckingButton() + private let sampleCustomRenderer = SampleCustomRenderer() + + let lastLoadedAdSizeLabel = UILabel() + private let configIdLabel = UILabel() + + required init(rootController: AdapterViewController) { + super.init() + self.rootController = rootController + Prebid.registerPluginRenderer(sampleCustomRenderer) + + reloadButton.addTarget(self, action: #selector(reload), for: .touchUpInside) + stopRefreshButton.addTarget(self, action: #selector(stopRefresh), for: .touchUpInside) + + setupAdapterController() + } + + deinit { + Targeting.shared.sourceapp = nil + Prebid.unregisterPluginRenderer(sampleCustomRenderer) + } + + func configurationController() -> BaseConfigurationController? { + return PrebidBannerConfigurationController(controller: self) + } + + func loadAd() { + configIdLabel.isHidden = false + configIdLabel.text = "Config ID: \(prebidConfigId)" + + let size = adSizes[0] + + adBannerView = BannerView(frame: CGRect(origin: .zero, size: size), + configID: prebidConfigId, + adSize: size) + + if (refreshInterval > 0) { + adBannerView?.refreshInterval = refreshInterval + } + + if adSizes.count > 1 { + adBannerView?.additionalSizes = Array(adSizes.suffix(from: 1)) + } + if let adFormat = adFormat { + adBannerView?.adFormat = adFormat + + if adFormat == .video { + adBannerView?.videoParameters.placement = AppConfiguration.shared.videoPlacementType ?? .InBanner + } + } + if let adPosition = AppConfiguration.shared.adPosition { + adBannerView?.adPosition = adPosition + } + + adBannerView?.delegate = self + adBannerView?.accessibilityIdentifier = "PrebidBannerView" + + // imp[].ext.data + if let adUnitContext = AppConfiguration.shared.adUnitContext { + for dataPair in adUnitContext { + adBannerView?.addContextData(dataPair.value, forKey: dataPair.key) + } + } + + // imp[].ext.keywords + if !AppConfiguration.shared.adUnitContextKeywords.isEmpty { + for keyword in AppConfiguration.shared.adUnitContextKeywords { + adBannerView?.addContextKeyword(keyword) + } + } + + // user.data + if let userData = AppConfiguration.shared.userData { + let ortbUserData = PBMORTBContentData() + ortbUserData.ext = [:] + + for dataPair in userData { + ortbUserData.ext?[dataPair.key] = dataPair.value + } + + adBannerView?.addUserData([ortbUserData]) + } + + // app.content.data + if let appData = AppConfiguration.shared.appContentData { + let ortbAppContentData = PBMORTBContentData() + ortbAppContentData.ext = [:] + + for dataPair in appData { + ortbAppContentData.ext?[dataPair.key] = dataPair.value + } + + adBannerView?.addAppContentData([ortbAppContentData]) + } + + adBannerView?.loadAd() + + rootController?.bannerView.addSubview(self.adBannerView!) + self.adBannerView!.translatesAutoresizingMaskIntoConstraints = false + let widthConstraint = NSLayoutConstraint(item: self.adBannerView!, + attribute: NSLayoutConstraint.Attribute.width, + relatedBy: NSLayoutConstraint.Relation.equal, + toItem: rootController?.bannerView, + attribute: NSLayoutConstraint.Attribute.width, + multiplier: 1, constant: 0) + let heightConstraint = NSLayoutConstraint(item: self.adBannerView!, + attribute: NSLayoutConstraint.Attribute.height, + relatedBy: NSLayoutConstraint.Relation.equal, + toItem: rootController?.bannerView, + attribute: NSLayoutConstraint.Attribute.height, + multiplier: 1, constant: 0) + rootController?.bannerView.addConstraints([widthConstraint, heightConstraint]) + } + + // MARK: - BannerViewDelegate + + func bannerViewPresentationController() -> UIViewController? { + return rootController + } + + func bannerView(_ bannerView: BannerView, + didReceiveAdWithAdSize adSize: CGSize) { + resetEvents() + reloadButton.isEnabled = true + adViewDidReceiveAdButton.isEnabled = true + rootController?.bannerView.constraints.first { $0.firstAttribute == .width }?.constant = adSize.width + rootController?.bannerView.constraints.first { $0.firstAttribute == .height }?.constant = adSize.height + lastLoadedAdSizeLabel.isHidden = false + lastLoadedAdSizeLabel.text = "Ad Size: \(adSize.width)x\(adSize.height)" + + // Creative Factory Timeout Check + + if testPBSSDKConfig { + let pbsSDKConfig = bannerView.lastBidResponse?.ext?.extPrebid?.passthrough?.filter({ + $0.type == "prebidmobilesdk" + }).first?.sdkConfiguration + + if pbsSDKConfig?.cftBanner?.doubleValue != Prebid.shared.creativeFactoryTimeout || pbsSDKConfig?.cftPreRender?.doubleValue != Prebid.shared.creativeFactoryTimeoutPreRenderContent { + resetEvents() + adViewDidFailToLoadAdButton.isEnabled = true + } + } + } + + func bannerView(_ bannerView: BannerView, didFailToReceiveAdWith error: Error) { + resetEvents() + reloadButton.isEnabled = true + adViewDidFailToLoadAdButton.isEnabled = true + } + + func bannerViewWillPresentModal(_ bannerView: BannerView) { + adViewWillPresentScreenButton.isEnabled = true + } + + func bannerViewDidDismissModal(_ bannerView: BannerView) { + adViewDidDismissScreenButton.isEnabled = true + } + + func bannerViewWillLeaveApplication(_ bannerView: BannerView) { + adViewWillLeaveApplicationButton.isEnabled = true + } + + // MARK: - Private Methods + + private func setupAdapterController() { + rootController?.showButton.isHidden = true + + setupActions() + configIdLabel.isHidden = true + rootController?.actionsView.addArrangedSubview(configIdLabel) + lastLoadedAdSizeLabel.isHidden = true + rootController?.actionsView.addArrangedSubview(lastLoadedAdSizeLabel) + } + + private func setupActions() { + rootController?.setupAction(adViewDidReceiveAdButton, "adViewDidReceiveAd called", accessibilityLabel: "adViewDidReceiveAd called") + rootController?.setupAction(adViewDidFailToLoadAdButton, "adViewDidFailToLoadAd called") + rootController?.setupAction(adViewWillPresentScreenButton, "adViewWillPresentScreen called") + rootController?.setupAction(adViewDidDismissScreenButton, "adViewDidDismissScreen called") + rootController?.setupAction(adViewWillLeaveApplicationButton, "adViewWillLeaveApplication called") + + rootController?.setupAction(reloadButton, "[Reload]") + rootController?.setupAction(stopRefreshButton, "[Stop Refresh]") + stopRefreshButton.isEnabled = true + } + + private func resetEvents() { + adViewDidReceiveAdButton.isEnabled = false + adViewDidFailToLoadAdButton.isEnabled = false + adViewWillPresentScreenButton.isEnabled = false + adViewDidDismissScreenButton.isEnabled = false + adViewWillLeaveApplicationButton.isEnabled = false + + lastLoadedAdSizeLabel.isHidden = true + } + + @objc private func reload() { + reloadButton.isEnabled = false + stopRefreshButton.isEnabled = true + + resetEvents() + + adBannerView?.loadAd() + } + + @objc private func stopRefresh() { + stopRefreshButton.isEnabled = false + adBannerView?.stopRefresh() + } +} diff --git a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/CustomRenderer/CustomRendererInterstitialController.swift b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/CustomRenderer/CustomRendererInterstitialController.swift new file mode 100644 index 000000000..42d8d7058 --- /dev/null +++ b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/CustomRenderer/CustomRendererInterstitialController.swift @@ -0,0 +1,211 @@ +/*   Copyright 2018-2021 Prebid.org, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import UIKit +import GoogleMobileAds +import PrebidMobile + +class CustomRendererInterstitialController: NSObject, AdaptedController, + PrebidConfigurableController, InterstitialAdUnitDelegate { + + var prebidConfigId = "" + var storedAuctionResponse: String? + + var adFormats: Set? + + private var interstitialController : InterstitialRenderingAdUnit? + + private weak var adapterViewController: AdapterViewController? + + private let interstitialDidReceiveAdButton = EventReportContainer() + private let interstitialDidFailToReceiveAdButton = EventReportContainer() + private let interstitialWillPresentAdButton = EventReportContainer() + private let interstitialDidDismissAdButton = EventReportContainer() + private let interstitialWillLeaveApplicationButton = EventReportContainer() + private let interstitialDidClickAdButton = EventReportContainer() + private let sampleCustomRenderer = SampleCustomRenderer() + + private let configIdLabel = UILabel() + + // Custom video configuarion + var maxDuration: Int? + var closeButtonArea: Double? + var closeButtonPosition: Position? + var skipButtonArea: Double? + var skipButtonPosition: Position? + var skipDelay: Double? + + // MARK: - AdaptedController + required init(rootController: AdapterViewController) { + self.adapterViewController = rootController + Prebid.registerPluginRenderer(sampleCustomRenderer) + super.init() + + setupAdapterController() + } + + deinit { + Targeting.shared.sourceapp = nil + Prebid.unregisterPluginRenderer(sampleCustomRenderer) + } + + func configurationController() -> BaseConfigurationController? { + return BaseConfigurationController(controller: self) + } + + // MARK: - Public Methods + func loadAd() { + configIdLabel.isHidden = false + configIdLabel.text = "Config ID: \(prebidConfigId)" + + if let storedAuctionResponse = storedAuctionResponse { + Prebid.shared.storedAuctionResponse = storedAuctionResponse + } + + interstitialController = InterstitialRenderingAdUnit(configID: prebidConfigId, + minSizePercentage: CGSize(width: 30, height: 30)) + interstitialController?.delegate = self + + // Custom video configuarion + if let maxDuration = maxDuration { + interstitialController?.videoParameters.maxDuration = SingleContainerInt(integerLiteral: maxDuration) + } + + if let closeButtonArea = closeButtonArea { + interstitialController?.closeButtonArea = closeButtonArea + } + + if let closeButtonPosition = closeButtonPosition { + interstitialController?.closeButtonPosition = closeButtonPosition + } + + if let skipButtonArea = skipButtonArea { + interstitialController?.skipButtonArea = skipButtonArea + } + + if let skipButtonPosition = skipButtonPosition { + interstitialController?.skipButtonPosition = skipButtonPosition + } + + if let skipDelay = skipDelay { + interstitialController?.skipDelay = skipDelay + } + + if let adFormats = adFormats { + interstitialController?.adFormats = adFormats + } + + // imp[].ext.data + if let adUnitContext = AppConfiguration.shared.adUnitContext { + for dataPair in adUnitContext { + interstitialController?.addContextData(dataPair.value, forKey: dataPair.key) + } + } + + // imp[].ext.keywords + if !AppConfiguration.shared.adUnitContextKeywords.isEmpty { + for keyword in AppConfiguration.shared.adUnitContextKeywords { + interstitialController?.addContextKeyword(keyword) + } + } + + // user.data + if let userData = AppConfiguration.shared.userData { + let ortbUserData = PBMORTBContentData() + ortbUserData.ext = [:] + + for dataPair in userData { + ortbUserData.ext?[dataPair.key] = dataPair.value + } + + interstitialController?.addUserData([ortbUserData]) + } + + // app.content.data + if let appData = AppConfiguration.shared.appContentData { + let ortbAppContentData = PBMORTBContentData() + ortbAppContentData.ext = [:] + + for dataPair in appData { + ortbAppContentData.ext?[dataPair.key] = dataPair.value + } + + interstitialController?.addAppContentData([ortbAppContentData]) + } + + interstitialController?.loadAd() + } + + // MARK: - GADInterstitialDelegate + + + func interstitialDidReceiveAd(_ interstitial: InterstitialRenderingAdUnit) { + adapterViewController?.showButton.isEnabled = true + interstitialDidReceiveAdButton.isEnabled = true + } + + func interstitial(_ interstitial: InterstitialRenderingAdUnit, + didFailToReceiveAdWithError error: Error?) { + interstitialDidFailToReceiveAdButton.isEnabled = true + } + + func interstitialWillPresentAd(_ interstitial: InterstitialRenderingAdUnit) { + interstitialWillPresentAdButton.isEnabled = true + } + + func interstitialDidDismissAd(_ interstitial: InterstitialRenderingAdUnit) { + interstitialDidDismissAdButton.isEnabled = true + } + + func interstitialWillLeaveApplication(_ interstitial: InterstitialRenderingAdUnit) { + interstitialWillLeaveApplicationButton.isEnabled = true + } + + func interstitialDidClickAd(_ interstitial: InterstitialRenderingAdUnit) { + interstitialDidClickAdButton.isEnabled = true + } + + // MARK: - Private Methods + private func setupAdapterController() { + adapterViewController?.bannerView.isHidden = true + + setupShowButton() + setupActions() + + configIdLabel.isHidden = true + adapterViewController?.actionsView.addArrangedSubview(configIdLabel) + } + + private func setupShowButton() { + adapterViewController?.showButton.isEnabled = false + adapterViewController?.showButton.addTarget(self, action:#selector(self.showButtonClicked), for: .touchUpInside) + } + + private func setupActions() { + adapterViewController?.setupAction(interstitialDidReceiveAdButton, "interstitialDidReceiveAd called") + adapterViewController?.setupAction(interstitialDidFailToReceiveAdButton, "interstitialDidFailToReceiveAd called") + adapterViewController?.setupAction(interstitialWillPresentAdButton, "interstitialWillPresentAd called") + adapterViewController?.setupAction(interstitialDidDismissAdButton, "interstitialDidDismissAd called") + adapterViewController?.setupAction(interstitialWillLeaveApplicationButton, "interstitialWillLeaveApplication called") + adapterViewController?.setupAction(interstitialDidClickAdButton, "interstitialDidClickAd called") + } + + @IBAction func showButtonClicked() { + if let interstitialController = interstitialController, interstitialController.isReady { + adapterViewController?.showButton.isEnabled = false + interstitialController.show(from: adapterViewController!) + } + } +} diff --git a/InternalTestApp/PrebidMobileDemoRenderingUITests/UITests/PrebidExamplesUITest.swift b/InternalTestApp/PrebidMobileDemoRenderingUITests/UITests/PrebidExamplesUITest.swift index 5f76b5d48..4ba3480e6 100644 --- a/InternalTestApp/PrebidMobileDemoRenderingUITests/UITests/PrebidExamplesUITest.swift +++ b/InternalTestApp/PrebidMobileDemoRenderingUITests/UITests/PrebidExamplesUITest.swift @@ -28,6 +28,10 @@ class PrebidExamplesUITest: AdsLoaderUITestCase { checkBannerLoadResult(exampleName: "Banner 320x50 (In-App)") } + func testInAppBanner_Custom_Renderer() { + checkBannerLoadResult(exampleName: "Banner 320x50 (CustomRenderer)") + } + func testInAppBanner_Small_noBids() { checkBannerLoadResult(exampleName: "Banner 320x50 (In-App) [noBids]", expectFailure: true) diff --git a/PrebidMobile.xcodeproj/project.pbxproj b/PrebidMobile.xcodeproj/project.pbxproj index c859a4954..ce63f7648 100644 --- a/PrebidMobile.xcodeproj/project.pbxproj +++ b/PrebidMobile.xcodeproj/project.pbxproj @@ -12,6 +12,10 @@ 34F51A562850F50E0063763D /* OMSDK-Static_Prebidorg.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 34F51A552850F50E0063763D /* OMSDK-Static_Prebidorg.xcframework */; }; 38F03B272576421400E026A2 /* CacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F03B262576421400E026A2 /* CacheManagerTests.swift */; }; 38F03B332576624C00E026A2 /* TrackerManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F03B322576624C00E026A2 /* TrackerManagerTests.swift */; }; + 3CADBD242BFDDE01007B6913 /* PBMORTBRendererConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 3CADBD222BFDDE01007B6913 /* PBMORTBRendererConfig.h */; }; + 3CADBD252BFDDE01007B6913 /* PBMORTBRendererConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CADBD232BFDDE01007B6913 /* PBMORTBRendererConfig.m */; }; + 3CD0ADEE2C5125E9006CDA6B /* PluginRegisterTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CD0ADED2C5125E9006CDA6B /* PluginRegisterTest.swift */; }; + 3CD0ADF32C512E5E006CDA6B /* RawSampleCustomRendererBidFabricator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CD0ADF22C512E5E006CDA6B /* RawSampleCustomRendererBidFabricator.swift */; }; 47D9A008EA5B9DF894E98CA2 /* ACJRefreshTest_0.json in Resources */ = {isa = PBXBuildFile; fileRef = 47D9AF9402144C399D6D4A1C /* ACJRefreshTest_0.json */; }; 47D9A16ECCE6B2FB6D603053 /* MockCTTelephonyNetworkInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D9A0E0865B22D90C20EF00 /* MockCTTelephonyNetworkInfo.swift */; }; 47D9A173B721CE2B39D98A82 /* MockMeasurementSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 47D9AA7EC422522283555E44 /* MockMeasurementSession.swift */; }; @@ -771,6 +775,11 @@ 9720387C2358771600F8025A /* NativeRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9720387B2358771600F8025A /* NativeRequestTests.swift */; }; 9743CB84235F1CDB002E2CAA /* NativeAssetTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9743CB83235F1CDB002E2CAA /* NativeAssetTests.swift */; }; 9743CB86235F264B002E2CAA /* NativeEventTrackerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9743CB85235F264B002E2CAA /* NativeEventTrackerTests.swift */; }; + A908694229E05EAF00B37479 /* PrebidMobilePluginRegister.swift in Sources */ = {isa = PBXBuildFile; fileRef = A908694129E05EAF00B37479 /* PrebidMobilePluginRegister.swift */; }; + A908694429E05ED500B37479 /* PrebidMobilePluginRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A908694329E05ED500B37479 /* PrebidMobilePluginRenderer.swift */; }; + A908694629E05F7900B37479 /* PrebidRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A908694529E05F7900B37479 /* PrebidRenderer.swift */; }; + A9750D7A2ABB9A300066E4E6 /* PluginEventListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9750D792ABB9A300066E4E6 /* PluginEventListener.swift */; }; + A9A3396B2AC57193006AD0E7 /* PBMAdViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9A3396A2AC57193006AD0E7 /* PBMAdViewDelegate.swift */; }; FA5AD5E42271FA4100C8F274 /* ConstantsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA5AD5E32271FA4100C8F274 /* ConstantsTest.swift */; }; FA9D7F2722E8A83D006FCBEF /* AdViewUtilsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9D7F2622E8A83D006FCBEF /* AdViewUtilsTests.swift */; }; FAA29904242D1C27002ACBF2 /* TargetingObjCTests.m in Sources */ = {isa = PBXBuildFile; fileRef = FAA29903242D1C27002ACBF2 /* TargetingObjCTests.m */; }; @@ -860,6 +869,10 @@ 34F51A552850F50E0063763D /* OMSDK-Static_Prebidorg.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = "OMSDK-Static_Prebidorg.xcframework"; path = "Frameworks/OMSDK-Static_Prebidorg.xcframework"; sourceTree = ""; }; 38F03B262576421400E026A2 /* CacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManagerTests.swift; sourceTree = ""; }; 38F03B322576624C00E026A2 /* TrackerManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackerManagerTests.swift; sourceTree = ""; }; + 3CADBD222BFDDE01007B6913 /* PBMORTBRendererConfig.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PBMORTBRendererConfig.h; sourceTree = ""; }; + 3CADBD232BFDDE01007B6913 /* PBMORTBRendererConfig.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PBMORTBRendererConfig.m; sourceTree = ""; }; + 3CD0ADED2C5125E9006CDA6B /* PluginRegisterTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginRegisterTest.swift; sourceTree = ""; }; + 3CD0ADF22C512E5E006CDA6B /* RawSampleCustomRendererBidFabricator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RawSampleCustomRendererBidFabricator.swift; sourceTree = ""; }; 47D9A0396ABC8830B365D7DD /* MockLocationManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockLocationManager.swift; sourceTree = ""; }; 47D9A0CD7FB1815DEF68BDCF /* MockPBMModalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockPBMModalViewController.swift; sourceTree = ""; }; 47D9A0E0865B22D90C20EF00 /* MockCTTelephonyNetworkInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockCTTelephonyNetworkInfo.swift; sourceTree = ""; }; @@ -1654,6 +1667,11 @@ 97826AA621FB4F1B001E2C05 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 9791778F2201AF4700E624CE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 979177962201AF5F00E624CE /* DispatcherTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DispatcherTests.swift; sourceTree = ""; }; + A908694129E05EAF00B37479 /* PrebidMobilePluginRegister.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidMobilePluginRegister.swift; sourceTree = ""; }; + A908694329E05ED500B37479 /* PrebidMobilePluginRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidMobilePluginRenderer.swift; sourceTree = ""; }; + A908694529E05F7900B37479 /* PrebidRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidRenderer.swift; sourceTree = ""; }; + A9750D792ABB9A300066E4E6 /* PluginEventListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginEventListener.swift; sourceTree = ""; }; + A9A3396A2AC57193006AD0E7 /* PBMAdViewDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PBMAdViewDelegate.swift; sourceTree = ""; }; FA4A88432497A99D00FDCBB6 /* Swizzling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Swizzling.swift; sourceTree = ""; }; FA5AD5E32271FA4100C8F274 /* ConstantsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstantsTest.swift; sourceTree = ""; }; FA85F9B4264946FC00B8BE72 /* TestUtils.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = TestUtils.xcodeproj; path = ../tools/TestUtils/TestUtils.xcodeproj; sourceTree = ""; }; @@ -1998,6 +2016,8 @@ 5BC37672271F1CFD00444D5E /* ORTB */ = { isa = PBXGroup; children = ( + 3CADBD222BFDDE01007B6913 /* PBMORTBRendererConfig.h */, + 3CADBD232BFDDE01007B6913 /* PBMORTBRendererConfig.m */, 5BC3768C271F1CFD00444D5E /* PBMORTB_NotImplemented.h */, 5BC37682271F1CFD00444D5E /* PBMORTB.h */, 5BC37693271F1CFD00444D5E /* PBMORTBAbstract.h */, @@ -2355,6 +2375,7 @@ 5BC3778E271F1CFE00444D5E /* InterstitialController.swift */, 5BC3778F271F1CFE00444D5E /* DisplayViewLoadingDelegate.swift */, 5BC37790271F1CFE00444D5E /* DisplayViewInteractionDelegate.swift */, + A908694529E05F7900B37479 /* PrebidRenderer.swift */, ); path = PBMCacheRenderers; sourceTree = ""; @@ -2702,6 +2723,7 @@ 5BC376A8271F1CFE00444D5E /* PBMVastGlobals.m */, 5BC376A7271F1CFD00444D5E /* PBMWebView+Internal.h */, 5BC37785271F1CFE00444D5E /* Prebid */, + A908694029E05E8D00B37479 /* PluginRenderer */, 5BC376F2271F1CFE00444D5E /* Prebid+TestExtension.h */, 5BC376A6271F1CFD00444D5E /* Prebid+TestExtension.m */, 5BC376FB271F1CFE00444D5E /* ServerSideConfiguration */, @@ -2988,6 +3010,7 @@ children = ( 925D5E0D2737DFCC00A8A2B5 /* RawWinningBidFabricator.swift */, 925D5E0F2737DFDE00A8A2B5 /* WinningBidResponseFabricator.swift */, + 3CD0ADF22C512E5E006CDA6B /* RawSampleCustomRendererBidFabricator.swift */, ); path = FakeDataFabricationHelpers; sourceTree = ""; @@ -3158,6 +3181,17 @@ path = FetchingLogictests; sourceTree = ""; }; + A908694029E05E8D00B37479 /* PluginRenderer */ = { + isa = PBXGroup; + children = ( + A908694129E05EAF00B37479 /* PrebidMobilePluginRegister.swift */, + A908694329E05ED500B37479 /* PrebidMobilePluginRenderer.swift */, + A9750D792ABB9A300066E4E6 /* PluginEventListener.swift */, + A9A3396A2AC57193006AD0E7 /* PBMAdViewDelegate.swift */, + ); + path = PluginRenderer; + sourceTree = ""; + }; EE09B07119B6562CC4356195 /* Pods */ = { isa = PBXGroup; children = ( @@ -3171,6 +3205,7 @@ FAC837D72321583500565051 /* CollectionExtensionTest.swift */, FA5AD5E32271FA4100C8F274 /* ConstantsTest.swift */, FAEBF2A1237ECFEF006BA972 /* StorageUtilsTests.swift */, + 3CD0ADED2C5125E9006CDA6B /* PluginRegisterTest.swift */, ); path = UnitTests; sourceTree = ""; @@ -3457,6 +3492,7 @@ 5BC3794D271F1D0000444D5E /* Prebid+TestExtension.h in Headers */, 5BC3796A271F1D0000444D5E /* PBMVastRequester.h in Headers */, 5BC379A2271F1D0000444D5E /* PBMModalAnimator.h in Headers */, + 3CADBD242BFDDE01007B6913 /* PBMORTBRendererConfig.h in Headers */, 5BC378F5271F1CFF00444D5E /* PBMORTBFormat.h in Headers */, 5BC379A5271F1D0000444D5E /* PBMModalManager.h in Headers */, 5BC3795D271F1D0000444D5E /* PBMVastParser+Private.h in Headers */, @@ -3824,6 +3860,7 @@ 925D5DB12737C54500A8A2B5 /* MockAdLoader.swift in Sources */, 925D5D742737B7FB00A8A2B5 /* UserConsentManagerTest.swift in Sources */, 922AFD2E2737260900732C53 /* PBMDeepLinkPlusHelperTest.swift in Sources */, + 3CD0ADF32C512E5E006CDA6B /* RawSampleCustomRendererBidFabricator.swift in Sources */, 925D5E622737F41D00A8A2B5 /* PBMRewardedVideoCreativeTest.swift in Sources */, 925D5E7C2737F66000A8A2B5 /* PBMVASTFailToLoadTest.swift in Sources */, 47D9A20753E95F99F258111F /* MockAlertAction.swift in Sources */, @@ -3844,6 +3881,7 @@ 928E5A7427F0F84C000ADA1A /* NativeMarkupRequestObjectTest.swift in Sources */, 925D5DB72737C60D00A8A2B5 /* PBMBidResponseTransformer+TestExtension.swift in Sources */, 922AFCF42736FDD500732C53 /* PBMFunctionsObjCTest.m in Sources */, + 3CD0ADEE2C5125E9006CDA6B /* PluginRegisterTest.swift in Sources */, 47D9AC0B7F63CB839FB2C751 /* MockCTCarrier.swift in Sources */, 922AFD6F2737333A00732C53 /* PBMORTBParameterBuilderTest.swift in Sources */, 925D5D9B2737C2A800A8A2B5 /* GeoLocationParameterBuilderTest.swift in Sources */, @@ -3958,6 +3996,7 @@ FAEE4D20262DC2B200AD9966 /* Signals.swift in Sources */, 5BC37A79271F1D0000444D5E /* PBMVastTransactionFactory.m in Sources */, 5BC37911271F1CFF00444D5E /* PBMDeepLinkPlus.m in Sources */, + A9A3396B2AC57193006AD0E7 /* PBMAdViewDelegate.swift in Sources */, 5BC378DD271F1CFF00444D5E /* PBMORTBPmp.m in Sources */, 5BC378CE271F1CFF00444D5E /* PBMTouchDownRecognizer.m in Sources */, 5BC378ED271F1CFF00444D5E /* PBMORTBAppExtPrebid.m in Sources */, @@ -4003,6 +4042,7 @@ 5BC37AC6271F1D0100444D5E /* PBMSKAdNetworksParameterBuilder.m in Sources */, 53CF5B3729DC690600613E84 /* VideoAdUnit.swift in Sources */, 5BC37A1C271F1D0000444D5E /* PBMErrorFamily.m in Sources */, + A9750D7A2ABB9A300066E4E6 /* PluginEventListener.swift in Sources */, FAEE4D28262DC2B200AD9966 /* PbFindSizeError.swift in Sources */, 5BC37990271F1D0000444D5E /* PBMVideoCreative.m in Sources */, FAEE4D2A262DC2B200AD9966 /* IMAUtils.swift in Sources */, @@ -4043,6 +4083,7 @@ 5BC37A6F271F1D0000444D5E /* PBMORTBAbstractResponse.m in Sources */, 5BC378AD271F1CFF00444D5E /* Targeting.swift in Sources */, 923B55002744F19600E00C88 /* PrebidMediationDelegate.swift in Sources */, + A908694229E05EAF00B37479 /* PrebidMobilePluginRegister.swift in Sources */, FAEE4D1D262DC2B200AD9966 /* BannerAdUnit.swift in Sources */, FAEE4D18262DC2B200AD9966 /* NativeAsset.swift in Sources */, 5BC37A24271F1D0000444D5E /* PBMError.m in Sources */, @@ -4072,6 +4113,7 @@ 5BC37AAB271F1D0000444D5E /* MediationInterstitialAdUnit.swift in Sources */, 5BC378FC271F1CFF00444D5E /* PBMORTBDevice.m in Sources */, 5BC37A98271F1D0000444D5E /* PBMBannerAdLoader.m in Sources */, + A908694429E05ED500B37479 /* PrebidMobilePluginRenderer.swift in Sources */, 5BC37935271F1D0000444D5E /* PBMAdModelEventTracker.m in Sources */, 536A427F282D11DA0069E9B2 /* PrebidServerConnection.swift in Sources */, 927ADB2428057B31006EB8D5 /* MediationUtils.swift in Sources */, @@ -4185,6 +4227,7 @@ 5BC37A8B271F1D0000444D5E /* BannerEventInteractionDelegate.swift in Sources */, FAEE4D0B262DC2B200AD9966 /* Dispatcher.swift in Sources */, 5BC37A10271F1D0000444D5E /* AdUnitConfig.swift in Sources */, + A908694629E05F7900B37479 /* PrebidRenderer.swift in Sources */, 5BC37926271F1CFF00444D5E /* PBMDeepLinkPlusHelper+Testing.m in Sources */, 5BC37A90271F1D0000444D5E /* InterstitialAdUnitDelegate.swift in Sources */, 5BC37A88271F1D0000444D5E /* RewardedAdUnitDelegate.swift in Sources */, @@ -4210,6 +4253,7 @@ 53322AA3282D45EE0049229D /* PrebidServerEventTracker.swift in Sources */, 5BC37901271F1CFF00444D5E /* PBMORTBImp.m in Sources */, 5BC37AE0271F1D0100444D5E /* AdFormat.swift in Sources */, + 3CADBD252BFDDE01007B6913 /* PBMORTBRendererConfig.m in Sources */, 5BC37AAC271F1D0000444D5E /* InterstitialEventHandlerStandalone.swift in Sources */, 5BC37954271F1D0000444D5E /* PBMCreativeViewabilityTracker.m in Sources */, 53B221CF2A0E3D2800C91CCB /* PrebidJSLibrary.swift in Sources */, diff --git a/PrebidMobile/ConfigurationAndTargeting/Prebid.swift b/PrebidMobile/ConfigurationAndTargeting/Prebid.swift index 2a1838435..9ac243f50 100644 --- a/PrebidMobile/ConfigurationAndTargeting/Prebid.swift +++ b/PrebidMobile/ConfigurationAndTargeting/Prebid.swift @@ -225,4 +225,16 @@ public class Prebid: NSObject { override init() { timeoutMillis = defaultTimeoutMillis } + + public static func registerPluginRenderer(_ prebidMobilePluginRenderer: PrebidMobilePluginRenderer) { + PrebidMobilePluginRegister.shared.registerPlugin(prebidMobilePluginRenderer); + } + + public static func unregisterPluginRenderer(_ prebidMobilePluginRenderer: PrebidMobilePluginRenderer) { + PrebidMobilePluginRegister.shared.unregisterPlugin(prebidMobilePluginRenderer); + } + + public static func containsPluginRenderer(_ prebidMobilePluginRenderer: PrebidMobilePluginRenderer) { + PrebidMobilePluginRegister.shared.containsPlugin(prebidMobilePluginRenderer); + } } diff --git a/PrebidMobile/PrebidMobileRendering/Networking/Parameters/PBMORTBParameterBuilder.h b/PrebidMobile/PrebidMobileRendering/Networking/Parameters/PBMORTBParameterBuilder.h index db5034009..2ac672dfb 100644 --- a/PrebidMobile/PrebidMobileRendering/Networking/Parameters/PBMORTBParameterBuilder.h +++ b/PrebidMobile/PrebidMobileRendering/Networking/Parameters/PBMORTBParameterBuilder.h @@ -15,6 +15,7 @@ #import #import "PBMParameterBuilderProtocol.h" +#import "PrebidMobileSwiftHeaders.h" @interface PBMORTBParameterBuilder : NSObject diff --git a/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequest.h b/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequest.h index 5ffe3c662..ec2c03136 100644 --- a/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequest.h +++ b/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequest.h @@ -23,121 +23,121 @@ @class PBMORTBSource; @class PBMORTBUser; -//This file holds the structure for an ORTB 2.5 Bid Request Object -//https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf +// This file holds the structure for an ORTB 2.5 Bid Request Object +// https://www.iab.com/wp-content/uploads/2016/03/OpenRTB-API-Specification-Version-2-5-FINAL.pdf NS_ASSUME_NONNULL_BEGIN #pragma mark - 3.2.1: BidRequest -//The top-level bid request object contains a globally unique bid request or auction ID. This id att;ribute is -// required as is at least one impression object (Section 3.2.4). Other attributes in this top-level object -//establish rules and restrictions that apply to all impressions being offered. -//There are also several subordinate objects that provide detailed data to potential buyers. Among these -//are the Site and App objects, which describe the type of published media in which the impression(s) -//appear. These objects are highly recommended, but only one applies to a given bid request depending -//on whether the media is browser-based web content or a non-browser application, respectively. +// The top-level bid request object contains a globally unique bid request or auction ID. This id att;ribute is +// required as is at least one impression object (Section 3.2.4). Other attributes in this top-level object +// establish rules and restrictions that apply to all impressions being offered. +// There are also several subordinate objects that provide detailed data to potential buyers. Among these +// are the Site and App objects, which describe the type of published media in which the impression(s) +// appear. These objects are highly recommended, but only one applies to a given bid request depending +// on whether the media is browser-based web content or a non-browser application, respectively. @interface PBMORTBBidRequest : PBMORTBAbstract - -//Unique ID of the bid request, provided by the exchange. -@property (nonatomic, copy, nullable) NSString *requestID; - -//Array of Imp objects (Section 3.2.4) representing the impressions offered. At least 1 Imp object is required. -@property (nonatomic, copy) NSArray *imp; - -//Note: Site object not supported. -//Details via a Site object (Section 3.2.13) about the publisher’s website. Only applicable and recommended for websites. - -//Details via an App object (Section 3.2.14) about the publisher’s -//app (i.e., non-browser applications). Only applicable and -//recommended for apps -@property (nonatomic, strong) PBMORTBApp *app; - -//Details via a Device object (Section 3.2.18) about the user’s -//device to which the impression will be delivered. -@property (nonatomic, strong) PBMORTBDevice *device; - -//Details via a User object (Section 3.2.20) about the human -//user of the device; the advertising audience. -@property (nonatomic, strong) PBMORTBUser *user; - -//Integer. Indicator of test mode in which auctions are not billable, -//where 0 = live mode, 1 = test mode. -@property (nonatomic, strong, nullable) NSNumber *test; - -//Auction type, where 1 = First Price, 2 = Second Price Plus. -//Exchange-specific auction types can be defined using values -//greater than 500. -//Note: at is not supported - -//Maximum time in milliseconds the exchange allows for bids to -//be received including Internet latency to avoid timeout. This -//value supersedes any a priori guidance from the exchange -@property (nonatomic, strong, nullable) NSNumber *tmax; - -//White list of buyer seats (e.g., advertisers, agencies) allowed -//to bid on this impression. IDs of seats and knowledge of the -//buyer’s customers to which they refer must be coordinated -//between bidders and the exchange a priori. At most, only one -//of wseat and bseat should be used in the same request. -//Omission of both implies no seat restrictions -//Note: wseat is not supported - -//Block list of buyer seats (e.g., advertisers, agencies) restricted -//from bidding on this impression. IDs of seats and knowledge -//of the buyer’s customers to which they refer must be -//coordinated between bidders and the exchange a priori. At -//most, only one of wseat and bseat should be used in the -//same request. Omission of both implies no seat restrictions. -//Note: bseat is not supported - -//Flag to indicate if Exchange can verify that the impressions -//offered represent all of the impressions available in context + +// Unique ID of the bid request, provided by the exchange. +@property(nonatomic, copy, nullable) NSString *requestID; + +// Array of Imp objects (Section 3.2.4) representing the impressions offered. At least 1 Imp object is required. +@property(nonatomic, copy) NSArray *imp; + +// Note: Site object not supported. +// Details via a Site object (Section 3.2.13) about the publisher’s website. Only applicable and recommended for websites. + +// Details via an App object (Section 3.2.14) about the publisher’s +// app (i.e., non-browser applications). Only applicable and +// recommended for apps +@property(nonatomic, strong) PBMORTBApp *app; + +// Details via a Device object (Section 3.2.18) about the user’s +// device to which the impression will be delivered. +@property(nonatomic, strong) PBMORTBDevice *device; + +// Details via a User object (Section 3.2.20) about the human +// user of the device; the advertising audience. +@property(nonatomic, strong) PBMORTBUser *user; + +// Integer. Indicator of test mode in which auctions are not billable, +// where 0 = live mode, 1 = test mode. +@property(nonatomic, strong, nullable) NSNumber *test; + +// Auction type, where 1 = First Price, 2 = Second Price Plus. +// Exchange-specific auction types can be defined using values +// greater than 500. +// Note: at is not supported + +// Maximum time in milliseconds the exchange allows for bids to +// be received including Internet latency to avoid timeout. This +// value supersedes any a priori guidance from the exchange +@property(nonatomic, strong, nullable) NSNumber *tmax; + +// White list of buyer seats (e.g., advertisers, agencies) allowed +// to bid on this impression. IDs of seats and knowledge of the +// buyer’s customers to which they refer must be coordinated +// between bidders and the exchange a priori. At most, only one +// of wseat and bseat should be used in the same request. +// Omission of both implies no seat restrictions +// Note: wseat is not supported + +// Block list of buyer seats (e.g., advertisers, agencies) restricted +// from bidding on this impression. IDs of seats and knowledge +// of the buyer’s customers to which they refer must be +// coordinated between bidders and the exchange a priori. At +// most, only one of wseat and bseat should be used in the +// same request. Omission of both implies no seat restrictions. +// Note: bseat is not supported + +// Flag to indicate if Exchange can verify that the impressions +// offered represent all of the impressions available in context //(e.g., all on the web page, all video spots such as pre/mid/post -//roll) to support road-blocking. 0 = no or unknown, 1 = yes, the -//impressions offered represent all that are available. -//Note: allimps is not supported +// roll) to support road-blocking. 0 = no or unknown, 1 = yes, the +// impressions offered represent all that are available. +// Note: allimps is not supported -//Array of allowed currencies for bids on this bid request using -//ISO-4217 alpha codes. Recommended only if the exchange -//accepts multiple currencies. -//Note: cur is not supported +// Array of allowed currencies for bids on this bid request using +// ISO-4217 alpha codes. Recommended only if the exchange +// accepts multiple currencies. +// Note: cur is not supported -//White list of languages for creatives using ISO-639-1-alpha-2. -//Omission implies no specific restrictions, but buyers would be -//advised to consider language attribute in the Device and/or -//Content objects if available. -//Note: wlang is not supported +// White list of languages for creatives using ISO-639-1-alpha-2. +// Omission implies no specific restrictions, but buyers would be +// advised to consider language attribute in the Device and/or +// Content objects if available. +// Note: wlang is not supported -//Blocked advertiser categories using the IAB content -//categories. Refer to List 5.1. -//Note: bcat is not supported +// Blocked advertiser categories using the IAB content +// categories. Refer to List 5.1. +// Note: bcat is not supported -//Block list of advertisers by their domains (e.g., “ford.com”). -//Note: badv is not supported +// Block list of advertisers by their domains (e.g., “ford.com”). +// Note: badv is not supported -//Block list of applications by their platform-specific exchangeindependent -//application identifiers. On Android, these should -//be bundle or package names (e.g., com.foo.mygame). On iOS, -//these are numeric IDs -//Note: bapp is not supported +// Block list of applications by their platform-specific exchangeindependent +// application identifiers. On Android, these should +// be bundle or package names (e.g., com.foo.mygame). On iOS, +// these are numeric IDs +// Note: bapp is not supported -//A Source object (Section 3.2.2) that provides data about the -//inventory source and which entity makes the final decision -@property (nonatomic, readonly) PBMORTBSource *source; +// A Source object (Section 3.2.2) that provides data about the +// inventory source and which entity makes the final decision +@property(nonatomic, readonly) PBMORTBSource *source; -//A Regs object (Section 3.2.3) that specifies any industry, legal, -//or governmental regulations in force for this request. -@property (nonatomic, strong) PBMORTBRegs *regs; +// A Regs object (Section 3.2.3) that specifies any industry, legal, +// or governmental regulations in force for this request. +@property(nonatomic, strong) PBMORTBRegs *regs; -//Placeholder for exchange-specific extensions to OpenRTB. -//Note: ext object not supported. +// Placeholder for exchange-specific extensions to OpenRTB. +// Note: ext object not supported. -@property (nonatomic, strong) PBMORTBBidRequestExtPrebid *extPrebid; +@property(nonatomic, strong) PBMORTBBidRequestExtPrebid *extPrebid; -@property (nonatomic, strong, nullable) NSDictionary *arbitraryJsonConfig; +@property(nonatomic, strong, nullable) NSDictionary *arbitraryJsonConfig; -@property (nonatomic, strong, nullable) NSDictionary *ortbObject; +@property(nonatomic, strong, nullable) NSDictionary *ortbObject; - (instancetype)init; diff --git a/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequest.m b/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequest.m index 4cbff955b..cec9acb1a 100644 --- a/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequest.m +++ b/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequest.m @@ -153,7 +153,6 @@ - (instancetype)initWithJsonDictionary:(nonnull PBMJsonDictionary *)jsonDictiona _tmax = jsonDictionary[@"tmax"]; _regs = [[PBMORTBRegs alloc] initWithJsonDictionary:jsonDictionary[@"regs"]]; _source = [[PBMORTBSource alloc] initWithJsonDictionary:jsonDictionary[@"source"]]; - _extPrebid = [[PBMORTBBidRequestExtPrebid alloc] initWithJsonDictionary:jsonDictionary[@"ext"][@"prebid"] ?: @{}]; _arbitraryJsonConfig = jsonDictionary; diff --git a/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequestExtPrebid.h b/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequestExtPrebid.h index 5f8b12b82..0e1a79d67 100644 --- a/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequestExtPrebid.h +++ b/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequestExtPrebid.h @@ -32,10 +32,14 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong, nullable) NSArray *> *storedBidResponses; +@property (nonatomic, strong, nullable) NSArray *> *sdkRenderers; + @property (nonatomic, strong, nullable) NSMutableDictionary *cache; @property (nonatomic, strong) PBMMutableJsonDictionary *targeting; +@property (nonatomic, strong, nullable) PBMJsonDictionary *sdk; + @end NS_ASSUME_NONNULL_END diff --git a/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequestExtPrebid.m b/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequestExtPrebid.m index 281687f4a..9ead83ca4 100644 --- a/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequestExtPrebid.m +++ b/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBBidRequestExtPrebid.m @@ -15,6 +15,13 @@ #import "PBMORTBBidRequestExtPrebid.h" +#import "PrebidMobileSwiftHeaders.h" +#if __has_include("PrebidMobile-Swift.h") +#import "PrebidMobile-Swift.h" +#else +#import +#endif + @implementation PBMORTBBidRequestExtPrebid : PBMORTBAbstract - (instancetype)init { @@ -41,6 +48,9 @@ - (nonnull PBMJsonDictionary *)toJsonDictionary { PBMMutableJsonDictionary * const storedRequest = [PBMMutableJsonDictionary new]; ret[@"storedrequest"] = storedRequest; storedRequest[@"id"] = self.storedRequestID; + if (self.sdk != nil) { + ret[@"sdk"] = self.sdk; + } ret[@"targeting"] = self.targeting; diff --git a/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBRendererConfig.h b/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBRendererConfig.h new file mode 100644 index 000000000..7023a1f10 --- /dev/null +++ b/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBRendererConfig.h @@ -0,0 +1,30 @@ +/*   Copyright 2018-2021 Prebid.org, Inc. + +  Licensed under the Apache License, Version 2.0 (the "License"); +  you may not use this file except in compliance with the License. +  You may obtain a copy of the License at + +  http://www.apache.org/licenses/LICENSE-2.0 + +  Unless required by applicable law or agreed to in writing, software +  distributed under the License is distributed on an "AS IS" BASIS, +  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +  See the License for the specific language governing permissions and +  limitations under the License. +  */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface PBMORTBRendererConfig : NSObject + +@property (nonatomic, copy) NSString *name; +@property (nonatomic, copy) NSString *version; +@property (nonatomic, copy) NSDictionary *data; + +- (instancetype)initWithName:(NSString *)name version:(NSString *)version data:(NSDictionary *)data; + +@end + +NS_ASSUME_NONNULL_END diff --git a/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBRendererConfig.m b/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBRendererConfig.m new file mode 100644 index 000000000..b0c5a6a18 --- /dev/null +++ b/PrebidMobile/PrebidMobileRendering/ORTB/PBMORTBRendererConfig.m @@ -0,0 +1,22 @@ +// +// PBMORTBRendererConfig.m..m +// BitByteData-iOS11.0 +// +// Created by Richard Dépierre on 17/05/2024. +// + +#import "PBMORTBRendererConfig.h" + +@implementation PBMORTBRendererConfig + +- (instancetype)initWithName:(NSString *)name version:(NSString *)version data:(NSDictionary *)data { + self = [super init]; + if (self) { + _name = [name copy]; + _version = [version copy]; + _data = [data copy]; + } + return self; +} + +@end diff --git a/PrebidMobile/PrebidMobileRendering/PluginRenderer/PBMAdViewDelegate.swift b/PrebidMobile/PrebidMobileRendering/PluginRenderer/PBMAdViewDelegate.swift new file mode 100644 index 000000000..93ccf278f --- /dev/null +++ b/PrebidMobile/PrebidMobileRendering/PluginRenderer/PBMAdViewDelegate.swift @@ -0,0 +1,14 @@ +// +// PBMAdViewDelegate.swift +// PrebidMobile +// +// Created by Paul NICOLAS on 28/09/2023. +// Copyright © 2023 AppNexus. All rights reserved. +// + +import UIKit + +@objc public protocol PBMThirdPartyAdViewLoader: NSObjectProtocol { +} + +public typealias PBMAdViewDelegate = PBMThirdPartyAdViewLoader & PBMAdViewManagerDelegate & PBMModalManagerDelegate diff --git a/PrebidMobile/PrebidMobileRendering/PluginRenderer/PluginEventListener.swift b/PrebidMobile/PrebidMobileRendering/PluginRenderer/PluginEventListener.swift new file mode 100644 index 000000000..06e1f61a9 --- /dev/null +++ b/PrebidMobile/PrebidMobileRendering/PluginRenderer/PluginEventListener.swift @@ -0,0 +1,26 @@ +/*   Copyright 2018-2021 Prebid.org, Inc. + +  Licensed under the Apache License, Version 2.0 (the "License"); +  you may not use this file except in compliance with the License. +  You may obtain a copy of the License at + +  http://www.apache.org/licenses/LICENSE-2.0 + +  Unless required by applicable law or agreed to in writing, software +  distributed under the License is distributed on an "AS IS" BASIS, +  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +  See the License for the specific language governing permissions and +  limitations under the License. +  */ + +import Foundation + +@objc public protocol PluginEventDelegate: NSObjectProtocol { + @objc func getPluginName() -> String +} + +@objc open class DefaultPluginEventDelegate: NSObject, PluginEventDelegate { + open func getPluginName() -> String { + return "DefaultPluginName" + } +} diff --git a/PrebidMobile/PrebidMobileRendering/PluginRenderer/PrebidMobilePluginRegister.swift b/PrebidMobile/PrebidMobileRendering/PluginRenderer/PrebidMobilePluginRegister.swift new file mode 100644 index 000000000..ef41871b1 --- /dev/null +++ b/PrebidMobile/PrebidMobileRendering/PluginRenderer/PrebidMobilePluginRegister.swift @@ -0,0 +1,121 @@ +/*   Copyright 2018-2021 Prebid.org, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +/// Global singleton responsible to store plugin renderer instances +@objc public class PrebidMobilePluginRegister: NSObject { + @objc public static let shared = PrebidMobilePluginRegister() + + private let queue = DispatchQueue(label: "PrebidMobilePluginRegisterQueue", attributes: .concurrent) + private var plugins = [String: PrebidMobilePluginRenderer]() + + private let defaultRenderer = PrebidRenderer() + + private override init() { + super.init() + } + + /// Register plugin as renderer + @objc public func registerPlugin(_ renderer: PrebidMobilePluginRenderer) { + let rendererName = renderer.name + + queue.async(flags: .barrier) { [weak self] in + if self?.plugins[rendererName] != nil { + Log.debug("Plugin with name \(rendererName) is already registered.") + return + } + self?.plugins[rendererName] = renderer + } + } + + @objc public func unregisterPlugin(_ renderer: PrebidMobilePluginRenderer) { + queue.async(flags: .barrier) { [weak self] in + self?.plugins.removeValue(forKey: renderer.name) + } + } + + /// Contains plugin + @objc public func containsPlugin(_ renderer: PrebidMobilePluginRenderer) -> Bool { + queue.sync { + plugins.contains { $0.value === renderer } + } + } + + private func getPluginRenderer(for key: String) -> PrebidMobilePluginRenderer? { + queue.sync { + plugins[key] + } + } + + /// Register event delegate + @objc public func registerEventDelegate(_ pluginEventDelegate: PluginEventDelegate, adUnitConfigFingerprint: String) { + queue.async(flags: .barrier) { [plugins] in + plugins + .values + .forEach { + $0.registerEventDelegate?(pluginEventDelegate: pluginEventDelegate, adUnitConfigFingerprint: adUnitConfigFingerprint) + } + } + } + + /// Unregister event delegate + @objc public func unregisterEventDelegate(_ pluginEventDelegate: PluginEventDelegate, adUnitConfigFingerprint: String) { + queue.async(flags: .barrier) { [plugins] in + plugins + .values + .forEach { + $0.unregisterEventDelegate?(pluginEventDelegate: pluginEventDelegate, adUnitConfigFingerprint: adUnitConfigFingerprint) + } + } + } + + /// Returns the list of available renderers for the given ad unit for RTB request + @objc public func getRTBListOfRenderersFor(for adUnit: AdUnitConfig) -> [PrebidMobilePluginRenderer] { + queue.sync { + plugins + .values + .filter { renderer in + adUnit.adFormats.contains { format in + renderer.isSupportRendering(for: format) + } + } + } + } + + /// Returns the registered renderer according to the preferred renderer name in the bid response + /// If no preferred renderer is found, it returns PrebidRenderer to perform default behavior + /// Once bid is win we want to resolve the best PluginRenderer candidate to render the ad + @objc public func getPluginForPreferredRenderer(bid: Bid) -> PrebidMobilePluginRenderer { + if let preferredRendererName = bid.getPreferredPluginRendererName(), + let preferredPlugin = getPluginRenderer(for: preferredRendererName), + preferredPlugin.version == bid.getPreferredPluginRendererVersion(), + preferredPlugin.isSupportRendering(for: bid.adFormat) { + return preferredPlugin + } else { + return defaultRenderer + } + } + + @objc public func getAllPlugins() -> [PrebidMobilePluginRenderer] { + queue.sync { + if plugins.isEmpty { + return [] + } + let allPlugins = Array(plugins.values) + return allPlugins + } + } +} diff --git a/PrebidMobile/PrebidMobileRendering/PluginRenderer/PrebidMobilePluginRenderer.swift b/PrebidMobile/PrebidMobileRendering/PluginRenderer/PrebidMobilePluginRenderer.swift new file mode 100644 index 000000000..4f9377fb7 --- /dev/null +++ b/PrebidMobile/PrebidMobileRendering/PluginRenderer/PrebidMobilePluginRenderer.swift @@ -0,0 +1,53 @@ +/*   Copyright 2018-2021 Prebid.org, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import Foundation + +@objc public protocol PrebidMobilePluginRenderer: AnyObject { + + @objc var name: String { get } + @objc var version: String { get } + @objc var data: [AnyHashable: Any]? { get } + + /// Creates and returns Banner View for a given Bid Response + /// Returns nil in the case of an internal error + @objc func createBannerAdView(with frame: CGRect, bid: Bid, adConfiguration: AdUnitConfig, + connection: PrebidServerConnectionProtocol, adViewDelegate: (any PBMAdViewDelegate)?) + + /// Creates and returns an implementation of PrebidMobileInterstitialControllerInterface for a given bid response + /// Returns nil in the case of an internal error + @objc optional func createInterstitialController( bid: Bid, adConfiguration: AdUnitConfig, + connection: PrebidServerConnectionProtocol, + adViewManagerDelegate: InterstitialController?, + videoControlsConfig: VideoControlsConfiguration?) + + /// Returns true only if the given ad unit could be renderer by the plugin + @objc func isSupportRendering(for format: AdFormat?) -> Bool + + /// Register a listener related to a specific ad unit config fingerprint in order to dispatch specific ad events + @objc optional func registerEventDelegate(pluginEventDelegate: PluginEventDelegate, + adUnitConfigFingerprint: String) + + /// Unregister a listener related to a specific ad unit config fingerprint in order to dispatch specific ad events + @objc optional func unregisterEventDelegate(pluginEventDelegate: PluginEventDelegate, + adUnitConfigFingerprint: String) + + /// Setup a bid for a given ad unit configuration + @objc func setupBid(_ bid: PrebidMobile.Bid, adConfiguration: PrebidMobile.AdUnitConfig, + connection: PrebidServerConnectionProtocol) + + + +} diff --git a/PrebidMobile/PrebidMobileRendering/Prebid/Integrations/GAM/BannerView.swift b/PrebidMobile/PrebidMobileRendering/Prebid/Integrations/GAM/BannerView.swift index 82a1cefa1..5f4028764 100644 --- a/PrebidMobile/PrebidMobileRendering/Prebid/Integrations/GAM/BannerView.swift +++ b/PrebidMobile/PrebidMobileRendering/Prebid/Integrations/GAM/BannerView.swift @@ -71,6 +71,11 @@ public class BannerView: UIView, @objc public weak var delegate: BannerViewDelegate? + /// Subscribe to plugin renderer events + @objc public func setPluginEventDelegate(_ pluginEventDelegate: PluginEventDelegate) { + PrebidMobilePluginRegister.shared.registerEventDelegate(pluginEventDelegate, adUnitConfigFingerprint: adUnitConfig.fingerprint) + } + // MARK: Readonly storage var autoRefreshManager: PBMAutoRefreshManager? diff --git a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCacheRenderers/InterstitialController.swift b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCacheRenderers/InterstitialController.swift index bf0077f6d..b4108bcdf 100644 --- a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCacheRenderers/InterstitialController.swift +++ b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCacheRenderers/InterstitialController.swift @@ -17,6 +17,8 @@ import Foundation import UIKit public class InterstitialController: NSObject, PBMAdViewManagerDelegate { + + private var renderer: PrebidMobilePluginRenderer? @objc public var adFormats: Set { get { adConfiguration.adFormats } @@ -46,7 +48,6 @@ public class InterstitialController: NSObject, PBMAdViewManagerDelegate { var adConfiguration: AdUnitConfig var displayProperties: PBMInterstitialDisplayProperties - var transactionFactory: PBMTransactionFactory? var adViewManager: PBMAdViewManager? // MARK: - Life cycle @@ -63,41 +64,14 @@ public class InterstitialController: NSObject, PBMAdViewManagerDelegate { self.init(bid: bid, adConfiguration: adConfig) } + // TODO: provide a more relevant name for this function. @objc public func loadAd() { - guard transactionFactory == nil else { - return - } - - adConfiguration.adConfiguration.winningBidAdFormat = bid.adFormat - videoControlsConfig.initialize(with: bid.videoAdConfiguration) - // This part is dedicating to test server-side ad configurations. - // Need to be removed when ext.prebid.passthrough will be available. - #if DEBUG - adConfiguration.adConfiguration.videoControlsConfig.initialize(with: bid.testVideoAdConfiguration) - #endif - - transactionFactory = PBMTransactionFactory(bid: bid, - adConfiguration: adConfiguration, - connection: PrebidServerConnection.shared, - callback: { [weak self] transaction, error in - - if let transaction = transaction { - self?.display(transaction: transaction) - } else { - self?.reportFailureWithError(error) - } - }) + self.renderer = PrebidMobilePluginRegister.shared.getPluginForPreferredRenderer(bid: bid) + - PBMWinNotifier.notifyThroughConnection(PrebidServerConnection.shared, - winning: bid, - callback: { [weak self] adMarkup in - if let adMarkup = adMarkup { - self?.transactionFactory?.load(withAdMarkup: adMarkup) - } else { - //TODO: inform failure - } - }) + let connection: PrebidServerConnectionProtocol = PrebidServerConnection.shared + self.renderer?.createInterstitialController?(bid: bid, adConfiguration: adConfiguration, connection: connection, adViewManagerDelegate: self, videoControlsConfig: videoControlsConfig) } @objc public func show() { @@ -181,8 +155,7 @@ public class InterstitialController: NSObject, PBMAdViewManagerDelegate { fatalError("Init is unavailable.") } - func reportFailureWithError(_ error: Error?) { - transactionFactory = nil + public func reportFailureWithError(_ error: Error?) { if let error = error, let loadingDelegate = loadingDelegate { loadingDelegate.interstitialController(self, didFailWithError: error) @@ -190,13 +163,12 @@ public class InterstitialController: NSObject, PBMAdViewManagerDelegate { } func reportSuccess() { - transactionFactory = nil if let loadingDelegate = loadingDelegate { loadingDelegate.interstitialControllerDidLoadAd(self) } } - func display(transaction: PBMTransaction) { + public func display(transaction: PBMTransaction) { adViewManager = PBMAdViewManager(connection: PrebidServerConnection.shared, modalManagerDelegate: nil) adViewManager?.adViewManagerDelegate = self diff --git a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCacheRenderers/PBMDisplayView.m b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCacheRenderers/PBMDisplayView.m index a0a053322..d8a66fc2a 100644 --- a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCacheRenderers/PBMDisplayView.m +++ b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCacheRenderers/PBMDisplayView.m @@ -34,19 +34,18 @@ #import "PBMMacros.h" -@interface PBMDisplayView () +@interface PBMDisplayView () @property (nonatomic, strong, readonly, nonnull) Bid *bid; @property (nonatomic, strong, readonly, nonnull) AdUnitConfig *adConfiguration; -@property (nonatomic, strong, nullable) PBMTransactionFactory *transactionFactory; @property (nonatomic, strong, nullable) PBMAdViewManager *adViewManager; @property (nonatomic, strong, readonly, nonnull) PBMInterstitialDisplayProperties *interstitialDisplayProperties; -@end - +@property (nonatomic, strong, nullable) id renderer; +@end @implementation PBMDisplayView @@ -66,36 +65,14 @@ - (instancetype)initWithFrame:(CGRect)frame bid:(Bid *)bid adConfiguration:(AdUn } - (void)displayAd { - if (self.transactionFactory) { - return; - } - + self.renderer = [[PrebidMobilePluginRegister shared] getPluginForPreferredRendererWithBid:self.bid]; + + NSLog(@"Renderer: %@", self.renderer); self.adConfiguration.adConfiguration.winningBidAdFormat = self.bid.adFormat; - - @weakify(self); - self.transactionFactory = [[PBMTransactionFactory alloc] initWithBid:self.bid - adConfiguration:self.adConfiguration - connection:self.connection ?: PrebidServerConnection.shared - callback:^(PBMTransaction * _Nullable transaction, - NSError * _Nullable error) { - @strongify(self); - if (!self) { return; } - - if (error) { - [self reportFailureWithError:error]; - } else { - [self displayTransaction:transaction]; - } - }]; - - [PBMWinNotifier notifyThroughConnection:PrebidServerConnection.shared - winningBid:self.bid - callback:^(NSString *adMarkup) { - @strongify(self); - if (!self) { return; } - - [self.transactionFactory loadWithAdMarkup:adMarkup]; - }]; + id const connection = self.connection ?: PrebidServerConnection.shared; + dispatch_async(dispatch_get_main_queue(), ^{ + [self.renderer createBannerAdViewWith:self.frame bid:self.bid adConfiguration:self.adConfiguration connection:connection adViewDelegate:self]; + }); } - (BOOL)isCreativeOpened { @@ -174,26 +151,13 @@ - (void)modalManagerDidDismissModal { // MARK: - Private Helpers - (void)reportFailureWithError:(NSError *)error { - self.transactionFactory = nil; [self.loadingDelegate displayView:self didFailWithError:error]; } - (void)reportSuccess { - self.transactionFactory = nil; [self.loadingDelegate displayViewDidLoadAd:self]; } -- (void)displayTransaction:(PBMTransaction *)transaction { - id const connection = self.connection ?: PrebidServerConnection.shared; - self.adViewManager = [[PBMAdViewManager alloc] initWithConnection:connection modalManagerDelegate:self]; - self.adViewManager.adViewManagerDelegate = self; - self.adViewManager.adConfiguration = self.adConfiguration.adConfiguration; - if (self.adConfiguration.adConfiguration.winningBidAdFormat == AdFormat.video) { - self.adConfiguration.adConfiguration.isBuiltInVideo = YES; - } - [self.adViewManager handleExternalTransaction:transaction]; -} - - (void)interactionDelegateWillPresentModal { NSObject const *delegate = self.interactionDelegate; if ([delegate respondsToSelector:@selector(willPresentModalFrom:)]) { diff --git a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCacheRenderers/PrebidRenderer.swift b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCacheRenderers/PrebidRenderer.swift new file mode 100644 index 000000000..c4eeb847b --- /dev/null +++ b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCacheRenderers/PrebidRenderer.swift @@ -0,0 +1,120 @@ +/*   Copyright 2018-2021 Prebid.org, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import Foundation + +public class PrebidRenderer: NSObject, PrebidMobilePluginRenderer { + + public let name = "PrebidRenderer" + + public let version = Prebid.shared.version + + public var data: [AnyHashable: Any]? = nil + + private var adViewManager: PBMAdViewManager? + + public var transactionFactory: PBMTransactionFactory? + + public func isSupportRendering(for format: AdFormat?) -> Bool { + AdFormat.allCases.contains(where: { $0 == format }) + } + + public func setupBid(_ bid: Bid, adConfiguration: AdUnitConfig, connection: PrebidServerConnectionProtocol) { + + } + + public func createBannerAdView(with frame: CGRect, bid: Bid, adConfiguration: AdUnitConfig, + connection: PrebidServerConnectionProtocol, + adViewDelegate: (any PBMAdViewDelegate)?) { + + self.transactionFactory = PBMTransactionFactory(bid: bid, adConfiguration: adConfiguration, connection: connection) { [weak self] transaction, error in + self?.transactionFactory = nil + guard let transaction else { + adViewDelegate?.failed(toLoad: NSError(domain: "", code: 0)) + return + } + if let error { + adViewDelegate?.failed(toLoad: error) + return + } + self?.displayTransaction(transaction, adConfiguration: adConfiguration, connection: connection, adViewDelegate: adViewDelegate) + } + + PBMWinNotifier.notifyThroughConnection(PrebidServerConnection.shared, + winning: bid) { [weak self] adMarkup in + + self?.transactionFactory?.load(withAdMarkup: adMarkup!) + } + + } + + + private func displayTransaction(_ transaction: PBMTransaction, adConfiguration: AdUnitConfig, + connection: PrebidServerConnectionProtocol, + adViewDelegate: (any PBMAdViewDelegate)?) { + adViewManager = PBMAdViewManager(connection: connection, modalManagerDelegate: adViewDelegate) + adViewManager?.adViewManagerDelegate = adViewDelegate + adViewManager?.adConfiguration = adConfiguration.adConfiguration + + if adConfiguration.adConfiguration.winningBidAdFormat == .video { + adConfiguration.adConfiguration.isBuiltInVideo = true + } + + adViewManager?.handleExternalTransaction(transaction) + } + + + + + public func createInterstitialController(bid: Bid, adConfiguration: AdUnitConfig, + connection: PrebidServerConnectionProtocol, + adViewManagerDelegate adViewDelegate: InterstitialController?, + videoControlsConfig: VideoControlsConfiguration?) { + guard transactionFactory == nil else { + return + } + + adConfiguration.adConfiguration.winningBidAdFormat = bid.adFormat + videoControlsConfig?.initialize(with: bid.videoAdConfiguration) + + // This part is dedicating to test server-side ad configurations. + // Need to be removed when ext.prebid.passthrough will be available. + #if DEBUG + adConfiguration.adConfiguration.videoControlsConfig.initialize(with: bid.testVideoAdConfiguration) + #endif + transactionFactory = PBMTransactionFactory(bid: bid, + adConfiguration: adConfiguration, + connection: PrebidServerConnection.shared, + callback: { [weak adViewDelegate] transaction, error in + + if let transaction = transaction { + adViewDelegate?.display(transaction: transaction) + } else { + self.transactionFactory = nil + adViewDelegate?.reportFailureWithError(error) + } + }) + + PBMWinNotifier.notifyThroughConnection(PrebidServerConnection.shared, + winning: bid, + callback: { [weak self] adMarkup in + if let self = self, let adMarkup = adMarkup { + self.transactionFactory?.load(withAdMarkup: adMarkup) + } else { + Log.debug("Ad markup is empty") + } + }) + } + +} diff --git a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/AdUnitConfig.swift b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/AdUnitConfig.swift index d8b8707c1..68cbf069a 100644 --- a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/AdUnitConfig.swift +++ b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/AdUnitConfig.swift @@ -54,6 +54,8 @@ public class AdUnitConfig: NSObject, NSCopying { set { sizes = newValue } } + let fingerprint = UUID().uuidString + var _refreshInterval: TimeInterval = refreshIntervalDefault public var refreshInterval: TimeInterval { get { _refreshInterval } diff --git a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/Bid.swift b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/Bid.swift index 8bcabb4f6..da3b7e409 100644 --- a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/Bid.swift +++ b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/Bid.swift @@ -19,6 +19,11 @@ import StoreKit import UIKit public class Bid: NSObject { + + public static let KEY_RENDERER_NAME = "rendererName"; + + public static let KEY_RENDERER_VERSION = "rendererVersion"; + /// Bid price expressed as CPM although the actual transaction is for a unit impression only. /// Note that while the type indicates float, integer math is highly recommended /// when handling currencies (e.g., BigDecimal in Java). @@ -26,6 +31,7 @@ public class Bid: NSObject { bid.price.floatValue } + /// Win notice URL called by the exchange if the bid wins (not necessarily indicative of a delivered, /// viewed, or billable ad); optional means of serving ad markup. /// Substitution macros (Section 4.4) may be included in both the URL and optionally returned markup. @@ -50,6 +56,11 @@ public class Bid: NSObject { bid.ext.prebid?.targeting } + /// Targeting information that needs to be passed to the ad server SDK. + @objc public var meta: [String : String]? { + bid.ext.prebid?.meta + } + /** SKAdNetwork parameters about an App Store product. Used in the StoreKit @@ -108,4 +119,12 @@ public class Bid: NSObject { adm = macrosHelper.replaceMacros(in: bid.adm) nurl = macrosHelper.replaceMacros(in: bid.nurl) } + + public func getPreferredPluginRendererName() -> String? { + return meta?[Bid.KEY_RENDERER_NAME] + } + + public func getPreferredPluginRendererVersion() -> String? { + return meta?[Bid.KEY_RENDERER_VERSION] + } } diff --git a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/BidResponse.swift b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/BidResponse.swift index de99bde79..342a7d97e 100644 --- a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/BidResponse.swift +++ b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/BidResponse.swift @@ -101,4 +101,12 @@ public class BidResponse: NSObject { targetingInfo?[key] = value } + + public func getPreferredPluginRendererName() -> String? { + winningBid?.getPreferredPluginRendererName() + } + + public func getPreferredPluginRendererVersion() -> String? { + winningBid?.getPreferredPluginRendererVersion() + } } diff --git a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/ORTB/Prebid/PBMORTBBidExtPrebid.h b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/ORTB/Prebid/PBMORTBBidExtPrebid.h index 89b52fe46..b70c9e9e9 100644 --- a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/ORTB/Prebid/PBMORTBBidExtPrebid.h +++ b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/ORTB/Prebid/PBMORTBBidExtPrebid.h @@ -25,6 +25,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong, nullable) PBMORTBBidExtPrebidCache *cache; @property (nonatomic, copy, nullable) NSDictionary *targeting; +@property (nonatomic, copy, nullable) NSDictionary *meta; @property (nonatomic, copy, nullable) NSString *type; @property (nonatomic, copy, nullable) NSArray *passthrough; @property (nonatomic, strong, nullable) PBMORTBExtPrebidEvents *events; diff --git a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/ORTB/Prebid/PBMORTBBidExtPrebid.m b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/ORTB/Prebid/PBMORTBBidExtPrebid.m index 0296260c7..e7f3535d2 100644 --- a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/ORTB/Prebid/PBMORTBBidExtPrebid.m +++ b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/ORTB/Prebid/PBMORTBBidExtPrebid.m @@ -33,6 +33,7 @@ - (instancetype)initWithJsonDictionary:(PBMJsonDictionary *)jsonDictionary { } _targeting = jsonDictionary[@"targeting"]; + _meta = jsonDictionary[@"meta"]; _type = jsonDictionary[@"type"]; NSArray * const passthroughDics = jsonDictionary[@"passthrough"]; @@ -64,6 +65,7 @@ - (PBMJsonDictionary *)toJsonDictionary { ret[@"cache"] = [[self.cache toJsonDictionary] nullIfEmpty]; ret[@"targeting"] = self.targeting; + ret[@"meta"] = self.meta; ret[@"type"] = self.type; NSMutableArray * const passthroughDicArr = [[NSMutableArray alloc] initWithCapacity:self.passthrough.count]; diff --git a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/PBMPrebidParameterBuilder.m b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/PBMPrebidParameterBuilder.m index 01aca12f6..edae54fd6 100644 --- a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/PBMPrebidParameterBuilder.m +++ b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/PBMPrebidParameterBuilder.m @@ -74,6 +74,11 @@ - (void)buildBidRequest:(nonnull PBMORTBBidRequest *)bidRequest { bidRequest.extPrebid.storedBidResponses = [Prebid.shared getStoredBidResponses]; bidRequest.ortbObject = [self.adConfiguration.adConfiguration getCheckedOrtbConfig]; + NSArray *renderers = [[PrebidMobilePluginRegister shared] getAllPlugins]; + if (renderers.count != 0 && self.adConfiguration.adConfiguration.isOriginalAPI == false) { + bidRequest.extPrebid.sdk = [PBMPrebidParameterBuilder getRenderersJson]; + } + if (Prebid.shared.pbsDebug) { bidRequest.test = @1; } @@ -301,6 +306,20 @@ - (void)buildBidRequest:(nonnull PBMORTBBidRequest *)bidRequest { } } ++ (nonnull PBMJsonDictionary *)getRenderersJson { + PBMMutableJsonDictionary * const sdk = [PBMMutableJsonDictionary new]; + NSMutableArray *renderersArray = [NSMutableArray array]; + NSArray *renderers = [[PrebidMobilePluginRegister shared] getAllPlugins]; + for (id renderer in renderers) { + PBMMutableJsonDictionary *rendererDict = [PBMMutableJsonDictionary new]; + rendererDict[@"name"] = renderer.name; + rendererDict[@"version"] = renderer.version; + [renderersArray addObject:rendererDict]; + } + sdk[@"renderers"] = renderersArray; + return sdk; +} + + (PBMORTBFormat *)ortbFormatWithSize:(NSValue *)size { PBMORTBFormat * const format = [[PBMORTBFormat alloc] init]; CGSize const cgSize = size.CGSizeValue; diff --git a/PrebidMobileTests/NativeUnitTests/NativeAdMarkupTest.swift b/PrebidMobileTests/NativeUnitTests/NativeAdMarkupTest.swift index 6c3ad976e..8df31b0e1 100644 --- a/PrebidMobileTests/NativeUnitTests/NativeAdMarkupTest.swift +++ b/PrebidMobileTests/NativeUnitTests/NativeAdMarkupTest.swift @@ -3,7 +3,7 @@ Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at - +f http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software diff --git a/PrebidMobileTests/RenderingTests/Tests/FakeDataFabricationHelpers/RawSampleCustomRendererBidFabricator.swift b/PrebidMobileTests/RenderingTests/Tests/FakeDataFabricationHelpers/RawSampleCustomRendererBidFabricator.swift new file mode 100644 index 000000000..c879d6f4b --- /dev/null +++ b/PrebidMobileTests/RenderingTests/Tests/FakeDataFabricationHelpers/RawSampleCustomRendererBidFabricator.swift @@ -0,0 +1,33 @@ +// +// RawSampleCustomRendererBidFabricator.swift +// PrebidMobileTests +// +// Created by Richard Dépierre on 24/07/2024. +// Copyright © 2024 AppNexus. All rights reserved. +// + +import Foundation + +public class RawSampleCustomRendererBidFabricator { + static func makeSampleCustomRendererBid( + rendererName: String, + rendererVersion: String + ) -> PBMORTBBid { + let rawBid = PBMORTBBid() + rawBid.ext = .init() + rawBid.ext.prebid = .init() + + rawBid.price = NSNumber(value: 1.2) + rawBid.ext.prebid?.targeting = [ + "hb_pb": "1.2" + ] + rawBid.ext.prebid?.targeting?["hb_bidder"] = "appnexus" + rawBid.ext.prebid?.targeting?["hb_cache_id"] = "cacheid" + rawBid.ext.prebid?.type = "banner" + rawBid.ext.prebid?.meta = [ + Bid.KEY_RENDERER_NAME: rendererName, + Bid.KEY_RENDERER_VERSION: rendererVersion + ] + return rawBid + } +} diff --git a/PrebidMobileTests/UnitTests/PluginRegisterTest.swift b/PrebidMobileTests/UnitTests/PluginRegisterTest.swift new file mode 100644 index 000000000..ae32aaa8b --- /dev/null +++ b/PrebidMobileTests/UnitTests/PluginRegisterTest.swift @@ -0,0 +1,76 @@ +// +// PluginRegisterTest.swift +// PrebidMobileTests +// +// Created by Richard Dépierre on 24/07/2024. +// Copyright © 2024 AppNexus. All rights reserved. +// + +import XCTest + +@testable import PrebidMobile +import TestUtils + +class PluginRegisterTest: XCTestCase { + let prebidMobilePluginRegister = PrebidMobilePluginRegister.shared + let plugin = SampleCustomRenderer() + + override func setUp() { + super.setUp() + unregisterAllPlugins() + prebidMobilePluginRegister.registerPlugin(plugin) + } + + func unregisterAllPlugins() { + let plugins = prebidMobilePluginRegister.getAllPlugins() + for plugin in plugins { + prebidMobilePluginRegister.unregisterPlugin(plugin) + } + } + + func testRegisterPlugin() { + let containsPlugin = prebidMobilePluginRegister.containsPlugin(plugin) + let plugins = prebidMobilePluginRegister.getAllPlugins() + XCTAssertEqual(true, containsPlugin) + XCTAssertEqual(1, plugins.count) + } + + func testUnregisterPlugin() { + prebidMobilePluginRegister.unregisterPlugin(plugin) + + let containsPlugin = prebidMobilePluginRegister.containsPlugin(plugin) + XCTAssertEqual(false, containsPlugin) + } + + func testGetPluginForBidContainingSampleCustomRenderer() { + let bidResponse = Bid( + bid: RawSampleCustomRendererBidFabricator.makeSampleCustomRendererBid( + rendererName: "SampleCustomRenderer", + rendererVersion: "1.0.0" + ) + ) + + let pluginRenderer = prebidMobilePluginRegister.getPluginForPreferredRenderer( + bid: bidResponse + ) + XCTAssertEqual(pluginRenderer.name, plugin.name) + XCTAssertEqual(pluginRenderer.version, plugin.version) + } + + func testGetRTBListOfRenderersFor() { + let adUnitConfigBanner = AdUnitConfig( + configId: "configID", + size: CGSize( + width: 300, + height: 250 + ) + ) + let adUnitConfigError = AdUnitConfig(configId: "configID") + + var renderers = prebidMobilePluginRegister.getRTBListOfRenderersFor(for: adUnitConfigBanner) + XCTAssertEqual(1, renderers.count) + + renderers = prebidMobilePluginRegister.getRTBListOfRenderersFor(for: adUnitConfigError) + XCTAssertEqual(1, renderers.count) + } +} diff --git a/tools/PrebidValidator/Dr.Prebid/TestSummary/AdServerValidation/PBVLineItemsSetupValidator.m b/tools/PrebidValidator/Dr.Prebid/TestSummary/AdServerValidation/PBVLineItemsSetupValidator.m index 434def1c3..84c9dba85 100644 --- a/tools/PrebidValidator/Dr.Prebid/TestSummary/AdServerValidation/PBVLineItemsSetupValidator.m +++ b/tools/PrebidValidator/Dr.Prebid/TestSummary/AdServerValidation/PBVLineItemsSetupValidator.m @@ -89,13 +89,13 @@ - (void)startTest NSString *adUnitID = [[NSUserDefaults standardUserDefaults] stringForKey:kAdUnitIdKey]; NSString *bidPrice = [[NSUserDefaults standardUserDefaults] stringForKey:kBidPriceKey]; - GADAdSize GADAdSize = kGADAdSizeInvalid; + GADAdSize GADAdSize = GADAdSizeInvalid; CGSize adSize = CGSizeZero; if ([adSizeString isEqualToString:kSizeString320x50]) { - GADAdSize = kGADAdSizeBanner; + GADAdSize = GADAdSizeBanner; adSize = CGSizeMake(320, 50); } else if ([adSizeString isEqualToString:kSizeString300x250]) { - GADAdSize = kGADAdSizeMediumRectangle; + GADAdSize = GADAdSizeMediumRectangle; adSize = CGSizeMake(300, 250); } else if ([adSizeString isEqualToString:kSizeString320x480]) { adSize = CGSizeMake(320, 480); @@ -105,10 +105,10 @@ - (void)startTest GADAdSize = GADAdSizeFromCGSize(adSize); } else if ([adSizeString isEqualToString:kSizeString320x100]) { adSize = CGSizeMake(320, 100); - GADAdSize = kGADAdSizeLargeBanner; + GADAdSize = GADAdSizeLargeBanner; } else if ([adSizeString isEqualToString:kSizeString1x1]) { adSize = CGSizeMake(1, 1); - GADAdSize = kGADAdSizeFluid; + GADAdSize = GADAdSizeFluid; } if([adServerName isEqualToString:kDFPString]){ if ([adFormatName isEqualToString:kBannerString] || [adFormatName isEqualToString:kNativeString]) { diff --git a/tools/TestUtils/TestUtils.xcodeproj/project.pbxproj b/tools/TestUtils/TestUtils.xcodeproj/project.pbxproj index 9f30c677e..2663a5f43 100644 --- a/tools/TestUtils/TestUtils.xcodeproj/project.pbxproj +++ b/tools/TestUtils/TestUtils.xcodeproj/project.pbxproj @@ -3,10 +3,11 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + 3CD0ADF12C51274A006CDA6B /* SampleCustomRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CD0ADF02C51274A006CDA6B /* SampleCustomRenderer.swift */; }; 53AB8AB22851D9EF00837C70 /* OMSDK-Static_Prebidorg.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 53AB8AB12851D9EF00837C70 /* OMSDK-Static_Prebidorg.xcframework */; }; 53AB8AB32851D9EF00837C70 /* OMSDK-Static_Prebidorg.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 53AB8AB12851D9EF00837C70 /* OMSDK-Static_Prebidorg.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 92C0CEA027BD411E00C480EB /* emptyResponse.json in Resources */ = {isa = PBXBuildFile; fileRef = 92C0CE9F27BD411E00C480EB /* emptyResponse.json */; }; @@ -60,6 +61,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 3CD0ADF02C51274A006CDA6B /* SampleCustomRenderer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleCustomRenderer.swift; sourceTree = ""; }; 537B669C283DFCCD008AE9D1 /* OMSDK_Prebidorg.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = OMSDK_Prebidorg.xcframework; path = ../../Frameworks/OMSDK_Prebidorg.xcframework; sourceTree = ""; }; 53AB8AB12851D9EF00837C70 /* OMSDK-Static_Prebidorg.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = "OMSDK-Static_Prebidorg.xcframework"; path = "../../Frameworks/OMSDK-Static_Prebidorg.xcframework"; sourceTree = ""; }; 92C0CE9F27BD411E00C480EB /* emptyResponse.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = emptyResponse.json; sourceTree = ""; }; @@ -112,6 +114,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 3CD0ADEF2C5126FD006CDA6B /* SamplePluginRegister */ = { + isa = PBXGroup; + children = ( + 3CD0ADF02C51274A006CDA6B /* SampleCustomRenderer.swift */, + ); + path = SamplePluginRegister; + sourceTree = ""; + }; 537B669B283DFCCD008AE9D1 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -141,6 +151,7 @@ FA9E5E0F2649420C0049B214 /* TestUtils */ = { isa = PBXGroup; children = ( + 3CD0ADEF2C5126FD006CDA6B /* SamplePluginRegister */, FAC853582649462E009DC0F3 /* Category */, FAC8536726494646009DC0F3 /* Responses */, FAC8537B26494647009DC0F3 /* Stubbing */, @@ -253,6 +264,7 @@ TargetAttributes = { FA9E5E0C2649420C0049B214 = { CreatedOnToolsVersion = 12.4; + LastSwiftMigration = 1540; }; }; }; @@ -310,6 +322,7 @@ buildActionMask = 2147483647; files = ( FAC8535B2649462E009DC0F3 /* NSURLRequest+HTTPBodyTesting.m in Sources */, + 3CD0ADF12C51274A006CDA6B /* SampleCustomRenderer.swift in Sources */, FAC8539926494647009DC0F3 /* PBHTTPStubbingManager.m in Sources */, FAC8539A26494647009DC0F3 /* PBURLConnectionStub+NSURLSessionConfiguration.m in Sources */, FAC853652649463A009DC0F3 /* PBTestGlobal.m in Sources */, @@ -443,6 +456,7 @@ FA9E5E162649420C0049B214 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -459,6 +473,8 @@ PRODUCT_BUNDLE_IDENTIFIER = org.prebid.mobile.TestUtils; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -466,6 +482,7 @@ FA9E5E172649420C0049B214 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -482,6 +499,7 @@ PRODUCT_BUNDLE_IDENTIFIER = org.prebid.mobile.TestUtils; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/tools/TestUtils/TestUtils/SamplePluginRegister/SampleCustomRenderer.swift b/tools/TestUtils/TestUtils/SamplePluginRegister/SampleCustomRenderer.swift new file mode 100644 index 000000000..f27d68dbd --- /dev/null +++ b/tools/TestUtils/TestUtils/SamplePluginRegister/SampleCustomRenderer.swift @@ -0,0 +1,83 @@ +/*   Copyright 2018-2021 Prebid.org, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ +import Foundation +import PrebidMobile +import UIKit + +public class SampleCustomRenderer: NSObject, PrebidMobilePluginRenderer { + + public let name = "SampleCustomRenderer" + + public let version = "1.0.0" + + public var data: [AnyHashable: Any]? = nil + + private var adViewManager: PBMAdViewManager? + + public func isSupportRendering(for format: AdFormat?) -> Bool { + AdFormat.allCases.contains(where: { $0 == format }) + } + + public func setupBid(_ bid: Bid, adConfiguration: AdUnitConfig, connection: PrebidServerConnectionProtocol) { + + } + + public func createBannerAdView(with frame: CGRect, bid: Bid, adConfiguration: AdUnitConfig, + connection: PrebidServerConnectionProtocol, adViewDelegate: (any PBMAdViewDelegate)?) { + DispatchQueue.main.async { + if let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }), + let rootView = window.rootViewController?.view { + if let prebidBannerView = self.findPrebidBannerView(in: rootView) { + print("Found PrebidBannerView: \(prebidBannerView)") + + let label = UILabel() + label.text = "Prebid SDK - Custom Renderer" + label.textAlignment = .center + label.textColor = .black + label.backgroundColor = .yellow + label.translatesAutoresizingMaskIntoConstraints = false + + prebidBannerView.addSubview(label) + + NSLayoutConstraint.activate([ + label.centerXAnchor.constraint(equalTo: prebidBannerView.centerXAnchor), + label.centerYAnchor.constraint(equalTo: prebidBannerView.centerYAnchor), + label.widthAnchor.constraint(equalTo: prebidBannerView.widthAnchor), + label.heightAnchor.constraint(equalTo: prebidBannerView.heightAnchor) + ]) + + } else { + print("PrebidBannerView not found.") + } + } + } + } + + private func findPrebidBannerView(in view: UIView) -> UIView? { + if view.accessibilityIdentifier == "PrebidBannerView" { + return view + } + for subview in view.subviews { + if let foundView = findPrebidBannerView(in: subview) { + return foundView + } + } + return nil + } + + public func createInterstitialController(bid: Bid, adConfiguration: AdUnitConfig, connection: PrebidServerConnectionProtocol, + adViewManagerDelegate adViewDelegate: InterstitialController?, videoControlsConfig: VideoControlsConfiguration?) { + } +}