From 50488cae74f55d683c012329040a44d3e55b5d99 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Tue, 14 Jan 2025 14:55:19 +0200 Subject: [PATCH 01/21] feat: support SKAdN in native --- PrebidMobile.xcodeproj/project.pbxproj | 8 +++ PrebidMobile/AdUnits/Native/NativeAd.swift | 55 ++++++++++++++----- .../Addendum/PrebidSKAdNetworkHelper.swift | 22 ++++++++ .../PBMSKAdNetworksParameterBuilder.m | 8 +-- .../Utils/UIApplication+Extensions.swift | 32 +++++++++++ PrebidMobile/Utils/UIView+Extensions.swift | 11 ++++ 6 files changed, 116 insertions(+), 20 deletions(-) create mode 100644 PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift create mode 100644 PrebidMobile/Utils/UIApplication+Extensions.swift diff --git a/PrebidMobile.xcodeproj/project.pbxproj b/PrebidMobile.xcodeproj/project.pbxproj index 3755b9203..4237952d0 100644 --- a/PrebidMobile.xcodeproj/project.pbxproj +++ b/PrebidMobile.xcodeproj/project.pbxproj @@ -120,6 +120,8 @@ 5355ACAB29C454770014F16E /* CreativeModelCollectionMakerVASTTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5355ACAA29C454770014F16E /* CreativeModelCollectionMakerVASTTests.swift */; }; 535ADE0B2D2E970200DB888F /* SDKConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9F0999C2C78CC8A007DB464 /* SDKConsoleLogger.swift */; }; 535ADE102D2E987E00DB888F /* PluginEventDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535ADE0F2D2E987E00DB888F /* PluginEventDelegate.swift */; }; + 536469C92D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536469C82D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift */; }; + 53646A652D368DFB00F50B6D /* UIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53646A642D368DFB00F50B6D /* UIApplication+Extensions.swift */; }; 535ADE122D2EA2F500DB888F /* PrebidLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535ADE112D2EA2F500DB888F /* PrebidLogger.swift */; }; 536A39262A84C50F00B1CCEA /* StringExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536A39252A84C50F00B1CCEA /* StringExtensionsTest.swift */; }; 536A427F282D11DA0069E9B2 /* PrebidServerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536A427E282D11DA0069E9B2 /* PrebidServerConnection.swift */; }; @@ -1031,6 +1033,8 @@ 5355ACA829C454070014F16E /* VAST_with_empty_companion.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = VAST_with_empty_companion.xml; sourceTree = ""; }; 5355ACAA29C454770014F16E /* CreativeModelCollectionMakerVASTTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreativeModelCollectionMakerVASTTests.swift; sourceTree = ""; }; 535ADE0F2D2E987E00DB888F /* PluginEventDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginEventDelegate.swift; sourceTree = ""; }; + 536469C82D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidSKAdNetworkHelper.swift; sourceTree = ""; }; + 53646A642D368DFB00F50B6D /* UIApplication+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extensions.swift"; sourceTree = ""; }; 535ADE112D2EA2F500DB888F /* PrebidLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidLogger.swift; sourceTree = ""; }; 536A39252A84C50F00B1CCEA /* StringExtensionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensionsTest.swift; sourceTree = ""; }; 536A427E282D11DA0069E9B2 /* PrebidServerConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidServerConnection.swift; sourceTree = ""; }; @@ -1995,6 +1999,7 @@ 53F35F6E292BAE9B001C1183 /* UserDefaults+Extensions.swift */, 53842BBB29E565030069A4B7 /* NSString+Extensions.swift */, 53D3C3872C2BEE1E0074D99B /* URL+Extensions.swift */, + 53646A642D368DFB00F50B6D /* UIApplication+Extensions.swift */, ); path = Utils; sourceTree = ""; @@ -3474,6 +3479,7 @@ FAEE4D00262DC2B200AD9966 /* PbWebViewSearchError.swift */, FAEE4D01262DC2B200AD9966 /* AdViewUtils.swift */, FAEE4D02262DC2B200AD9966 /* IMAUtils.swift */, + 536469C82D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift */, ); path = Addendum; sourceTree = ""; @@ -4262,6 +4268,7 @@ FAEE4D16262DC2B200AD9966 /* NativeAdEventDelegate.swift in Sources */, 5BC379B2271F1D0000444D5E /* PBMCreativeModelCollectionMakerVAST.m in Sources */, 929638C727ABD66D00D30F3D /* JsonDecodable.swift in Sources */, + 53646A652D368DFB00F50B6D /* UIApplication+Extensions.swift in Sources */, 5BC37A8E271F1D0000444D5E /* RewardedAdUnit.swift in Sources */, 5BC37A9A271F1D0000444D5E /* BannerAdLoaderDelegate.swift in Sources */, 5BC37AF9271F2D3500444D5E /* NativeDataAssetType.swift in Sources */, @@ -4279,6 +4286,7 @@ 5BC37AE2271F1D0100444D5E /* PBMInterstitialLayoutConfigurator.m in Sources */, 53A368EB2AB2E95E00A03B3E /* NativeParameters.swift in Sources */, 5388F32627F46A7F00319FE4 /* AdViewButtonDecorator.swift in Sources */, + 536469C92D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift in Sources */, 5BC37AB6271F1D0000444D5E /* PBMOpenMeasurementFriendlyObstructionTypeBridge.m in Sources */, FAEE4D2C262DC2B200AD9966 /* Host.swift in Sources */, 5BC37AC8271F1D0100444D5E /* PBMBasicParameterBuilder.m in Sources */, diff --git a/PrebidMobile/AdUnits/Native/NativeAd.swift b/PrebidMobile/AdUnits/Native/NativeAd.swift index fe340e3cc..f33cba7b0 100644 --- a/PrebidMobile/AdUnits/Native/NativeAd.swift +++ b/PrebidMobile/AdUnits/Native/NativeAd.swift @@ -15,6 +15,7 @@ import Foundation import UIKit +import StoreKit /// Represents a native ad and handles its various properties and functionalities. @objcMembers @@ -30,6 +31,8 @@ public class NativeAd: NSObject, CacheExpiryDelegate { // MARK: - Internal properties + var bid: Bid? + private static let nativeAdIABShouldBeViewableForTrackingDuration = 1.0 private static let nativeAdCheckViewabilityForTrackingFrequency = 0.25 @@ -141,9 +144,12 @@ public class NativeAd: NSObject, CacheExpiryDelegate { return nil } - let bid = Bid(bid: rawBid) + let macrosHelper = PBMORTBMacrosHelper(bid: rawBid) + rawBid.adm = macrosHelper.replaceMacros(in: rawBid.adm) + rawBid.nurl = macrosHelper.replaceMacros(in: rawBid.nurl) let ad = NativeAd() + ad.bid = bid let internalEventTracker = PrebidServerEventTracker() @@ -169,6 +175,13 @@ public class NativeAd: NSObject, CacheExpiryDelegate { ad.eventManager.registerTracker(internalEventTracker) + if #available(iOS 14.5, *) { + if let skadn = bid.skadn, let imp = SkadnParametersManager.getSkadnImpression(for: skadn) { + let skadnEventTracker = SkadnEventTracker(with: imp) + ad.eventManager.registerTracker(skadnEventTracker) + } + } + // Track win event immediately ad.eventManager.trackEvent(.prebidWin) @@ -192,18 +205,6 @@ public class NativeAd: NSObject, CacheExpiryDelegate { unregisterViewFromTracking() } - private func canOpenString(_ string: String?) -> Bool { - guard let string = string else { - return false - } - let detector = try! NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) - if let match = detector.firstMatch(in: string, options: [], range: NSRange(location: 0, length: string.utf16.count)) { - return match.range.length == string.utf16.count - } else { - return false - } - } - /// Registers a view for tracking viewability and click events. /// - Parameters: /// - view: The view to register. @@ -353,6 +354,12 @@ public class NativeAd: NSObject, CacheExpiryDelegate { if let clickTrackers = nativeAdMarkup?.link?.clicktrackers { fireClickTrackers(clickTrackersUrls: clickTrackers) } + + // SKAdN + if let skadn = bid?.skadn, + let productParameters = SkadnParametersManager.getSkadnProductParameters(for: skadn) { + presentSKStoreProductViewController(with: productParameters) + } } else { Log.debug("Could not open click URL: \(clickUrl)") } @@ -368,7 +375,7 @@ public class NativeAd: NSObject, CacheExpiryDelegate { } } - private func openURLWithExternalBrowser(url : URL) -> Bool { + private func openURLWithExternalBrowser(url: URL) -> Bool { if UIApplication.shared.canOpenURL(url) { UIApplication.shared.open(url, options: [:], completionHandler: nil) return true @@ -377,9 +384,27 @@ public class NativeAd: NSObject, CacheExpiryDelegate { } } + private func presentSKStoreProductViewController(with productParameters: [String: Any]) { + DispatchQueue.main.async { + let skadnController = SKStoreProductViewController() + var viewControllerForPresentingModals = UIApplication.topViewController() + + if let viewForTracking = self.viewForTracking, + let viewController = viewForTracking.parentViewController { + viewControllerForPresentingModals = viewController + } + + viewControllerForPresentingModals?.present(skadnController, animated: true) + skadnController.loadProduct(withParameters: productParameters) { _, error in + if let error { + Log.error("Error occurred during SKStoreProductViewController product loading: \(error.localizedDescription)") + } + } + } + } } -private class NativeAdGestureRecognizerRecord : NSObject { +class NativeAdGestureRecognizerRecord : NSObject { weak var viewWithTracking: UIView? weak var gestureRecognizer: UIGestureRecognizer? diff --git a/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift b/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift new file mode 100644 index 000000000..d1fca3374 --- /dev/null +++ b/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift @@ -0,0 +1,22 @@ +// +// PrebidSKAdNetworkHelper.swift +// PrebidMobile +// +// Created by Olena Stepaniuk on 12.01.2025. +// Copyright © 2025 AppNexus. All rights reserved. +// + +import UIKit +import WebKit + +@objcMembers +public class PrebidSKAdNetworkHelper: NSObject { + + public func subscribeOnAdClickedBanner(viewController: UIViewController, adView: UIView) { + + } + + public func subscribeOnAdClickedVideo(viewController: UIViewController, adView: UIView) { + + } +} diff --git a/PrebidMobile/PrebidMobileRendering/Networking/Parameters/PBMSKAdNetworksParameterBuilder.m b/PrebidMobile/PrebidMobileRendering/Networking/Parameters/PBMSKAdNetworksParameterBuilder.m index 8364df3f1..15e190876 100644 --- a/PrebidMobile/PrebidMobileRendering/Networking/Parameters/PBMSKAdNetworksParameterBuilder.m +++ b/PrebidMobile/PrebidMobileRendering/Networking/Parameters/PBMSKAdNetworksParameterBuilder.m @@ -87,11 +87,9 @@ - (void)buildBidRequest:(PBMORTBBidRequest *)bidRequest { PBMLogError(@"Info.plist contains SKAdNetwork but sourceapp is nil!"); } - if (!self.adConfiguration.isOriginalAPI) { - for (PBMORTBImp *imp in bidRequest.imp) { - imp.extSkadn.sourceapp = [sourceapp copy]; - imp.extSkadn.skadnetids = skadnetids; - } + for (PBMORTBImp *imp in bidRequest.imp) { + imp.extSkadn.sourceapp = [sourceapp copy]; + imp.extSkadn.skadnetids = skadnetids; } } diff --git a/PrebidMobile/Utils/UIApplication+Extensions.swift b/PrebidMobile/Utils/UIApplication+Extensions.swift new file mode 100644 index 000000000..e88532de8 --- /dev/null +++ b/PrebidMobile/Utils/UIApplication+Extensions.swift @@ -0,0 +1,32 @@ +/*   Copyright 2018-2025 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 + +extension UIApplication { + + static func topViewController() -> UIViewController? { + var topController = UIApplication.shared + .windows + .filter({ $0.isKeyWindow }).first? + .rootViewController + + while let presentedViewController = topController?.presentedViewController { + topController = presentedViewController + } + + return topController + } +} diff --git a/PrebidMobile/Utils/UIView+Extensions.swift b/PrebidMobile/Utils/UIView+Extensions.swift index 3b2839066..85914b89f 100644 --- a/PrebidMobile/Utils/UIView+Extensions.swift +++ b/PrebidMobile/Utils/UIView+Extensions.swift @@ -34,4 +34,15 @@ extension UIView { getSubview(view: self) return all } + + var parentViewController: UIViewController? { + var responder = self.next + while responder != nil { + if let viewController = responder as? UIViewController { + return viewController + } + responder = responder?.next + } + return nil + } } From 0324cc7a670372ab3976a2106591fb567060f8e3 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Tue, 14 Jan 2025 15:24:42 +0200 Subject: [PATCH 02/21] feat: proccess url in hidden manager if SKAdN is present --- .build/workspace-state.json | 11 +++ PrebidMobile/AdUnits/Native/NativeAd.swift | 67 +++++++++++-------- .../AdTypes/AdView/HiddenWebViewManager.swift | 10 ++- .../AdTypes/AdView/PBMAbstractCreative.m | 7 +- 4 files changed, 58 insertions(+), 37 deletions(-) create mode 100644 .build/workspace-state.json diff --git a/.build/workspace-state.json b/.build/workspace-state.json new file mode 100644 index 000000000..71deae90f --- /dev/null +++ b/.build/workspace-state.json @@ -0,0 +1,11 @@ +{ + "object" : { + "artifacts" : [ + + ], + "dependencies" : [ + + ] + }, + "version" : 6 +} \ No newline at end of file diff --git a/PrebidMobile/AdUnits/Native/NativeAd.swift b/PrebidMobile/AdUnits/Native/NativeAd.swift index f33cba7b0..1a6f46a2c 100644 --- a/PrebidMobile/AdUnits/Native/NativeAd.swift +++ b/PrebidMobile/AdUnits/Native/NativeAd.swift @@ -16,6 +16,7 @@ import Foundation import UIKit import StoreKit +import WebKit /// Represents a native ad and handles its various properties and functionalities. @objcMembers @@ -347,43 +348,42 @@ public class NativeAd: NSObject, CacheExpiryDelegate { } @objc private func handleClick() { - self.delegate?.adWasClicked?(ad: self) - if let clickUrl = nativeAdMarkup?.link?.url, - let url = clickUrl.encodedURL(with: .urlQueryAllowed) { - if openURLWithExternalBrowser(url: url) { - if let clickTrackers = nativeAdMarkup?.link?.clicktrackers { - fireClickTrackers(clickTrackersUrls: clickTrackers) - } - - // SKAdN - if let skadn = bid?.skadn, - let productParameters = SkadnParametersManager.getSkadnProductParameters(for: skadn) { - presentSKStoreProductViewController(with: productParameters) - } - } else { - Log.debug("Could not open click URL: \(clickUrl)") - } + delegate?.adWasClicked?(ad: self) + + guard let clickUrl = nativeAdMarkup?.link?.url, + let url = clickUrl.encodedURL(with: .urlQueryAllowed) else { + return + } + + // SKAdN + if let skadn = bid?.skadn, + let productParameters = SkadnParametersManager.getSkadnProductParameters(for: skadn) { + HiddenWebViewManager( + frame: .zero, + landingPageString: url + ).openHiddenWebView() + + presentSKStoreProductViewController(with: productParameters) + fireClickTrackers() + } + // Normal clickthrough + else if openURLWithExternalBrowser(url: url) { + fireClickTrackers() + } else { + Log.debug("Could not open click URL: \(clickUrl)") } } - - private func fireClickTrackers(clickTrackersUrls: [String]) { - if clickTrackersUrls.count > 0 { - TrackerManager.shared.fireTrackerURLArray(arrayWithURLs: clickTrackersUrls) { + private func fireClickTrackers() { + guard let clickTrackersURLs = nativeAdMarkup?.link?.clicktrackers else { return } + + if clickTrackersURLs.count > 0 { + TrackerManager.shared.fireTrackerURLArray(arrayWithURLs: clickTrackersURLs) { _ in } } } - private func openURLWithExternalBrowser(url: URL) -> Bool { - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - return true - } else { - return false - } - } - private func presentSKStoreProductViewController(with productParameters: [String: Any]) { DispatchQueue.main.async { let skadnController = SKStoreProductViewController() @@ -402,6 +402,15 @@ public class NativeAd: NSObject, CacheExpiryDelegate { } } } + + private func openURLWithExternalBrowser(url: URL) -> Bool { + if UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + return true + } else { + return false + } + } } class NativeAdGestureRecognizerRecord : NSObject { diff --git a/PrebidMobile/PrebidMobileRendering/AdTypes/AdView/HiddenWebViewManager.swift b/PrebidMobile/PrebidMobileRendering/AdTypes/AdView/HiddenWebViewManager.swift index 42745324f..5fcdd80d3 100644 --- a/PrebidMobile/PrebidMobileRendering/AdTypes/AdView/HiddenWebViewManager.swift +++ b/PrebidMobile/PrebidMobileRendering/AdTypes/AdView/HiddenWebViewManager.swift @@ -24,9 +24,12 @@ public class HiddenWebViewManager: NSObject { private let landingPageURL: URL? - public init(webView: WKWebView, landingPageString: String) { - hiddenWebView = webView - landingPageURL = URL(string: landingPageString) + public init(frame: CGRect, landingPageString: URL) { + hiddenWebView = WKWebView(frame: frame) + hiddenWebView.isHidden = true + + self.landingPageURL = landingPageString + super.init() } @@ -35,6 +38,7 @@ public class HiddenWebViewManager: NSObject { Log.error("landingPageURL is not valid.") return } + let request = URLRequest(url: landingPageURL) hiddenWebView.load(request) } diff --git a/PrebidMobile/PrebidMobileRendering/AdTypes/AdView/PBMAbstractCreative.m b/PrebidMobile/PrebidMobileRendering/AdTypes/AdView/PBMAbstractCreative.m index f108f7cd0..9e646d9d2 100644 --- a/PrebidMobile/PrebidMobileRendering/AdTypes/AdView/PBMAbstractCreative.m +++ b/PrebidMobile/PrebidMobileRendering/AdTypes/AdView/PBMAbstractCreative.m @@ -52,8 +52,6 @@ @interface PBMAbstractCreative() @property (nonatomic, assign) BOOL adWasShown; -@property (nonatomic, nonnull) WKWebView *hiddenWebView; - @property (nonatomic, strong, nullable) PBMSafariVCOpener * safariOpener; @end @@ -351,9 +349,8 @@ - (BOOL)handleNormalClickthrough:(NSURL *)url - (BOOL)handleProductClickthrough:(NSURL*)url productParams:(NSDictionary *)productParams onExit:(nonnull PBMVoidBlock)onClickthroughExitBlock { - self.hiddenWebView = [[WKWebView alloc] initWithFrame:self.view.frame]; - PBMHiddenWebViewManager *webViewManager = [[PBMHiddenWebViewManager alloc] initWithWebView:self.hiddenWebView landingPageString:url.absoluteString]; - [self.hiddenWebView setHidden:YES]; + PBMHiddenWebViewManager *webViewManager = [[PBMHiddenWebViewManager alloc] initWithFrame:self.view.frame + landingPageString:url]; [webViewManager openHiddenWebView]; if (!self.viewControllerForPresentingModals) { From b146fcb0600650e0a5ee34b7891577fc6fdb3a83 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Tue, 14 Jan 2025 15:26:23 +0200 Subject: [PATCH 03/21] feat: update gitignore --- .build/workspace-state.json | 11 ----------- .gitignore | 5 ++++- 2 files changed, 4 insertions(+), 12 deletions(-) delete mode 100644 .build/workspace-state.json diff --git a/.build/workspace-state.json b/.build/workspace-state.json deleted file mode 100644 index 71deae90f..000000000 --- a/.build/workspace-state.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "object" : { - "artifacts" : [ - - ], - "dependencies" : [ - - ] - }, - "version" : 6 -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index cb5c53f03..a35e57db1 100644 --- a/.gitignore +++ b/.gitignore @@ -111,4 +111,7 @@ xcuserdata/ # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # -Pods/ \ No newline at end of file +Pods/ + +# Visual Studio Code +.build/ \ No newline at end of file From eed44807c0528849c33807611c6c541c75fe9cf2 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 15 Jan 2025 11:58:57 +0200 Subject: [PATCH 04/21] feat: dismiss SKStoreProductViewController when finished --- PrebidMobile/AdUnits/Native/NativeAd.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/PrebidMobile/AdUnits/Native/NativeAd.swift b/PrebidMobile/AdUnits/Native/NativeAd.swift index 1a6f46a2c..50b7f88e8 100644 --- a/PrebidMobile/AdUnits/Native/NativeAd.swift +++ b/PrebidMobile/AdUnits/Native/NativeAd.swift @@ -50,6 +50,8 @@ public class NativeAd: NSObject, CacheExpiryDelegate { private let eventManager = EventManager() + private var viewControllerForPresentingModals: UIViewController? + public var privacyUrl: String? { set { nativeAdMarkup?.privacy = newValue @@ -387,6 +389,7 @@ public class NativeAd: NSObject, CacheExpiryDelegate { private func presentSKStoreProductViewController(with productParameters: [String: Any]) { DispatchQueue.main.async { let skadnController = SKStoreProductViewController() + skadnController.delegate = self var viewControllerForPresentingModals = UIApplication.topViewController() if let viewForTracking = self.viewForTracking, @@ -394,6 +397,8 @@ public class NativeAd: NSObject, CacheExpiryDelegate { viewControllerForPresentingModals = viewController } + self.viewControllerForPresentingModals = viewControllerForPresentingModals + viewControllerForPresentingModals?.present(skadnController, animated: true) skadnController.loadProduct(withParameters: productParameters) { _, error in if let error { @@ -413,6 +418,13 @@ public class NativeAd: NSObject, CacheExpiryDelegate { } } +extension NativeAd: SKStoreProductViewControllerDelegate { + + public func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { + viewControllerForPresentingModals?.dismiss(animated: true) + } +} + class NativeAdGestureRecognizerRecord : NSObject { weak var viewWithTracking: UIView? weak var gestureRecognizer: UIGestureRecognizer? From fd53185902c7ee9f15bdc0c78053627698f3210d Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Thu, 16 Jan 2025 09:40:51 +0200 Subject: [PATCH 05/21] feat: implement SKAdN helper for ads that contain hb_ keys --- ...iginalAPIDisplayBannerViewController.swift | 6 +- ...OriginalAPIVideoBannerViewController.swift | 3 + PrebidMobile/AdUnits/AdUnit.swift | 17 ++- PrebidMobile/Addendum/AdViewUtils.swift | 18 +++ .../Addendum/PrebidSKAdNetworkHelper.swift | 113 ++++++++++++++++-- .../String+Extensions.swift | 4 +- 6 files changed, 145 insertions(+), 16 deletions(-) diff --git a/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIDisplayBannerViewController.swift b/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIDisplayBannerViewController.swift index aea4db635..395f8a550 100644 --- a/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIDisplayBannerViewController.swift +++ b/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIDisplayBannerViewController.swift @@ -17,7 +17,7 @@ import UIKit import PrebidMobile import GoogleMobileAds -fileprivate let storedImpDisplayBanner = "prebid-demo-banner-320-50" +fileprivate let storedImpDisplayBanner = "prebid-demo-banner-320-50-skadn" fileprivate let gamAdUnitDisplayBannerOriginal = "/21808260008/prebid_demo_app_original_api_banner" class GAMOriginalAPIDisplayBannerViewController: BannerBaseViewController, GADBannerViewDelegate { @@ -25,6 +25,8 @@ class GAMOriginalAPIDisplayBannerViewController: BannerBaseViewController, GADBa // Prebid private var adUnit: BannerAdUnit! + private let skadnHelper = PrebidSKAdNetworkHelper() + // GAM private var gamBanner: GAMBannerView! @@ -94,6 +96,8 @@ class GAMOriginalAPIDisplayBannerViewController: BannerBaseViewController, GADBa }, failure: { (error) in PrebidDemoLogger.shared.error("Error occuring during searching for Prebid creative size: \(error)") }) + + skadnHelper.subscribeOnAdClicked(adView: bannerView, viewController: self) } func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) { diff --git a/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIVideoBannerViewController.swift b/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIVideoBannerViewController.swift index 776e370cc..26e211a7b 100644 --- a/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIVideoBannerViewController.swift +++ b/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIVideoBannerViewController.swift @@ -24,6 +24,7 @@ class GAMOriginalAPIVideoBannerViewController: BannerBaseViewController, GADBann // Prebid private var adUnit: BannerAdUnit! + private let skadnHelper = PrebidSKAdNetworkHelper() // GAM private var gamBanner: GAMBannerView! @@ -77,6 +78,8 @@ class GAMOriginalAPIVideoBannerViewController: BannerBaseViewController, GADBann }, failure: { (error) in PrebidDemoLogger.shared.error("Error occuring during searching for Prebid creative size: \(error)") }) + + skadnHelper.subscribeOnAdClicked(adView: bannerView, viewController: self) } func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) { diff --git a/PrebidMobile/AdUnits/AdUnit.swift b/PrebidMobile/AdUnits/AdUnit.swift index c201e0819..dfc28371e 100644 --- a/PrebidMobile/AdUnits/AdUnit.swift +++ b/PrebidMobile/AdUnits/AdUnit.swift @@ -251,15 +251,24 @@ public class AdUnit: NSObject, DispatcherDelegate { return .prebidDemandFetchSuccess } - private func cacheBidIfNeeded(_ winningBid: Bid) -> String? { - guard winningBid.adFormat == .native else { + private func cacheBidIfNeeded(_ winningBid: Bid) -> String? { + let isNative = winningBid.adFormat == .native + let isSkadnPresent = winningBid.skadn != nil && SkadnParametersManager + .getSkadnProductParameters(for: winningBid.skadn!) != nil + + guard isNative || isSkadnPresent else { return nil } - let expireInterval = TimeInterval(truncating: winningBid.bid.exp ?? CacheManager.cacheManagerExpireInterval as NSNumber) + let expireInterval = TimeInterval( + truncating: winningBid.bid.exp ?? CacheManager.cacheManagerExpireInterval as NSNumber + ) do { - if let cacheId = CacheManager.shared.save(content: try winningBid.bid.toJsonString(), expireInterval: expireInterval), !cacheId.isEmpty { + if let cacheId = CacheManager.shared.save( + content: try winningBid.bid.toJsonString(), + expireInterval: expireInterval + ), !cacheId.isEmpty { return cacheId } } catch { diff --git a/PrebidMobile/Addendum/AdViewUtils.swift b/PrebidMobile/Addendum/AdViewUtils.swift index 860d87da0..476fa73cb 100644 --- a/PrebidMobile/Addendum/AdViewUtils.swift +++ b/PrebidMobile/Addendum/AdViewUtils.swift @@ -29,6 +29,10 @@ public final class AdViewUtils: NSObject { // hb_cache_id static let cacheIDValueRegexExpression = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}" static let cacheIDObjectRegexExpression = "hb_cache_id\\W+\(cacheIDValueRegexExpression)" + + // hb_cache_id_local + static let cacheIDLocalValueRegexExpression = "Prebid_[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}" + static let cacheIDLocalObjectRegexExpression = "hb_cache_id_local\\W+\(cacheIDLocalValueRegexExpression)" private override init() {} @@ -76,6 +80,19 @@ public final class AdViewUtils: NSObject { completion: completion ) } + + static func findPrebidLocalCacheID( + _ adView: UIView, + completion: @escaping (Result) -> Void + ) { + findValueInCreative( + adView, + objectRegex: cacheIDLocalObjectRegexExpression, + valueRegex: cacheIDLocalValueRegexExpression, + parseResult: { .success($0) }, + completion: completion + ) + } static func findValueInCreative( _ adView: UIView, @@ -208,3 +225,4 @@ final class PbWebViewSearchErrorFactory { return PbWebViewSearchError(domain: "com.prebidmobile.ios", code: code, userInfo: [NSLocalizedDescriptionKey: description]) } } + diff --git a/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift b/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift index d1fca3374..757f996dc 100644 --- a/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift +++ b/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift @@ -1,22 +1,117 @@ -// -// PrebidSKAdNetworkHelper.swift -// PrebidMobile -// -// Created by Olena Stepaniuk on 12.01.2025. -// Copyright © 2025 AppNexus. All rights reserved. -// +/* Copyright 2018-2025 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 WebKit +import StoreKit +/// This class provides utilities for tracking ad clicks and presenting the SKStoreProductViewController @objcMembers public class PrebidSKAdNetworkHelper: NSObject { - public func subscribeOnAdClickedBanner(viewController: UIViewController, adView: UIView) { + private weak var viewControllerForPresentingModals: UIViewController? + + + /// Subscribes to ad click events on the provided ad view and sets up the SKAdNetwork parameters. + /// - Parameters: + /// - adView: The ad view where click events will be tracked. + /// - viewController: The view controller used to present modals, such as the SKStoreProductViewController. + public func subscribeOnAdClicked(adView: UIView, viewController: UIViewController) { + self.viewControllerForPresentingModals = viewController + AdViewUtils.findPrebidLocalCacheID(adView) { result in + guard case .success(let cacheID) = result else { + return + } + + guard let bidString = CacheManager.shared.get(cacheId: cacheID), + let bidDic = Utils.shared.getDictionaryFromString(bidString) else { + Log.error("No bid response for provided cache id.") + return + } + + guard let rawBid = PBMORTBBid(jsonDictionary: bidDic, extParser: { extDic in + return PBMORTBBidExt(jsonDictionary: extDic) + }) else { + return + } + + let bid = Bid(bid: rawBid) + + guard let skadn = bid.skadn, + let skadnParameters = SkadnParametersManager.getSkadnProductParameters(for: skadn) else { + return + } + + adView.subviews.forEach({ + if $0 is TouchTrackingOverlayView { + $0.removeFromSuperview() + } + }) + + let overlayView = TouchTrackingOverlayView(frame: adView.frame) + overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5) + adView.addSubview(overlayView) + + overlayView.onClick = { [weak self] in + self?.presentSKStoreProductViewController(with: skadnParameters) + } + } + } + + private func presentSKStoreProductViewController(with productParameters: [String: Any]) { + DispatchQueue.main.async { + let skadnController = SKStoreProductViewController() + skadnController.delegate = self + + self.viewControllerForPresentingModals?.present(skadnController, animated: true) + skadnController.loadProduct(withParameters: productParameters) { _, error in + if let error { + Log.error("Error occurred during SKStoreProductViewController product loading: \(error.localizedDescription)") + } + } + } } +} + +// MARK: - SKStoreProductViewControllerDelegate + +extension PrebidSKAdNetworkHelper: SKStoreProductViewControllerDelegate { + + public func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { + viewControllerForPresentingModals?.dismiss(animated: true) + } +} + +/// A custom overlay view for tracking touch interactions. +private class TouchTrackingOverlayView: UIView { + + var onClick: (() -> Void)? + + private var lastTimestamp: TimeInterval? - public func subscribeOnAdClickedVideo(viewController: UIViewController, adView: UIView) { + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.point(inside: point, with: event) { + if event?.timestamp != lastTimestamp { + onClick?() + } + + lastTimestamp = event?.timestamp + return nil + } + return super.hitTest(point, with: event) } } diff --git a/PrebidMobile/PrebidMobileRendering/ExtensionsAndWrappers/String+Extensions.swift b/PrebidMobile/PrebidMobileRendering/ExtensionsAndWrappers/String+Extensions.swift index 0823d8dd7..be6086268 100644 --- a/PrebidMobile/PrebidMobileRendering/ExtensionsAndWrappers/String+Extensions.swift +++ b/PrebidMobile/PrebidMobileRendering/ExtensionsAndWrappers/String+Extensions.swift @@ -79,12 +79,12 @@ extension String { // MARK: - Regex Extensions extension String { - + func matchAndCheck(regex: String) -> String? { let matched = self.matches(for: regex) return matched.isEmpty ? nil : matched[0] } - + func matches(for regex: String) -> [String] { do { let regex = try NSRegularExpression(pattern: regex) From c97b03bf0a9573d24e4a810b34eaaf7611c37c49 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Thu, 16 Jan 2025 09:55:22 +0200 Subject: [PATCH 06/21] feat: update tests --- PrebidMobile.xcodeproj/project.pbxproj | 8 +- .../Addendum/PrebidSKAdNetworkHelper.swift | 1 - .../StringExtensionsTest.swift | 67 ------ .../TestStringExtension.swift | 47 +++++ .../addendum/AdViewUtilsTests.swift | 195 +++++++++++++++--- 5 files changed, 218 insertions(+), 100 deletions(-) delete mode 100644 PrebidMobileTests/RenderingTests/Tests/UtilitiesExtensionsTests/StringExtensionsTest.swift diff --git a/PrebidMobile.xcodeproj/project.pbxproj b/PrebidMobile.xcodeproj/project.pbxproj index 4237952d0..7b1abe52c 100644 --- a/PrebidMobile.xcodeproj/project.pbxproj +++ b/PrebidMobile.xcodeproj/project.pbxproj @@ -120,10 +120,9 @@ 5355ACAB29C454770014F16E /* CreativeModelCollectionMakerVASTTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5355ACAA29C454770014F16E /* CreativeModelCollectionMakerVASTTests.swift */; }; 535ADE0B2D2E970200DB888F /* SDKConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9F0999C2C78CC8A007DB464 /* SDKConsoleLogger.swift */; }; 535ADE102D2E987E00DB888F /* PluginEventDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535ADE0F2D2E987E00DB888F /* PluginEventDelegate.swift */; }; + 535ADE122D2EA2F500DB888F /* PrebidLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535ADE112D2EA2F500DB888F /* PrebidLogger.swift */; }; 536469C92D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536469C82D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift */; }; 53646A652D368DFB00F50B6D /* UIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53646A642D368DFB00F50B6D /* UIApplication+Extensions.swift */; }; - 535ADE122D2EA2F500DB888F /* PrebidLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535ADE112D2EA2F500DB888F /* PrebidLogger.swift */; }; - 536A39262A84C50F00B1CCEA /* StringExtensionsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536A39252A84C50F00B1CCEA /* StringExtensionsTest.swift */; }; 536A427F282D11DA0069E9B2 /* PrebidServerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536A427E282D11DA0069E9B2 /* PrebidServerConnection.swift */; }; 536A4283282D12E80069E9B2 /* PrebidServerConnectionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536A4282282D12E80069E9B2 /* PrebidServerConnectionProtocol.swift */; }; 5379F6BA2ABB711500B0B7A9 /* PrebidAdUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5379F6B92ABB711500B0B7A9 /* PrebidAdUnitTests.swift */; }; @@ -1033,10 +1032,9 @@ 5355ACA829C454070014F16E /* VAST_with_empty_companion.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = VAST_with_empty_companion.xml; sourceTree = ""; }; 5355ACAA29C454770014F16E /* CreativeModelCollectionMakerVASTTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreativeModelCollectionMakerVASTTests.swift; sourceTree = ""; }; 535ADE0F2D2E987E00DB888F /* PluginEventDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginEventDelegate.swift; sourceTree = ""; }; + 535ADE112D2EA2F500DB888F /* PrebidLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidLogger.swift; sourceTree = ""; }; 536469C82D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidSKAdNetworkHelper.swift; sourceTree = ""; }; 53646A642D368DFB00F50B6D /* UIApplication+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extensions.swift"; sourceTree = ""; }; - 535ADE112D2EA2F500DB888F /* PrebidLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidLogger.swift; sourceTree = ""; }; - 536A39252A84C50F00B1CCEA /* StringExtensionsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensionsTest.swift; sourceTree = ""; }; 536A427E282D11DA0069E9B2 /* PrebidServerConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidServerConnection.swift; sourceTree = ""; }; 536A4282282D12E80069E9B2 /* PrebidServerConnectionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidServerConnectionProtocol.swift; sourceTree = ""; }; 5379F6B92ABB711500B0B7A9 /* PrebidAdUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidAdUnitTests.swift; sourceTree = ""; }; @@ -3240,7 +3238,6 @@ 925D5E542737F2A900A8A2B5 /* PBMFunctionsPrivateTest.m */, 925D5E562737F2D900A8A2B5 /* PBMMacrosTest.m */, 925D5E582737F35500A8A2B5 /* UIViewExtensionsTest.swift */, - 536A39252A84C50F00B1CCEA /* StringExtensionsTest.swift */, ); path = UtilitiesExtensionsTests; sourceTree = ""; @@ -4105,7 +4102,6 @@ 925D5E302737ED8C00A8A2B5 /* PBMRewardedVideoViewTest.swift in Sources */, 922AFCCB27353B9A00732C53 /* MockServerRule.m in Sources */, 925D5D872737BAB300A8A2B5 /* PBMOpenMeasurementWrapperTest.swift in Sources */, - 536A39262A84C50F00B1CCEA /* StringExtensionsTest.swift in Sources */, 925D5E782737F60100A8A2B5 /* VastEventTrackingTest.swift in Sources */, 53514CC82D01B5F900A480C0 /* RewardedAdUnitTest.swift in Sources */, 533FDF852A12030C0066ED5A /* MockPrebidJSLibraryManager.swift in Sources */, diff --git a/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift b/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift index 757f996dc..b89463e04 100644 --- a/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift +++ b/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift @@ -23,7 +23,6 @@ public class PrebidSKAdNetworkHelper: NSObject { private weak var viewControllerForPresentingModals: UIViewController? - /// Subscribes to ad click events on the provided ad view and sets up the SKAdNetwork parameters. /// - Parameters: /// - adView: The ad view where click events will be tracked. diff --git a/PrebidMobileTests/RenderingTests/Tests/UtilitiesExtensionsTests/StringExtensionsTest.swift b/PrebidMobileTests/RenderingTests/Tests/UtilitiesExtensionsTests/StringExtensionsTest.swift deleted file mode 100644 index 8b2fbd8d8..000000000 --- a/PrebidMobileTests/RenderingTests/Tests/UtilitiesExtensionsTests/StringExtensionsTest.swift +++ /dev/null @@ -1,67 +0,0 @@ -/*   Copyright 2018-2023 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 XCTest -@testable import PrebidMobile - -class StringExtensionsTest: XCTestCase { - - func testEncodeURL() { - let str1 = "https://example.com/search?q=Prebid mobile&page=1" - let encodedStr1 = str1.encodedURL(with: .urlQueryAllowed)?.absoluteString - - XCTAssertNotEqual(str1, encodedStr1) - XCTAssertTrue(encodedStr1!.contains("%20")) - - let str2 = "https://example.com/search?q=Prebid%20mobile&page=1" - let encodedStr2 = str2.encodedURL(with: .urlQueryAllowed)?.absoluteString - - XCTAssertEqual(str2, encodedStr2) - } - - func testRegexMatches() { - var result = "aaa aaa".matches(for: "^a") - XCTAssert(result.count == 1) - XCTAssert(result[0] == "a") - - result = "aaa aaa".matches(for: "^b") - XCTAssert(result.count == 0) - - result = "^a".matches(for: "aaa aaa") - XCTAssert(result.count == 0) - - result = "{ \n adManagerResponse:\"hb_size\":[\"728x90\"],\"hb_size_rubicon\":[\"1x1\"],moPubResponse:\"hb_size:300x250\" \n }".matches(for: "[0-9]+x[0-9]+") - XCTAssert(result.count == 3) - XCTAssert(result[0] == "728x90") - XCTAssert(result[1] == "1x1") - XCTAssert(result[2] == "300x250") - - result = "{ \n adManagerResponse:\"hb_size\":[\"728x90\"],\"hb_size_rubicon\":[\"1x1\"],moPubResponse:\"hb_size:300x250\" \n }".matches(for: "hb_size\\W+[0-9]+x[0-9]+") - - XCTAssert(result.count == 2) - XCTAssert(result[0] == "hb_size\":[\"728x90") - XCTAssert(result[1] == "hb_size:300x250") - } - - func testRegexMatchAndCheck() { - var result = "aaa aaa".matchAndCheck(regex: "^a") - - XCTAssertNotNil(result) - XCTAssert(result == "a") - - result = "aaa aaa".matchAndCheck(regex: "^b") - XCTAssertNil(result) - } -} diff --git a/PrebidMobileTests/RenderingTests/Tests/UtilitiesExtensionsTests/TestStringExtension.swift b/PrebidMobileTests/RenderingTests/Tests/UtilitiesExtensionsTests/TestStringExtension.swift index fee7d9406..f5bc602fa 100644 --- a/PrebidMobileTests/RenderingTests/Tests/UtilitiesExtensionsTests/TestStringExtension.swift +++ b/PrebidMobileTests/RenderingTests/Tests/UtilitiesExtensionsTests/TestStringExtension.swift @@ -245,6 +245,19 @@ class TestStringExtension: XCTestCase { XCTAssert(expected == actual, "expected \(String(describing: expected)), got \(String(describing: actual))") } + func testEncodeURL() { + let str1 = "https://example.com/search?q=Prebid mobile&page=1" + let encodedStr1 = str1.encodedURL(with: .urlQueryAllowed)?.absoluteString + + XCTAssertNotEqual(str1, encodedStr1) + XCTAssertTrue(encodedStr1!.contains("%20")) + + let str2 = "https://example.com/search?q=Prebid%20mobile&page=1" + let encodedStr2 = str2.encodedURL(with: .urlQueryAllowed)?.absoluteString + + XCTAssertEqual(str2, encodedStr2) + } + func testStringToCGSize() { var result = "300x250".toCGSize() XCTAssertNotNil(result) @@ -259,4 +272,38 @@ class TestStringExtension: XCTestCase { result = "300x250ERROR".toCGSize() XCTAssertNil(result) } + + func testRegexMatches() { + var result = "aaa aaa".matches(for: "^a") + XCTAssert(result.count == 1) + XCTAssert(result[0] == "a") + + result = "aaa aaa".matches(for: "^b") + XCTAssert(result.count == 0) + + result = "^a".matches(for: "aaa aaa") + XCTAssert(result.count == 0) + + result = "{ \n adManagerResponse:\"hb_size\":[\"728x90\"],\"hb_size_rubicon\":[\"1x1\"],moPubResponse:\"hb_size:300x250\" \n }".matches(for: "[0-9]+x[0-9]+") + XCTAssert(result.count == 3) + XCTAssert(result[0] == "728x90") + XCTAssert(result[1] == "1x1") + XCTAssert(result[2] == "300x250") + + result = "{ \n adManagerResponse:\"hb_size\":[\"728x90\"],\"hb_size_rubicon\":[\"1x1\"],moPubResponse:\"hb_size:300x250\" \n }".matches(for: "hb_size\\W+[0-9]+x[0-9]+") + + XCTAssert(result.count == 2) + XCTAssert(result[0] == "hb_size\":[\"728x90") + XCTAssert(result[1] == "hb_size:300x250") + } + + func testRegexMatchAndCheck() { + var result = "aaa aaa".matchAndCheck(regex: "^a") + + XCTAssertNotNil(result) + XCTAssert(result == "a") + + result = "aaa aaa".matchAndCheck(regex: "^b") + XCTAssertNil(result) + } } diff --git a/PrebidMobileTests/addendum/AdViewUtilsTests.swift b/PrebidMobileTests/addendum/AdViewUtilsTests.swift index c5d922ff5..29cbd2a94 100644 --- a/PrebidMobileTests/addendum/AdViewUtilsTests.swift +++ b/PrebidMobileTests/addendum/AdViewUtilsTests.swift @@ -19,6 +19,7 @@ import WebKit class AdViewUtilsTests: XCTestCase { + func testFindHbSizeValue() { let body = "{ \n adManagerResponse:\"hb_size\":[\"728x90\"],\"hb_size_rubicon\":[\"728x90\"],moPubResponse:\"hb_size:300x250\" \n }" @@ -29,6 +30,21 @@ class AdViewUtilsTests: XCTestCase { parseResult: { .success($0) } ) + switch result { + case .success(let size): + XCTAssert(size == "728x90") + case .failure(let error): + XCTFail("AdViewUtils unexpectedly failed with error: \(error.localizedDescription)") + } + let body = "{ \n adManagerResponse:\"hb_size\":[\"728x90\"],\"hb_size_rubicon\":[\"728x90\"],moPubResponse:\"hb_size:300x250\" \n }" + + let result = AdViewUtils.findValueInHtml( + body: body, + objectRegex: AdViewUtils.sizeObjectRegexExpression, + valueRegex: AdViewUtils.sizeValueRegexExpression, + parseResult: { .success($0) } + ) + switch result { case .success(let size): XCTAssert(size == "728x90") @@ -37,6 +53,26 @@ class AdViewUtilsTests: XCTestCase { } } + func testFailureFindASizeInNilHtmlCode() { + let exp = expectation(description: "findPrebidCreativeSize should fail") + + AdViewUtils.findPrebidCreativeSize(WKWebView()) { size in + XCTFail("Expected to fail but found creative size.") + } failure: { error in + exp.fulfill() + XCTAssert((error as NSError).code == PbWebViewSearchErrorFactory.noHtmlCode) + } + + waitForExpectations(timeout: 30) + let exp = expectation(description: "findPrebidCreativeSize should fail") + + result = AdViewUtils.stringToCGSize("ERROR") + XCTAssertNil(result) + + result = AdViewUtils.stringToCGSize("300x250ERROR") + XCTAssertNil(result) + } + func testFailureFindASizeInNilHtmlCode() { let exp = expectation(description: "findPrebidCreativeSize should fail") @@ -55,6 +91,10 @@ class AdViewUtilsTests: XCTestCase { body: "", expectedErrorCode: PbWebViewSearchErrorFactory.noObjectCode ) + findSizeInHtmlFailureHelper( + body: "", + expectedErrorCode: PbWebViewSearchErrorFactory.noObjectCode + ) } func testFailureFindASizeIfItHasTheWrongType() { @@ -62,6 +102,10 @@ class AdViewUtilsTests: XCTestCase { body: "", expectedErrorCode: PbWebViewSearchErrorFactory.noObjectCode ) + findSizeInHtmlFailureHelper( + body: "", + expectedErrorCode: PbWebViewSearchErrorFactory.noObjectCode + ) } func testSuccessFindASizeIfProperlyFormatted() { @@ -139,7 +183,7 @@ class AdViewUtilsTests: XCTestCase { let uiView = UIView() findSizeInViewFailureHelper(uiView, expectedErrorCode: PbWebViewSearchErrorFactory.noWKWebViewCode) } - + func testFindPrebidCacheIDSuccess() { let webView = WKWebView() setHtmlIntoWkWebView(successHtmlWithSize728x90, webView) @@ -185,7 +229,59 @@ class AdViewUtilsTests: XCTestCase { waitForExpectations(timeout: 30, handler: nil) } - class TestingWKNavigationDelegate: NSObject, WKNavigationDelegate { + func testFindPrebidLocalCacheIDSuccess() { + let webView = WKWebView() + setHtmlIntoWkWebView(successHtmlWithSize728x90, webView) + + let adView = UIView() + adView.addSubview(webView) + + let expectation = expectation(description: "Expected to find a cache ID successfully") + + AdViewUtils.findPrebidLocalCacheID(adView) { result in + switch result { + case .success(let cacheID): + XCTAssertEqual(cacheID, "Prebid_6DE2F419-1B19-4B9B-ABD8-099334CC636A") + case .failure: + XCTFail("Expected to find cache ID, but failed") + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testFindPrebidLocalCacheIDFailureNoLocalCacheID() { + let html = "
No cache ID here
" + let webView = WKWebView() + setHtmlIntoWkWebView(html, webView) + + let adView = UIView() + adView.addSubview(webView) + + let expectation = expectation(description: "Expected to fail due to missing cache ID") + + AdViewUtils.findPrebidLocalCacheID(adView) { result in + switch result { + case .success: + XCTFail("Expected to fail, but found a cache ID") + case .failure(let error): + XCTAssertEqual((error as? PbWebViewSearchError)?.code, PbWebViewSearchErrorFactory.noObjectCode) + } + expectation.fulfill() + } + + waitForExpectations(timeout: 30, handler: nil) + } + + func testSuccessFindSizeInWkWebView() { + let wkWebView = WKWebView() + + setHtmlIntoWkWebView(successHtmlWithSize728x90, wkWebView) + findSizeInViewSuccessHelper(wkWebView, expectedSize: CGSize(width: 728, height: 90)) + } + + private class TestingWKNavigationDelegate: NSObject, WKNavigationDelegate { let loadSuccesfulException: XCTestExpectation init(_ loadSuccesfulException: XCTestExpectation) { @@ -203,30 +299,23 @@ class AdViewUtilsTests: XCTestCase { } } - let successHtmlWithSize728x90 = """ - -
- """ - - func testSuccessFindSizeInWkWebView() { - let wkWebView = WKWebView() + private let successHtmlWithSize728x90 = """ + +
+ """ - func setHtmlIntoWkWebView(_ html: String, _ wkWebView: WKWebView) { + private func setHtmlIntoWkWebView(_ html: String, _ wkWebView: WKWebView) { let loadSuccesfulException = expectation(description: "\(#function)") let testingWKNavigationDelegate = TestingWKNavigationDelegate(loadSuccesfulException) @@ -238,7 +327,7 @@ class AdViewUtilsTests: XCTestCase { wkWebView.navigationDelegate = nil } - func findSizeInViewFailureHelper(_ view: UIView, expectedErrorCode: Int) { + private func findSizeInViewFailureHelper(_ view: UIView, expectedErrorCode: Int) { let loadSuccesfulException = expectation(description: "\(#function)") // given @@ -265,7 +354,7 @@ class AdViewUtilsTests: XCTestCase { } - func findSizeInViewSuccessHelper(_ view: UIView, expectedSize: CGSize) { + private func findSizeInViewSuccessHelper(_ view: UIView, expectedSize: CGSize) { let loadSuccesfulException = expectation(description: "\(#function)") // given @@ -290,4 +379,58 @@ class AdViewUtilsTests: XCTestCase { XCTAssertEqual(expectedSize, result) XCTAssertNil(error) } + + private func findSizeInHtmlFailureHelper(body: String, expectedErrorCode: Int) { + // when + let result = AdViewUtils.findValueInHtml( + body: body, + objectRegex: AdViewUtils.sizeObjectRegexExpression, + valueRegex: AdViewUtils.sizeValueRegexExpression, + parseResult: { + if let cgSize = $0.toCGSize() { + return .success(cgSize) + } else { + return .failure(NSError( + domain: "com.prebid.tests", + code: PbWebViewSearchErrorFactory.valueUnparsedCode + )) + } + } + ) + + // then + switch result { + case .success(_): + XCTFail("Expected failure") + case .failure(let error as NSError): + XCTAssertEqual(expectedErrorCode, error.code) + } + } + + private func findSizeInHtmlSuccessHelper(body: String, expectedSize: CGSize) { + // when + let result = AdViewUtils.findValueInHtml( + body: body, + objectRegex: AdViewUtils.sizeObjectRegexExpression, + valueRegex: AdViewUtils.sizeValueRegexExpression, + parseResult: { + if let cgSize = $0.toCGSize() { + return .success(cgSize) + } else { + return .failure(NSError( + domain: "com.prebid.tests", + code: PbWebViewSearchErrorFactory.valueUnparsedCode + )) + } + } + ) + + // then + switch result { + case .success(let size): + XCTAssert(expectedSize == size) + case .failure(_): + XCTFail("Expected success") + } + } } From 2e60b799b223dce3010a3d466c83d5d5011fb783 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Thu, 16 Jan 2025 11:00:22 +0200 Subject: [PATCH 07/21] feat: add examples to InternalTestApp --- ...iginalAPIDisplayBannerViewController.swift | 6 +---- .../Model/TestCasesManager.swift | 26 +++++++++++++++++-- ...idOriginalAPIDisplayBannerController.swift | 11 ++++++++ 3 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIDisplayBannerViewController.swift b/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIDisplayBannerViewController.swift index 395f8a550..aea4db635 100644 --- a/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIDisplayBannerViewController.swift +++ b/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIDisplayBannerViewController.swift @@ -17,7 +17,7 @@ import UIKit import PrebidMobile import GoogleMobileAds -fileprivate let storedImpDisplayBanner = "prebid-demo-banner-320-50-skadn" +fileprivate let storedImpDisplayBanner = "prebid-demo-banner-320-50" fileprivate let gamAdUnitDisplayBannerOriginal = "/21808260008/prebid_demo_app_original_api_banner" class GAMOriginalAPIDisplayBannerViewController: BannerBaseViewController, GADBannerViewDelegate { @@ -25,8 +25,6 @@ class GAMOriginalAPIDisplayBannerViewController: BannerBaseViewController, GADBa // Prebid private var adUnit: BannerAdUnit! - private let skadnHelper = PrebidSKAdNetworkHelper() - // GAM private var gamBanner: GAMBannerView! @@ -96,8 +94,6 @@ class GAMOriginalAPIDisplayBannerViewController: BannerBaseViewController, GADBa }, failure: { (error) in PrebidDemoLogger.shared.error("Error occuring during searching for Prebid creative size: \(error)") }) - - skadnHelper.subscribeOnAdClicked(adView: bannerView, viewController: self) } func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) { diff --git a/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift b/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift index b360132c6..81d20c94f 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift @@ -269,7 +269,7 @@ struct TestCaseManager { setupCustomParams(for: bannerController.prebidConfigId) }), - TestCase(title: "Banner 320x50 (GAM Original) [NO SKAdN]", + TestCase(title: "Banner 320x50 (GAM Original) [SKAdN]", tags: [.banner, .originalAPI, .server], exampleVCStoryboardID: "AdapterViewController", configurationClosure: { vc in @@ -281,9 +281,10 @@ struct TestCaseManager { Targeting.shared.sourceapp = "InternalTestApp" let bannerController = PrebidOriginalAPIDisplayBannerController(rootController: adapterVC) + bannerController.activatePrebidSKAdNHelper = true bannerController.adSize = CGSize(width: 320, height: 50) - bannerController.prebidConfigId = "prebid-ita-banner-320-50"; + bannerController.prebidConfigId = "prebid-demo-banner-320-50-skadn" bannerController.adUnitID = "/21808260008/prebid_demo_app_original_api_banner" adapterVC.setup(adapter: bannerController) @@ -533,6 +534,27 @@ struct TestCaseManager { setupCustomParams(for: nativeController.prebidConfigId) }), + TestCase(title: "Native In-App (GAM Original) [SKAdN]", + tags: [.native, .originalAPI, .server], + exampleVCStoryboardID: "AdapterViewController", + configurationClosure: { vc in + + guard let adapterVC = vc as? AdapterViewController else { + return + } + + let nativeController = PrebidOriginalAPINativeController(rootController: adapterVC) + nativeController.setupNativeAdView(NativeAdViewBox()) + + nativeController.adUnitID = "/21808260008/apollo_custom_template_native_ad_unit" + nativeController.prebidConfigId = "prebid-demo-banner-native-styles-skadn" + nativeController.nativeAssets = .defaultNativeRequestAssets + nativeController.eventTrackers = .defaultNativeEventTrackers + + adapterVC.setup(adapter: nativeController) + setupCustomParams(for: nativeController.prebidConfigId) + }), + // MARK: ---- In-Stream (Original API) TestCase(title: "Instream Video (GAM Original) [OK, PUC]", diff --git a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIDisplayBannerController.swift b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIDisplayBannerController.swift index 357896616..5ce5e21b9 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIDisplayBannerController.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIDisplayBannerController.swift @@ -32,7 +32,10 @@ class PrebidOriginalAPIDisplayBannerController: var gamSizes = [GADAdSize]() // Prebid + var activatePrebidSKAdNHelper = false + private var adUnit: BannerAdUnit! + private let skadnHelper = PrebidSKAdNetworkHelper() // GAM private var gamBanner: GAMBannerView! @@ -61,6 +64,10 @@ class PrebidOriginalAPIDisplayBannerController: setupAdapterController() } + deinit { + Targeting.shared.sourceapp = nil + } + func configurationController() -> BaseConfigurationController? { return PrebidBannerConfigurationController(controller: self) } @@ -191,6 +198,10 @@ class PrebidOriginalAPIDisplayBannerController: }, failure: { (error) in Log.error("Error occuring during searching for Prebid creative size: \(error)") }) + + if let rootController, activatePrebidSKAdNHelper == true { + skadnHelper.subscribeOnAdClicked(adView: bannerView, viewController: rootController) + } } func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) { From 8c92cec6c823834fada6db67fdb95c91d1f986b6 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Thu, 16 Jan 2025 11:45:38 +0200 Subject: [PATCH 08/21] feat: clean up --- .../OriginalAPI/GAMOriginalAPIVideoBannerViewController.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIVideoBannerViewController.swift b/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIVideoBannerViewController.swift index 26e211a7b..776e370cc 100644 --- a/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIVideoBannerViewController.swift +++ b/Example/PrebidDemo/PrebidDemoSwift/Examples/GAM/OriginalAPI/GAMOriginalAPIVideoBannerViewController.swift @@ -24,7 +24,6 @@ class GAMOriginalAPIVideoBannerViewController: BannerBaseViewController, GADBann // Prebid private var adUnit: BannerAdUnit! - private let skadnHelper = PrebidSKAdNetworkHelper() // GAM private var gamBanner: GAMBannerView! @@ -78,8 +77,6 @@ class GAMOriginalAPIVideoBannerViewController: BannerBaseViewController, GADBann }, failure: { (error) in PrebidDemoLogger.shared.error("Error occuring during searching for Prebid creative size: \(error)") }) - - skadnHelper.subscribeOnAdClicked(adView: bannerView, viewController: self) } func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) { From c8a8b183fdffc29f4e2983c4bb49127f389f9518 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Fri, 17 Jan 2025 10:06:03 +0200 Subject: [PATCH 09/21] feat: clean up & change intervals --- PrebidMobile/AdUnits/Native/NativeAd.swift | 2 +- PrebidMobileTests/addendum/AdViewUtilsTests.swift | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/PrebidMobile/AdUnits/Native/NativeAd.swift b/PrebidMobile/AdUnits/Native/NativeAd.swift index 50b7f88e8..82d421628 100644 --- a/PrebidMobile/AdUnits/Native/NativeAd.swift +++ b/PrebidMobile/AdUnits/Native/NativeAd.swift @@ -425,7 +425,7 @@ extension NativeAd: SKStoreProductViewControllerDelegate { } } -class NativeAdGestureRecognizerRecord : NSObject { +private class NativeAdGestureRecognizerRecord : NSObject { weak var viewWithTracking: UIView? weak var gestureRecognizer: UIGestureRecognizer? diff --git a/PrebidMobileTests/addendum/AdViewUtilsTests.swift b/PrebidMobileTests/addendum/AdViewUtilsTests.swift index 29cbd2a94..3c66a9d86 100644 --- a/PrebidMobileTests/addendum/AdViewUtilsTests.swift +++ b/PrebidMobileTests/addendum/AdViewUtilsTests.swift @@ -248,7 +248,7 @@ class AdViewUtilsTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: 30, handler: nil) + waitForExpectations(timeout: 45) } func testFindPrebidLocalCacheIDFailureNoLocalCacheID() { @@ -271,7 +271,7 @@ class AdViewUtilsTests: XCTestCase { expectation.fulfill() } - waitForExpectations(timeout: 30, handler: nil) + waitForExpectations(timeout: 45) } func testSuccessFindSizeInWkWebView() { @@ -323,7 +323,7 @@ class AdViewUtilsTests: XCTestCase { wkWebView.loadHTMLString(html, baseURL: nil) - waitForExpectations(timeout: 30, handler: nil) + waitForExpectations(timeout: 45) wkWebView.navigationDelegate = nil } @@ -345,7 +345,7 @@ class AdViewUtilsTests: XCTestCase { // when AdViewUtils.findPrebidCreativeSize(view, success: success, failure: failure) - waitForExpectations(timeout: 30, handler: nil) + waitForExpectations(timeout: 45) // then XCTAssertNil(size) @@ -372,7 +372,7 @@ class AdViewUtilsTests: XCTestCase { // when AdViewUtils.findPrebidCreativeSize(view, success: success, failure: failure) - waitForExpectations(timeout: 30, handler: nil) + waitForExpectations(timeout: 45) // then XCTAssertNotNil(result) From aeb467283587398ee9c95f6d76a2b0fc1c2210ed Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Fri, 17 Jan 2025 10:45:25 +0200 Subject: [PATCH 10/21] feat: inscrease interval --- .../Tests/VASTTests/RewardedVideoEventsTest.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/PrebidMobileTests/RenderingTests/Tests/VASTTests/RewardedVideoEventsTest.swift b/PrebidMobileTests/RenderingTests/Tests/VASTTests/RewardedVideoEventsTest.swift index 75ca71c63..6a4ca16a2 100644 --- a/PrebidMobileTests/RenderingTests/Tests/VASTTests/RewardedVideoEventsTest.swift +++ b/PrebidMobileTests/RenderingTests/Tests/VASTTests/RewardedVideoEventsTest.swift @@ -92,7 +92,7 @@ class RewardedVideoEventsTest : XCTestCase, PBMCreativeViewDelegate { requester.buildAdsArray(data) } - self.wait(for: [vastRequestSuccessfulExpectation], timeout: 2) + self.wait(for: [vastRequestSuccessfulExpectation], timeout: 10) XCTAssertNotNil(self.vastServerResponse) if self.vastServerResponse == nil { @@ -147,7 +147,7 @@ class RewardedVideoEventsTest : XCTestCase, PBMCreativeViewDelegate { XCTFail(error.localizedDescription) }) - self.waitForExpectations(timeout: 10, handler: nil) + self.waitForExpectations(timeout: 30, handler: nil) } //MARK: - CreativeViewDelegate From e3c0f261aa74926d7bf23ec43e00270763124314 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Mon, 20 Jan 2025 10:35:24 +0200 Subject: [PATCH 11/21] feat: add support for video format --- .../Model/TestCasesManager.swift | 23 ++++- ...ebidOriginalAPIVideoBannerController.swift | 7 ++ .../Addendum/PrebidSKAdNetworkHelper.swift | 86 +++++++++++++------ .../Prebid/PBMCore/Bid.swift | 15 ++++ 4 files changed, 103 insertions(+), 28 deletions(-) diff --git a/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift b/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift index 81d20c94f..2bb98f14e 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift @@ -353,7 +353,7 @@ struct TestCaseManager { }), TestCase(title: "Video Outstream (GAM Original) [OK, PUC]", - tags: [.banner, .originalAPI, .server], + tags: [.video, .originalAPI, .server], exampleVCStoryboardID: "AdapterViewController", configurationClosure: { vc in @@ -362,6 +362,7 @@ struct TestCaseManager { } let bannerController = PrebidOriginalAPIVideoBannerController(rootController: adapterVC) + bannerController.activatePrebidSKAdNHelper = true bannerController.adSize = CGSize(width: 300, height: 250) bannerController.prebidConfigId = "prebid-ita-video-outstream-original-api" bannerController.adUnitID = "/21808260008/prebid-demo-original-api-video-banner" @@ -371,6 +372,26 @@ struct TestCaseManager { setupCustomParams(for: bannerController.prebidConfigId) }), + TestCase(title: "Video Outstream (GAM Original) [SKAdN]", + tags: [.video, .originalAPI, .server], + exampleVCStoryboardID: "AdapterViewController", + configurationClosure: { vc in + + guard let adapterVC = vc as? AdapterViewController else { + return + } + + let bannerController = PrebidOriginalAPIVideoBannerController(rootController: adapterVC) + bannerController.activatePrebidSKAdNHelper = true + bannerController.adSize = CGSize(width: 300, height: 250) + bannerController.prebidConfigId = "prebid-demo-video-outstream-original-api-skadn" + bannerController.adUnitID = "/21808260008/prebid-demo-original-api-video-banner" + + adapterVC.setup(adapter: bannerController) + + setupCustomParams(for: bannerController.prebidConfigId) + }), + TestCase(title: "Multiformat Banner (GAM Original) [OK, PUC]", tags: [.multiformat, .originalAPI, .server], exampleVCStoryboardID: "AdapterViewController", diff --git a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoBannerController.swift b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoBannerController.swift index 171efcd2d..e1306ff42 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoBannerController.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoBannerController.swift @@ -32,7 +32,10 @@ class PrebidOriginalAPIVideoBannerController: var gamSizes = [GADAdSize]() // Prebid + var activatePrebidSKAdNHelper = false + private var adUnit: VideoAdUnit! + private let skadnHelper = PrebidSKAdNetworkHelper() // GAM private var gamBanner: GAMBannerView! @@ -190,6 +193,10 @@ class PrebidOriginalAPIVideoBannerController: }, failure: { (error) in Log.error("Error occuring during searching for Prebid creative size: \(error)") }) + + if let rootController, activatePrebidSKAdNHelper == true { + skadnHelper.subscribeOnAdClicked(adView: bannerView, viewController: rootController) + } } func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) { diff --git a/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift b/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift index b89463e04..b609285bc 100644 --- a/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift +++ b/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift @@ -21,52 +21,84 @@ import StoreKit @objcMembers public class PrebidSKAdNetworkHelper: NSObject { + private weak var adView: UIView? private weak var viewControllerForPresentingModals: UIViewController? - /// Subscribes to ad click events on the provided ad view and sets up the SKAdNetwork parameters. + /// Subscribes to ad click events on the provided ad view and configures it with SKAdN tracking.. /// - Parameters: /// - adView: The ad view where click events will be tracked. /// - viewController: The view controller used to present modals, such as the SKStoreProductViewController. public func subscribeOnAdClicked(adView: UIView, viewController: UIViewController) { + self.adView = adView self.viewControllerForPresentingModals = viewController - AdViewUtils.findPrebidLocalCacheID(adView) { result in + AdViewUtils.findPrebidLocalCacheID(adView) { [weak self] result in guard case .success(let cacheID) = result else { + self?.attemptFindUUID() return } - guard let bidString = CacheManager.shared.get(cacheId: cacheID), - let bidDic = Utils.shared.getDictionaryFromString(bidString) else { - Log.error("No bid response for provided cache id.") + guard let bidString = CacheManager.shared.get(cacheId: cacheID) else { + Log.warn("SDK could not find the bid response for provided cache id.") return } - guard let rawBid = PBMORTBBid(jsonDictionary: bidDic, extParser: { extDic in - return PBMORTBBidExt(jsonDictionary: extDic) - }) else { - return - } - - let bid = Bid(bid: rawBid) - - guard let skadn = bid.skadn, - let skadnParameters = SkadnParametersManager.getSkadnProductParameters(for: skadn) else { + self?.configureAdViewWithSkadn(using: bidString) + } + } + + /// Mainly used for video creatives. Searches for **hb_uuid** keys from saved bids in third-party web views. + private func attemptFindUUID() { + guard let adView = adView, + let webView = adView.allSubViewsOf(type: WKWebView.self).first else { + return + } + + webView.evaluateJavaScript("document.body.innerHTML", completionHandler: { html, error in + guard let html = html as? String else { return } - adView.subviews.forEach({ - if $0 is TouchTrackingOverlayView { - $0.removeFromSuperview() + CacheManager.shared + .savedValuesDict + .values + .forEach { [weak self] value in + if let bid = Bid.bid(from: value), + let hbUUID = bid.targetingInfo?["hb_uuid"], + html.contains(hbUUID) { + self?.configureAdViewWithSkadn(using: bid) + } } - }) - - let overlayView = TouchTrackingOverlayView(frame: adView.frame) - overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.5) - adView.addSubview(overlayView) - - overlayView.onClick = { [weak self] in - self?.presentSKStoreProductViewController(with: skadnParameters) - } + }) + } + + private func configureAdViewWithSkadn(using bidString: String) { + guard let bid = Bid.bid(from: bidString) else { + return + } + + configureAdViewWithSkadn(using: bid) + } + + private func configureAdViewWithSkadn(using bid: Bid) { + guard let adView = adView else { + return + } + + guard let skadn = bid.skadn, + let skadnParameters = SkadnParametersManager.getSkadnProductParameters(for: skadn) else { + return + } + + adView.subviews + .filter({ $0 is TouchTrackingOverlayView }) + .forEach({ $0.removeFromSuperview()}) + + let overlayView = TouchTrackingOverlayView(frame: adView.bounds) + adView.addSubview(overlayView) + + overlayView.onClick = { [weak self] in + self?.presentSKStoreProductViewController(with: skadnParameters) } } diff --git a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/Bid.swift b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/Bid.swift index 3006d57ee..d0f5c9319 100644 --- a/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/Bid.swift +++ b/PrebidMobile/PrebidMobileRendering/Prebid/PBMCore/Bid.swift @@ -142,6 +142,21 @@ public class Bid: NSObject { } } +extension Bid { + + static func bid(from bidString: String) -> Bid? { + guard let bidDic = Utils.shared.getDictionaryFromString(bidString), + let rawBid = PBMORTBBid( + jsonDictionary: bidDic, + extParser: { PBMORTBBidExt(jsonDictionary: $0)} + ) else { + return nil + } + + return Bid(bid: rawBid) + } +} + // MARK: Impression Tracking Helpers @objc public extension Bid { From 5e15ca5bda2b4c5ed5a9e723a888a133e6e6d8b7 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Tue, 21 Jan 2025 10:28:08 +0200 Subject: [PATCH 12/21] feat: small enhancement --- .../Model/TestCasesManager.swift | 1 - PrebidMobile.xcodeproj/project.pbxproj | 16 +-- PrebidMobile/AdUnits/Native/NativeAd.swift | 15 ++- .../UIApplication+Extensions.swift | 13 +++ .../Utils/UIApplication+Extensions.swift | 32 ------ .../addendum/AdViewUtilsTests.swift | 100 +----------------- 6 files changed, 24 insertions(+), 153 deletions(-) delete mode 100644 PrebidMobile/Utils/UIApplication+Extensions.swift diff --git a/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift b/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift index 2bb98f14e..c17e396b2 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift @@ -362,7 +362,6 @@ struct TestCaseManager { } let bannerController = PrebidOriginalAPIVideoBannerController(rootController: adapterVC) - bannerController.activatePrebidSKAdNHelper = true bannerController.adSize = CGSize(width: 300, height: 250) bannerController.prebidConfigId = "prebid-ita-video-outstream-original-api" bannerController.adUnitID = "/21808260008/prebid-demo-original-api-video-banner" diff --git a/PrebidMobile.xcodeproj/project.pbxproj b/PrebidMobile.xcodeproj/project.pbxproj index 7b1abe52c..6b2847c85 100644 --- a/PrebidMobile.xcodeproj/project.pbxproj +++ b/PrebidMobile.xcodeproj/project.pbxproj @@ -75,11 +75,11 @@ 53169F1D2D4243DE007355E8 /* UserUniqueIDTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53169F1C2D4243DE007355E8 /* UserUniqueIDTests.swift */; }; 531CF21927E8FC1B005E5ABE /* LogLevel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 531CF21827E8FC1B005E5ABE /* LogLevel.swift */; }; 53269D55282E6D0F0098550D /* ServerEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53269D54282E6D0F0098550D /* ServerEvent.swift */; }; - 532936862CEDE8E00056FD8D /* PrebidImpressionTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 532936852CEDE8E00056FD8D /* PrebidImpressionTracker.swift */; }; - 5329368C2CEE14320056FD8D /* PrebidImpressionTrackerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5329368B2CEE14320056FD8D /* PrebidImpressionTrackerProtocol.swift */; }; 532936792CEB887D0056FD8D /* ArbitraryORTBService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 532936782CEB887D0056FD8D /* ArbitraryORTBService.swift */; }; 5329367B2CEBE92F0056FD8D /* ArbitraryORTBHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5329367A2CEBE92F0056FD8D /* ArbitraryORTBHelper.swift */; }; 5329367E2CEC92340056FD8D /* ArbitraryORTBServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5329367D2CEC92340056FD8D /* ArbitraryORTBServiceTests.swift */; }; + 532936862CEDE8E00056FD8D /* PrebidImpressionTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 532936852CEDE8E00056FD8D /* PrebidImpressionTracker.swift */; }; + 5329368C2CEE14320056FD8D /* PrebidImpressionTrackerProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5329368B2CEE14320056FD8D /* PrebidImpressionTrackerProtocol.swift */; }; 532936932CEF1FFE0056FD8D /* ArbitraryORTBHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 532936922CEF1FFE0056FD8D /* ArbitraryORTBHelperTests.swift */; }; 533135C6282A869800AA1E4D /* BidTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 533135C5282A869800AA1E4D /* BidTest.swift */; }; 53322AA3282D45EE0049229D /* PrebidServerEventTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53322AA2282D45EE0049229D /* PrebidServerEventTracker.swift */; }; @@ -122,7 +122,6 @@ 535ADE102D2E987E00DB888F /* PluginEventDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535ADE0F2D2E987E00DB888F /* PluginEventDelegate.swift */; }; 535ADE122D2EA2F500DB888F /* PrebidLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535ADE112D2EA2F500DB888F /* PrebidLogger.swift */; }; 536469C92D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536469C82D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift */; }; - 53646A652D368DFB00F50B6D /* UIApplication+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53646A642D368DFB00F50B6D /* UIApplication+Extensions.swift */; }; 536A427F282D11DA0069E9B2 /* PrebidServerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536A427E282D11DA0069E9B2 /* PrebidServerConnection.swift */; }; 536A4283282D12E80069E9B2 /* PrebidServerConnectionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536A4282282D12E80069E9B2 /* PrebidServerConnectionProtocol.swift */; }; 5379F6BA2ABB711500B0B7A9 /* PrebidAdUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5379F6B92ABB711500B0B7A9 /* PrebidAdUnitTests.swift */; }; @@ -989,11 +988,11 @@ 53169F1C2D4243DE007355E8 /* UserUniqueIDTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserUniqueIDTests.swift; sourceTree = ""; }; 531CF21827E8FC1B005E5ABE /* LogLevel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogLevel.swift; sourceTree = ""; }; 53269D54282E6D0F0098550D /* ServerEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerEvent.swift; sourceTree = ""; }; - 532936852CEDE8E00056FD8D /* PrebidImpressionTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidImpressionTracker.swift; sourceTree = ""; }; - 5329368B2CEE14320056FD8D /* PrebidImpressionTrackerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidImpressionTrackerProtocol.swift; sourceTree = ""; }; 532936782CEB887D0056FD8D /* ArbitraryORTBService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArbitraryORTBService.swift; sourceTree = ""; }; 5329367A2CEBE92F0056FD8D /* ArbitraryORTBHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArbitraryORTBHelper.swift; sourceTree = ""; }; 5329367D2CEC92340056FD8D /* ArbitraryORTBServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArbitraryORTBServiceTests.swift; sourceTree = ""; }; + 532936852CEDE8E00056FD8D /* PrebidImpressionTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidImpressionTracker.swift; sourceTree = ""; }; + 5329368B2CEE14320056FD8D /* PrebidImpressionTrackerProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidImpressionTrackerProtocol.swift; sourceTree = ""; }; 532936922CEF1FFE0056FD8D /* ArbitraryORTBHelperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArbitraryORTBHelperTests.swift; sourceTree = ""; }; 533135C5282A869800AA1E4D /* BidTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BidTest.swift; sourceTree = ""; }; 53322AA2282D45EE0049229D /* PrebidServerEventTracker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidServerEventTracker.swift; sourceTree = ""; }; @@ -1034,7 +1033,6 @@ 535ADE0F2D2E987E00DB888F /* PluginEventDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginEventDelegate.swift; sourceTree = ""; }; 535ADE112D2EA2F500DB888F /* PrebidLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidLogger.swift; sourceTree = ""; }; 536469C82D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidSKAdNetworkHelper.swift; sourceTree = ""; }; - 53646A642D368DFB00F50B6D /* UIApplication+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Extensions.swift"; sourceTree = ""; }; 536A427E282D11DA0069E9B2 /* PrebidServerConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidServerConnection.swift; sourceTree = ""; }; 536A4282282D12E80069E9B2 /* PrebidServerConnectionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidServerConnectionProtocol.swift; sourceTree = ""; }; 5379F6B92ABB711500B0B7A9 /* PrebidAdUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidAdUnitTests.swift; sourceTree = ""; }; @@ -1997,7 +1995,6 @@ 53F35F6E292BAE9B001C1183 /* UserDefaults+Extensions.swift */, 53842BBB29E565030069A4B7 /* NSString+Extensions.swift */, 53D3C3872C2BEE1E0074D99B /* URL+Extensions.swift */, - 53646A642D368DFB00F50B6D /* UIApplication+Extensions.swift */, ); path = Utils; sourceTree = ""; @@ -2108,9 +2105,7 @@ 5BC3764E271F1CFD00444D5E /* ExtensionsAndWrappers */ = { isa = PBXGroup; children = ( - 5BC37666271F1CFD00444D5E /* NSTimer */, 5BC37652271F1CFD00444D5E /* Exposure */, - 5BC37666271F1CFD00444D5E /* NSTimer */, 5BC37659271F1CFD00444D5E /* NSDictionary+PBMExtensions.h */, 5BC3765F271F1CFD00444D5E /* NSDictionary+PBMExtensions.m */, 5BC37660271F1CFD00444D5E /* NSException+PBMExtensions.h */, @@ -2124,7 +2119,6 @@ 5BC37666271F1CFD00444D5E /* NSTimer */, 5BC3765B271F1CFD00444D5E /* PBMTouchDownRecognizer.h */, 5BC3766E271F1CFD00444D5E /* PBMTouchDownRecognizer.m */, - 53C925012990FB30009E6F94 /* String+Extensions.swift */, 53A0E7E32CDCAF74007887F5 /* UIApplication+Extensions.swift */, 5BC37650271F1CFD00444D5E /* UIView+PBMExtensions.h */, 5BC37661271F1CFD00444D5E /* UIView+PBMExtensions.m */, @@ -4245,7 +4239,6 @@ 5BC37AC6271F1D0100444D5E /* PBMSKAdNetworksParameterBuilder.m in Sources */, 53CF5B3729DC690600613E84 /* VideoAdUnit.swift in Sources */, 5BC37A1C271F1D0000444D5E /* PBMErrorFamily.m in Sources */, - A9750D7A2ABB9A300066E4E6 /* PluginEventListener.swift in Sources */, FAEE4D28262DC2B200AD9966 /* PbWebViewSearchError.swift in Sources */, 5BC37990271F1D0000444D5E /* PBMVideoCreative.m in Sources */, FAEE4D2A262DC2B200AD9966 /* IMAUtils.swift in Sources */, @@ -4264,7 +4257,6 @@ FAEE4D16262DC2B200AD9966 /* NativeAdEventDelegate.swift in Sources */, 5BC379B2271F1D0000444D5E /* PBMCreativeModelCollectionMakerVAST.m in Sources */, 929638C727ABD66D00D30F3D /* JsonDecodable.swift in Sources */, - 53646A652D368DFB00F50B6D /* UIApplication+Extensions.swift in Sources */, 5BC37A8E271F1D0000444D5E /* RewardedAdUnit.swift in Sources */, 5BC37A9A271F1D0000444D5E /* BannerAdLoaderDelegate.swift in Sources */, 5BC37AF9271F2D3500444D5E /* NativeDataAssetType.swift in Sources */, diff --git a/PrebidMobile/AdUnits/Native/NativeAd.swift b/PrebidMobile/AdUnits/Native/NativeAd.swift index 82d421628..03cfe6bba 100644 --- a/PrebidMobile/AdUnits/Native/NativeAd.swift +++ b/PrebidMobile/AdUnits/Native/NativeAd.swift @@ -147,9 +147,7 @@ public class NativeAd: NSObject, CacheExpiryDelegate { return nil } - let macrosHelper = PBMORTBMacrosHelper(bid: rawBid) - rawBid.adm = macrosHelper.replaceMacros(in: rawBid.adm) - rawBid.nurl = macrosHelper.replaceMacros(in: rawBid.nurl) + let bid = Bid(bid: rawBid) let ad = NativeAd() ad.bid = bid @@ -377,13 +375,12 @@ public class NativeAd: NSObject, CacheExpiryDelegate { } private func fireClickTrackers() { - guard let clickTrackersURLs = nativeAdMarkup?.link?.clicktrackers else { return } - - if clickTrackersURLs.count > 0 { - TrackerManager.shared.fireTrackerURLArray(arrayWithURLs: clickTrackersURLs) { - _ in - } + guard let clickTrackersURLs = nativeAdMarkup?.link?.clicktrackers, + clickTrackersURLs.count > 0 else { + return } + + TrackerManager.shared.fireTrackerURLArray(arrayWithURLs: clickTrackersURLs) { _ in } } private func presentSKStoreProductViewController(with productParameters: [String: Any]) { diff --git a/PrebidMobile/PrebidMobileRendering/ExtensionsAndWrappers/UIApplication+Extensions.swift b/PrebidMobile/PrebidMobileRendering/ExtensionsAndWrappers/UIApplication+Extensions.swift index 12e34c8cc..b88eafa02 100644 --- a/PrebidMobile/PrebidMobileRendering/ExtensionsAndWrappers/UIApplication+Extensions.swift +++ b/PrebidMobile/PrebidMobileRendering/ExtensionsAndWrappers/UIApplication+Extensions.swift @@ -29,4 +29,17 @@ extension UIApplication { return UIApplication.shared.keyWindow } } + + static func topViewController() -> UIViewController? { + var topController = UIApplication.shared + .windows + .filter({ $0.isKeyWindow }).first? + .rootViewController + + while let presentedViewController = topController?.presentedViewController { + topController = presentedViewController + } + + return topController + } } diff --git a/PrebidMobile/Utils/UIApplication+Extensions.swift b/PrebidMobile/Utils/UIApplication+Extensions.swift deleted file mode 100644 index e88532de8..000000000 --- a/PrebidMobile/Utils/UIApplication+Extensions.swift +++ /dev/null @@ -1,32 +0,0 @@ -/*   Copyright 2018-2025 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 - -extension UIApplication { - - static func topViewController() -> UIViewController? { - var topController = UIApplication.shared - .windows - .filter({ $0.isKeyWindow }).first? - .rootViewController - - while let presentedViewController = topController?.presentedViewController { - topController = presentedViewController - } - - return topController - } -} diff --git a/PrebidMobileTests/addendum/AdViewUtilsTests.swift b/PrebidMobileTests/addendum/AdViewUtilsTests.swift index 3c66a9d86..aa7604911 100644 --- a/PrebidMobileTests/addendum/AdViewUtilsTests.swift +++ b/PrebidMobileTests/addendum/AdViewUtilsTests.swift @@ -19,7 +19,6 @@ import WebKit class AdViewUtilsTests: XCTestCase { - func testFindHbSizeValue() { let body = "{ \n adManagerResponse:\"hb_size\":[\"728x90\"],\"hb_size_rubicon\":[\"728x90\"],moPubResponse:\"hb_size:300x250\" \n }" @@ -30,21 +29,6 @@ class AdViewUtilsTests: XCTestCase { parseResult: { .success($0) } ) - switch result { - case .success(let size): - XCTAssert(size == "728x90") - case .failure(let error): - XCTFail("AdViewUtils unexpectedly failed with error: \(error.localizedDescription)") - } - let body = "{ \n adManagerResponse:\"hb_size\":[\"728x90\"],\"hb_size_rubicon\":[\"728x90\"],moPubResponse:\"hb_size:300x250\" \n }" - - let result = AdViewUtils.findValueInHtml( - body: body, - objectRegex: AdViewUtils.sizeObjectRegexExpression, - valueRegex: AdViewUtils.sizeValueRegexExpression, - parseResult: { .success($0) } - ) - switch result { case .success(let size): XCTAssert(size == "728x90") @@ -53,26 +37,6 @@ class AdViewUtilsTests: XCTestCase { } } - func testFailureFindASizeInNilHtmlCode() { - let exp = expectation(description: "findPrebidCreativeSize should fail") - - AdViewUtils.findPrebidCreativeSize(WKWebView()) { size in - XCTFail("Expected to fail but found creative size.") - } failure: { error in - exp.fulfill() - XCTAssert((error as NSError).code == PbWebViewSearchErrorFactory.noHtmlCode) - } - - waitForExpectations(timeout: 30) - let exp = expectation(description: "findPrebidCreativeSize should fail") - - result = AdViewUtils.stringToCGSize("ERROR") - XCTAssertNil(result) - - result = AdViewUtils.stringToCGSize("300x250ERROR") - XCTAssertNil(result) - } - func testFailureFindASizeInNilHtmlCode() { let exp = expectation(description: "findPrebidCreativeSize should fail") @@ -91,10 +55,6 @@ class AdViewUtilsTests: XCTestCase { body: "", expectedErrorCode: PbWebViewSearchErrorFactory.noObjectCode ) - findSizeInHtmlFailureHelper( - body: "", - expectedErrorCode: PbWebViewSearchErrorFactory.noObjectCode - ) } func testFailureFindASizeIfItHasTheWrongType() { @@ -102,10 +62,6 @@ class AdViewUtilsTests: XCTestCase { body: "", expectedErrorCode: PbWebViewSearchErrorFactory.noObjectCode ) - findSizeInHtmlFailureHelper( - body: "", - expectedErrorCode: PbWebViewSearchErrorFactory.noObjectCode - ) } func testSuccessFindASizeIfProperlyFormatted() { @@ -115,60 +71,6 @@ class AdViewUtilsTests: XCTestCase { ) } - func findSizeInHtmlFailureHelper(body: String, expectedErrorCode: Int) { - // when - let result = AdViewUtils.findValueInHtml( - body: body, - objectRegex: AdViewUtils.sizeObjectRegexExpression, - valueRegex: AdViewUtils.sizeValueRegexExpression, - parseResult: { - if let cgSize = $0.toCGSize() { - return .success(cgSize) - } else { - return .failure(NSError( - domain: "com.prebid.tests", - code: PbWebViewSearchErrorFactory.valueUnparsedCode - )) - } - } - ) - - // then - switch result { - case .success(_): - XCTFail("Expected failure") - case .failure(let error as NSError): - XCTAssertEqual(expectedErrorCode, error.code) - } - } - - func findSizeInHtmlSuccessHelper(body: String, expectedSize: CGSize) { - // when - let result = AdViewUtils.findValueInHtml( - body: body, - objectRegex: AdViewUtils.sizeObjectRegexExpression, - valueRegex: AdViewUtils.sizeValueRegexExpression, - parseResult: { - if let cgSize = $0.toCGSize() { - return .success(cgSize) - } else { - return .failure(NSError( - domain: "com.prebid.tests", - code: PbWebViewSearchErrorFactory.valueUnparsedCode - )) - } - } - ) - - // then - switch result { - case .success(let size): - XCTAssert(expectedSize == size) - case .failure(_): - XCTFail("Expected success") - } - } - func testFailureFindSizeInViewIfThereIsNoWebView() { let uiView = UIView() findSizeInViewFailureHelper(uiView, expectedErrorCode: PbWebViewSearchErrorFactory.noWKWebViewCode) @@ -183,7 +85,7 @@ class AdViewUtilsTests: XCTestCase { let uiView = UIView() findSizeInViewFailureHelper(uiView, expectedErrorCode: PbWebViewSearchErrorFactory.noWKWebViewCode) } - + func testFindPrebidCacheIDSuccess() { let webView = WKWebView() setHtmlIntoWkWebView(successHtmlWithSize728x90, webView) From f7f7a925f94f31de1525448e3d3a98d52110e15a Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Tue, 18 Feb 2025 12:08:09 +0200 Subject: [PATCH 13/21] feat: introduce SKStoreProductViewControllerPresenter --- PrebidMobile.xcodeproj/project.pbxproj | 14 +++- PrebidMobile/AdUnits/Native/NativeAd.swift | 67 +++++-------------- .../PrebidSKAdNetworkHelper.swift | 36 +++------- ...KStoreProductViewControllerPresenter.swift | 47 +++++++++++++ .../UIApplication+Extensions.swift | 7 ++ 5 files changed, 94 insertions(+), 77 deletions(-) rename PrebidMobile/Addendum/{ => SKAdNetwork}/PrebidSKAdNetworkHelper.swift (78%) create mode 100644 PrebidMobile/Addendum/SKAdNetwork/SKStoreProductViewControllerPresenter.swift diff --git a/PrebidMobile.xcodeproj/project.pbxproj b/PrebidMobile.xcodeproj/project.pbxproj index 6b2847c85..07666fd0b 100644 --- a/PrebidMobile.xcodeproj/project.pbxproj +++ b/PrebidMobile.xcodeproj/project.pbxproj @@ -129,6 +129,7 @@ 537B651E2833A3DA008AE9D1 /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537B651D2833A3DA008AE9D1 /* Reachability.swift */; }; 537B65402833C091008AE9D1 /* NetworkType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537B653F2833C091008AE9D1 /* NetworkType.swift */; }; 537B6581283524BB008AE9D1 /* PrebidInitializationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537B6580283524BB008AE9D1 /* PrebidInitializationStatus.swift */; }; + 537F3F9C2D648A3000584F9C /* SKStoreProductViewControllerPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537F3F9B2D648A3000584F9C /* SKStoreProductViewControllerPresenter.swift */; }; 53842BB829E561750069A4B7 /* PrebidImagesRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53842BB729E561750069A4B7 /* PrebidImagesRepository.swift */; }; 53842BBC29E565030069A4B7 /* NSString+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53842BBB29E565030069A4B7 /* NSString+Extensions.swift */; }; 5388F32627F46A7F00319FE4 /* AdViewButtonDecorator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5388F32527F46A7F00319FE4 /* AdViewButtonDecorator.swift */; }; @@ -1040,6 +1041,7 @@ 537B651D2833A3DA008AE9D1 /* Reachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reachability.swift; sourceTree = ""; }; 537B653F2833C091008AE9D1 /* NetworkType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkType.swift; sourceTree = ""; }; 537B6580283524BB008AE9D1 /* PrebidInitializationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidInitializationStatus.swift; sourceTree = ""; }; + 537F3F9B2D648A3000584F9C /* SKStoreProductViewControllerPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SKStoreProductViewControllerPresenter.swift; sourceTree = ""; }; 53842BB729E561750069A4B7 /* PrebidImagesRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidImagesRepository.swift; sourceTree = ""; }; 53842BBB29E565030069A4B7 /* NSString+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSString+Extensions.swift"; sourceTree = ""; }; 5388F32527F46A7F00319FE4 /* AdViewButtonDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdViewButtonDecorator.swift; sourceTree = ""; }; @@ -2023,6 +2025,15 @@ path = Parameters; sourceTree = ""; }; + 537F3F9A2D6489C800584F9C /* SKAdNetwork */ = { + isa = PBXGroup; + children = ( + 536469C82D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift */, + 537F3F9B2D648A3000584F9C /* SKStoreProductViewControllerPresenter.swift */, + ); + path = SKAdNetwork; + sourceTree = ""; + }; 53A0E7E72CDCCBAB007887F5 /* ImpressionTracking */ = { isa = PBXGroup; children = ( @@ -3467,10 +3478,10 @@ FAEE4CFF262DC2B200AD9966 /* Addendum */ = { isa = PBXGroup; children = ( + 537F3F9A2D6489C800584F9C /* SKAdNetwork */, FAEE4D00262DC2B200AD9966 /* PbWebViewSearchError.swift */, FAEE4D01262DC2B200AD9966 /* AdViewUtils.swift */, FAEE4D02262DC2B200AD9966 /* IMAUtils.swift */, - 536469C82D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift */, ); path = Addendum; sourceTree = ""; @@ -4440,6 +4451,7 @@ 5BC3799E271F1D0000444D5E /* PBMModalPresentationController.m in Sources */, 5BC37A6D271F1D0000444D5E /* PBMORTBBid.m in Sources */, 5BC37A8B271F1D0000444D5E /* BannerEventInteractionDelegate.swift in Sources */, + 537F3F9C2D648A3000584F9C /* SKStoreProductViewControllerPresenter.swift in Sources */, FAEE4D0B262DC2B200AD9966 /* Dispatcher.swift in Sources */, 5BC37A10271F1D0000444D5E /* AdUnitConfig.swift in Sources */, A908694629E05F7900B37479 /* PrebidRenderer.swift in Sources */, diff --git a/PrebidMobile/AdUnits/Native/NativeAd.swift b/PrebidMobile/AdUnits/Native/NativeAd.swift index 03cfe6bba..27bc88054 100644 --- a/PrebidMobile/AdUnits/Native/NativeAd.swift +++ b/PrebidMobile/AdUnits/Native/NativeAd.swift @@ -50,16 +50,7 @@ public class NativeAd: NSObject, CacheExpiryDelegate { private let eventManager = EventManager() - private var viewControllerForPresentingModals: UIViewController? - - public var privacyUrl: String? { - set { - nativeAdMarkup?.privacy = newValue - } - get { - return nativeAdMarkup?.privacy - } - } + private var productControllerPresenter: SKStoreProductViewControllerPresenter? // MARK: - Array getters @@ -83,6 +74,11 @@ public class NativeAd: NSObject, CacheExpiryDelegate { return nativeAdMarkup?.eventtrackers } + public var privacyUrl: String? { + set { nativeAdMarkup?.privacy = newValue } + get { nativeAdMarkup?.privacy } + } + // MARK: - Filtered array getters /// Returns an array of data objects filtered by the specified data type. @@ -363,11 +359,20 @@ public class NativeAd: NSObject, CacheExpiryDelegate { landingPageString: url ).openHiddenWebView() - presentSKStoreProductViewController(with: productParameters) + if let viewControllerForPresentingModals = viewForTracking?.parentViewController ?? UIApplication.topViewController() { + productControllerPresenter = SKStoreProductViewControllerPresenter() + productControllerPresenter?.present( + from: viewControllerForPresentingModals, + using: productParameters + ) + } else { + Log.error("SDK couldn't find a view controller to present the SKStoreProductViewController from.") + } + fireClickTrackers() } // Normal clickthrough - else if openURLWithExternalBrowser(url: url) { + else if UIApplication.shared.openExternalURL(url) { fireClickTrackers() } else { Log.debug("Could not open click URL: \(clickUrl)") @@ -382,44 +387,6 @@ public class NativeAd: NSObject, CacheExpiryDelegate { TrackerManager.shared.fireTrackerURLArray(arrayWithURLs: clickTrackersURLs) { _ in } } - - private func presentSKStoreProductViewController(with productParameters: [String: Any]) { - DispatchQueue.main.async { - let skadnController = SKStoreProductViewController() - skadnController.delegate = self - var viewControllerForPresentingModals = UIApplication.topViewController() - - if let viewForTracking = self.viewForTracking, - let viewController = viewForTracking.parentViewController { - viewControllerForPresentingModals = viewController - } - - self.viewControllerForPresentingModals = viewControllerForPresentingModals - - viewControllerForPresentingModals?.present(skadnController, animated: true) - skadnController.loadProduct(withParameters: productParameters) { _, error in - if let error { - Log.error("Error occurred during SKStoreProductViewController product loading: \(error.localizedDescription)") - } - } - } - } - - private func openURLWithExternalBrowser(url: URL) -> Bool { - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - return true - } else { - return false - } - } -} - -extension NativeAd: SKStoreProductViewControllerDelegate { - - public func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { - viewControllerForPresentingModals?.dismiss(animated: true) - } } private class NativeAdGestureRecognizerRecord : NSObject { diff --git a/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkHelper.swift similarity index 78% rename from PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift rename to PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkHelper.swift index b609285bc..90441e4c1 100644 --- a/PrebidMobile/Addendum/PrebidSKAdNetworkHelper.swift +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkHelper.swift @@ -24,6 +24,8 @@ public class PrebidSKAdNetworkHelper: NSObject { private weak var adView: UIView? private weak var viewControllerForPresentingModals: UIViewController? + private var productControllerPresenter: SKStoreProductViewControllerPresenter? + /// Subscribes to ad click events on the provided ad view and configures it with SKAdN tracking.. /// - Parameters: /// - adView: The ad view where click events will be tracked. @@ -81,12 +83,13 @@ public class PrebidSKAdNetworkHelper: NSObject { } private func configureAdViewWithSkadn(using bid: Bid) { - guard let adView = adView else { + guard let adView = adView, let viewControllerForPresentingModals = viewControllerForPresentingModals else { return } guard let skadn = bid.skadn, - let skadnParameters = SkadnParametersManager.getSkadnProductParameters(for: skadn) else { + let productParameters = SkadnParametersManager.getSkadnProductParameters(for: skadn) else { + Log.error("SDK couldn't retrieve SKAdN product parameters from bid response.") return } @@ -98,34 +101,15 @@ public class PrebidSKAdNetworkHelper: NSObject { adView.addSubview(overlayView) overlayView.onClick = { [weak self] in - self?.presentSKStoreProductViewController(with: skadnParameters) - } - } - - private func presentSKStoreProductViewController(with productParameters: [String: Any]) { - DispatchQueue.main.async { - let skadnController = SKStoreProductViewController() - skadnController.delegate = self - - self.viewControllerForPresentingModals?.present(skadnController, animated: true) - skadnController.loadProduct(withParameters: productParameters) { _, error in - if let error { - Log.error("Error occurred during SKStoreProductViewController product loading: \(error.localizedDescription)") - } - } + self?.productControllerPresenter = SKStoreProductViewControllerPresenter() + self?.productControllerPresenter?.present( + from: viewControllerForPresentingModals, + using: productParameters + ) } } } -// MARK: - SKStoreProductViewControllerDelegate - -extension PrebidSKAdNetworkHelper: SKStoreProductViewControllerDelegate { - - public func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { - viewControllerForPresentingModals?.dismiss(animated: true) - } -} - /// A custom overlay view for tracking touch interactions. private class TouchTrackingOverlayView: UIView { diff --git a/PrebidMobile/Addendum/SKAdNetwork/SKStoreProductViewControllerPresenter.swift b/PrebidMobile/Addendum/SKAdNetwork/SKStoreProductViewControllerPresenter.swift new file mode 100644 index 000000000..fef69bd5d --- /dev/null +++ b/PrebidMobile/Addendum/SKAdNetwork/SKStoreProductViewControllerPresenter.swift @@ -0,0 +1,47 @@ +/* Copyright 2018-2025 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 StoreKit + +class SKStoreProductViewControllerPresenter: NSObject { + + private weak var presentingViewController: UIViewController? + + func present(from viewController: UIViewController, using productParameters: [String: Any]) { + self.presentingViewController = viewController + + DispatchQueue.main.async { + let skadnController = SKStoreProductViewController() + skadnController.delegate = self + + self.presentingViewController?.present(skadnController, animated: true) + skadnController.loadProduct(withParameters: productParameters) { _, error in + if let error { + Log.error("Error occurred during SKStoreProductViewController product loading: \(error.localizedDescription)") + } + } + } + } +} + +// MARK: - SKStoreProductViewControllerDelegate + +extension SKStoreProductViewControllerPresenter: SKStoreProductViewControllerDelegate { + + func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { + presentingViewController?.dismiss(animated: true) + } +} diff --git a/PrebidMobile/PrebidMobileRendering/ExtensionsAndWrappers/UIApplication+Extensions.swift b/PrebidMobile/PrebidMobileRendering/ExtensionsAndWrappers/UIApplication+Extensions.swift index b88eafa02..a344a1c2d 100644 --- a/PrebidMobile/PrebidMobileRendering/ExtensionsAndWrappers/UIApplication+Extensions.swift +++ b/PrebidMobile/PrebidMobileRendering/ExtensionsAndWrappers/UIApplication+Extensions.swift @@ -42,4 +42,11 @@ extension UIApplication { return topController } + + func openExternalURL(_ url: URL) -> Bool { + guard canOpenURL(url) else { return false } + + open(url, options: [:], completionHandler: nil) + return true + } } From ae4dbec2232ffae221d9f9fc94a5c56b322c1d54 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Tue, 18 Feb 2025 13:40:30 +0200 Subject: [PATCH 14/21] feat: add support of SKAdN view-through ads --- PrebidMobile/AdUnits/AdUnit.swift | 26 ++++++++++++++++--- .../BannerViewImpressionTracker.swift | 21 ++++++++------- .../InterstitialImpressionTracker.swift | 23 +++++++++------- .../PrebidImpressionTracker.swift | 25 +++++++++--------- .../PrebidImpressionTrackerProtocol.swift | 4 ++- 5 files changed, 65 insertions(+), 34 deletions(-) diff --git a/PrebidMobile/AdUnits/AdUnit.swift b/PrebidMobile/AdUnits/AdUnit.swift index dfc28371e..abb7e202c 100644 --- a/PrebidMobile/AdUnits/AdUnit.swift +++ b/PrebidMobile/AdUnits/AdUnit.swift @@ -58,6 +58,8 @@ public class AdUnit: NSObject, DispatcherDelegate { isInterstitial: adUnitConfig.adConfiguration.isInterstitialAd ) + private let eventManager = EventManager() + /// Initializes a new `AdUnit` instance with the specified configuration ID, size, and ad formats. /// /// - Parameters: @@ -200,12 +202,30 @@ public class AdUnit: NSObject, DispatcherDelegate { return } - let impressionTrackingPayload = PrebidImpressionTrackerPayload( - cacheID: bidResponse.winningBid?.targetingInfo?["hb_cache_id"], - trackingURLs: bidResponse.winningBid?.impressionTrackingURLs ?? [] + // Create a tracker for server event, f.e. firing `burl` + let serverEventTracker = PrebidServerEventTracker() + eventManager.registerTracker(serverEventTracker) + + // Register impression URLs + let impServerEvents = bidResponse.winningBid? + .impressionTrackingURLs + .map { ServerEvent(url: $0, expectedEventType: .impression) } ?? [] + serverEventTracker.addServerEvents(impServerEvents) + + if #available(iOS 14.5, *) { + if let skadn = bidResponse.winningBid?.skadn, + let imp = SkadnParametersManager.getSkadnImpression(for: skadn) { + let skadnEventTracker = SkadnEventTracker(with: imp) + eventManager.registerTracker(skadnEventTracker) + } + } + + let impressionTrackingPayload = PrebidImpressionTracker.Payload( + cacheID: bidResponse.winningBid?.targetingInfo?["hb_cache_id"] ) self.impressionTracker.register(payload: impressionTrackingPayload) + self.impressionTracker.register(eventManager: eventManager) if (!self.timeOutSignalSent) { let resultCode = self.setUp(adObject, with: bidResponse) diff --git a/PrebidMobile/AdUnits/ImpressionTracking/BannerViewImpressionTracker.swift b/PrebidMobile/AdUnits/ImpressionTracking/BannerViewImpressionTracker.swift index 18ece22d7..4d1ee4852 100644 --- a/PrebidMobile/AdUnits/ImpressionTracking/BannerViewImpressionTracker.swift +++ b/PrebidMobile/AdUnits/ImpressionTracking/BannerViewImpressionTracker.swift @@ -24,7 +24,8 @@ class BannerViewImpressionTracker: PrebidImpressionTrackerProtocol { private var reloadTracker: BannerViewReloadTracker? private var viewabilityTracker: PBMCreativeViewabilityTracker? - private var payload: PrebidImpressionTrackerPayload? + private var eventManager: EventManager? + private var payload: PrebidImpressionTracker.Payload? private var isImpressionTracked = false @@ -49,6 +50,8 @@ class BannerViewImpressionTracker: PrebidImpressionTrackerProtocol { func stop() { reloadTracker?.stop() viewabilityTracker?.stop() + payload = nil + eventManager = nil } private func attachViewabilityTracker() { @@ -66,16 +69,12 @@ class BannerViewImpressionTracker: PrebidImpressionTrackerProtocol { // Ensure that we found Prebid creative AdViewUtils.findPrebidCacheID(monitoredView) { [weak self] result in - guard let self = self else { return } + guard let self = self, let eventManager = self.eventManager else { return } switch result { case .success(let foundCacheID): - if let trackingURLs = self.payload?.trackingURLs, - let creativeCacheID = self.payload?.cacheID, - foundCacheID == creativeCacheID { - for trackingURL in trackingURLs { - PrebidServerConnection.shared.fireAndForget(trackingURL) - } + if let creativeCacheID = self.payload?.cacheID, foundCacheID == creativeCacheID { + eventManager.trackEvent(.impression) } case .failure(let error): Log.warn(error.localizedDescription) @@ -88,7 +87,11 @@ class BannerViewImpressionTracker: PrebidImpressionTrackerProtocol { viewabilityTracker?.start() } - func register(payload: PrebidImpressionTrackerPayload) { + func register(payload: PrebidImpressionTracker.Payload) { self.payload = payload } + + func register(eventManager: EventManager) { + self.eventManager = eventManager + } } diff --git a/PrebidMobile/AdUnits/ImpressionTracking/InterstitialImpressionTracker.swift b/PrebidMobile/AdUnits/ImpressionTracking/InterstitialImpressionTracker.swift index 1c7f3d0a6..df077a8f9 100644 --- a/PrebidMobile/AdUnits/ImpressionTracking/InterstitialImpressionTracker.swift +++ b/PrebidMobile/AdUnits/ImpressionTracking/InterstitialImpressionTracker.swift @@ -21,7 +21,8 @@ class InterstitialImpressionTracker: PrebidImpressionTrackerProtocol { private var interstitialObserver: InterstitialObserver? private var viewabilityTracker: PBMCreativeViewabilityTracker? - private var payload: PrebidImpressionTrackerPayload? + private var eventManager: EventManager? + private var payload: PrebidImpressionTracker.Payload? private var pollingInterval: TimeInterval { 0.2 @@ -38,6 +39,8 @@ class InterstitialImpressionTracker: PrebidImpressionTrackerProtocol { func stop() { interstitialObserver?.stop() viewabilityTracker?.stop() + payload = nil + eventManager = nil } private func attachViewabilityTracker(to view: UIView) { @@ -51,15 +54,13 @@ class InterstitialImpressionTracker: PrebidImpressionTrackerProtocol { self.stop() // Ensure that we found Prebid creative - AdViewUtils.findPrebidCacheID(view) { result in + AdViewUtils.findPrebidCacheID(view) { [weak self] result in + guard let self = self, let eventManager = self.eventManager else { return } + switch result { case .success(let foundCacheID): - if let trackingURLs = self.payload?.trackingURLs, - let creativeCacheID = self.payload?.cacheID, - foundCacheID == creativeCacheID { - for trackingURL in trackingURLs { - PrebidServerConnection.shared.fireAndForget(trackingURL) - } + if let creativeCacheID = self.payload?.cacheID, foundCacheID == creativeCacheID { + eventManager.trackEvent(.impression) } case .failure(let error): Log.warn(error.localizedDescription) @@ -72,7 +73,11 @@ class InterstitialImpressionTracker: PrebidImpressionTrackerProtocol { viewabilityTracker?.start() } - func register(payload: PrebidImpressionTrackerPayload) { + func register(payload: PrebidImpressionTracker.Payload) { self.payload = payload } + + func register(eventManager: EventManager) { + self.eventManager = eventManager + } } diff --git a/PrebidMobile/AdUnits/ImpressionTracking/PrebidImpressionTracker.swift b/PrebidMobile/AdUnits/ImpressionTracking/PrebidImpressionTracker.swift index e5adadce6..afa89c707 100644 --- a/PrebidMobile/AdUnits/ImpressionTracking/PrebidImpressionTracker.swift +++ b/PrebidMobile/AdUnits/ImpressionTracking/PrebidImpressionTracker.swift @@ -15,19 +15,16 @@ import UIKit -/// Payload for impression trackers. -struct PrebidImpressionTrackerPayload { - - /// seatbid.bid.ext.prebid.targeting.hb_cache_id. - /// Used to identify if SDK found Prebid creative. - let cacheID: String? - - /// URL strings to track when impression conditions are met. - let trackingURLs: [String] -} - class PrebidImpressionTracker { + /// Payload for impression trackers. + struct Payload { + + /// seatbid.bid.ext.prebid.targeting.hb_cache_id. + /// Used to identify if SDK found Prebid creative. + let cacheID: String? + } + private let tracker: PrebidImpressionTrackerProtocol init(isInterstitial: Bool) { @@ -38,9 +35,13 @@ class PrebidImpressionTracker { } } - func register(payload: PrebidImpressionTrackerPayload) { + func register(payload: PrebidImpressionTracker.Payload) { tracker.register(payload: payload) } + + func register(eventManager: EventManager) { + tracker.register(eventManager: eventManager) + } func start(in adView: UIView) { DispatchQueue.main.async { diff --git a/PrebidMobile/AdUnits/ImpressionTracking/PrebidImpressionTrackerProtocol.swift b/PrebidMobile/AdUnits/ImpressionTracking/PrebidImpressionTrackerProtocol.swift index bb5153b4a..1f71e3df3 100644 --- a/PrebidMobile/AdUnits/ImpressionTracking/PrebidImpressionTrackerProtocol.swift +++ b/PrebidMobile/AdUnits/ImpressionTracking/PrebidImpressionTrackerProtocol.swift @@ -18,5 +18,7 @@ import UIKit protocol PrebidImpressionTrackerProtocol { func start(in view: UIView) func stop() - func register(payload: PrebidImpressionTrackerPayload) + + func register(payload: PrebidImpressionTracker.Payload) + func register(eventManager: EventManager) } From 6861843fa16f469718d37a040b5f45f7bf59e306 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Tue, 18 Feb 2025 17:07:13 +0200 Subject: [PATCH 15/21] feat: introduce public api to activate SKAdN Store-Kit --- .../Model/TestCasesManager.swift | 78 ++++++---- ...idOriginalAPIDisplayBannerController.swift | 11 +- ...inalAPIDisplayInterstitialController.swift | 7 + ...ebidOriginalAPIVideoBannerController.swift | 8 - ...idOriginalAPIVideoRewardedController.swift | 7 + PrebidMobile.xcodeproj/project.pbxproj | 20 ++- PrebidMobile/AdUnits/AdUnit.swift | 4 + PrebidMobile/AdUnits/BannerAdUnit.swift | 12 ++ PrebidMobile/AdUnits/InterstitialAdUnit.swift | 13 ++ .../MultiformatAdUnit/PrebidAdUnit.swift | 20 +++ .../AdUnits/RewardedVideoAdUnit.swift | 10 ++ .../PrebidSKAdNetworkAdClickHandler.swift | 138 ++++++++++++++++++ .../SKAdNetwork/PrebidSKAdNetworkHelper.swift | 8 +- .../PrebidSKAdNetworkStoreKitHelper.swift | 102 +++++++++++++ .../PrebidStoreKitAdsBannerHelper.swift | 35 +++++ .../SKAdNetwork/PrebidStoreKitAdsHelper.swift | 42 ++++++ .../PrebidStoreKitAdsInterstitialHelper.swift | 42 ++++++ ...KStoreProductViewControllerPresenter.swift | 13 +- 18 files changed, 506 insertions(+), 64 deletions(-) create mode 100644 PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift create mode 100644 PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkStoreKitHelper.swift create mode 100644 PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsBannerHelper.swift create mode 100644 PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsHelper.swift create mode 100644 PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift diff --git a/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift b/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift index c17e396b2..7942ce0d9 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift @@ -261,7 +261,7 @@ struct TestCaseManager { let bannerController = PrebidOriginalAPIDisplayBannerController(rootController: adapterVC) bannerController.adSize = CGSize(width: 320, height: 50) - bannerController.prebidConfigId = "prebid-ita-banner-320-50"; + bannerController.prebidConfigId = "prebid-ita-banner-320-50" bannerController.adUnitID = "/21808260008/prebid_demo_app_original_api_banner" adapterVC.setup(adapter: bannerController) @@ -269,7 +269,7 @@ struct TestCaseManager { setupCustomParams(for: bannerController.prebidConfigId) }), - TestCase(title: "Banner 320x50 (GAM Original) [SKAdN]", + TestCase(title: "Banner 320x50 SKAdN (GAM Original) [OK, PUC]", tags: [.banner, .originalAPI, .server], exampleVCStoryboardID: "AdapterViewController", configurationClosure: { vc in @@ -281,7 +281,7 @@ struct TestCaseManager { Targeting.shared.sourceapp = "InternalTestApp" let bannerController = PrebidOriginalAPIDisplayBannerController(rootController: adapterVC) - bannerController.activatePrebidSKAdNHelper = true + bannerController.activatePrebidSKAdN = true bannerController.adSize = CGSize(width: 320, height: 50) bannerController.prebidConfigId = "prebid-demo-banner-320-50-skadn" @@ -371,26 +371,6 @@ struct TestCaseManager { setupCustomParams(for: bannerController.prebidConfigId) }), - TestCase(title: "Video Outstream (GAM Original) [SKAdN]", - tags: [.video, .originalAPI, .server], - exampleVCStoryboardID: "AdapterViewController", - configurationClosure: { vc in - - guard let adapterVC = vc as? AdapterViewController else { - return - } - - let bannerController = PrebidOriginalAPIVideoBannerController(rootController: adapterVC) - bannerController.activatePrebidSKAdNHelper = true - bannerController.adSize = CGSize(width: 300, height: 250) - bannerController.prebidConfigId = "prebid-demo-video-outstream-original-api-skadn" - bannerController.adUnitID = "/21808260008/prebid-demo-original-api-video-banner" - - adapterVC.setup(adapter: bannerController) - - setupCustomParams(for: bannerController.prebidConfigId) - }), - TestCase(title: "Multiformat Banner (GAM Original) [OK, PUC]", tags: [.multiformat, .originalAPI, .server], exampleVCStoryboardID: "AdapterViewController", @@ -432,6 +412,25 @@ struct TestCaseManager { setupCustomParams(for: interstitialController.prebidConfigId) }), + TestCase(title: "Display Interstitial 320x480 SKAdN (GAM Original) [OK, PUC]", + tags: [.interstitial, .originalAPI, .server], + exampleVCStoryboardID: "AdapterViewController", + configurationClosure: { vc in + + guard let adapterVC = vc as? AdapterViewController else { + return + } + + let interstitialController = PrebidOriginalAPIDisplayInterstitialController(rootController: adapterVC) + interstitialController.prebidConfigId = "prebid-demo-display-interstitial-320-480-skadn" + interstitialController.adUnitID = "/21808260008/prebid-demo-app-original-api-display-interstitial" + interstitialController.activatePrebidSKAdN = true + + adapterVC.setup(adapter: interstitialController) + + setupCustomParams(for: interstitialController.prebidConfigId) + }), + TestCase(title: "Video Interstitial 320x480 (GAM Original) [OK, PUC]", tags: [.interstitial, .originalAPI, .server, .video], exampleVCStoryboardID: "AdapterViewController", @@ -468,6 +467,25 @@ struct TestCaseManager { setupCustomParams(for: interstitialController.prebidConfigId) }), + TestCase(title: "Video Rewarded SKAdN 320x480 (GAM Original) [OK, PUC]", + tags: [.interstitial, .originalAPI, .server, .video], + exampleVCStoryboardID: "AdapterViewController", + configurationClosure: { vc in + + guard let adapterVC = vc as? AdapterViewController else { + return + } + + let interstitialController = PrebidOriginalAPIVideoRewardedController(rootController: adapterVC) + interstitialController.activatePrebidSKAdN = true + interstitialController.prebidConfigId = "prebid-ita-video-rewarded-320-480-original-api-skadn" + interstitialController.adUnitID = "/21808260008/prebid-demo-app-original-api-video-interstitial" + + adapterVC.setup(adapter: interstitialController) + + setupCustomParams(for: interstitialController.prebidConfigId) + }), + TestCase(title: "Multiformat Interstitial 320x480 (GAM Original) [OK, PUC]", tags: [.multiformat, .originalAPI, .server], exampleVCStoryboardID: "AdapterViewController", @@ -533,7 +551,7 @@ struct TestCaseManager { setupCustomParams(for: nativeController.prebidConfigId) }), - TestCase(title: "Native In-App (GAM Original) [OK, PUC]", + TestCase(title: "Native In-App (GAM Original) [OK]", tags: [.native, .originalAPI, .server], exampleVCStoryboardID: "AdapterViewController", configurationClosure: { vc in @@ -554,7 +572,7 @@ struct TestCaseManager { setupCustomParams(for: nativeController.prebidConfigId) }), - TestCase(title: "Native In-App (GAM Original) [SKAdN]", + TestCase(title: "Native In-App SKAdN (GAM Original) [OK]", tags: [.native, .originalAPI, .server], exampleVCStoryboardID: "AdapterViewController", configurationClosure: { vc in @@ -881,7 +899,7 @@ struct TestCaseManager { }), // NOTE: works only with InternalTestApp-Skadn target - TestCase(title: "Banner 320x50 (In-App) [SKAdN]", + TestCase(title: "Banner 320x50 SKAdN (In-App) [OK]", tags: [.banner, .inapp, .server], exampleVCStoryboardID: "AdapterViewController", configurationClosure: { vc in @@ -1157,7 +1175,7 @@ struct TestCaseManager { }), // NOTE: works only with InternalTestApp-Skadn target - TestCase(title: "Display Interstitial 320x480 (In-App) [SKAdN]", + TestCase(title: "Display Interstitial 320x480 SKAdN (In-App) [OK]", tags: [.interstitial, .inapp, .server], exampleVCStoryboardID: "AdapterViewController", configurationClosure: { vc in @@ -1600,7 +1618,7 @@ struct TestCaseManager { }), // NOTE: works only with InternalTestApp-Skadn target - TestCase(title: "Video Interstitial 320x480 (In-App) [SKAdN]", + TestCase(title: "Video Interstitial 320x480 SKAdN (In-App) [OK]", tags: [.video, .inapp, .server], exampleVCStoryboardID: "AdapterViewController", configurationClosure: { vc in @@ -1885,7 +1903,7 @@ struct TestCaseManager { }), // NOTE: works only with InternalTestApp-Skadn target - TestCase(title: "Video Outstream (In-App) [SKAdN]", + TestCase(title: "Video Outstream SKAdN (In-App) [OK]", tags: [.video, .inapp, .server], exampleVCStoryboardID: "AdapterViewController", configurationClosure: { vc in @@ -2121,7 +2139,7 @@ struct TestCaseManager { setupCustomParams(for: rewardedAdController.prebidConfigId) }), - TestCase(title: "Video Rewarded Time 320x480 (In-App) [SKAdN]", + TestCase(title: "Video Rewarded Time 320x480 SKAdN (In-App) [OK]", tags: [.interstitial, .video, .inapp, .server], exampleVCStoryboardID: "AdapterViewController", configurationClosure: { vc in diff --git a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIDisplayBannerController.swift b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIDisplayBannerController.swift index 5ce5e21b9..f82fba092 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIDisplayBannerController.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIDisplayBannerController.swift @@ -32,10 +32,9 @@ class PrebidOriginalAPIDisplayBannerController: var gamSizes = [GADAdSize]() // Prebid - var activatePrebidSKAdNHelper = false + var activatePrebidSKAdN = false private var adUnit: BannerAdUnit! - private let skadnHelper = PrebidSKAdNetworkHelper() // GAM private var gamBanner: GAMBannerView! @@ -192,16 +191,16 @@ class PrebidOriginalAPIDisplayBannerController: rootController?.bannerView.constraints.first { $0.firstAttribute == .width }?.constant = bannerView.adSize.size.width rootController?.bannerView.constraints.first { $0.firstAttribute == .height }?.constant = bannerView.adSize.size.height + if activatePrebidSKAdN { + adUnit.activatePrebidSKAdNetworkStoreKitAdsFlow(adView: gamBanner) + } + AdViewUtils.findPrebidCreativeSize(bannerView, success: { size in guard let bannerView = bannerView as? GAMBannerView else { return } bannerView.resize(GADAdSizeFromCGSize(size)) }, failure: { (error) in Log.error("Error occuring during searching for Prebid creative size: \(error)") }) - - if let rootController, activatePrebidSKAdNHelper == true { - skadnHelper.subscribeOnAdClicked(adView: bannerView, viewController: rootController) - } } func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) { diff --git a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIDisplayInterstitialController.swift b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIDisplayInterstitialController.swift index 8df8986c5..ca9e91876 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIDisplayInterstitialController.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIDisplayInterstitialController.swift @@ -29,6 +29,8 @@ class PrebidOriginalAPIDisplayInterstitialController: var refreshInterval: TimeInterval = 0 + var activatePrebidSKAdN = false + // Prebid private var adUnit: InterstitialAdUnit! @@ -153,6 +155,11 @@ class PrebidOriginalAPIDisplayInterstitialController: @IBAction func showButtonClicked() { if let gamInterstitial = gamInterstitial { rootController?.showButton.isEnabled = false + + if activatePrebidSKAdN { + adUnit.activatePrebidSKAdNetworkStoreKitAdsFlow() + } + gamInterstitial.present(fromRootViewController: rootController!) } } diff --git a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoBannerController.swift b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoBannerController.swift index e1306ff42..3df5f8ebd 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoBannerController.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoBannerController.swift @@ -31,11 +31,7 @@ class PrebidOriginalAPIVideoBannerController: var adSize = CGSize.zero var gamSizes = [GADAdSize]() - // Prebid - var activatePrebidSKAdNHelper = false - private var adUnit: VideoAdUnit! - private let skadnHelper = PrebidSKAdNetworkHelper() // GAM private var gamBanner: GAMBannerView! @@ -193,10 +189,6 @@ class PrebidOriginalAPIVideoBannerController: }, failure: { (error) in Log.error("Error occuring during searching for Prebid creative size: \(error)") }) - - if let rootController, activatePrebidSKAdNHelper == true { - skadnHelper.subscribeOnAdClicked(adView: bannerView, viewController: rootController) - } } func bannerView(_ bannerView: GADBannerView, didFailToReceiveAdWithError error: Error) { diff --git a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoRewardedController.swift b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoRewardedController.swift index 53ede36b9..c052fc0df 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoRewardedController.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoRewardedController.swift @@ -27,6 +27,8 @@ class PrebidOriginalAPIVideoRewardedController: var prebidConfigId = "" var adUnitID = "" + var activatePrebidSKAdN = false + var refreshInterval: TimeInterval = 0 // Prebid @@ -147,6 +149,11 @@ class PrebidOriginalAPIVideoRewardedController: @IBAction func showButtonClicked() { if let gamRewarded = gamRewarded { rootController?.showButton.isEnabled = false + + if activatePrebidSKAdN { + adUnit.activatePrebidSKAdNetworkStoreKitAdsFlow() + } + gamRewarded.present(fromRootViewController: rootController!) {} } } diff --git a/PrebidMobile.xcodeproj/project.pbxproj b/PrebidMobile.xcodeproj/project.pbxproj index 07666fd0b..c4dd3ab54 100644 --- a/PrebidMobile.xcodeproj/project.pbxproj +++ b/PrebidMobile.xcodeproj/project.pbxproj @@ -121,9 +121,12 @@ 535ADE0B2D2E970200DB888F /* SDKConsoleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9F0999C2C78CC8A007DB464 /* SDKConsoleLogger.swift */; }; 535ADE102D2E987E00DB888F /* PluginEventDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535ADE0F2D2E987E00DB888F /* PluginEventDelegate.swift */; }; 535ADE122D2EA2F500DB888F /* PrebidLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 535ADE112D2EA2F500DB888F /* PrebidLogger.swift */; }; - 536469C92D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536469C82D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift */; }; + 536469C92D341A8100F50B6D /* PrebidSKAdNetworkAdClickHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536469C82D341A8100F50B6D /* PrebidSKAdNetworkAdClickHandler.swift */; }; 536A427F282D11DA0069E9B2 /* PrebidServerConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536A427E282D11DA0069E9B2 /* PrebidServerConnection.swift */; }; 536A4283282D12E80069E9B2 /* PrebidServerConnectionProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 536A4282282D12E80069E9B2 /* PrebidServerConnectionProtocol.swift */; }; + 53760E062D64B9B7008B9DFD /* PrebidStoreKitAdsHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53760E052D64B9B7008B9DFD /* PrebidStoreKitAdsHelper.swift */; }; + 53760E082D64B9D5008B9DFD /* PrebidStoreKitAdsBannerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53760E072D64B9D5008B9DFD /* PrebidStoreKitAdsBannerHelper.swift */; }; + 53760E0A2D64BA05008B9DFD /* PrebidStoreKitAdsInterstitialHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53760E092D64BA05008B9DFD /* PrebidStoreKitAdsInterstitialHelper.swift */; }; 5379F6BA2ABB711500B0B7A9 /* PrebidAdUnitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5379F6B92ABB711500B0B7A9 /* PrebidAdUnitTests.swift */; }; 537B6518283372FD008AE9D1 /* PathBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537B6517283372FD008AE9D1 /* PathBuilder.swift */; }; 537B651E2833A3DA008AE9D1 /* Reachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 537B651D2833A3DA008AE9D1 /* Reachability.swift */; }; @@ -1033,9 +1036,12 @@ 5355ACAA29C454770014F16E /* CreativeModelCollectionMakerVASTTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreativeModelCollectionMakerVASTTests.swift; sourceTree = ""; }; 535ADE0F2D2E987E00DB888F /* PluginEventDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginEventDelegate.swift; sourceTree = ""; }; 535ADE112D2EA2F500DB888F /* PrebidLogger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidLogger.swift; sourceTree = ""; }; - 536469C82D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidSKAdNetworkHelper.swift; sourceTree = ""; }; + 536469C82D341A8100F50B6D /* PrebidSKAdNetworkAdClickHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidSKAdNetworkAdClickHandler.swift; sourceTree = ""; }; 536A427E282D11DA0069E9B2 /* PrebidServerConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidServerConnection.swift; sourceTree = ""; }; 536A4282282D12E80069E9B2 /* PrebidServerConnectionProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidServerConnectionProtocol.swift; sourceTree = ""; }; + 53760E052D64B9B7008B9DFD /* PrebidStoreKitAdsHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidStoreKitAdsHelper.swift; sourceTree = ""; }; + 53760E072D64B9D5008B9DFD /* PrebidStoreKitAdsBannerHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidStoreKitAdsBannerHelper.swift; sourceTree = ""; }; + 53760E092D64BA05008B9DFD /* PrebidStoreKitAdsInterstitialHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidStoreKitAdsInterstitialHelper.swift; sourceTree = ""; }; 5379F6B92ABB711500B0B7A9 /* PrebidAdUnitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrebidAdUnitTests.swift; sourceTree = ""; }; 537B6517283372FD008AE9D1 /* PathBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PathBuilder.swift; sourceTree = ""; }; 537B651D2833A3DA008AE9D1 /* Reachability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Reachability.swift; sourceTree = ""; }; @@ -2028,7 +2034,10 @@ 537F3F9A2D6489C800584F9C /* SKAdNetwork */ = { isa = PBXGroup; children = ( - 536469C82D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift */, + 53760E052D64B9B7008B9DFD /* PrebidStoreKitAdsHelper.swift */, + 53760E072D64B9D5008B9DFD /* PrebidStoreKitAdsBannerHelper.swift */, + 53760E092D64BA05008B9DFD /* PrebidStoreKitAdsInterstitialHelper.swift */, + 536469C82D341A8100F50B6D /* PrebidSKAdNetworkAdClickHandler.swift */, 537F3F9B2D648A3000584F9C /* SKStoreProductViewControllerPresenter.swift */, ); path = SKAdNetwork; @@ -4234,6 +4243,7 @@ 5BC37970271F1D0000444D5E /* PBMVastAbstractAd.m in Sources */, 53BDBF89293F5EFF004B6DE8 /* InternalUserConsentDataManager.m in Sources */, 92C85D5E27A96DBB0080BAC5 /* NativeData.swift in Sources */, + 53760E0A2D64BA05008B9DFD /* PrebidStoreKitAdsInterstitialHelper.swift in Sources */, 53D934FF282CE62E0092E243 /* AgeUtils.swift in Sources */, 5BC378E4271F1CFF00444D5E /* PBMORTBImpExtSkadn.m in Sources */, 53138B2A2A71152200B18B5C /* PrebidSDKInitializer.swift in Sources */, @@ -4285,7 +4295,7 @@ 5BC37AE2271F1D0100444D5E /* PBMInterstitialLayoutConfigurator.m in Sources */, 53A368EB2AB2E95E00A03B3E /* NativeParameters.swift in Sources */, 5388F32627F46A7F00319FE4 /* AdViewButtonDecorator.swift in Sources */, - 536469C92D341A8100F50B6D /* PrebidSKAdNetworkHelper.swift in Sources */, + 536469C92D341A8100F50B6D /* PrebidSKAdNetworkAdClickHandler.swift in Sources */, 5BC37AB6271F1D0000444D5E /* PBMOpenMeasurementFriendlyObstructionTypeBridge.m in Sources */, FAEE4D2C262DC2B200AD9966 /* Host.swift in Sources */, 5BC37AC8271F1D0100444D5E /* PBMBasicParameterBuilder.m in Sources */, @@ -4311,6 +4321,7 @@ 5BC37A62271F1D0000444D5E /* PBMORTBBidExtPrebidCacheBids.m in Sources */, 929865392806CCC4007A2F34 /* UIView+Extensions.swift in Sources */, 5BC37997271F1D0000444D5E /* PBMAbstractCreative.m in Sources */, + 53760E082D64B9D5008B9DFD /* PrebidStoreKitAdsBannerHelper.swift in Sources */, 53A447D12CAECA3C008DE6C0 /* BaseRewardedAdUnit.swift in Sources */, 92EE5A0D27F9D292003D7691 /* Position.swift in Sources */, 2A9DDDCC2C5BE190000EA4A0 /* PrebidEventDelegate.swift in Sources */, @@ -4444,6 +4455,7 @@ 5BC3798B271F1D0000444D5E /* PBMTrackingEvent.m in Sources */, 5BC37A74271F1D0000444D5E /* PBMWinNotifier.m in Sources */, 5BC37A00271F1D0000444D5E /* PBMTrackingURLVisitors.m in Sources */, + 53760E062D64B9B7008B9DFD /* PrebidStoreKitAdsHelper.swift in Sources */, 5BC379A7271F1D0000444D5E /* PBMDeferredModalState.m in Sources */, 5BC379D7271F1D0000444D5E /* PBMDisplayView.m in Sources */, 5BC37AA3271F1D0000444D5E /* AdLoadFlowControllerDelegate.swift in Sources */, diff --git a/PrebidMobile/AdUnits/AdUnit.swift b/PrebidMobile/AdUnits/AdUnit.swift index abb7e202c..88e08de94 100644 --- a/PrebidMobile/AdUnits/AdUnit.swift +++ b/PrebidMobile/AdUnits/AdUnit.swift @@ -58,6 +58,10 @@ public class AdUnit: NSObject, DispatcherDelegate { isInterstitial: adUnitConfig.adConfiguration.isInterstitialAd ) + private(set) lazy var skadnStoreKitAdsHelper = PrebidStoreKitAdsHelper( + isInterstitial: adUnitConfig.adConfiguration.isInterstitialAd + ) + private let eventManager = EventManager() /// Initializes a new `AdUnit` instance with the specified configuration ID, size, and ad formats. diff --git a/PrebidMobile/AdUnits/BannerAdUnit.swift b/PrebidMobile/AdUnits/BannerAdUnit.swift index bbd1d1ab3..422a883a8 100644 --- a/PrebidMobile/AdUnits/BannerAdUnit.swift +++ b/PrebidMobile/AdUnits/BannerAdUnit.swift @@ -69,4 +69,16 @@ public class BannerAdUnit: AdUnit, BannerBasedAdUnitProtocol, VideoBasedAdUnitPr public func activatePrebidImpressionTracker(adView: UIView) { impressionTracker.start(in: adView) } + + // MARK: SKAdNetwork + + /// Activates Prebid's SKAdNetwork StoreKit ads flow for the provided ad view. + /// Note: Ensure this method is called within the Google Mobile Ads ad received method + /// (e.g., in the GADBannerViewDelegate's bannerViewDidReceiveAd or similar callbacks). + /// + /// - Parameters: + /// - adView: The ad view that contains ad creative(f.e. GAMBannerView). + public func activatePrebidSKAdNetworkStoreKitAdsFlow(adView: UIView) { + skadnStoreKitAdsHelper.start(in: adView) + } } diff --git a/PrebidMobile/AdUnits/InterstitialAdUnit.swift b/PrebidMobile/AdUnits/InterstitialAdUnit.swift index 90a63283b..ff2073284 100644 --- a/PrebidMobile/AdUnits/InterstitialAdUnit.swift +++ b/PrebidMobile/AdUnits/InterstitialAdUnit.swift @@ -63,9 +63,22 @@ public class InterstitialAdUnit: AdUnit, BannerBasedAdUnitProtocol, VideoBasedAd adUnitConfig.minSizePerc = NSValue(cgSize: CGSize(width: minWidthPerc, height: minHeightPerc)) } + // MARK: Prebid Impression Tracking + + /// Sets the view in which Prebid will start tracking an impression. public func activatePrebidImpressionTracker() { if let window = UIWindow.firstKeyWindow { impressionTracker.start(in: window) } } + + // MARK: SKAdNetwork + + /// Activates Prebid's SKAdNetwork StoreKit ads flow. + /// Note: Ensure this method is called before presenting interstitials. + public func activatePrebidSKAdNetworkStoreKitAdsFlow() { + if let window = UIWindow.firstKeyWindow { + skadnStoreKitAdsHelper.start(in: window) + } + } } diff --git a/PrebidMobile/AdUnits/MultiformatAdUnit/PrebidAdUnit.swift b/PrebidMobile/AdUnits/MultiformatAdUnit/PrebidAdUnit.swift index 9960481ed..bc91d52f5 100644 --- a/PrebidMobile/AdUnits/MultiformatAdUnit/PrebidAdUnit.swift +++ b/PrebidMobile/AdUnits/MultiformatAdUnit/PrebidAdUnit.swift @@ -98,6 +98,26 @@ public class PrebidAdUnit: NSObject { } } + // MARK: SKAdNetwork + + /// Activates Prebid's SKAdNetwork StoreKit ads flow for the provided ad view. + /// Note: Ensure this method is called within the Google Mobile Ads ad received method + /// (e.g., in the GADBannerViewDelegate's bannerViewDidReceiveAd or similar callbacks). + /// + /// - Parameters: + /// - adView: The ad view that contains ad creative(f.e. GAMBannerView). + public func activatePrebidBannerSKAdNetworkStoreKitAdsFlow(adView: UIView) { + adUnit.skadnStoreKitAdsHelper.start(in: adView) + } + + /// Activates Prebid's SKAdNetwork StoreKit ads flow. + /// Note: Ensure this method is called before presenting interstitials. + public func activatePrebidInterstitialSKAdNetworkStoreKitAdsFlow() { + if let window = UIWindow.firstKeyWindow { + adUnit.skadnStoreKitAdsHelper.start(in: window) + } + } + // MARK: - Auto refresh API diff --git a/PrebidMobile/AdUnits/RewardedVideoAdUnit.swift b/PrebidMobile/AdUnits/RewardedVideoAdUnit.swift index a8584072f..9510ee7b8 100644 --- a/PrebidMobile/AdUnits/RewardedVideoAdUnit.swift +++ b/PrebidMobile/AdUnits/RewardedVideoAdUnit.swift @@ -53,4 +53,14 @@ public class RewardedVideoAdUnit: AdUnit, VideoBasedAdUnitProtocol { self.init(configId: configId) adUnitConfig.minSizePerc = NSValue(cgSize: CGSize(width: minWidthPerc, height: minHeightPerc)) } + + // MARK: SKAdNetwork + + /// Activates Prebid's SKAdNetwork StoreKit ads flow. + /// Note: Ensure this method is called before presenting interstitials. + public func activatePrebidSKAdNetworkStoreKitAdsFlow() { + if let window = UIWindow.firstKeyWindow { + skadnStoreKitAdsHelper.start(in: window) + } + } } diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift new file mode 100644 index 000000000..c02db852c --- /dev/null +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift @@ -0,0 +1,138 @@ +/* Copyright 2018-2025 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 WebKit +import StoreKit + +/// Helper class that invokes SKStoreProductViewController on ad click. +@objcMembers +class PrebidSKAdNetworkAdClickHandler: NSObject { + + private weak var adView: UIView? + private weak var viewControllerForPresentingModals: UIViewController? + + private var productControllerPresenter: SKStoreProductViewControllerPresenter? + + /// Configures the provided ad view with SKAdN on click event.. + /// - Parameters: + /// - adView: The ad view where click events is tracked. + /// - viewController: The view controller used to present modals, such as the SKStoreProductViewController. + func start(in adView: UIView, viewController: UIViewController) { + self.adView = adView + self.viewControllerForPresentingModals = viewController + + AdViewUtils.findPrebidLocalCacheID(adView) { [weak self] result in + guard case .success(let cacheID) = result else { + self?.attemptFindUUID() + return + } + + guard let bidString = CacheManager.shared.get(cacheId: cacheID) else { + Log.warn("SDK could not find the bid response for provided cache id.") + return + } + + self?.configureAdViewWithSkadn(using: bidString) + } + } + + func stop() { + adView = nil + viewControllerForPresentingModals = nil + productControllerPresenter = nil + } + + /// Mainly used for video creatives. Searches for **hb_uuid** keys from saved bids in third-party web views. + private func attemptFindUUID() { + guard let adView = adView, + let webView = adView.allSubViewsOf(type: WKWebView.self).first else { + return + } + + webView.evaluateJavaScript("document.body.innerHTML", completionHandler: { html, error in + guard let html = html as? String else { + return + } + + CacheManager.shared + .savedValuesDict + .values + .forEach { [weak self] value in + if let bid = Bid.bid(from: value), + let hbUUID = bid.targetingInfo?["hb_uuid"], + html.contains(hbUUID) { + self?.configureAdViewWithSkadn(using: bid) + } + } + }) + } + + private func configureAdViewWithSkadn(using bidString: String) { + guard let bid = Bid.bid(from: bidString) else { + return + } + + configureAdViewWithSkadn(using: bid) + } + + private func configureAdViewWithSkadn(using bid: Bid) { + guard let adView = adView, let viewControllerForPresentingModals = viewControllerForPresentingModals else { + return + } + + guard let skadn = bid.skadn, + let productParameters = SkadnParametersManager.getSkadnProductParameters(for: skadn) else { + Log.error("SDK couldn't retrieve SKAdN product parameters from bid response.") + return + } + + adView.subviews + .filter({ $0 is TouchTrackingOverlayView }) + .forEach({ $0.removeFromSuperview()}) + + let overlayView = TouchTrackingOverlayView(frame: adView.bounds) + adView.addSubview(overlayView) + + overlayView.onClick = { [weak self] in + self?.productControllerPresenter = SKStoreProductViewControllerPresenter() + self?.productControllerPresenter?.present( + from: viewControllerForPresentingModals, + using: productParameters + ) + } + } +} + +/// A custom overlay view for tracking touch interactions. +private class TouchTrackingOverlayView: UIView { + + var onClick: (() -> Void)? + + private var lastTimestamp: TimeInterval? + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + if self.point(inside: point, with: event) { + if event?.timestamp != lastTimestamp { + onClick?() + } + + lastTimestamp = event?.timestamp + return nil + } + + return super.hitTest(point, with: event) + } +} diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkHelper.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkHelper.swift index 90441e4c1..69a00cc28 100644 --- a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkHelper.swift +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkHelper.swift @@ -19,18 +19,18 @@ import StoreKit /// This class provides utilities for tracking ad clicks and presenting the SKStoreProductViewController @objcMembers -public class PrebidSKAdNetworkHelper: NSObject { +class PrebidSKAdNetworkStoreKitHelper: NSObject { private weak var adView: UIView? private weak var viewControllerForPresentingModals: UIViewController? private var productControllerPresenter: SKStoreProductViewControllerPresenter? - /// Subscribes to ad click events on the provided ad view and configures it with SKAdN tracking.. + /// Configures the provided ad view with SKAdN on click event.. /// - Parameters: - /// - adView: The ad view where click events will be tracked. + /// - adView: The ad view where click events is tracked. /// - viewController: The view controller used to present modals, such as the SKStoreProductViewController. - public func subscribeOnAdClicked(adView: UIView, viewController: UIViewController) { + func start(in adView: UIView, viewController: UIViewController) { self.adView = adView self.viewControllerForPresentingModals = viewController diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkStoreKitHelper.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkStoreKitHelper.swift new file mode 100644 index 000000000..eda396098 --- /dev/null +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkStoreKitHelper.swift @@ -0,0 +1,102 @@ +/* Copyright 2018-2025 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 WebKit +import StoreKit + +/// Helper class that invokes SKStoreProductViewController on ad click. +@objcMembers +class PrebidSKAdNetworkStoreKitAdsHelper: NSObject { + + private weak var adView: UIView? + private weak var viewControllerForPresentingModals: UIViewController? + + private var productControllerPresenter: SKStoreProductViewControllerPresenter? + + /// Configures the provided ad view with SKAdN on click event.. + /// - Parameters: + /// - adView: The ad view where click events is tracked. + /// - viewController: The view controller used to present modals, such as the SKStoreProductViewController. + func start(in adView: UIView, viewController: UIViewController) { + self.adView = adView + self.viewControllerForPresentingModals = viewController + + AdViewUtils.findPrebidLocalCacheID(adView) { [weak self] result in + guard case .success(let cacheID) = result else { + self?.attemptFindUUID() + return + } + + guard let bidString = CacheManager.shared.get(cacheId: cacheID) else { + Log.warn("SDK could not find the bid response for provided cache id.") + return + } + + self?.configureAdViewWithSkadn(using: bidString) + } + } + + /// Mainly used for video creatives. Searches for **hb_uuid** keys from saved bids in third-party web views. + private func attemptFindUUID() { + guard let adView = adView, + let webView = adView.allSubViewsOf(type: WKWebView.self).first else { + return + } + + webView.evaluateJavaScript("document.body.innerHTML", completionHandler: { html, error in + guard let html = html as? String else { + return + } + + CacheManager.shared + .savedValuesDict + .values + .forEach { [weak self] value in + if let bid = Bid.bid(from: value), + let hbUUID = bid.targetingInfo?["hb_uuid"], + html.contains(hbUUID) { + self?.configureAdViewWithSkadn(using: bid) + } + } + }) + } + + private func configureAdViewWithSkadn(using bidString: String) { + guard let bid = Bid.bid(from: bidString) else { + return + } + + configureAdViewWithSkadn(using: bid) + } + + private func configureAdViewWithSkadn(using bid: Bid) { + guard let adView = adView, let viewControllerForPresentingModals = viewControllerForPresentingModals else { + return + } + + guard let skadn = bid.skadn, + let productParameters = SkadnParametersManager.getSkadnProductParameters(for: skadn) else { + Log.error("SDK couldn't retrieve SKAdN product parameters from bid response.") + return + } + + productControllerPresenter = SKStoreProductViewControllerPresenter() + productControllerPresenter?.present( + from: viewControllerForPresentingModals, + using: productParameters + ) + } +} diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsBannerHelper.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsBannerHelper.swift new file mode 100644 index 000000000..aa1d47dd2 --- /dev/null +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsBannerHelper.swift @@ -0,0 +1,35 @@ +/* Copyright 2018-2025 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 + +class PrebidStoreKitAdsBannerHelper: PrebidSKAdNetworkStoreKitAdsHelperProtocol { + + private var skadnClickHandler: PrebidSKAdNetworkAdClickHandler? + + func start(in adView: UIView) { + if let presentingViewController = adView.parentViewController ?? UIApplication.topViewController() { + skadnClickHandler = PrebidSKAdNetworkAdClickHandler() + skadnClickHandler?.start(in: adView, viewController: presentingViewController) + } else { + Log.error("SDK couldn't find a view controller to present the SKStoreProductViewController from.") + } + } + + func stop() { + skadnClickHandler?.stop() + skadnClickHandler = nil + } +} diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsHelper.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsHelper.swift new file mode 100644 index 000000000..77e19eaee --- /dev/null +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsHelper.swift @@ -0,0 +1,42 @@ +/* Copyright 2018-2025 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 + +protocol PrebidSKAdNetworkStoreKitAdsHelperProtocol { + func start(in adView: UIView) + func stop() +} + +class PrebidStoreKitAdsHelper { + + private let storeKitAdsHelper: PrebidSKAdNetworkStoreKitAdsHelperProtocol + + init(isInterstitial: Bool) { + if isInterstitial { + storeKitAdsHelper = PrebidStoreKitAdsInterstitialHelper() + } else { + storeKitAdsHelper = PrebidStoreKitAdsBannerHelper() + } + } + + func start(in adView: UIView) { + storeKitAdsHelper.start(in: adView) + } + + func stop() { + storeKitAdsHelper.stop() + } +} diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift new file mode 100644 index 000000000..d0df5f5d6 --- /dev/null +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift @@ -0,0 +1,42 @@ +/* Copyright 2018-2025 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 + +class PrebidStoreKitAdsInterstitialHelper: PrebidSKAdNetworkStoreKitAdsHelperProtocol { + + private var skadnClickHandler: PrebidSKAdNetworkAdClickHandler? + private var interstitialObserver: InterstitialObserver? + + func start(in adView: UIView) { + interstitialObserver = InterstitialObserver(window: adView as? UIWindow) { [weak self] foundView in + if let presentingViewController = adView.parentViewController ?? UIApplication.topViewController() { + self?.skadnClickHandler = PrebidSKAdNetworkAdClickHandler() + self?.skadnClickHandler?.start(in: foundView, viewController: presentingViewController) + } else { + Log.error("SDK couldn't find a view controller to present the SKStoreProductViewController from.") + } + } + + interstitialObserver?.start() + } + + func stop() { + interstitialObserver?.stop() + interstitialObserver = nil + skadnClickHandler?.stop() + skadnClickHandler = nil + } +} diff --git a/PrebidMobile/Addendum/SKAdNetwork/SKStoreProductViewControllerPresenter.swift b/PrebidMobile/Addendum/SKAdNetwork/SKStoreProductViewControllerPresenter.swift index fef69bd5d..ee21fc4d6 100644 --- a/PrebidMobile/Addendum/SKAdNetwork/SKStoreProductViewControllerPresenter.swift +++ b/PrebidMobile/Addendum/SKAdNetwork/SKStoreProductViewControllerPresenter.swift @@ -24,9 +24,7 @@ class SKStoreProductViewControllerPresenter: NSObject { self.presentingViewController = viewController DispatchQueue.main.async { - let skadnController = SKStoreProductViewController() - skadnController.delegate = self - + let skadnController = SKStoreProductViewController() self.presentingViewController?.present(skadnController, animated: true) skadnController.loadProduct(withParameters: productParameters) { _, error in if let error { @@ -36,12 +34,3 @@ class SKStoreProductViewControllerPresenter: NSObject { } } } - -// MARK: - SKStoreProductViewControllerDelegate - -extension SKStoreProductViewControllerPresenter: SKStoreProductViewControllerDelegate { - - func productViewControllerDidFinish(_ viewController: SKStoreProductViewController) { - presentingViewController?.dismiss(animated: true) - } -} From a0799373a07f52ecb9a5642c6b9d0deb8381efc9 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Tue, 18 Feb 2025 17:18:08 +0200 Subject: [PATCH 16/21] feat: remove duplicated file --- .../PrebidSKAdNetworkStoreKitHelper.swift | 102 ------------------ 1 file changed, 102 deletions(-) delete mode 100644 PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkStoreKitHelper.swift diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkStoreKitHelper.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkStoreKitHelper.swift deleted file mode 100644 index eda396098..000000000 --- a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkStoreKitHelper.swift +++ /dev/null @@ -1,102 +0,0 @@ -/* Copyright 2018-2025 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 WebKit -import StoreKit - -/// Helper class that invokes SKStoreProductViewController on ad click. -@objcMembers -class PrebidSKAdNetworkStoreKitAdsHelper: NSObject { - - private weak var adView: UIView? - private weak var viewControllerForPresentingModals: UIViewController? - - private var productControllerPresenter: SKStoreProductViewControllerPresenter? - - /// Configures the provided ad view with SKAdN on click event.. - /// - Parameters: - /// - adView: The ad view where click events is tracked. - /// - viewController: The view controller used to present modals, such as the SKStoreProductViewController. - func start(in adView: UIView, viewController: UIViewController) { - self.adView = adView - self.viewControllerForPresentingModals = viewController - - AdViewUtils.findPrebidLocalCacheID(adView) { [weak self] result in - guard case .success(let cacheID) = result else { - self?.attemptFindUUID() - return - } - - guard let bidString = CacheManager.shared.get(cacheId: cacheID) else { - Log.warn("SDK could not find the bid response for provided cache id.") - return - } - - self?.configureAdViewWithSkadn(using: bidString) - } - } - - /// Mainly used for video creatives. Searches for **hb_uuid** keys from saved bids in third-party web views. - private func attemptFindUUID() { - guard let adView = adView, - let webView = adView.allSubViewsOf(type: WKWebView.self).first else { - return - } - - webView.evaluateJavaScript("document.body.innerHTML", completionHandler: { html, error in - guard let html = html as? String else { - return - } - - CacheManager.shared - .savedValuesDict - .values - .forEach { [weak self] value in - if let bid = Bid.bid(from: value), - let hbUUID = bid.targetingInfo?["hb_uuid"], - html.contains(hbUUID) { - self?.configureAdViewWithSkadn(using: bid) - } - } - }) - } - - private func configureAdViewWithSkadn(using bidString: String) { - guard let bid = Bid.bid(from: bidString) else { - return - } - - configureAdViewWithSkadn(using: bid) - } - - private func configureAdViewWithSkadn(using bid: Bid) { - guard let adView = adView, let viewControllerForPresentingModals = viewControllerForPresentingModals else { - return - } - - guard let skadn = bid.skadn, - let productParameters = SkadnParametersManager.getSkadnProductParameters(for: skadn) else { - Log.error("SDK couldn't retrieve SKAdN product parameters from bid response.") - return - } - - productControllerPresenter = SKStoreProductViewControllerPresenter() - productControllerPresenter?.present( - from: viewControllerForPresentingModals, - using: productParameters - ) - } -} From 5b132e8b31b546f1846a988ece27a623bee855c7 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Tue, 18 Feb 2025 17:20:48 +0200 Subject: [PATCH 17/21] feat: remove duplicated file --- .../SKAdNetwork/PrebidSKAdNetworkHelper.swift | 132 ------------------ 1 file changed, 132 deletions(-) delete mode 100644 PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkHelper.swift diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkHelper.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkHelper.swift deleted file mode 100644 index 69a00cc28..000000000 --- a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkHelper.swift +++ /dev/null @@ -1,132 +0,0 @@ -/* Copyright 2018-2025 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 WebKit -import StoreKit - -/// This class provides utilities for tracking ad clicks and presenting the SKStoreProductViewController -@objcMembers -class PrebidSKAdNetworkStoreKitHelper: NSObject { - - private weak var adView: UIView? - private weak var viewControllerForPresentingModals: UIViewController? - - private var productControllerPresenter: SKStoreProductViewControllerPresenter? - - /// Configures the provided ad view with SKAdN on click event.. - /// - Parameters: - /// - adView: The ad view where click events is tracked. - /// - viewController: The view controller used to present modals, such as the SKStoreProductViewController. - func start(in adView: UIView, viewController: UIViewController) { - self.adView = adView - self.viewControllerForPresentingModals = viewController - - AdViewUtils.findPrebidLocalCacheID(adView) { [weak self] result in - guard case .success(let cacheID) = result else { - self?.attemptFindUUID() - return - } - - guard let bidString = CacheManager.shared.get(cacheId: cacheID) else { - Log.warn("SDK could not find the bid response for provided cache id.") - return - } - - self?.configureAdViewWithSkadn(using: bidString) - } - } - - /// Mainly used for video creatives. Searches for **hb_uuid** keys from saved bids in third-party web views. - private func attemptFindUUID() { - guard let adView = adView, - let webView = adView.allSubViewsOf(type: WKWebView.self).first else { - return - } - - webView.evaluateJavaScript("document.body.innerHTML", completionHandler: { html, error in - guard let html = html as? String else { - return - } - - CacheManager.shared - .savedValuesDict - .values - .forEach { [weak self] value in - if let bid = Bid.bid(from: value), - let hbUUID = bid.targetingInfo?["hb_uuid"], - html.contains(hbUUID) { - self?.configureAdViewWithSkadn(using: bid) - } - } - }) - } - - private func configureAdViewWithSkadn(using bidString: String) { - guard let bid = Bid.bid(from: bidString) else { - return - } - - configureAdViewWithSkadn(using: bid) - } - - private func configureAdViewWithSkadn(using bid: Bid) { - guard let adView = adView, let viewControllerForPresentingModals = viewControllerForPresentingModals else { - return - } - - guard let skadn = bid.skadn, - let productParameters = SkadnParametersManager.getSkadnProductParameters(for: skadn) else { - Log.error("SDK couldn't retrieve SKAdN product parameters from bid response.") - return - } - - adView.subviews - .filter({ $0 is TouchTrackingOverlayView }) - .forEach({ $0.removeFromSuperview()}) - - let overlayView = TouchTrackingOverlayView(frame: adView.bounds) - adView.addSubview(overlayView) - - overlayView.onClick = { [weak self] in - self?.productControllerPresenter = SKStoreProductViewControllerPresenter() - self?.productControllerPresenter?.present( - from: viewControllerForPresentingModals, - using: productParameters - ) - } - } -} - -/// A custom overlay view for tracking touch interactions. -private class TouchTrackingOverlayView: UIView { - - var onClick: (() -> Void)? - - private var lastTimestamp: TimeInterval? - - override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.point(inside: point, with: event) { - if event?.timestamp != lastTimestamp { - onClick?() - } - - lastTimestamp = event?.timestamp - return nil - } - - return super.hitTest(point, with: event) - } -} From 0208b61ee49b4b996e1fc07f088024969abf406c Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Fri, 21 Feb 2025 12:29:33 +0200 Subject: [PATCH 18/21] feat: add display delay --- .../PrebidSKAdNetworkAdClickHandler.swift | 29 ++++++++++++++----- .../PrebidStoreKitAdsInterstitialHelper.swift | 6 +++- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift index c02db852c..5353fe9b9 100644 --- a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift @@ -24,15 +24,23 @@ class PrebidSKAdNetworkAdClickHandler: NSObject { private weak var adView: UIView? private weak var viewControllerForPresentingModals: UIViewController? + private var displayDelay = 0 + private var productControllerPresenter: SKStoreProductViewControllerPresenter? - /// Configures the provided ad view with SKAdN on click event.. + /// Presents `SKStoreProductViewController` on click event.. /// - Parameters: /// - adView: The ad view where click events is tracked. - /// - viewController: The view controller used to present modals, such as the SKStoreProductViewController. - func start(in adView: UIView, viewController: UIViewController) { + /// - viewController: The view controller used to present modals, such as the `SKStoreProductViewController`. + /// - displayDelay: Delay for displaying `SKStoreProductViewController`. + func start( + in adView: UIView, + viewController: UIViewController, + displayDelay: Int = 0 + ) { self.adView = adView self.viewControllerForPresentingModals = viewController + self.displayDelay = displayDelay AdViewUtils.findPrebidLocalCacheID(adView) { [weak self] result in guard case .success(let cacheID) = result else { @@ -107,11 +115,16 @@ class PrebidSKAdNetworkAdClickHandler: NSObject { adView.addSubview(overlayView) overlayView.onClick = { [weak self] in - self?.productControllerPresenter = SKStoreProductViewControllerPresenter() - self?.productControllerPresenter?.present( - from: viewControllerForPresentingModals, - using: productParameters - ) + guard let self else { return } + DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(self.displayDelay)) { + if self.adView != nil { + self.productControllerPresenter = SKStoreProductViewControllerPresenter() + self.productControllerPresenter?.present( + from: viewControllerForPresentingModals, + using: productParameters + ) + } + } } } } diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift index d0df5f5d6..b6c9fca40 100644 --- a/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift @@ -24,7 +24,11 @@ class PrebidStoreKitAdsInterstitialHelper: PrebidSKAdNetworkStoreKitAdsHelperPro interstitialObserver = InterstitialObserver(window: adView as? UIWindow) { [weak self] foundView in if let presentingViewController = adView.parentViewController ?? UIApplication.topViewController() { self?.skadnClickHandler = PrebidSKAdNetworkAdClickHandler() - self?.skadnClickHandler?.start(in: foundView, viewController: presentingViewController) + self?.skadnClickHandler?.start( + in: foundView, + viewController: presentingViewController, + displayDelay: 1 + ) } else { Log.error("SDK couldn't find a view controller to present the SKStoreProductViewController from.") } From 0f5f4d90849d90ee083e068fadd5e488589f6e2c Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 26 Feb 2025 12:09:00 +0200 Subject: [PATCH 19/21] Revert "feat: add display delay" This reverts commit 0208b61ee49b4b996e1fc07f088024969abf406c. --- .../PrebidSKAdNetworkAdClickHandler.swift | 29 +++++-------------- .../PrebidStoreKitAdsInterstitialHelper.swift | 6 +--- 2 files changed, 9 insertions(+), 26 deletions(-) diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift index 5353fe9b9..c02db852c 100644 --- a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift @@ -24,23 +24,15 @@ class PrebidSKAdNetworkAdClickHandler: NSObject { private weak var adView: UIView? private weak var viewControllerForPresentingModals: UIViewController? - private var displayDelay = 0 - private var productControllerPresenter: SKStoreProductViewControllerPresenter? - /// Presents `SKStoreProductViewController` on click event.. + /// Configures the provided ad view with SKAdN on click event.. /// - Parameters: /// - adView: The ad view where click events is tracked. - /// - viewController: The view controller used to present modals, such as the `SKStoreProductViewController`. - /// - displayDelay: Delay for displaying `SKStoreProductViewController`. - func start( - in adView: UIView, - viewController: UIViewController, - displayDelay: Int = 0 - ) { + /// - viewController: The view controller used to present modals, such as the SKStoreProductViewController. + func start(in adView: UIView, viewController: UIViewController) { self.adView = adView self.viewControllerForPresentingModals = viewController - self.displayDelay = displayDelay AdViewUtils.findPrebidLocalCacheID(adView) { [weak self] result in guard case .success(let cacheID) = result else { @@ -115,16 +107,11 @@ class PrebidSKAdNetworkAdClickHandler: NSObject { adView.addSubview(overlayView) overlayView.onClick = { [weak self] in - guard let self else { return } - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(self.displayDelay)) { - if self.adView != nil { - self.productControllerPresenter = SKStoreProductViewControllerPresenter() - self.productControllerPresenter?.present( - from: viewControllerForPresentingModals, - using: productParameters - ) - } - } + self?.productControllerPresenter = SKStoreProductViewControllerPresenter() + self?.productControllerPresenter?.present( + from: viewControllerForPresentingModals, + using: productParameters + ) } } } diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift index b6c9fca40..d0df5f5d6 100644 --- a/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift @@ -24,11 +24,7 @@ class PrebidStoreKitAdsInterstitialHelper: PrebidSKAdNetworkStoreKitAdsHelperPro interstitialObserver = InterstitialObserver(window: adView as? UIWindow) { [weak self] foundView in if let presentingViewController = adView.parentViewController ?? UIApplication.topViewController() { self?.skadnClickHandler = PrebidSKAdNetworkAdClickHandler() - self?.skadnClickHandler?.start( - in: foundView, - viewController: presentingViewController, - displayDelay: 1 - ) + self?.skadnClickHandler?.start(in: foundView, viewController: presentingViewController) } else { Log.error("SDK couldn't find a view controller to present the SKStoreProductViewController from.") } From 8b43b742aa8c06ff562d9e20718d97a180c3d5e3 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 26 Feb 2025 12:10:58 +0200 Subject: [PATCH 20/21] feat: minor changes --- .../SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift | 2 +- .../SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift index c02db852c..043439f1a 100644 --- a/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidSKAdNetworkAdClickHandler.swift @@ -26,7 +26,7 @@ class PrebidSKAdNetworkAdClickHandler: NSObject { private var productControllerPresenter: SKStoreProductViewControllerPresenter? - /// Configures the provided ad view with SKAdN on click event.. + /// Presents `SKStoreProductViewController` on click event. /// - Parameters: /// - adView: The ad view where click events is tracked. /// - viewController: The view controller used to present modals, such as the SKStoreProductViewController. diff --git a/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift b/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift index d0df5f5d6..318bea310 100644 --- a/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift +++ b/PrebidMobile/Addendum/SKAdNetwork/PrebidStoreKitAdsInterstitialHelper.swift @@ -24,7 +24,10 @@ class PrebidStoreKitAdsInterstitialHelper: PrebidSKAdNetworkStoreKitAdsHelperPro interstitialObserver = InterstitialObserver(window: adView as? UIWindow) { [weak self] foundView in if let presentingViewController = adView.parentViewController ?? UIApplication.topViewController() { self?.skadnClickHandler = PrebidSKAdNetworkAdClickHandler() - self?.skadnClickHandler?.start(in: foundView, viewController: presentingViewController) + self?.skadnClickHandler?.start( + in: foundView, + viewController: presentingViewController + ) } else { Log.error("SDK couldn't find a view controller to present the SKStoreProductViewController from.") } From 4afc97e0d02ac65fe24f90660777baf688914329 Mon Sep 17 00:00:00 2001 From: Olena Stepaniuk Date: Wed, 26 Feb 2025 12:34:43 +0200 Subject: [PATCH 21/21] feat: remove support of SKAdN StoreKit for video ads --- .../Model/TestCasesManager.swift | 19 ------------------- ...idOriginalAPIVideoRewardedController.swift | 7 ------- PrebidMobile/AdUnits/BannerAdUnit.swift | 12 ++++++++++-- PrebidMobile/AdUnits/InterstitialAdUnit.swift | 10 +++++++++- .../AdUnits/RewardedVideoAdUnit.swift | 10 ---------- 5 files changed, 19 insertions(+), 39 deletions(-) diff --git a/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift b/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift index 7942ce0d9..59a61dc51 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/Model/TestCasesManager.swift @@ -467,25 +467,6 @@ struct TestCaseManager { setupCustomParams(for: interstitialController.prebidConfigId) }), - TestCase(title: "Video Rewarded SKAdN 320x480 (GAM Original) [OK, PUC]", - tags: [.interstitial, .originalAPI, .server, .video], - exampleVCStoryboardID: "AdapterViewController", - configurationClosure: { vc in - - guard let adapterVC = vc as? AdapterViewController else { - return - } - - let interstitialController = PrebidOriginalAPIVideoRewardedController(rootController: adapterVC) - interstitialController.activatePrebidSKAdN = true - interstitialController.prebidConfigId = "prebid-ita-video-rewarded-320-480-original-api-skadn" - interstitialController.adUnitID = "/21808260008/prebid-demo-app-original-api-video-interstitial" - - adapterVC.setup(adapter: interstitialController) - - setupCustomParams(for: interstitialController.prebidConfigId) - }), - TestCase(title: "Multiformat Interstitial 320x480 (GAM Original) [OK, PUC]", tags: [.multiformat, .originalAPI, .server], exampleVCStoryboardID: "AdapterViewController", diff --git a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoRewardedController.swift b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoRewardedController.swift index c052fc0df..53ede36b9 100644 --- a/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoRewardedController.swift +++ b/InternalTestApp/PrebidMobileDemoRendering/ViewControllers/Adapters/Prebid/OriginalAPI/PrebidOriginalAPIVideoRewardedController.swift @@ -27,8 +27,6 @@ class PrebidOriginalAPIVideoRewardedController: var prebidConfigId = "" var adUnitID = "" - var activatePrebidSKAdN = false - var refreshInterval: TimeInterval = 0 // Prebid @@ -149,11 +147,6 @@ class PrebidOriginalAPIVideoRewardedController: @IBAction func showButtonClicked() { if let gamRewarded = gamRewarded { rootController?.showButton.isEnabled = false - - if activatePrebidSKAdN { - adUnit.activatePrebidSKAdNetworkStoreKitAdsFlow() - } - gamRewarded.present(fromRootViewController: rootController!) {} } } diff --git a/PrebidMobile/AdUnits/BannerAdUnit.swift b/PrebidMobile/AdUnits/BannerAdUnit.swift index 422a883a8..89b8549ea 100644 --- a/PrebidMobile/AdUnits/BannerAdUnit.swift +++ b/PrebidMobile/AdUnits/BannerAdUnit.swift @@ -73,12 +73,20 @@ public class BannerAdUnit: AdUnit, BannerBasedAdUnitProtocol, VideoBasedAdUnitPr // MARK: SKAdNetwork /// Activates Prebid's SKAdNetwork StoreKit ads flow for the provided ad view. - /// Note: Ensure this method is called within the Google Mobile Ads ad received method - /// (e.g., in the GADBannerViewDelegate's bannerViewDidReceiveAd or similar callbacks). + /// + /// Ensure this method is called within the Google Mobile Ads ad received method + /// (e.g., in the GADBannerViewDelegate's `bannerViewDidReceiveAd` or similar callbacks). + /// + /// This feature is not available for video ads. /// /// - Parameters: /// - adView: The ad view that contains ad creative(f.e. GAMBannerView). public func activatePrebidSKAdNetworkStoreKitAdsFlow(adView: UIView) { + guard !adFormats.contains(.video) else { + Log.warn("SKAdNetwork StoreKit ads flow is not supported for video ads.") + return + } + skadnStoreKitAdsHelper.start(in: adView) } } diff --git a/PrebidMobile/AdUnits/InterstitialAdUnit.swift b/PrebidMobile/AdUnits/InterstitialAdUnit.swift index ff2073284..12eaeaece 100644 --- a/PrebidMobile/AdUnits/InterstitialAdUnit.swift +++ b/PrebidMobile/AdUnits/InterstitialAdUnit.swift @@ -75,8 +75,16 @@ public class InterstitialAdUnit: AdUnit, BannerBasedAdUnitProtocol, VideoBasedAd // MARK: SKAdNetwork /// Activates Prebid's SKAdNetwork StoreKit ads flow. - /// Note: Ensure this method is called before presenting interstitials. + /// + /// Ensure this method is called before presenting interstitials. + /// + /// This feature is not available for video ads. public func activatePrebidSKAdNetworkStoreKitAdsFlow() { + guard !adFormats.contains(.video) else { + Log.warn("SKAdNetwork StoreKit ads flow is not supported for video ads.") + return + } + if let window = UIWindow.firstKeyWindow { skadnStoreKitAdsHelper.start(in: window) } diff --git a/PrebidMobile/AdUnits/RewardedVideoAdUnit.swift b/PrebidMobile/AdUnits/RewardedVideoAdUnit.swift index 9510ee7b8..a8584072f 100644 --- a/PrebidMobile/AdUnits/RewardedVideoAdUnit.swift +++ b/PrebidMobile/AdUnits/RewardedVideoAdUnit.swift @@ -53,14 +53,4 @@ public class RewardedVideoAdUnit: AdUnit, VideoBasedAdUnitProtocol { self.init(configId: configId) adUnitConfig.minSizePerc = NSValue(cgSize: CGSize(width: minWidthPerc, height: minHeightPerc)) } - - // MARK: SKAdNetwork - - /// Activates Prebid's SKAdNetwork StoreKit ads flow. - /// Note: Ensure this method is called before presenting interstitials. - public func activatePrebidSKAdNetworkStoreKitAdsFlow() { - if let window = UIWindow.firstKeyWindow { - skadnStoreKitAdsHelper.start(in: window) - } - } }